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.
Files changed (27) hide show
  1. seattle_open_data-0.1.0/LICENSE +21 -0
  2. seattle_open_data-0.1.0/PKG-INFO +82 -0
  3. seattle_open_data-0.1.0/README.md +54 -0
  4. seattle_open_data-0.1.0/pyproject.toml +142 -0
  5. seattle_open_data-0.1.0/pyproject.toml.orig +91 -0
  6. seattle_open_data-0.1.0/src/seattle_open_data/__init__.py +5 -0
  7. seattle_open_data-0.1.0/src/seattle_open_data/constants.py +129 -0
  8. seattle_open_data-0.1.0/src/seattle_open_data/core/__init__.py +0 -0
  9. seattle_open_data-0.1.0/src/seattle_open_data/core/arguments.py +96 -0
  10. seattle_open_data-0.1.0/src/seattle_open_data/core/catalog.py +217 -0
  11. seattle_open_data-0.1.0/src/seattle_open_data/core/context.py +33 -0
  12. seattle_open_data-0.1.0/src/seattle_open_data/core/cursor.py +91 -0
  13. seattle_open_data-0.1.0/src/seattle_open_data/core/describe.py +510 -0
  14. seattle_open_data-0.1.0/src/seattle_open_data/core/errors.py +320 -0
  15. seattle_open_data-0.1.0/src/seattle_open_data/core/guides.py +353 -0
  16. seattle_open_data-0.1.0/src/seattle_open_data/core/http.py +309 -0
  17. seattle_open_data-0.1.0/src/seattle_open_data/core/models.py +212 -0
  18. seattle_open_data-0.1.0/src/seattle_open_data/core/query.py +501 -0
  19. seattle_open_data-0.1.0/src/seattle_open_data/core/reference.py +543 -0
  20. seattle_open_data-0.1.0/src/seattle_open_data/core/reference_service.py +86 -0
  21. seattle_open_data-0.1.0/src/seattle_open_data/core/text.py +42 -0
  22. seattle_open_data-0.1.0/src/seattle_open_data/mcp/__init__.py +0 -0
  23. seattle_open_data-0.1.0/src/seattle_open_data/mcp/resources.py +108 -0
  24. seattle_open_data-0.1.0/src/seattle_open_data/mcp/result.py +34 -0
  25. seattle_open_data-0.1.0/src/seattle_open_data/mcp/schema.py +42 -0
  26. seattle_open_data-0.1.0/src/seattle_open_data/mcp/server.py +161 -0
  27. 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
+ [![CI](https://github.com/briandconnelly/seattle-open-data/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/briandconnelly/seattle-open-data/actions/workflows/ci.yml)
32
+ [![Python](https://img.shields.io/python/required-version-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2Fbriandconnelly%2Fseattle-open-data%2Fmain%2Fpyproject.toml)](pyproject.toml)
33
+ [![License: MIT](https://img.shields.io/github/license/briandconnelly/seattle-open-data)](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
+ [![CI](https://github.com/briandconnelly/seattle-open-data/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/briandconnelly/seattle-open-data/actions/workflows/ci.yml)
4
+ [![Python](https://img.shields.io/python/required-version-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2Fbriandconnelly%2Fseattle-open-data%2Fmain%2Fpyproject.toml)](pyproject.toml)
5
+ [![License: MIT](https://img.shields.io/github/license/briandconnelly/seattle-open-data)](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,5 @@
1
+ """Agent-friendly MCP server for Seattle Open Data."""
2
+
3
+ from importlib.metadata import version
4
+
5
+ __version__ = version("seattle-open-data")
@@ -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
@@ -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