Coverage for python / lsst / analysis / tools / utils.py: 31%

40 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-05 18:53 +0000

1# This file is part of analysis_tools. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21 

22__all__ = ( 

23 "getTractCorners", 

24 "getPatchCorners", 

25 "http_client", 

26) 

27 

28from collections.abc import Generator 

29from contextlib import contextmanager 

30 

31import numpy as np 

32import requests 

33from lsst.geom import Box2D 

34from requests.adapters import HTTPAdapter 

35from urllib3 import Retry 

36 

37 

38def getTractCorners(skymap, tractId): 

39 """Calculate the corners of a tract, given a skymap. 

40 

41 Parameters 

42 ---------- 

43 skymap : `lsst.skymap` 

44 tractId : `int` 

45 Identification number of the tract whose corner coordinates 

46 are returned. 

47 

48 Returns 

49 ------- 

50 corners : `list` of `tuples` of `float` 

51 

52 Notes 

53 ----- 

54 Corners are returned in degrees and wrapped in ra. 

55 """ 

56 tractCorners = skymap[tractId].getVertexList() 

57 corners = _wrapRa([(corner.getRa().asDegrees(), corner.getDec().asDegrees()) for corner in tractCorners]) 

58 

59 return corners 

60 

61 

62def getPatchCorners(tractInfo, patchId): 

63 """Calculate the corners of a patch, given tractInfo. 

64 

65 Parameters 

66 ---------- 

67 tractInfo : `lsst.skymap.tractInfo.ExplicitTractInfo` 

68 Tract info object of the tract containing the patch whose 

69 corner coordinates are returned. 

70 patchId : `int` 

71 Identification number of the patch whose corner coordinates 

72 are returned. 

73 

74 Returns 

75 ------- 

76 corners : `list` of `tuples` of `float` 

77 

78 Notes 

79 ----- 

80 Corners are returned in degrees and are wrapped in ra. 

81 """ 

82 patchInfo = tractInfo.getPatchInfo(patchId) 

83 patchCorners = Box2D(patchInfo.getInnerBBox()).getCorners() 

84 

85 tractWcs = tractInfo.getWcs() 

86 patchCorners = tractWcs.pixelToSky(patchCorners) 

87 corners = _wrapRa([(corner.getRa().asDegrees(), corner.getDec().asDegrees()) for corner in patchCorners]) 

88 

89 return corners 

90 

91 

92def _wrapRa(corners): 

93 """Wrap in right ascension if the corners span RA=0 

94 

95 Parameters 

96 ---------- 

97 corners : `list` of `tuples` of `float` 

98 Pairs of coordinates representing tract or patch corners. 

99 

100 Returns 

101 ------- 

102 corners : `list` of `tuples` of `float` 

103 Pairs of coordinates representing tract or patch corners, 

104 wrapped in RA. 

105 """ 

106 

107 minRa = np.min([corner[0] for corner in corners]) 

108 maxRa = np.max([corner[0] for corner in corners]) 

109 # If the tract needs wrapping in ra, wrap it 

110 if maxRa - minRa > 10: 

111 x = maxRa 

112 maxRa = 360 + minRa 

113 minRa = x 

114 minDec = np.min([corner[1] for corner in corners]) 

115 maxDec = np.max([corner[1] for corner in corners]) 

116 corners = [(minRa, minDec), (maxRa, minDec), (maxRa, maxDec), (minRa, maxDec)] 

117 

118 return corners 

119 

120 

121@contextmanager 

122def http_client() -> Generator[requests.Session]: 

123 """Creates a requests session with a custom transport to support 

124 automatic retries with backoff for dealing with transient server-side 

125 issues. 

126 

127 Notes 

128 ----- 

129 The goal of the adapter defined here is to avoid premature client abends 

130 when transient server or infrastructure issues prevent good-faith attempts 

131 at accessing APIs. To the extent that we want to balance "eventually 

132 successful" HTTP requests with the desire to vacate the compute resources 

133 our process is occupying, these retries should not overstay their welcome. 

134 

135 The "POST" HTTP verb is not usually part of the allowed methods for retries 

136 because unlike "PUT", "POST" is not considered idempotent by default. It is 

137 partially for this reason that a custom Retry adapter is needed, because 

138 by default "POST" requests would not be retried for status. 

139 

140 The backoff_factor is an exponential factor used to calculate how long to 

141 sleep between the third and subsequent tries, in seconds. The first retry 

142 is immediate and the total backoff won't exceed backoff_max, which defaults 

143 to 120 seconds. 

144 """ 

145 

146 retriable_statuses = [ 

147 requests.codes.too_many_requests, 

148 requests.codes.server_error, 

149 requests.codes.bad_gateway, 

150 requests.codes.service_unavailable, 

151 requests.codes.gateway_timeout, 

152 ] 

153 session = requests.Session() 

154 retry_strategy = Retry( 

155 total=None, # use specific conditional constraints 

156 connect=3, # network or tcp errors 

157 read=0, # request sent, response is bad 

158 status=5, # retries based on bad response status (see retriable_statuses) 

159 redirect=3, # default value, follow 3 redirects 

160 other=0, # edge cases and weird stuff 

161 backoff_factor=0.1, # sleep == {factor} * 2^(previous tries) 

162 status_forcelist=retriable_statuses, 

163 raise_on_status=True, 

164 allowed_methods={"GET", "HEAD", "POST", "PUT"}, 

165 ) 

166 session.mount("http://", HTTPAdapter(max_retries=retry_strategy)) 

167 session.mount("https://", HTTPAdapter(max_retries=retry_strategy)) 

168 try: 

169 yield session 

170 finally: 

171 session.close()