Coverage for python/lsst/analysis/tools/utils.py: 100%
43 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-17 10:03 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-17 10:03 +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/>.
22__all__ = (
23 "getTractCorners",
24 "getPatchCorners",
25 "http_client",
26)
28from collections.abc import Generator
29from contextlib import contextmanager
31import numpy as np
32import requests
33from requests.adapters import HTTPAdapter
34from urllib3 import Retry
36from lsst.geom import Box2D
39def getTractCorners(skymap, tractId):
40 """Calculate the corners of a tract, given a skymap.
42 Parameters
43 ----------
44 skymap : `lsst.skymap`
45 tractId : `int`
46 Identification number of the tract whose corner coordinates
47 are returned.
49 Returns
50 -------
51 corners : `list` of `tuples` of `float`
53 Notes
54 -----
55 Corners are returned in degrees and wrapped in ra.
56 """
57 # Get bounding box is needed if the skymap isn't rings
58 # The discrete skymaps don't return a box from
59 # getInnerSkyRegion
60 tractBox = skymap[tractId].getInnerSkyRegion().getBoundingBox()
61 minRa, maxRa = tractBox.getLon().getA().asDegrees(), tractBox.getLon().getB().asDegrees()
62 minDec, maxDec = tractBox.getLat().getA().asDegrees(), tractBox.getLat().getB().asDegrees()
64 tractCorners = [(minRa, minDec), (maxRa, minDec), (maxRa, maxDec), (minRa, minDec)]
65 corners = _wrapRa(tractCorners)
67 return corners
70def getPatchCorners(tractInfo, patchId):
71 """Calculate the corners of a patch, given tractInfo.
73 Parameters
74 ----------
75 tractInfo : `lsst.skymap.tractInfo.ExplicitTractInfo`
76 Tract info object of the tract containing the patch whose
77 corner coordinates are returned.
78 patchId : `int`
79 Identification number of the patch whose corner coordinates
80 are returned.
82 Returns
83 -------
84 corners : `list` of `tuples` of `float`
86 Notes
87 -----
88 Corners are returned in degrees and are wrapped in ra.
89 """
90 patchInfo = tractInfo.getPatchInfo(patchId)
91 patchCorners = Box2D(patchInfo.getInnerBBox()).getCorners()
93 tractWcs = tractInfo.getWcs()
94 patchCorners = tractWcs.pixelToSky(patchCorners)
95 corners = _wrapRa([(corner.getRa().asDegrees(), corner.getDec().asDegrees()) for corner in patchCorners])
97 return corners
100def _wrapRa(corners):
101 """Wrap in right ascension if the corners span RA=0
103 Parameters
104 ----------
105 corners : `list` of `tuples` of `float`
106 Pairs of coordinates representing tract or patch corners.
108 Returns
109 -------
110 corners : `list` of `tuples` of `float`
111 Pairs of coordinates representing tract or patch corners,
112 wrapped in RA.
113 """
115 minRa = np.min([corner[0] for corner in corners])
116 maxRa = np.max([corner[0] for corner in corners])
117 # If the tract needs wrapping in ra, wrap it
118 if maxRa - minRa > 10:
119 x = maxRa
120 maxRa = 360 + minRa
121 minRa = x
122 minDec = np.min([corner[1] for corner in corners])
123 maxDec = np.max([corner[1] for corner in corners])
124 corners = [(minRa, minDec), (maxRa, minDec), (maxRa, maxDec), (minRa, maxDec)]
126 return corners
129@contextmanager
130def http_client() -> Generator[requests.Session]:
131 """Creates a requests session with a custom transport to support
132 automatic retries with backoff for dealing with transient server-side
133 issues.
135 Notes
136 -----
137 The goal of the adapter defined here is to avoid premature client abends
138 when transient server or infrastructure issues prevent good-faith attempts
139 at accessing APIs. To the extent that we want to balance "eventually
140 successful" HTTP requests with the desire to vacate the compute resources
141 our process is occupying, these retries should not overstay their welcome.
143 The "POST" HTTP verb is not usually part of the allowed methods for retries
144 because unlike "PUT", "POST" is not considered idempotent by default. It is
145 partially for this reason that a custom Retry adapter is needed, because
146 by default "POST" requests would not be retried for status.
148 The backoff_factor is an exponential factor used to calculate how long to
149 sleep between the third and subsequent tries, in seconds. The first retry
150 is immediate and the total backoff won't exceed backoff_max, which defaults
151 to 120 seconds.
152 """
154 retriable_statuses = [
155 requests.codes.too_many_requests,
156 requests.codes.server_error,
157 requests.codes.bad_gateway,
158 requests.codes.service_unavailable,
159 requests.codes.gateway_timeout,
160 ]
161 session = requests.Session()
162 retry_strategy = Retry(
163 total=None, # use specific conditional constraints
164 connect=3, # network or tcp errors
165 read=0, # request sent, response is bad
166 status=5, # retries based on bad response status (see retriable_statuses)
167 redirect=3, # default value, follow 3 redirects
168 other=0, # edge cases and weird stuff
169 backoff_factor=0.1, # sleep == {factor} * 2^(previous tries)
170 status_forcelist=retriable_statuses,
171 raise_on_status=True,
172 allowed_methods={"GET", "HEAD", "POST", "PUT"},
173 )
174 session.mount("http://", HTTPAdapter(max_retries=retry_strategy))
175 session.mount("https://", HTTPAdapter(max_retries=retry_strategy))
176 try:
177 yield session
178 finally:
179 session.close()