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
« 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/>.
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 lsst.geom import Box2D
34from requests.adapters import HTTPAdapter
35from urllib3 import Retry
38def getTractCorners(skymap, tractId):
39 """Calculate the corners of a tract, given a skymap.
41 Parameters
42 ----------
43 skymap : `lsst.skymap`
44 tractId : `int`
45 Identification number of the tract whose corner coordinates
46 are returned.
48 Returns
49 -------
50 corners : `list` of `tuples` of `float`
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])
59 return corners
62def getPatchCorners(tractInfo, patchId):
63 """Calculate the corners of a patch, given tractInfo.
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.
74 Returns
75 -------
76 corners : `list` of `tuples` of `float`
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()
85 tractWcs = tractInfo.getWcs()
86 patchCorners = tractWcs.pixelToSky(patchCorners)
87 corners = _wrapRa([(corner.getRa().asDegrees(), corner.getDec().asDegrees()) for corner in patchCorners])
89 return corners
92def _wrapRa(corners):
93 """Wrap in right ascension if the corners span RA=0
95 Parameters
96 ----------
97 corners : `list` of `tuples` of `float`
98 Pairs of coordinates representing tract or patch corners.
100 Returns
101 -------
102 corners : `list` of `tuples` of `float`
103 Pairs of coordinates representing tract or patch corners,
104 wrapped in RA.
105 """
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)]
118 return corners
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.
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.
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.
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 """
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()