seattle-open-data 0.1.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.
- seattle_open_data-0.1.0/LICENSE +21 -0
- seattle_open_data-0.1.0/PKG-INFO +82 -0
- seattle_open_data-0.1.0/README.md +54 -0
- seattle_open_data-0.1.0/pyproject.toml +142 -0
- seattle_open_data-0.1.0/pyproject.toml.orig +91 -0
- seattle_open_data-0.1.0/src/seattle_open_data/__init__.py +5 -0
- seattle_open_data-0.1.0/src/seattle_open_data/constants.py +129 -0
- seattle_open_data-0.1.0/src/seattle_open_data/core/__init__.py +0 -0
- seattle_open_data-0.1.0/src/seattle_open_data/core/arguments.py +96 -0
- seattle_open_data-0.1.0/src/seattle_open_data/core/catalog.py +217 -0
- seattle_open_data-0.1.0/src/seattle_open_data/core/context.py +33 -0
- seattle_open_data-0.1.0/src/seattle_open_data/core/cursor.py +91 -0
- seattle_open_data-0.1.0/src/seattle_open_data/core/describe.py +510 -0
- seattle_open_data-0.1.0/src/seattle_open_data/core/errors.py +320 -0
- seattle_open_data-0.1.0/src/seattle_open_data/core/guides.py +353 -0
- seattle_open_data-0.1.0/src/seattle_open_data/core/http.py +309 -0
- seattle_open_data-0.1.0/src/seattle_open_data/core/models.py +212 -0
- seattle_open_data-0.1.0/src/seattle_open_data/core/query.py +501 -0
- seattle_open_data-0.1.0/src/seattle_open_data/core/reference.py +543 -0
- seattle_open_data-0.1.0/src/seattle_open_data/core/reference_service.py +86 -0
- seattle_open_data-0.1.0/src/seattle_open_data/core/text.py +42 -0
- seattle_open_data-0.1.0/src/seattle_open_data/mcp/__init__.py +0 -0
- seattle_open_data-0.1.0/src/seattle_open_data/mcp/resources.py +108 -0
- seattle_open_data-0.1.0/src/seattle_open_data/mcp/result.py +34 -0
- seattle_open_data-0.1.0/src/seattle_open_data/mcp/schema.py +42 -0
- seattle_open_data-0.1.0/src/seattle_open_data/mcp/server.py +161 -0
- seattle_open_data-0.1.0/src/seattle_open_data/mcp/tools.py +318 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Brian Connelly
|
|
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,82 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: seattle-open-data
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Agent-friendly MCP server for Seattle Open Data (data.seattle.gov, Socrata SODA).
|
|
5
|
+
Keywords: seattle,open-data,socrata,soda,mcp,agent
|
|
6
|
+
Author: Brian Connelly
|
|
7
|
+
Author-email: Brian Connelly <bdc@bconnelly.net>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
17
|
+
Requires-Dist: fastmcp>=4.1.0,<4.2
|
|
18
|
+
Requires-Dist: httpx>=0.28
|
|
19
|
+
Requires-Dist: ijson>=3.3
|
|
20
|
+
Requires-Dist: pydantic>=2
|
|
21
|
+
Requires-Dist: tzdata>=2025.2
|
|
22
|
+
Requires-Python: >=3.13
|
|
23
|
+
Project-URL: Homepage, https://github.com/briandconnelly/seattle-open-data
|
|
24
|
+
Project-URL: Repository, https://github.com/briandconnelly/seattle-open-data
|
|
25
|
+
Project-URL: Issues, https://github.com/briandconnelly/seattle-open-data/issues
|
|
26
|
+
Project-URL: Changelog, https://github.com/briandconnelly/seattle-open-data/blob/main/CHANGELOG.md
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# seattle-open-data
|
|
30
|
+
|
|
31
|
+
[](https://github.com/briandconnelly/seattle-open-data/actions/workflows/ci.yml)
|
|
32
|
+
[](pyproject.toml)
|
|
33
|
+
[](LICENSE)
|
|
34
|
+
|
|
35
|
+
An agent-friendly [MCP](https://modelcontextprotocol.io/) server for [Seattle Open Data](https://data.seattle.gov/) (Socrata SODA2).
|
|
36
|
+
|
|
37
|
+
**Status: alpha (0.1.0).** All four tools work against the live portal, with curated guides for seven flagship datasets. See [evals/results](evals/results) for the agent evaluation.
|
|
38
|
+
|
|
39
|
+
## Tools
|
|
40
|
+
|
|
41
|
+
| Tool | Purpose |
|
|
42
|
+
| --- | --- |
|
|
43
|
+
| `seattle_data_search_datasets` | Find datasets by topic |
|
|
44
|
+
| `seattle_data_describe_dataset` | Columns, row count, time coverage, place fields, geo columns |
|
|
45
|
+
| `seattle_data_query_dataset` | Bounded reads and aggregates (SoQL clauses or raw SoQL) |
|
|
46
|
+
| `seattle_data_get_reference` | Capability summary, SoQL help, guides, error catalog |
|
|
47
|
+
|
|
48
|
+
## Run
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
uv run seattle-open-data # stdio
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Claude Code:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
claude mcp add seattle-open-data -- uv run --directory /path/to/seattle-open-data seattle-open-data
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Optional: set `SOCRATA_APP_TOKEN` to use a Socrata application token. A rejected token is reported as `app_token_rejected`, never silently dropped.
|
|
61
|
+
|
|
62
|
+
To build the installable MCPB bundle (needs the `mcpb` CLI): `make mcpb`.
|
|
63
|
+
|
|
64
|
+
## Develop
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
uv sync
|
|
68
|
+
uv run pytest
|
|
69
|
+
uv run ruff check && uv run ruff format --check && uv run ty check
|
|
70
|
+
UPDATE_SNAPSHOTS=1 uv run pytest tests/test_contract.py # after an intended tool-surface change
|
|
71
|
+
uv run pytest --run-integration -m integration # live tests against data.seattle.gov
|
|
72
|
+
uv run python evals/run.py [--host claude|codex] <out_dir> [task_id ...]
|
|
73
|
+
uv run python evals/report.py <out_dir> --answers
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Release
|
|
77
|
+
|
|
78
|
+
Tag `vX.Y.Z` after bumping the version: the release workflow builds and smoke-tests the wheel, sdist, and `.mcpb`, publishes to PyPI after maintainer approval, and attaches all three to a GitHub Release. See [docs/releasing.md](docs/releasing.md) for the steps, the TestPyPI rehearsal, and one-time setup.
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# seattle-open-data
|
|
2
|
+
|
|
3
|
+
[](https://github.com/briandconnelly/seattle-open-data/actions/workflows/ci.yml)
|
|
4
|
+
[](pyproject.toml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
An agent-friendly [MCP](https://modelcontextprotocol.io/) server for [Seattle Open Data](https://data.seattle.gov/) (Socrata SODA2).
|
|
8
|
+
|
|
9
|
+
**Status: alpha (0.1.0).** All four tools work against the live portal, with curated guides for seven flagship datasets. See [evals/results](evals/results) for the agent evaluation.
|
|
10
|
+
|
|
11
|
+
## Tools
|
|
12
|
+
|
|
13
|
+
| Tool | Purpose |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| `seattle_data_search_datasets` | Find datasets by topic |
|
|
16
|
+
| `seattle_data_describe_dataset` | Columns, row count, time coverage, place fields, geo columns |
|
|
17
|
+
| `seattle_data_query_dataset` | Bounded reads and aggregates (SoQL clauses or raw SoQL) |
|
|
18
|
+
| `seattle_data_get_reference` | Capability summary, SoQL help, guides, error catalog |
|
|
19
|
+
|
|
20
|
+
## Run
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
uv run seattle-open-data # stdio
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Claude Code:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
claude mcp add seattle-open-data -- uv run --directory /path/to/seattle-open-data seattle-open-data
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Optional: set `SOCRATA_APP_TOKEN` to use a Socrata application token. A rejected token is reported as `app_token_rejected`, never silently dropped.
|
|
33
|
+
|
|
34
|
+
To build the installable MCPB bundle (needs the `mcpb` CLI): `make mcpb`.
|
|
35
|
+
|
|
36
|
+
## Develop
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
uv sync
|
|
40
|
+
uv run pytest
|
|
41
|
+
uv run ruff check && uv run ruff format --check && uv run ty check
|
|
42
|
+
UPDATE_SNAPSHOTS=1 uv run pytest tests/test_contract.py # after an intended tool-surface change
|
|
43
|
+
uv run pytest --run-integration -m integration # live tests against data.seattle.gov
|
|
44
|
+
uv run python evals/run.py [--host claude|codex] <out_dir> [task_id ...]
|
|
45
|
+
uv run python evals/report.py <out_dir> --answers
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Release
|
|
49
|
+
|
|
50
|
+
Tag `vX.Y.Z` after bumping the version: the release workflow builds and smoke-tests the wheel, sdist, and `.mcpb`, publishes to PyPI after maintainer approval, and attaches all three to a GitHub Release. See [docs/releasing.md](docs/releasing.md) for the steps, the TestPyPI rehearsal, and one-time setup.
|
|
51
|
+
|
|
52
|
+
## License
|
|
53
|
+
|
|
54
|
+
MIT
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "seattle-open-data"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Agent-friendly MCP server for Seattle Open Data (data.seattle.gov, Socrata SODA)."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
keywords = [
|
|
10
|
+
"seattle",
|
|
11
|
+
"open-data",
|
|
12
|
+
"socrata",
|
|
13
|
+
"soda",
|
|
14
|
+
"mcp",
|
|
15
|
+
"agent",
|
|
16
|
+
]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Programming Language :: Python :: 3.14",
|
|
24
|
+
"Programming Language :: Python :: 3.15",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"fastmcp>=4.1.0,<4.2",
|
|
28
|
+
"httpx>=0.28",
|
|
29
|
+
"ijson>=3.3",
|
|
30
|
+
"pydantic>=2",
|
|
31
|
+
"tzdata>=2025.2",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[[project.authors]]
|
|
35
|
+
name = "Brian Connelly"
|
|
36
|
+
email = "bdc@bconnelly.net"
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/briandconnelly/seattle-open-data"
|
|
40
|
+
Repository = "https://github.com/briandconnelly/seattle-open-data"
|
|
41
|
+
Issues = "https://github.com/briandconnelly/seattle-open-data/issues"
|
|
42
|
+
Changelog = "https://github.com/briandconnelly/seattle-open-data/blob/main/CHANGELOG.md"
|
|
43
|
+
|
|
44
|
+
[project.scripts]
|
|
45
|
+
seattle-open-data = "seattle_open_data.mcp.server:main"
|
|
46
|
+
|
|
47
|
+
[dependency-groups]
|
|
48
|
+
dev = [
|
|
49
|
+
"jsonschema>=4.26.0",
|
|
50
|
+
"prek>=0.2",
|
|
51
|
+
"pytest>=8",
|
|
52
|
+
"pytest-asyncio>=0.24",
|
|
53
|
+
"pytest-cov>=5",
|
|
54
|
+
"ruff>=0.15",
|
|
55
|
+
"ty>=0.0.1a1",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
[build-system]
|
|
59
|
+
requires = ["uv_build>=0.13.0,<0.14.0"]
|
|
60
|
+
build-backend = "uv_build"
|
|
61
|
+
|
|
62
|
+
[tool.ruff]
|
|
63
|
+
line-length = 100
|
|
64
|
+
target-version = "py313"
|
|
65
|
+
src = [
|
|
66
|
+
"src",
|
|
67
|
+
"tests",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
[tool.ruff.lint]
|
|
71
|
+
select = [
|
|
72
|
+
"E",
|
|
73
|
+
"F",
|
|
74
|
+
"W",
|
|
75
|
+
"I",
|
|
76
|
+
"B",
|
|
77
|
+
"UP",
|
|
78
|
+
"SIM",
|
|
79
|
+
"PIE",
|
|
80
|
+
"RUF",
|
|
81
|
+
"PL",
|
|
82
|
+
"PT",
|
|
83
|
+
"TID",
|
|
84
|
+
"TCH",
|
|
85
|
+
"ARG",
|
|
86
|
+
"PTH",
|
|
87
|
+
]
|
|
88
|
+
ignore = [
|
|
89
|
+
"PLR0913",
|
|
90
|
+
"PLR0917",
|
|
91
|
+
"PLR2004",
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
[tool.ruff.lint.per-file-ignores]
|
|
95
|
+
"src/seattle_open_data/core/reference.py" = ["E501"]
|
|
96
|
+
"src/seattle_open_data/core/guides.py" = ["E501"]
|
|
97
|
+
"tests/**" = [
|
|
98
|
+
"ARG",
|
|
99
|
+
"PLR2004",
|
|
100
|
+
"S101",
|
|
101
|
+
"TC002",
|
|
102
|
+
"TC003",
|
|
103
|
+
"PT018",
|
|
104
|
+
"PLC0415",
|
|
105
|
+
"RUF002",
|
|
106
|
+
"RUF003",
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
[tool.ruff.format]
|
|
110
|
+
quote-style = "double"
|
|
111
|
+
|
|
112
|
+
[tool.ty.src]
|
|
113
|
+
include = [
|
|
114
|
+
"src",
|
|
115
|
+
"tests",
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
[tool.pytest.ini_options]
|
|
119
|
+
minversion = "8.0"
|
|
120
|
+
testpaths = ["tests"]
|
|
121
|
+
addopts = [
|
|
122
|
+
"-ra",
|
|
123
|
+
"--strict-markers",
|
|
124
|
+
"--strict-config",
|
|
125
|
+
]
|
|
126
|
+
asyncio_mode = "auto"
|
|
127
|
+
markers = [
|
|
128
|
+
"integration: live data.seattle.gov tests (skipped by default; run with --run-integration)",
|
|
129
|
+
"soda_fixture(name, overrides=None): record or replay tests/fixtures/http/<name>.json",
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
[tool.coverage.run]
|
|
133
|
+
source = ["src/seattle_open_data"]
|
|
134
|
+
branch = true
|
|
135
|
+
|
|
136
|
+
[tool.coverage.report]
|
|
137
|
+
exclude_lines = [
|
|
138
|
+
"pragma: no cover",
|
|
139
|
+
"if TYPE_CHECKING:",
|
|
140
|
+
"if __name__ == .__main__.:",
|
|
141
|
+
]
|
|
142
|
+
show_missing = true
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "seattle-open-data"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Agent-friendly MCP server for Seattle Open Data (data.seattle.gov, Socrata SODA)."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [
|
|
10
|
+
{ name = "Brian Connelly", email = "bdc@bconnelly.net" },
|
|
11
|
+
]
|
|
12
|
+
keywords = ["seattle", "open-data", "socrata", "soda", "mcp", "agent"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Programming Language :: Python :: 3.14",
|
|
20
|
+
"Programming Language :: Python :: 3.15",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"fastmcp>=4.1.0,<4.2",
|
|
24
|
+
"httpx>=0.28",
|
|
25
|
+
"ijson>=3.3",
|
|
26
|
+
"pydantic>=2",
|
|
27
|
+
"tzdata>=2025.2",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/briandconnelly/seattle-open-data"
|
|
32
|
+
Repository = "https://github.com/briandconnelly/seattle-open-data"
|
|
33
|
+
Issues = "https://github.com/briandconnelly/seattle-open-data/issues"
|
|
34
|
+
Changelog = "https://github.com/briandconnelly/seattle-open-data/blob/main/CHANGELOG.md"
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
seattle-open-data = "seattle_open_data.mcp.server:main"
|
|
38
|
+
|
|
39
|
+
[dependency-groups]
|
|
40
|
+
dev = [
|
|
41
|
+
"jsonschema>=4.26.0",
|
|
42
|
+
"prek>=0.2",
|
|
43
|
+
"pytest>=8",
|
|
44
|
+
"pytest-asyncio>=0.24",
|
|
45
|
+
"pytest-cov>=5",
|
|
46
|
+
"ruff>=0.15",
|
|
47
|
+
"ty>=0.0.1a1",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[build-system]
|
|
51
|
+
requires = ["uv_build>=0.13.0,<0.14.0"]
|
|
52
|
+
build-backend = "uv_build"
|
|
53
|
+
|
|
54
|
+
[tool.ruff]
|
|
55
|
+
line-length = 100
|
|
56
|
+
target-version = "py313"
|
|
57
|
+
src = ["src", "tests"]
|
|
58
|
+
|
|
59
|
+
[tool.ruff.lint]
|
|
60
|
+
select = ["E", "F", "W", "I", "B", "UP", "SIM", "PIE", "RUF", "PL", "PT", "TID", "TCH", "ARG", "PTH"]
|
|
61
|
+
ignore = ["PLR0913", "PLR0917", "PLR2004"]
|
|
62
|
+
|
|
63
|
+
[tool.ruff.lint.per-file-ignores]
|
|
64
|
+
# Reference prose and worked JSON examples read better unwrapped.
|
|
65
|
+
"src/seattle_open_data/core/reference.py" = ["E501"]
|
|
66
|
+
"src/seattle_open_data/core/guides.py" = ["E501"]
|
|
67
|
+
"tests/**" = ["ARG", "PLR2004", "S101", "TC002", "TC003", "PT018", "PLC0415", "RUF002", "RUF003"]
|
|
68
|
+
|
|
69
|
+
[tool.ruff.format]
|
|
70
|
+
quote-style = "double"
|
|
71
|
+
|
|
72
|
+
[tool.ty.src]
|
|
73
|
+
include = ["src", "tests"]
|
|
74
|
+
|
|
75
|
+
[tool.pytest.ini_options]
|
|
76
|
+
minversion = "8.0"
|
|
77
|
+
testpaths = ["tests"]
|
|
78
|
+
addopts = ["-ra", "--strict-markers", "--strict-config"]
|
|
79
|
+
asyncio_mode = "auto"
|
|
80
|
+
markers = [
|
|
81
|
+
"integration: live data.seattle.gov tests (skipped by default; run with --run-integration)",
|
|
82
|
+
"soda_fixture(name, overrides=None): record or replay tests/fixtures/http/<name>.json",
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
[tool.coverage.run]
|
|
86
|
+
source = ["src/seattle_open_data"]
|
|
87
|
+
branch = true
|
|
88
|
+
|
|
89
|
+
[tool.coverage.report]
|
|
90
|
+
exclude_lines = ["pragma: no cover", "if TYPE_CHECKING:", "if __name__ == .__main__.:"]
|
|
91
|
+
show_missing = true
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Seattle portal facts (docs/design.md: Scope).
|
|
2
|
+
|
|
3
|
+
Extract a `Portal` type only when a second portal arrives.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from zoneinfo import ZoneInfo
|
|
9
|
+
|
|
10
|
+
SERVER_NAME = "seattle-open-data"
|
|
11
|
+
TOOL_PREFIX = "seattle_data_"
|
|
12
|
+
URI_SCHEME = "seattle-data"
|
|
13
|
+
|
|
14
|
+
PUBLIC_HOST = "data.seattle.gov"
|
|
15
|
+
#: The Discovery API indexes Seattle under this name; `domains=data.seattle.gov` returns nothing.
|
|
16
|
+
CATALOG_DOMAIN = "cos-data.seattle.gov"
|
|
17
|
+
DISCOVERY_API = "https://api.us.socrata.com/api/catalog/v1"
|
|
18
|
+
|
|
19
|
+
SEATTLE_TZ = ZoneInfo("America/Los_Angeles")
|
|
20
|
+
|
|
21
|
+
DATASET_ID_PATTERN = r"^[a-z0-9]{4}-[a-z0-9]{4}$"
|
|
22
|
+
|
|
23
|
+
SEARCH_PAGE_SIZE_MAX = 50
|
|
24
|
+
QUERY_PAGE_SIZE_MAX = 500
|
|
25
|
+
QUERY_LOGICAL_CAP = 10_000
|
|
26
|
+
QUERY_TEXT_MAX_BYTES = 4096
|
|
27
|
+
SAMPLE_ROWS_MAX = 5
|
|
28
|
+
|
|
29
|
+
RESULT_BYTE_BUDGET = 24 * 1024
|
|
30
|
+
TOOLS_LIST_BYTE_BUDGET = 24 * 1024
|
|
31
|
+
|
|
32
|
+
CACHE_TTL_SECONDS = 3600
|
|
33
|
+
|
|
34
|
+
INSTRUCTIONS_SENTINEL = "END SEATTLE OPEN DATA INSTRUCTIONS"
|
|
35
|
+
|
|
36
|
+
UPSTREAM_TIMEOUT_SECONDS = 30.0
|
|
37
|
+
DESCRIBE_AGGREGATE_TIMEOUT_SECONDS = 10.0
|
|
38
|
+
UPSTREAM_MAX_BYTES = 8 * 1024 * 1024
|
|
39
|
+
CURSOR_TTL_SECONDS = 3600
|
|
40
|
+
|
|
41
|
+
#: Values publishers use instead of null in text place fields (docs/upstream-facts.md).
|
|
42
|
+
PLACEHOLDER_VALUES = frozenset({"-", "UNKNOWN", "NULL", "OOJ", "FK ERROR", "X", "0", ""})
|
|
43
|
+
|
|
44
|
+
#: Computed-region layers seen on Seattle datasets; label columns and dates verified 2026-10-15.
|
|
45
|
+
REGION_LAYERS: dict[str, dict[str, str]] = {
|
|
46
|
+
"ru88-fbhk": {
|
|
47
|
+
"kind": "neighborhood",
|
|
48
|
+
"label_column": "name",
|
|
49
|
+
"vintage": "SPD Micro Community Policing Plan Areas, updated 2016-03-03",
|
|
50
|
+
"reliability": "ok",
|
|
51
|
+
},
|
|
52
|
+
"kuhn-3gp2": {
|
|
53
|
+
"kind": "beat",
|
|
54
|
+
"label_column": "name",
|
|
55
|
+
"vintage": "SPD Beats, updated 2015-07-17 (51 beats; current SPD beats differ)",
|
|
56
|
+
"reliability": "partial",
|
|
57
|
+
"reason": "2015 beat boundaries; current SPD beats differ.",
|
|
58
|
+
},
|
|
59
|
+
"q256-3sug": {
|
|
60
|
+
"kind": "zip",
|
|
61
|
+
"label_column": "zcta5ce10",
|
|
62
|
+
"vintage": "2010 ZIP Code Tabulation Areas (national), updated 2015-02-19",
|
|
63
|
+
"reliability": "ok",
|
|
64
|
+
},
|
|
65
|
+
"cyqu-gs94": {
|
|
66
|
+
"kind": "council_district",
|
|
67
|
+
"label_column": "c_district",
|
|
68
|
+
"vintage": "City Council Districts, updated 2015-07-20 (predates 2022 redistricting)",
|
|
69
|
+
"reliability": "unreliable",
|
|
70
|
+
"reason": "Mostly null, only districts 1-3 appear, and the boundaries predate 2022.",
|
|
71
|
+
},
|
|
72
|
+
"2day-rhn5": {
|
|
73
|
+
"kind": "census_tract",
|
|
74
|
+
"label_column": "name10",
|
|
75
|
+
"vintage": "2010 Census Tracts, updated 2015-05-16",
|
|
76
|
+
"reliability": "unreliable",
|
|
77
|
+
"reason": "Mostly null on Seattle datasets; 2010 tracts.",
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#: Text place fields per dataset, with reliability from the 2026-10-15 probe
|
|
82
|
+
#: (docs/upstream-facts.md).
|
|
83
|
+
PLACE_FIELDS: dict[str, dict[str, dict[str, str]]] = {
|
|
84
|
+
"33kz-ixgy": {
|
|
85
|
+
"dispatch_neighborhood": {"kind": "neighborhood", "reliability": "ok"},
|
|
86
|
+
"dispatch_precinct": {"kind": "precinct", "reliability": "ok"},
|
|
87
|
+
"dispatch_sector": {"kind": "sector", "reliability": "ok"},
|
|
88
|
+
"dispatch_beat": {"kind": "beat", "reliability": "ok"},
|
|
89
|
+
},
|
|
90
|
+
"tazs-3rd5": {
|
|
91
|
+
"neighborhood": {
|
|
92
|
+
"kind": "neighborhood",
|
|
93
|
+
"reliability": "partial",
|
|
94
|
+
"reason": "Placeholder ('-') for nearly all offenses before 2017; 1-4% missing since. "
|
|
95
|
+
"Do not use for 2008-2016.",
|
|
96
|
+
},
|
|
97
|
+
"precinct": {"kind": "precinct", "reliability": "ok"},
|
|
98
|
+
"sector": {"kind": "sector", "reliability": "ok"},
|
|
99
|
+
"beat": {"kind": "beat", "reliability": "ok"},
|
|
100
|
+
},
|
|
101
|
+
"ppi5-g2bj": {
|
|
102
|
+
"precinct": {"kind": "precinct", "reliability": "ok"},
|
|
103
|
+
"sector": {"kind": "sector", "reliability": "ok"},
|
|
104
|
+
"beat": {"kind": "beat", "reliability": "ok"},
|
|
105
|
+
},
|
|
106
|
+
"76t5-zqzr": {
|
|
107
|
+
"originalzip": {"kind": "zip", "reliability": "ok"},
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#: Timezone evidence per dataset column; absent means unknown, never assumed.
|
|
112
|
+
TIME_EVIDENCE: dict[str, dict[str, dict[str, str]]] = {
|
|
113
|
+
"kzjm-xkqj": {
|
|
114
|
+
"datetime": {
|
|
115
|
+
"timezone": "America/Los_Angeles",
|
|
116
|
+
"evidence": "max(datetime) tracked Pacific wall-clock time when probed 2026-10-15.",
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
GEO_TYPES = frozenset({"point", "location"})
|
|
122
|
+
TIME_TYPES = frozenset({"floating_timestamp", "calendar_date", "fixed_timestamp", "date"})
|
|
123
|
+
|
|
124
|
+
#: Upper bound written into download_url; SODA2 accepted it when probed 2026-10-15.
|
|
125
|
+
DOWNLOAD_LIMIT = 1_000_000_000
|
|
126
|
+
URL_MAX_CHARS = 2048
|
|
127
|
+
#: Whole-call deadlines, metadata fetch included; tool descriptions state these.
|
|
128
|
+
QUERY_DEADLINE_SECONDS = 30.0
|
|
129
|
+
DESCRIBE_DEADLINE_SECONDS = 30.0
|
|
File without changes
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Cross-argument rules the JSON Schema cannot express portably (docs/design.md: Primitives).
|
|
2
|
+
|
|
3
|
+
Single-field constraints (types, enums, ranges, patterns, empty strings) live in
|
|
4
|
+
the published inputSchema; only combinations are checked here.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from seattle_open_data.constants import QUERY_TEXT_MAX_BYTES
|
|
12
|
+
from seattle_open_data.core import cursor as cursors
|
|
13
|
+
from seattle_open_data.core.errors import ErrorCode, ErrorDetails, Repair, SeattleDataError
|
|
14
|
+
|
|
15
|
+
CLAUSE_ARGS = ("select", "where", "group_by", "having", "order_by", "search_text", "near")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _provided(args: dict[str, Any]) -> list[str]:
|
|
19
|
+
return [name for name, value in args.items() if value is not None]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _reject(fields: list[str], reason: str, *, tool: str, repair: Repair | None = None) -> None:
|
|
23
|
+
details = (
|
|
24
|
+
ErrorDetails(field=fields[0], reason=reason)
|
|
25
|
+
if len(fields) == 1
|
|
26
|
+
else ErrorDetails(fields=fields, reason=reason)
|
|
27
|
+
)
|
|
28
|
+
raise SeattleDataError(
|
|
29
|
+
ErrorCode.INVALID_ARGUMENT,
|
|
30
|
+
f"Invalid argument combination for {tool}.",
|
|
31
|
+
details=details,
|
|
32
|
+
repair=repair,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def check_continuation(args: dict[str, Any], *, allowed: set[str], tool: str) -> None:
|
|
37
|
+
if args.get("cursor") is None:
|
|
38
|
+
return
|
|
39
|
+
extra = sorted(set(_provided(args)) - allowed - {"cursor"})
|
|
40
|
+
if extra:
|
|
41
|
+
_reject(
|
|
42
|
+
extra,
|
|
43
|
+
"A cursor continues the original query; send only "
|
|
44
|
+
+ ", ".join(sorted(allowed | {"cursor"}))
|
|
45
|
+
+ ".",
|
|
46
|
+
tool=tool,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def check_search(args: dict[str, Any]) -> None:
|
|
51
|
+
check_continuation(args, allowed={"detail", "page_size"}, tool="seattle_data_search_datasets")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def check_query(args: dict[str, Any]) -> None:
|
|
55
|
+
tool = "seattle_data_query_dataset"
|
|
56
|
+
check_continuation(args, allowed={"dataset_id", "detail", "page_size"}, tool=tool)
|
|
57
|
+
if args.get("cursor") is not None:
|
|
58
|
+
return
|
|
59
|
+
mode = args.get("mode") or "clauses"
|
|
60
|
+
if mode == "soql":
|
|
61
|
+
if args.get("soql") is None:
|
|
62
|
+
_reject(["soql"], "mode='soql' requires soql.", tool=tool)
|
|
63
|
+
clauses = [name for name in CLAUSE_ARGS if args.get(name) is not None]
|
|
64
|
+
if clauses:
|
|
65
|
+
_reject(
|
|
66
|
+
clauses,
|
|
67
|
+
"mode='soql' runs soql verbatim; clause arguments are not allowed.",
|
|
68
|
+
tool=tool,
|
|
69
|
+
)
|
|
70
|
+
return
|
|
71
|
+
if args.get("soql") is not None:
|
|
72
|
+
_reject(["soql"], "soql requires mode='soql'.", tool=tool)
|
|
73
|
+
# Measured as a cursor stores the clauses, so the cap also bounds the cursor (≤ 6 KiB).
|
|
74
|
+
present = {n: _plain(args[n]) for n in CLAUSE_ARGS if args.get(n) is not None}
|
|
75
|
+
if len(cursors.dump(present)) > QUERY_TEXT_MAX_BYTES:
|
|
76
|
+
_reject(
|
|
77
|
+
list(present),
|
|
78
|
+
f"Clause arguments exceed {QUERY_TEXT_MAX_BYTES} bytes in total, measured as JSON.",
|
|
79
|
+
tool=tool,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def check_reference(args: dict[str, Any]) -> None:
|
|
84
|
+
tool = "seattle_data_get_reference"
|
|
85
|
+
topic = args.get("topic") or "index"
|
|
86
|
+
if args.get("item_id") is not None and topic in ("index", "capabilities"):
|
|
87
|
+
_reject(
|
|
88
|
+
["topic", "item_id"],
|
|
89
|
+
f"Topic '{topic}' has no items; item_id needs soql, errors, or guides.",
|
|
90
|
+
tool=tool,
|
|
91
|
+
repair=Repair(next_step="list_topics", tool=tool, arguments={}),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _plain(value: Any) -> Any:
|
|
96
|
+
return value.model_dump() if hasattr(value, "model_dump") else value
|