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

40 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-24 08: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 requests.adapters import HTTPAdapter 

34from urllib3 import Retry 

35 

36from lsst.geom import Box2D 

37 

38 

39def getTractCorners(skymap, tractId): 

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

41 

42 Parameters 

43 ---------- 

44 skymap : `lsst.skymap` 

45 tractId : `int` 

46 Identification number of the tract whose corner coordinates 

47 are returned. 

48 

49 Returns 

50 ------- 

51 corners : `list` of `tuples` of `float` 

52 

53 Notes 

54 ----- 

55 Corners are returned in degrees and wrapped in ra. 

56 """ 

57 tractCorners = skymap[tractId].getVertexList() 

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

59 

60 return corners 

61 

62 

63def getPatchCorners(tractInfo, patchId): 

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

65 

66 Parameters 

67 ---------- 

68 tractInfo : `lsst.skymap.tractInfo.ExplicitTractInfo` 

69 Tract info object of the tract containing the patch whose 

70 corner coordinates are returned. 

71 patchId : `int` 

72 Identification number of the patch whose corner coordinates 

73 are returned. 

74 

75 Returns 

76 ------- 

77 corners : `list` of `tuples` of `float` 

78 

79 Notes 

80 ----- 

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

82 """ 

83 patchInfo = tractInfo.getPatchInfo(patchId) 

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

85 

86 tractWcs = tractInfo.getWcs() 

87 patchCorners = tractWcs.pixelToSky(patchCorners) 

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

89 

90 return corners 

91 

92 

93def _wrapRa(corners): 

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

95 

96 Parameters 

97 ---------- 

98 corners : `list` of `tuples` of `float` 

99 Pairs of coordinates representing tract or patch corners. 

100 

101 Returns 

102 ------- 

103 corners : `list` of `tuples` of `float` 

104 Pairs of coordinates representing tract or patch corners, 

105 wrapped in RA. 

106 """ 

107 

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

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

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

111 if maxRa - minRa > 10: 

112 x = maxRa 

113 maxRa = 360 + minRa 

114 minRa = x 

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

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

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

118 

119 return corners 

120 

121 

122@contextmanager 

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

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

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

126 issues. 

127 

128 Notes 

129 ----- 

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

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

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

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

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

135 

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

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

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

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

140 

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

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

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

144 to 120 seconds. 

145 """ 

146 

147 retriable_statuses = [ 

148 requests.codes.too_many_requests, 

149 requests.codes.server_error, 

150 requests.codes.bad_gateway, 

151 requests.codes.service_unavailable, 

152 requests.codes.gateway_timeout, 

153 ] 

154 session = requests.Session() 

155 retry_strategy = Retry( 

156 total=None, # use specific conditional constraints 

157 connect=3, # network or tcp errors 

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

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

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

161 other=0, # edge cases and weird stuff 

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

163 status_forcelist=retriable_statuses, 

164 raise_on_status=True, 

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

166 ) 

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

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

169 try: 

170 yield session 

171 finally: 

172 session.close()