forked from ChelseaKR/gtfs-scorecard
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpyproject.toml
More file actions
178 lines (160 loc) · 7.31 KB
/
Copy pathpyproject.toml
File metadata and controls
178 lines (160 loc) · 7.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
[project]
name = "scorecard-pipeline"
# Single source of truth for the repo's version (REL-02/REL-03/REL-05). Mirrored
# in CITATION.cff and server.json; a `make verify` check (scripts/check_versions.py)
# fails CI if they drift. A protected release tag is cut only after these
# declarations and CI agree; this file never treats an uncut tag as published.
version = "1.5.0"
description = "Ingestion and scoring pipeline for the small-agency GTFS quality scorecard"
license = "Apache-2.0"
requires-python = ">=3.12"
dependencies = [
"requests>=2.32",
"gtfs-realtime-bindings>=1.0",
"pyyaml>=6.0",
# Machine-enforces the per-agency data contract: publish() validates every
# artifact against web/schemas/artifact.schema.json before writing it.
"jsonschema>=4.21",
# Render authored fix guides with CommonMark semantics. The previous
# line-by-line renderer split wrapped paragraphs and list continuations.
"markdown-it-py>=4.0",
]
[project.optional-dependencies]
# DuckDB powers the cross-agency query layer and the Parquet export (ADR 0013).
# Kept optional so the core pipeline stays lean; `scorecard query` and the
# parquet artifact are the only things that need it.
query = ["duckdb>=1.0"]
[project.scripts]
scorecard = "scorecard_pipeline.cli:main"
scorecard-mcp = "scorecard_pipeline.mcp_server:main"
[dependency-groups]
dev = [
# The structural SEO gate parses rendered sitemap XML. Keep entity
# expansion and external references disabled even for local build output.
"defusedxml>=0.7.1",
"pytest>=8.0",
"ruff>=0.15",
"mypy>=1.10",
# Generation-only sources for the committed global ISO location vocabulary.
# Runtime and installed-wheel consumers read the generated JSON package data.
"pycountry==26.2.16",
"babel==2.18.0",
"types-requests",
"types-PyYAML",
"duckdb>=1.0",
"pytest-cov>=5.0",
# Advisory mutation testing of the scoring logic (CODE-QUALITY-STANDARD §10).
# Not part of `make verify`; run via `make mutation`. See docs/mutation-testing.md.
"mutmut>=3.0",
"types-jsonschema>=4.26.0.20260518",
# Property-based invariant tests of the scoring math (tests/test_properties.py).
# Bounded by the "scorecard" profile registered there, so runtime stays small.
"hypothesis>=6",
]
# Behavioral browser tests over the served site (tests/e2e). A separate group so
# the default dev environment and the coverage-gated unit run stay browser-free;
# only .github/workflows/e2e.yml installs it (uv sync --group e2e).
e2e = [
"pytest-playwright>=0.7",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/scorecard_pipeline"]
# Receipt generation validates against the exact public 1.2 contract. Map that
# versioned schema into the wheel so an installed CLI does not depend on a
# repository checkout having web/schemas beside it.
[tool.hatch.build.targets.wheel.force-include]
"../web/schemas/sync-source-metadata-1.2.schema.json" = "scorecard_pipeline/data/schemas/sync-source-metadata-1.2.schema.json"
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.ruff.lint]
select = ["E", "W", "F", "I", "UP", "B", "SIM", "S", "C90", "RUF"]
# Typographic characters used deliberately in plain-language civic prose
# (en dash, right single quote/apostrophe, multiplication sign in a comment).
# Reviewed allowlist, not a blanket suppression of RUF001-003.
allowed-confusables = ["–", "’", "×"]
[tool.ruff.lint.mccabe]
max-complexity = 10
[tool.ruff.lint.per-file-ignores]
# Bandit rules that are structurally false-positive in tests: S101 (assert is
# the test framework), S108 (hardcoded /tmp paths are inert FetchResult
# fixture placeholders, never written to), S314 (parsing this repo's own
# freshly-rendered XML fixtures, not untrusted input), S105/S106 (test
# fixtures name an "unsub_token" variable, not a real secret), S608 (the one
# instance reads back the test's own just-written tmp Parquet file path, not
# externally supplied input).
"tests/**" = ["S101", "S108", "S314", "S105", "S106", "S608"]
[tool.mypy]
strict = true
files = ["src", "tests"]
[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_decorators = false
[[tool.mypy.overrides]]
module = ["google.transit.*", "google.protobuf.*"]
ignore_missing_imports = true
# The e2e dependency group is not part of the default environment, so mypy in
# `make verify` / ci.yml runs without playwright installed; tests/e2e still
# type-checks (playwright names resolve to Any there, precisely when absent).
[[tool.mypy.overrides]]
module = ["playwright.*", "pytest_playwright.*"]
ignore_missing_imports = true
[tool.pytest.ini_options]
testpaths = ["tests"]
# Browser tests are opt-in: the default run (and the 92%-branch coverage gate in
# ci.yml / `make verify`) deselects them; .github/workflows/e2e.yml runs
# `pytest tests/e2e -m e2e`, where the explicit -m overrides this one.
addopts = "-m 'not e2e' --strict-markers --strict-config --import-mode=importlib"
markers = [
"e2e: behavioral browser test over the statically served site (needs the e2e dependency group and a Playwright chromium)",
]
[tool.coverage.run]
branch = true
source = ["scorecard_pipeline"]
omit = [
# CLI argparse dispatch + __main__ entrypoint: thin orchestration over modules
# that are unit-tested directly, exercised end-to-end by the daily build. Not
# worth mocking the world to unit-test for a coverage number.
"*/scorecard_pipeline/cli.py",
# Static-site HTML/JS string assembly: its logic-bearing helpers are covered
# behaviorally by tests/test_render_site.py; a % over the templating glue would
# measure markup, not correctness.
"*/scorecard_pipeline/render_site.py",
]
[tool.coverage.report]
show_missing = true
# Advisory mutation testing (CODE-QUALITY-STANDARD §10). Scoped to the
# correctness-critical scoring math: the grade ladder + fix-priority tiers in
# score.py, the deduction arithmetic + freshness slope in metrics.py, and the
# realtime component weighting in rt.py — the places where a silent bug
# mis-grades an agency. Mutation score is the signal coverage cannot give: does
# the suite's *assertions* actually catch a regression, not just run the line?
#
# rt.py also holds impure fetch/capture plumbing (fetch_sample, capture_window);
# mutants there mostly survive because no unit test exercises the network path.
# That is acceptable: mutation here is ADVISORY (a REVIEW-GATE, never a merge
# gate) — run manually (`make mutation`) or on the weekly mutation.yml workflow,
# never as a per-PR blocker. See docs/mutation-testing.md.
[tool.mutmut]
# Copy the whole package so mutated code stays importable by the tests, but
# generate mutants for the scoring modules only (`only_mutate`). Every other
# file is copied verbatim into mutants/ and left unmutated.
source_paths = ["src"]
only_mutate = [
"src/scorecard_pipeline/score.py",
"src/scorecard_pipeline/metrics.py",
"src/scorecard_pipeline/rt.py",
]
# Kill signal: the dedicated unit tests for each mutated module, plus the
# Hypothesis invariant tests and the frozen mini-corpus canary. Targeted
# selection (not the whole suite) keeps the run bounded (minutes, not hours).
pytest_add_cli_args_test_selection = [
"tests/test_score.py",
"tests/test_metrics.py",
"tests/test_rt.py",
"tests/test_properties.py",
"tests/test_score_corpus.py",
]