Coverage for python / lsst / analysis / tools / utils.py: 31%
40 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-18 09:19 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-18 09:19 +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 tractCorners = skymap[tractId].getVertexList()
58 corners = _wrapRa([(corner.getRa().asDegrees(), corner.getDec().asDegrees()) for corner in tractCorners])
60 return corners
63def getPatchCorners(tractInfo, patchId):
64 """Calculate the corners of a patch, given tractInfo.
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.
75 Returns
76 -------
77 corners : `list` of `tuples` of `float`
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()
86 tractWcs = tractInfo.getWcs()
87 patchCorners = tractWcs.pixelToSky(patchCorners)
88 corners = _wrapRa([(corner.getRa().asDegrees(), corner.getDec().asDegrees()) for corner in patchCorners])
90 return corners
93def _wrapRa(corners):
94 """Wrap in right ascension if the corners span RA=0
96 Parameters
97 ----------
98 corners : `list` of `tuples` of `float`
99 Pairs of coordinates representing tract or patch corners.
101 Returns
102 -------
103 corners : `list` of `tuples` of `float`
104 Pairs of coordinates representing tract or patch corners,
105 wrapped in RA.
106 """
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)]
119 return corners
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.
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.
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.
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 """
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()