shearline 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Backshear LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,149 @@
1
+ Metadata-Version: 2.4
2
+ Name: shearline
3
+ Version: 1.0.0
4
+ Summary: Analyst-grade US severe-weather tools for AI agents over MCP: warning polygons with IBW tags, SPC outlooks, RAP point environments, MRMS hail/rotation, storm reports, and a composite threat brief.
5
+ Keywords: mcp,weather,severe-weather,meteorology,nws,spc,mrms,radar,ai-agents
6
+ Author: Backshear LLC
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
14
+ Requires-Dist: boto3>=1.43.27
15
+ Requires-Dist: cfgrib>=0.9.15.1
16
+ Requires-Dist: eccodeslib>=2.47.1.20
17
+ Requires-Dist: httpx>=0.28.1
18
+ Requires-Dist: mcp[cli]>=1.27.2
19
+ Requires-Dist: metpy>=1.7.1
20
+ Requires-Dist: numpy>=2.4.6
21
+ Requires-Dist: pydantic>=2.13.4
22
+ Requires-Dist: shapely>=2.1.2
23
+ Requires-Dist: xarray>=2026.4.0
24
+ Requires-Python: >=3.12
25
+ Project-URL: Homepage, https://github.com/lostnumber07/shearline
26
+ Project-URL: Repository, https://github.com/lostnumber07/shearline
27
+ Project-URL: Issues, https://github.com/lostnumber07/shearline/issues
28
+ Description-Content-Type: text/markdown
29
+
30
+ <!-- mcp-name: io.github.lostnumber07/shearline -->
31
+
32
+ # SHEARLINE
33
+
34
+ **The severe-weather analyst your agent doesn't have.** SHEARLINE is a free, MIT-licensed MCP server that gives AI agents analyst-grade US severe-weather tools: live warning polygons with Impact-Based Warning tags, SPC convective outlooks, RAP-derived point environments (CAPE/shear/SRH/STP computed with MetPy), MRMS radar-derived hail and rotation products, ground-truth storm reports, and a composite threat brief that synthesizes all of it. A dozen weather MCPs already wrap the basic forecast API; SHEARLINE deliberately skips everything they do and ships only what requires radar meteorology to expose correctly.
35
+
36
+ > **Informational only. Not a substitute for official NWS warnings.** Every tool repeats this, because it matters: when weather threatens, follow official warnings from weather.gov and local authorities.
37
+
38
+ ## Tools
39
+
40
+ | Tool | What it returns |
41
+ | --- | --- |
42
+ | `get_active_warnings(lat, lon, radius_km=40)` | Active tornado/severe-thunderstorm/flash-flood warning polygons with IBW tags (max hail size, max gust, tornado detection/damage threat), parsed storm motion, expirations, and whether the exact point is inside a polygon. Watches listed separately. |
43
+ | `get_spc_outlook(lat, lon, day=1)` | SPC categorical risk (TSTM→HIGH) at the point plus tornado/hail/wind probabilities and significant-severe flags, days 1–3, with interpretation calibrated to the category. |
44
+ | `get_point_environment(lat, lon)` | Latest RAP 13-km analysis profile computed with MetPy: MLCAPE/MUCAPE/CINs, LCL, 0–1/0–6 km shear, 0–1/0–3 km SRH, Bunkers motion, effective inflow layer, effective SRH/shear, SCP, and significant-tornado parameter — interpreted like an analyst (pulse vs. cool-season high-shear vs. classic supercell parameter space). |
45
+ | `get_mrms_severe(lat, lon, radius_km=40)` | MRMS maxima within radius: 60-min MESH (hail, inches and mm), low-level and mid-level rotation tracks (azimuthal shear), VIL, composite reflectivity — each with valid time and distance/bearing of the max. |
46
+ | `get_storm_reports(lat, lon, radius_km=80, hours=6)` | Normalized Local Storm Reports: type, magnitude with units, time, location, distance/bearing, remarks. |
47
+ | `get_threat_brief(lat, lon)` | The showpiece: runs everything above concurrently and synthesizes a threat level (none/marginal/elevated/significant/extreme) **with stated logic**, hazards ranked, environment summary, nearest storm signature, and a recommended attention window. |
48
+ | `get_radar_snapshot(lat, lon)` | Nearest WSR-88D's latest Level 2 volume metadata: VCP (scan strategy), max reflectivity with range/azimuth, coarse echo-top estimate. |
49
+
50
+ Every tool returns structured JSON with `data` (numeric fields, units stated), `interpretation` (plain-language analyst sentences), `degraded` (which upstream sources failed, if any — partial data instead of errors), and the safety `disclaimer`.
51
+
52
+ ## Example: threat brief during a real outbreak
53
+
54
+ Real output from 2026-06-10, point inside an active tornado warning in northern Missouri:
55
+
56
+ ```json
57
+ {
58
+ "threat_level": "extreme",
59
+ "threat_logic": [
60
+ "Tornado Warning in effect at the point, corroborated by confirmed tornado reports nearby — treat as an immediate life-safety situation.",
61
+ "Severe Thunderstorm Warning at the point tagged 'Considerable' (hail to 1.75\", gusts to 60 mph).",
62
+ "Significant-tornado parameter of 4.0 with storms ongoing — environment strongly supports tornadic supercells.",
63
+ "MRMS MESH of 2.3\" hail within radius in the last hour.",
64
+ "Intense rotation track (azimuthal shear 0.013 /s) nearby in the last hour.",
65
+ "6 tornado report(s) near the point in the report window."
66
+ ],
67
+ "hazards_ranked": [
68
+ {"hazard": "tornado", "level": "extreme"},
69
+ {"hazard": "hail", "level": "extreme"},
70
+ {"hazard": "damaging_wind", "level": "extreme"},
71
+ {"hazard": "flash_flood", "level": "moderate"}
72
+ ],
73
+ "nearest_storm_signature": {
74
+ "signature": "composite reflectivity", "value": "58.5 dBZ",
75
+ "distance_km": 18.0, "direction": "ENE", "valid_utc": "2026-06-10T22:14Z"
76
+ },
77
+ "attention_window": {"window": "now", "until_utc": "2026-06-10T21:00:00-05:00"}
78
+ }
79
+ ```
80
+
81
+ And the same tool for a quiet coastal Maine point reads as confidently quiet — not as an error: `"threat_level": "none"` with the environment numbers shown so the agent can see *why* it's quiet.
82
+
83
+ ## Install
84
+
85
+ Requires Python 3.12+ and [uv](https://docs.astral.sh/uv/). No API keys — every data source is public and anonymous.
86
+
87
+ **Claude Code:**
88
+
89
+ ```sh
90
+ claude mcp add shearline -- uvx --from git+https://github.com/lostnumber07/shearline shearline
91
+ ```
92
+
93
+ **Claude Desktop** (`claude_desktop_config.json`):
94
+
95
+ ```json
96
+ {
97
+ "mcpServers": {
98
+ "shearline": {
99
+ "command": "uvx",
100
+ "args": ["--from", "git+https://github.com/lostnumber07/shearline", "shearline"]
101
+ }
102
+ }
103
+ }
104
+ ```
105
+
106
+ (Once published to PyPI, `uvx shearline` alone works.)
107
+
108
+ **Streamable HTTP** (for remote/agent-platform use):
109
+
110
+ ```sh
111
+ uvx --from git+https://github.com/lostnumber07/shearline shearline --http --port 8741
112
+ # serves at http://127.0.0.1:8741/mcp
113
+ ```
114
+
115
+ ## Why these tools
116
+
117
+ A forecast API tells you it might rain. None of the questions that matter on a severe weather day — *is this storm rotating, how big is the hail, is the environment loaded for tornadoes, am I inside the polygon* — are answerable from a forecast endpoint. They require the warning's IBW tags, radar-derived products, and a real sounding:
118
+
119
+ - **Warnings with IBW tags, not just warning text.** A base-tier Severe Thunderstorm Warning and one tagged `DESTRUCTIVE` with 80 mph gusts are different planning problems. SHEARLINE parses the machine-readable tags (max hail size, max gust, tornado detection/damage threat) and the storm-motion vector, and does the point-in-polygon test for you.
120
+ - **The environment, computed honestly.** CAPE without shear is a pulse-storm day; shear without CAPE is wind-driven rain. SHEARLINE pulls the current RAP analysis profile and computes the discriminating quantities with MetPy — including the effective inflow layer, effective SRH/shear, SCP, and STP — because high-CAPE/low-shear, low-CAPE/high-shear, and classic supercell parameter spaces produce very different hazards, and the interpretation says which one you're in.
121
+ - **MRMS, because warnings lag storms.** MESH tells you what hail a storm has *already* produced; rotation tracks show where mesocyclones have tracked in the last hour — both on a ~2-minute cadence from the national radar mosaic, often ahead of the next warning update.
122
+ - **LSRs, because radar isn't ground truth.** Spotter reports confirm what's actually reaching the ground.
123
+ - **One brief that reasons across all of it.** The threat level is rule-based with the triggered rules quoted back, so an agent can audit the logic instead of trusting a vibe.
124
+
125
+ ## Data sources (all public, no keys)
126
+
127
+ - Warnings: [api.weather.gov](https://www.weather.gov/documentation/services-web-api) (NWS)
128
+ - Outlooks: [Storm Prediction Center](https://www.spc.noaa.gov/) public GeoJSON
129
+ - Point environment: [NOMADS](https://nomads.ncep.noaa.gov/) RAP grib filter, derived with [MetPy](https://unidata.github.io/MetPy/)
130
+ - MRMS: [NOAA MRMS on AWS Open Data](https://registry.opendata.aws/noaa-mrms-pds/)
131
+ - Storm reports: [Iowa Environmental Mesonet](https://mesonet.agron.iastate.edu/) LSR service
132
+ - NEXRAD Level 2: [Unidata on AWS Open Data](https://registry.opendata.aws/noaa-nexrad/)
133
+
134
+ Coverage is **continental US only** — out-of-bounds coordinates are rejected with a clear error. Upstream fetches are cached (warnings 60 s, MRMS 120 s, LSRs 300 s, outlooks/RAP 30 min) and degrade gracefully: if one source is down, you get partial data plus a `degraded` field, never a bare exception.
135
+
136
+ ## Development
137
+
138
+ ```sh
139
+ git clone https://github.com/lostnumber07/shearline && cd shearline
140
+ uv sync
141
+ uv run pytest # offline test suite against recorded fixtures
142
+ uv run ruff check .
143
+ uv run shearline # stdio
144
+ uv run python scripts/smoke.py # live smoke test, both transports
145
+ ```
146
+
147
+ ## License
148
+
149
+ MIT © Backshear LLC. Weather data is produced by NOAA/NWS and other public services; this project is not affiliated with or endorsed by NOAA.
@@ -0,0 +1,120 @@
1
+ <!-- mcp-name: io.github.lostnumber07/shearline -->
2
+
3
+ # SHEARLINE
4
+
5
+ **The severe-weather analyst your agent doesn't have.** SHEARLINE is a free, MIT-licensed MCP server that gives AI agents analyst-grade US severe-weather tools: live warning polygons with Impact-Based Warning tags, SPC convective outlooks, RAP-derived point environments (CAPE/shear/SRH/STP computed with MetPy), MRMS radar-derived hail and rotation products, ground-truth storm reports, and a composite threat brief that synthesizes all of it. A dozen weather MCPs already wrap the basic forecast API; SHEARLINE deliberately skips everything they do and ships only what requires radar meteorology to expose correctly.
6
+
7
+ > **Informational only. Not a substitute for official NWS warnings.** Every tool repeats this, because it matters: when weather threatens, follow official warnings from weather.gov and local authorities.
8
+
9
+ ## Tools
10
+
11
+ | Tool | What it returns |
12
+ | --- | --- |
13
+ | `get_active_warnings(lat, lon, radius_km=40)` | Active tornado/severe-thunderstorm/flash-flood warning polygons with IBW tags (max hail size, max gust, tornado detection/damage threat), parsed storm motion, expirations, and whether the exact point is inside a polygon. Watches listed separately. |
14
+ | `get_spc_outlook(lat, lon, day=1)` | SPC categorical risk (TSTM→HIGH) at the point plus tornado/hail/wind probabilities and significant-severe flags, days 1–3, with interpretation calibrated to the category. |
15
+ | `get_point_environment(lat, lon)` | Latest RAP 13-km analysis profile computed with MetPy: MLCAPE/MUCAPE/CINs, LCL, 0–1/0–6 km shear, 0–1/0–3 km SRH, Bunkers motion, effective inflow layer, effective SRH/shear, SCP, and significant-tornado parameter — interpreted like an analyst (pulse vs. cool-season high-shear vs. classic supercell parameter space). |
16
+ | `get_mrms_severe(lat, lon, radius_km=40)` | MRMS maxima within radius: 60-min MESH (hail, inches and mm), low-level and mid-level rotation tracks (azimuthal shear), VIL, composite reflectivity — each with valid time and distance/bearing of the max. |
17
+ | `get_storm_reports(lat, lon, radius_km=80, hours=6)` | Normalized Local Storm Reports: type, magnitude with units, time, location, distance/bearing, remarks. |
18
+ | `get_threat_brief(lat, lon)` | The showpiece: runs everything above concurrently and synthesizes a threat level (none/marginal/elevated/significant/extreme) **with stated logic**, hazards ranked, environment summary, nearest storm signature, and a recommended attention window. |
19
+ | `get_radar_snapshot(lat, lon)` | Nearest WSR-88D's latest Level 2 volume metadata: VCP (scan strategy), max reflectivity with range/azimuth, coarse echo-top estimate. |
20
+
21
+ Every tool returns structured JSON with `data` (numeric fields, units stated), `interpretation` (plain-language analyst sentences), `degraded` (which upstream sources failed, if any — partial data instead of errors), and the safety `disclaimer`.
22
+
23
+ ## Example: threat brief during a real outbreak
24
+
25
+ Real output from 2026-06-10, point inside an active tornado warning in northern Missouri:
26
+
27
+ ```json
28
+ {
29
+ "threat_level": "extreme",
30
+ "threat_logic": [
31
+ "Tornado Warning in effect at the point, corroborated by confirmed tornado reports nearby — treat as an immediate life-safety situation.",
32
+ "Severe Thunderstorm Warning at the point tagged 'Considerable' (hail to 1.75\", gusts to 60 mph).",
33
+ "Significant-tornado parameter of 4.0 with storms ongoing — environment strongly supports tornadic supercells.",
34
+ "MRMS MESH of 2.3\" hail within radius in the last hour.",
35
+ "Intense rotation track (azimuthal shear 0.013 /s) nearby in the last hour.",
36
+ "6 tornado report(s) near the point in the report window."
37
+ ],
38
+ "hazards_ranked": [
39
+ {"hazard": "tornado", "level": "extreme"},
40
+ {"hazard": "hail", "level": "extreme"},
41
+ {"hazard": "damaging_wind", "level": "extreme"},
42
+ {"hazard": "flash_flood", "level": "moderate"}
43
+ ],
44
+ "nearest_storm_signature": {
45
+ "signature": "composite reflectivity", "value": "58.5 dBZ",
46
+ "distance_km": 18.0, "direction": "ENE", "valid_utc": "2026-06-10T22:14Z"
47
+ },
48
+ "attention_window": {"window": "now", "until_utc": "2026-06-10T21:00:00-05:00"}
49
+ }
50
+ ```
51
+
52
+ And the same tool for a quiet coastal Maine point reads as confidently quiet — not as an error: `"threat_level": "none"` with the environment numbers shown so the agent can see *why* it's quiet.
53
+
54
+ ## Install
55
+
56
+ Requires Python 3.12+ and [uv](https://docs.astral.sh/uv/). No API keys — every data source is public and anonymous.
57
+
58
+ **Claude Code:**
59
+
60
+ ```sh
61
+ claude mcp add shearline -- uvx --from git+https://github.com/lostnumber07/shearline shearline
62
+ ```
63
+
64
+ **Claude Desktop** (`claude_desktop_config.json`):
65
+
66
+ ```json
67
+ {
68
+ "mcpServers": {
69
+ "shearline": {
70
+ "command": "uvx",
71
+ "args": ["--from", "git+https://github.com/lostnumber07/shearline", "shearline"]
72
+ }
73
+ }
74
+ }
75
+ ```
76
+
77
+ (Once published to PyPI, `uvx shearline` alone works.)
78
+
79
+ **Streamable HTTP** (for remote/agent-platform use):
80
+
81
+ ```sh
82
+ uvx --from git+https://github.com/lostnumber07/shearline shearline --http --port 8741
83
+ # serves at http://127.0.0.1:8741/mcp
84
+ ```
85
+
86
+ ## Why these tools
87
+
88
+ A forecast API tells you it might rain. None of the questions that matter on a severe weather day — *is this storm rotating, how big is the hail, is the environment loaded for tornadoes, am I inside the polygon* — are answerable from a forecast endpoint. They require the warning's IBW tags, radar-derived products, and a real sounding:
89
+
90
+ - **Warnings with IBW tags, not just warning text.** A base-tier Severe Thunderstorm Warning and one tagged `DESTRUCTIVE` with 80 mph gusts are different planning problems. SHEARLINE parses the machine-readable tags (max hail size, max gust, tornado detection/damage threat) and the storm-motion vector, and does the point-in-polygon test for you.
91
+ - **The environment, computed honestly.** CAPE without shear is a pulse-storm day; shear without CAPE is wind-driven rain. SHEARLINE pulls the current RAP analysis profile and computes the discriminating quantities with MetPy — including the effective inflow layer, effective SRH/shear, SCP, and STP — because high-CAPE/low-shear, low-CAPE/high-shear, and classic supercell parameter spaces produce very different hazards, and the interpretation says which one you're in.
92
+ - **MRMS, because warnings lag storms.** MESH tells you what hail a storm has *already* produced; rotation tracks show where mesocyclones have tracked in the last hour — both on a ~2-minute cadence from the national radar mosaic, often ahead of the next warning update.
93
+ - **LSRs, because radar isn't ground truth.** Spotter reports confirm what's actually reaching the ground.
94
+ - **One brief that reasons across all of it.** The threat level is rule-based with the triggered rules quoted back, so an agent can audit the logic instead of trusting a vibe.
95
+
96
+ ## Data sources (all public, no keys)
97
+
98
+ - Warnings: [api.weather.gov](https://www.weather.gov/documentation/services-web-api) (NWS)
99
+ - Outlooks: [Storm Prediction Center](https://www.spc.noaa.gov/) public GeoJSON
100
+ - Point environment: [NOMADS](https://nomads.ncep.noaa.gov/) RAP grib filter, derived with [MetPy](https://unidata.github.io/MetPy/)
101
+ - MRMS: [NOAA MRMS on AWS Open Data](https://registry.opendata.aws/noaa-mrms-pds/)
102
+ - Storm reports: [Iowa Environmental Mesonet](https://mesonet.agron.iastate.edu/) LSR service
103
+ - NEXRAD Level 2: [Unidata on AWS Open Data](https://registry.opendata.aws/noaa-nexrad/)
104
+
105
+ Coverage is **continental US only** — out-of-bounds coordinates are rejected with a clear error. Upstream fetches are cached (warnings 60 s, MRMS 120 s, LSRs 300 s, outlooks/RAP 30 min) and degrade gracefully: if one source is down, you get partial data plus a `degraded` field, never a bare exception.
106
+
107
+ ## Development
108
+
109
+ ```sh
110
+ git clone https://github.com/lostnumber07/shearline && cd shearline
111
+ uv sync
112
+ uv run pytest # offline test suite against recorded fixtures
113
+ uv run ruff check .
114
+ uv run shearline # stdio
115
+ uv run python scripts/smoke.py # live smoke test, both transports
116
+ ```
117
+
118
+ ## License
119
+
120
+ MIT © Backshear LLC. Weather data is produced by NOAA/NWS and other public services; this project is not affiliated with or endorsed by NOAA.
@@ -0,0 +1,60 @@
1
+ [project]
2
+ name = "shearline"
3
+ version = "1.0.0"
4
+ description = "Analyst-grade US severe-weather tools for AI agents over MCP: warning polygons with IBW tags, SPC outlooks, RAP point environments, MRMS hail/rotation, storm reports, and a composite threat brief."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [{ name = "Backshear LLC" }]
9
+ requires-python = ">=3.12"
10
+ keywords = ["mcp", "weather", "severe-weather", "meteorology", "nws", "spc", "mrms", "radar", "ai-agents"]
11
+ classifiers = [
12
+ "Development Status :: 5 - Production/Stable",
13
+ "Intended Audience :: Developers",
14
+ "Intended Audience :: Science/Research",
15
+ "Programming Language :: Python :: 3.12",
16
+ "Topic :: Scientific/Engineering :: Atmospheric Science",
17
+ ]
18
+ dependencies = [
19
+ "boto3>=1.43.27",
20
+ "cfgrib>=0.9.15.1",
21
+ "eccodeslib>=2.47.1.20",
22
+ "httpx>=0.28.1",
23
+ "mcp[cli]>=1.27.2",
24
+ "metpy>=1.7.1",
25
+ "numpy>=2.4.6",
26
+ "pydantic>=2.13.4",
27
+ "shapely>=2.1.2",
28
+ "xarray>=2026.4.0",
29
+ ]
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/lostnumber07/shearline"
33
+ Repository = "https://github.com/lostnumber07/shearline"
34
+ Issues = "https://github.com/lostnumber07/shearline/issues"
35
+
36
+ [project.scripts]
37
+ shearline = "shearline.server:main"
38
+
39
+ [build-system]
40
+ requires = ["uv_build>=0.11.20,<0.12.0"]
41
+ build-backend = "uv_build"
42
+
43
+ [dependency-groups]
44
+ dev = [
45
+ "pytest>=9.0.3",
46
+ "pytest-asyncio>=1.4.0",
47
+ "respx>=0.23.1",
48
+ "ruff>=0.15.16",
49
+ ]
50
+
51
+ [tool.pytest.ini_options]
52
+ asyncio_mode = "auto"
53
+ testpaths = ["tests"]
54
+
55
+ [tool.ruff]
56
+ line-length = 110
57
+ target-version = "py312"
58
+
59
+ [tool.ruff.lint]
60
+ select = ["E", "F", "I", "W", "UP", "B"]
@@ -0,0 +1,3 @@
1
+ """SHEARLINE — analyst-grade US severe-weather tools for AI agents over MCP."""
2
+
3
+ __version__ = "1.0.0"
@@ -0,0 +1,24 @@
1
+ """CONUS bounds enforcement (invariant 2)."""
2
+
3
+ CONUS_LAT_MIN = 24.0
4
+ CONUS_LAT_MAX = 50.0
5
+ CONUS_LON_MIN = -125.5
6
+ CONUS_LON_MAX = -66.5
7
+
8
+
9
+ class OutOfBoundsError(ValueError):
10
+ """Raised when coordinates fall outside the continental United States."""
11
+
12
+
13
+ def check_conus(lat: float, lon: float) -> None:
14
+ if not isinstance(lat, (int, float)) or not isinstance(lon, (int, float)):
15
+ raise OutOfBoundsError("Latitude and longitude must be numbers.")
16
+ if not (CONUS_LAT_MIN <= lat <= CONUS_LAT_MAX) or not (
17
+ CONUS_LON_MIN <= lon <= CONUS_LON_MAX
18
+ ):
19
+ raise OutOfBoundsError(
20
+ f"Point ({lat}, {lon}) is outside the continental United States. "
21
+ f"SHEARLINE covers CONUS only (lat {CONUS_LAT_MIN} to {CONUS_LAT_MAX}, "
22
+ f"lon {CONUS_LON_MIN} to {CONUS_LON_MAX}). Longitude west of the prime "
23
+ "meridian must be negative, e.g. Oklahoma City is lon=-97.5."
24
+ )
@@ -0,0 +1,71 @@
1
+ """In-memory TTL cache for upstream fetches.
2
+
3
+ Every upstream request goes through this cache so repeat tool calls within a
4
+ product's freshness window never re-hit NOAA servers (invariant 5). Expired
5
+ entries are evicted on access and on every insert, so a long-running server
6
+ does not pin stale multi-MB payloads (NEXRAD volumes, RAP subsets, rotating
7
+ MRMS sample keys) indefinitely.
8
+ """
9
+
10
+ import asyncio
11
+ import time
12
+ from collections.abc import Awaitable, Callable
13
+ from typing import Any
14
+
15
+ # TTLs in seconds (invariant 5)
16
+ TTL_ALERTS = 60
17
+ TTL_MRMS = 120
18
+ TTL_LSR = 300
19
+ TTL_OUTLOOK = 1800
20
+ TTL_RAP = 1800
21
+
22
+
23
+ class TTLCache:
24
+ """Async-safe cache with per-key locks so concurrent callers of the same
25
+ key trigger exactly one upstream fetch (no thundering herd)."""
26
+
27
+ def __init__(self) -> None:
28
+ self._entries: dict[str, tuple[float, Any]] = {}
29
+ self._locks: dict[str, asyncio.Lock] = {}
30
+
31
+ def _lock_for(self, key: str) -> asyncio.Lock:
32
+ lock = self._locks.get(key)
33
+ if lock is None:
34
+ lock = self._locks.setdefault(key, asyncio.Lock())
35
+ return lock
36
+
37
+ def _sweep(self) -> None:
38
+ now = time.monotonic()
39
+ for key in [k for k, (exp, _) in self._entries.items() if exp <= now]:
40
+ self._entries.pop(key, None)
41
+ lock = self._locks.get(key)
42
+ if lock is not None and not lock.locked():
43
+ self._locks.pop(key, None)
44
+
45
+ async def get_or_fetch(
46
+ self, key: str, ttl: float, fetch: Callable[[], Awaitable[Any]]
47
+ ) -> Any:
48
+ hit = self._entries.get(key)
49
+ if hit is not None:
50
+ if hit[0] > time.monotonic():
51
+ return hit[1]
52
+ self._entries.pop(key, None)
53
+ async with self._lock_for(key):
54
+ hit = self._entries.get(key)
55
+ if hit is not None and hit[0] > time.monotonic():
56
+ return hit[1]
57
+ value = await fetch()
58
+ self._entries[key] = (time.monotonic() + ttl, value)
59
+ self._sweep()
60
+ return value
61
+
62
+ def put(self, key: str, ttl: float, value: Any) -> None:
63
+ self._entries[key] = (time.monotonic() + ttl, value)
64
+ self._sweep()
65
+
66
+ def clear(self) -> None:
67
+ self._entries.clear()
68
+ self._locks.clear()
69
+
70
+
71
+ CACHE = TTLCache()