contrails 0.4.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 (61) hide show
  1. contrails-0.4.0/.gitattributes +1 -0
  2. contrails-0.4.0/.github/dependabot.yml +23 -0
  3. contrails-0.4.0/.github/workflows/ci.yml +79 -0
  4. contrails-0.4.0/.github/workflows/pr-title.yml +21 -0
  5. contrails-0.4.0/.github/workflows/release-please.yml +56 -0
  6. contrails-0.4.0/.gitignore +34 -0
  7. contrails-0.4.0/.markdownlint-cli2.yaml +39 -0
  8. contrails-0.4.0/.pre-commit-config.yaml +39 -0
  9. contrails-0.4.0/.prettierignore +7 -0
  10. contrails-0.4.0/.prettierrc.json +4 -0
  11. contrails-0.4.0/.release-please-manifest.json +3 -0
  12. contrails-0.4.0/AGENTS.md +161 -0
  13. contrails-0.4.0/CHANGELOG.md +102 -0
  14. contrails-0.4.0/CLAUDE.md +1 -0
  15. contrails-0.4.0/LICENSE +21 -0
  16. contrails-0.4.0/PKG-INFO +530 -0
  17. contrails-0.4.0/README.md +476 -0
  18. contrails-0.4.0/config.example.json +13 -0
  19. contrails-0.4.0/config.example.yaml +77 -0
  20. contrails-0.4.0/docs/contrail-gh.md +102 -0
  21. contrails-0.4.0/docs/emissions.md +121 -0
  22. contrails-0.4.0/docs/parsing.md +144 -0
  23. contrails-0.4.0/docs/resync.md +139 -0
  24. contrails-0.4.0/docs/storage.md +60 -0
  25. contrails-0.4.0/docs/tripit-api.md +97 -0
  26. contrails-0.4.0/pyproject.toml +74 -0
  27. contrails-0.4.0/release-please-config.json +19 -0
  28. contrails-0.4.0/scripts/refresh_airline_codes.py +247 -0
  29. contrails-0.4.0/src/contrail/__init__.py +10 -0
  30. contrails-0.4.0/src/contrail/airlines.py +268 -0
  31. contrails-0.4.0/src/contrail/airports.py +101 -0
  32. contrails-0.4.0/src/contrail/cli.py +760 -0
  33. contrails-0.4.0/src/contrail/config.py +179 -0
  34. contrails-0.4.0/src/contrail/data/__init__.py +5 -0
  35. contrails-0.4.0/src/contrail/data/airline_codes.csv +2225 -0
  36. contrails-0.4.0/src/contrail/emissions/__init__.py +24 -0
  37. contrails-0.4.0/src/contrail/emissions/base.py +31 -0
  38. contrails-0.4.0/src/contrail/emissions/tim.py +316 -0
  39. contrails-0.4.0/src/contrail/importers/__init__.py +33 -0
  40. contrails-0.4.0/src/contrail/importers/base.py +28 -0
  41. contrails-0.4.0/src/contrail/importers/flighty_csv.py +274 -0
  42. contrails-0.4.0/src/contrail/importers/tripit_ical.py +281 -0
  43. contrails-0.4.0/src/contrail/models.py +149 -0
  44. contrails-0.4.0/src/contrail/resync.py +249 -0
  45. contrails-0.4.0/src/contrail/storage/__init__.py +77 -0
  46. contrails-0.4.0/src/contrail/storage/base.py +24 -0
  47. contrails-0.4.0/src/contrail/storage/local_csv.py +184 -0
  48. contrails-0.4.0/src/contrail/storage/raw_log.py +107 -0
  49. contrails-0.4.0/tests/conftest.py +20 -0
  50. contrails-0.4.0/tests/fixtures/sample_feed.ics +79 -0
  51. contrails-0.4.0/tests/fixtures/sample_flighty.csv +12 -0
  52. contrails-0.4.0/tests/test_airlines.py +150 -0
  53. contrails-0.4.0/tests/test_airports.py +82 -0
  54. contrails-0.4.0/tests/test_cli.py +621 -0
  55. contrails-0.4.0/tests/test_config.py +129 -0
  56. contrails-0.4.0/tests/test_flighty_csv_importer.py +191 -0
  57. contrails-0.4.0/tests/test_local_csv_storage.py +310 -0
  58. contrails-0.4.0/tests/test_raw_log.py +114 -0
  59. contrails-0.4.0/tests/test_resync.py +632 -0
  60. contrails-0.4.0/tests/test_tim_emissions.py +316 -0
  61. contrails-0.4.0/tests/test_tripit_ical_importer.py +230 -0
@@ -0,0 +1 @@
1
+ * text=auto
@@ -0,0 +1,23 @@
1
+ version: 2
2
+ updates:
3
+ # A runtime dependency bump changes what users install, so it has to reach the
4
+ # changelog and cut a release: `fix` is shown by release-please, `chore` is
5
+ # hidden by default and would ship the change invisibly. Dev dependencies
6
+ # affect nobody downstream, so they stay `chore`.
7
+ - package-ecosystem: pip
8
+ directory: /
9
+ schedule:
10
+ interval: weekly
11
+ commit-message:
12
+ prefix: fix
13
+ prefix-development: chore
14
+ include: scope
15
+
16
+ # CI only — nothing here reaches an installed contrail, so it stays hidden.
17
+ - package-ecosystem: github-actions
18
+ directory: /
19
+ schedule:
20
+ interval: weekly
21
+ commit-message:
22
+ prefix: chore
23
+ include: scope
@@ -0,0 +1,79 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ lint:
13
+ name: ruff
14
+ # Ten minutes against a job that takes ten seconds. The point is a `pip`
15
+ # fetch that stalls rather than fails, which would otherwise sit until
16
+ # GitHub's six-hour default.
17
+ timeout-minutes: 10
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@v7
21
+ - uses: actions/setup-python@v7
22
+ with:
23
+ python-version: "3.12"
24
+ - run: pip install -e ".[dev]"
25
+ - run: ruff check .
26
+ - run: ruff format --check .
27
+
28
+ test:
29
+ # Interpolated, not a fixed string: a matrix job with a static `name:`
30
+ # produces three checks called the same thing, and the version under test
31
+ # is the only thing that tells them apart.
32
+ name: pytest ${{ matrix.python-version }}
33
+ timeout-minutes: 10
34
+ runs-on: ubuntu-latest
35
+ strategy:
36
+ fail-fast: false
37
+ matrix:
38
+ python-version: ["3.11", "3.12", "3.13"]
39
+ steps:
40
+ - uses: actions/checkout@v7
41
+ - uses: actions/setup-python@v7
42
+ with:
43
+ python-version: ${{ matrix.python-version }}
44
+ - run: pip install -e ".[dev]"
45
+ - name: Run tests
46
+ run: pytest -q
47
+ - name: Dry-run against the fixture sources
48
+ # Catches a broken parser here, before it can reach anyone's production
49
+ # instance through a version tag. Both importers, since they also have to
50
+ # agree with each other about which flights are the same one. Needs no
51
+ # secrets: --dry-run never calls the emissions API, and both importers
52
+ # accept a local path.
53
+ env:
54
+ TRIPIT_ICAL_URL: tests/fixtures/sample_feed.ics
55
+ FLIGHTY_CSV_PATH: tests/fixtures/sample_flighty.csv
56
+ run: contrail sync --dry-run --csv-path "${RUNNER_TEMP}/should-not-be-written.csv"
57
+
58
+ docs:
59
+ name: markdown
60
+ # `npx` fetches Prettier from the registry on every run, and a stalled
61
+ # fetch hangs rather than failing. The same line in atdr/contrail-gh has
62
+ # sat silent for exactly 301 seconds, npm's fetch-timeout expiring and the
63
+ # retry succeeding, in a job that takes twelve seconds when the fetch is
64
+ # clean. Ten minutes bounds a stall that never recovers.
65
+ timeout-minutes: 10
66
+ runs-on: ubuntu-latest
67
+ steps:
68
+ - uses: actions/checkout@v7
69
+ # An action rather than `npx`, so the github-actions dependabot ecosystem
70
+ # already configured for this repo keeps it current. Prettier has no
71
+ # first-party action, so its pin below is a hand edit — the same deal as
72
+ # the `rev:` pins in .pre-commit-config.yaml.
73
+ - uses: DavidAnson/markdownlint-cli2-action@v24
74
+ - uses: actions/setup-node@v7
75
+ with:
76
+ node-version: "24"
77
+ # Prettier owns table alignment and whitespace; markdownlint owns the
78
+ # rules it can't express. Neither is redundant.
79
+ - run: npx prettier@3.9.6 --check "**/*.md"
@@ -0,0 +1,21 @@
1
+ name: pr-title
2
+
3
+ on:
4
+ pull_request_target:
5
+ types: [opened, edited, synchronize, reopened]
6
+
7
+ permissions:
8
+ pull-requests: read
9
+
10
+ jobs:
11
+ lint:
12
+ name: conventional title
13
+ timeout-minutes: 5
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ # On a squash merge the PR title becomes the commit subject, which is what
17
+ # release-please reads. An unconventional title silently drops the change
18
+ # from the changelog, so validate it up front.
19
+ - uses: amannn/action-semantic-pull-request@v6
20
+ env:
21
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,56 @@
1
+ name: release-please
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+
7
+ permissions:
8
+ contents: write
9
+ pull-requests: write
10
+
11
+ jobs:
12
+ release-please:
13
+ name: maintain the release PR
14
+ # This one holds every release behind it, so a hang here is a release that
15
+ # silently never happens. Ten minutes against a job that takes seconds.
16
+ timeout-minutes: 10
17
+ runs-on: ubuntu-latest
18
+ outputs:
19
+ release_created: ${{ steps.release.outputs.release_created }}
20
+ steps:
21
+ # Maintains a rolling release PR from the conventional commits on main.
22
+ # Merging it bumps the version in pyproject.toml, updates CHANGELOG.md,
23
+ # tags the release, and publishes it.
24
+ #
25
+ # Note: PRs opened by the default GITHUB_TOKEN do not trigger other
26
+ # workflows, so the release PR itself shows no CI run. The commits it
27
+ # releases were already tested on main.
28
+ - uses: googleapis/release-please-action@v5
29
+ id: release
30
+ with:
31
+ config-file: release-please-config.json
32
+ manifest-file: .release-please-manifest.json
33
+
34
+ publish:
35
+ name: publish to PyPI
36
+ needs: release-please
37
+ if: needs.release-please.outputs.release_created
38
+ runs-on: ubuntu-latest
39
+ environment: pypi
40
+ permissions:
41
+ # No API token: PyPI trusts this workflow (owner/repo/environment) to
42
+ # mint its own short-lived credential via OIDC.
43
+ id-token: write
44
+ steps:
45
+ - uses: actions/checkout@v7
46
+
47
+ - uses: actions/setup-python@v7
48
+ with:
49
+ python-version: "3.12"
50
+
51
+ - name: Build
52
+ run: |
53
+ pip install build
54
+ python -m build
55
+
56
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,34 @@
1
+ # Local configuration and personal data — never commit these.
2
+ # `contrail sync` writes ./flight_emissions.csv by default, and this repo is public.
3
+ config.json
4
+ config.yaml
5
+ config.yml
6
+ # Glob, not just the default filename: backups and per-account variants
7
+ # (flight_emissions.backup-*.csv, flight_emissions-work.csv) are real flight
8
+ # data too, and this repo is public.
9
+ flight_emissions*.csv
10
+ *.backup-*.csv
11
+ # The raw provider log holds real itineraries too.
12
+ *.raw.jsonl
13
+ last_checked.txt
14
+ .env
15
+ .env.*
16
+
17
+ # Python
18
+ __pycache__/
19
+ *.py[cod]
20
+ *.egg-info/
21
+ build/
22
+ dist/
23
+ .pytest_cache/
24
+ .ruff_cache/
25
+ venv/
26
+ .venv/
27
+
28
+ # Editors / OS
29
+ .DS_Store
30
+ .idea/
31
+ .vscode/
32
+
33
+ # Planning notes stay local — this repo is public.
34
+ _planning/
@@ -0,0 +1,39 @@
1
+ # Prettier owns whitespace and table alignment. These are the rules it cannot
2
+ # express. Kept in step by hand across contrail, contrail-gh and any instance
3
+ # created from it — the three files are copies, not a shared package.
4
+ #
5
+ # .prettierrc.json carries two settings and no room to explain them:
6
+ # `proseWrap: preserve`, because the prose here is wrapped by hand at
7
+ # deliberate points and `always` would rewrap every paragraph in the repo; and
8
+ # `embeddedLanguageFormatting: off`, because the code inside a fence is a
9
+ # worked example or a command someone will paste, not source to be tidied.
10
+ gitignore: true
11
+
12
+ # Without this, a bare `markdownlint-cli2` finds nothing and reports success.
13
+ globs:
14
+ - "**/*.md"
15
+
16
+ config:
17
+ MD013:
18
+ # AGENTS.md and docs/ already hold 80. README.md opts up to 100 with a
19
+ # markdownlint-configure-file comment at the foot of the file.
20
+ line_length: 80
21
+ # A padded table row is wider than any prose limit, and a fenced block is
22
+ # usually a command that must not be broken.
23
+ tables: false
24
+ code_blocks: false
25
+ MD046:
26
+ # GitHub only syntax-highlights fenced blocks, so an indented one silently
27
+ # loses the highlighting the author meant to have.
28
+ style: fenced
29
+
30
+ ignores:
31
+ # release-please regenerates this from commit subjects. Anything done to it
32
+ # here is clobbered on the next release.
33
+ - CHANGELOG.md
34
+ # A symlink to AGENTS.md. Linting it reports every line of that file twice.
35
+ - CLAUDE.md
36
+ # Local agent scratch, kept out of this public repo via .git/info/exclude —
37
+ # which `gitignore: true` above does not read, so it needs naming here. CI
38
+ # never sees the file; a full-tree run on a dev machine does.
39
+ - .claude/**
@@ -0,0 +1,39 @@
1
+ # Optional local convenience: `pre-commit install`. CI is the real gate.
2
+ repos:
3
+ - repo: https://github.com/astral-sh/ruff-pre-commit
4
+ rev: v0.16.3
5
+ hooks:
6
+ # `ruff-check`, not `ruff`: the latter is a deprecated alias kept for
7
+ # compatibility, running the identical `ruff check`. It will be removed
8
+ # eventually, and the failure would land on an unrelated `rev` bump.
9
+ - id: ruff-check
10
+ args: [--fix]
11
+ - id: ruff-format
12
+
13
+ - repo: https://github.com/pre-commit/pre-commit-hooks
14
+ rev: v6.0.0
15
+ hooks:
16
+ - id: trailing-whitespace
17
+ - id: end-of-file-fixer
18
+ - id: check-yaml
19
+ - id: check-json
20
+ - id: check-added-large-files
21
+
22
+ # Markdown. Keep these two in step with the pins in .github/workflows/ci.yml —
23
+ # a hook that lints to a different version than CI is worse than no hook.
24
+ - repo: https://github.com/DavidAnson/markdownlint-cli2
25
+ rev: v0.23.2
26
+ hooks:
27
+ - id: markdownlint-cli2
28
+ # The `globs` in .markdownlint-cli2.yaml already cover every file, and
29
+ # markdownlint-cli2 adds any filenames it is given to them rather than
30
+ # replacing them — so passing the staged files just reports each of
31
+ # them twice.
32
+ pass_filenames: false
33
+
34
+ # The maintained fork; pre-commit/mirrors-prettier is archived.
35
+ - repo: https://github.com/rbubley/mirrors-prettier
36
+ rev: v3.9.6
37
+ hooks:
38
+ - id: prettier
39
+ types_or: [markdown]
@@ -0,0 +1,7 @@
1
+ # Regenerated by release-please; see .markdownlint-cli2.yaml for why.
2
+ CHANGELOG.md
3
+ # Symlink to AGENTS.md — formatting it writes through to the same file twice.
4
+ CLAUDE.md
5
+ # Local agent scratch, excluded via .git/info/exclude, which Prettier does not
6
+ # read. See .markdownlint-cli2.yaml.
7
+ .claude/
@@ -0,0 +1,4 @@
1
+ {
2
+ "proseWrap": "preserve",
3
+ "embeddedLanguageFormatting": "off"
4
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.4.0"
3
+ }
@@ -0,0 +1,161 @@
1
+ # contrail
2
+
3
+ Estimates the CO2e of flights taken or booked and keeps a log. Reads a TripIt
4
+ iCal feed and Flighty CSV exports, prices each flight with Google's Travel Impact
5
+ Model, writes a CSV.
6
+
7
+ ## Commands
8
+
9
+ ```bash
10
+ python3.12 -m venv venv && ./venv/bin/pip install -e ".[dev]"
11
+ ./venv/bin/pytest -q
12
+ ./venv/bin/ruff check . && ./venv/bin/ruff format .
13
+ TRIPIT_ICAL_URL=tests/fixtures/sample_feed.ics \
14
+ FLIGHTY_CSV_PATH=tests/fixtures/sample_flighty.csv ./venv/bin/contrail sync --dry-run
15
+ ./venv/bin/python scripts/refresh_airline_codes.py # needs network; run by hand
16
+ ```
17
+
18
+ Python 3.11+. The default `python3` on this machine is
19
+ 3.7 — use `/usr/local/bin/python3.12` explicitly.
20
+
21
+ ## Architecture
22
+
23
+ Three protocol seams:
24
+
25
+ | Seam | Protocol | Implementations |
26
+ | ------------ | ----------------------------------------------------------------------- | ---------------------------- |
27
+ | `importers/` | `Importer.fetch(config) -> Iterable[FlightRecord \| UnparsedEvent]` | `tripit_ical`, `flighty_csv` |
28
+ | `emissions/` | `EmissionsProvider.compute(flights, now) -> dict[key, EmissionsResult]` | `tim` |
29
+ | `storage/` | `Storage.load() -> list[dict]`, `save(rows)` | `local_csv` |
30
+
31
+ `cli.py` owns the flow: load config → load storage → collect from every source →
32
+ collapse records that are the same flight → reconcile against what's stored →
33
+ price → build rows → normalize → save.
34
+
35
+ Two keys, and the difference matters:
36
+
37
+ - **`f"{source}:{source_id}"`** identifies a _record_. It is the dedup key and
38
+ what `also_seen_as` stores.
39
+ - **`(flight_date, origin, destination)`** identifies a _flight_, across sources.
40
+ `resync.identity()`. Never the flight number — see the gotchas.
41
+
42
+ ## Conventions
43
+
44
+ - Conventional commits. release-please owns versions, tags and `CHANGELOG.md` —
45
+ never hand-edit the changelog or the version in `pyproject.toml`.
46
+ - `__version__` is read from installed package metadata, so `pyproject.toml` is
47
+ the single source of truth. Code that needs the version imports `__version__`
48
+ rather than spelling it out — see the gotcha on version pins for the two
49
+ literals that remain.
50
+ - **No test may make a real network call.** Mock `requests` in both directions.
51
+ - **Every workflow is named after its own file**, and the description goes on
52
+ the job. GitHub labels a check `<workflow name> / <job name>` and never shows
53
+ the filename, so `pr-title / conventional title` names both the file to open
54
+ and what it did. A matrix job's `name:` has to interpolate the matrix value
55
+ (`pytest ${{ matrix.python-version }}`) or all three legs produce checks
56
+ called the same thing. `atdr/contrail-gh` enforces this in CI; here it is
57
+ convention. The `Analyze (python)` and `Analyze (actions)` checks are the
58
+ exception and always will be: CodeQL runs from default setup, which is a repo
59
+ setting rather than a file, so its name and its timeout are GitHub's to
60
+ choose.
61
+ - **Every job that can carry `timeout-minutes` sets one.** GitHub's default is
62
+ six hours, and the failure that matters is a stall rather than an error: a
63
+ `pip` or `npx` fetch that hangs never fails on its own. Ten minutes
64
+ everywhere except `pr-title.yml`, which gets five. A job whose only key is
65
+ `uses:` cannot carry it, which is why the setting lives inside a reusable
66
+ workflow rather than on its callers.
67
+ - **Markdown is formatted, not hand-aligned.** Prettier owns table padding and
68
+ whitespace; markdownlint-cli2 owns line length and the rest. Run
69
+ `npx prettier@3.9.6 --write "**/*.md"` rather than lining a table up by
70
+ hand. Prose wraps at 80, except `README.md`, which wraps at 100 and says so
71
+ in a `markdownlint-configure-file` comment at its foot.
72
+ - This repo is public. Never commit a real CSV, a raw log, or `config.json` —
73
+ all are gitignored.
74
+ - **When updating docs at the end of a change, skim the open issues**
75
+ (`gh issue list`) for any the change touched. Cheap, and it catches both
76
+ directions: an issue quietly fixed, and one made easier to hit.
77
+
78
+ **Findings about your own change belong in the same PR** — that is not two
79
+ things in one PR, it is one change described accurately. A doc line your change
80
+ just made wrong, or an issue whose shape it altered, is part of that change; a
81
+ reviewer needs it in front of them, and splitting it out means `main` is briefly
82
+ wrong on purpose. Only a finding that stands entirely apart from what you built
83
+ earns its own PR; note it on the issue either way.
84
+
85
+ The one exception is the release PR: `chore(main): release X.Y.Z` is generated
86
+ by release-please from commit subjects, so anything hand-added there is
87
+ clobbered on the next regeneration.
88
+
89
+ ## Depth
90
+
91
+ - [docs/parsing.md](docs/parsing.md) — the ported regexes, local dates,
92
+ codeshares, why `parse()` is two-pass, reading a Flighty export
93
+ - [docs/emissions.md](docs/emissions.md) — exact vs route average, the past-date
94
+ 400, what gets kept, and the open question about timing
95
+ - [docs/resync.md](docs/resync.md) — what a sync may change, the freeze boundary,
96
+ cancellation, matching one flight across two sources
97
+ - [docs/storage.md](docs/storage.md) — CSV invariants and why there's no total
98
+ - [docs/tripit-api.md](docs/tripit-api.md) — investigated, not used, and why
99
+ - [docs/contrail-gh.md](docs/contrail-gh.md) — **what a change here obliges in the
100
+ template repo**
101
+
102
+ ## Gotchas most likely to bite
103
+
104
+ - **The regexes in `importers/tripit_ical.py` are validated against real TripIt
105
+ feeds.** They look untidy because real feeds are. Don't rewrite one without a
106
+ failing test that proves the current form is wrong.
107
+ - **Mutation is confined to flights that haven't departed.** Past rows are never
108
+ touched — that's what makes "absent from the feed" unambiguous. One exception:
109
+ `resync.backfill()` fills a _blank_ cabin, aircraft or reason on a row of any
110
+ age, because a Flighty export is almost entirely past flights and it re-prices
111
+ nothing.
112
+ - **A flight is identified by route and date, never by flight number.** `BA16` is
113
+ SYD-SIN-LHR on one day: two legs, two cabins, two rows. Any "dedup by flight
114
+ number" idea silently merges them and loses a figure.
115
+ - **The file is written only when content actually changed**, or contrail-gh
116
+ commits every day for nothing.
117
+ - **Every value in a row dict is a string**, so a fresh row compares equal to one
118
+ loaded from the CSV. Every row builder must set every column.
119
+ - **TIM cannot be asked twice.** It won't price a departed flight, so anything not
120
+ captured while it was upcoming is gone permanently.
121
+ - **A runtime dependency bump must be `fix(deps):`, not `chore(deps):`.**
122
+ release-please hides `chore` by default and counts only `feat` and breaking
123
+ changes toward a bump, so a `chore(deps)` bump of a runtime dependency changes
124
+ what users install with nothing in the changelog and no release. Dev
125
+ dependencies stay `chore(deps-dev):`.
126
+ - **The two install pins in `README.md` are rewritten by release-please**, not by
127
+ hand. They sit inside `<!-- x-release-please-start-version -->` /
128
+ `<!-- x-release-please-end -->` comments, and `README.md` is listed under
129
+ `extra-files` in `release-please-config.json` — both halves are needed. Delete
130
+ a comment and the pin quietly rots to whatever release it was written against,
131
+ which is exactly how it drifted before. A _new_ literal version anywhere else
132
+ is a bug: import `__version__`.
133
+ - **release-please needs the repo setting "Allow GitHub Actions to create and
134
+ approve pull requests"** (Settings → Actions → General). `permissions:
135
+ pull-requests: write` in the workflow is _not_ sufficient on its own, and the
136
+ API can report the flag as enabled while it is still blocked. Without it the
137
+ release job fails with "GitHub Actions is not permitted to create or approve
138
+ pull requests".
139
+ - **`CHANGELOG.md` and `CLAUDE.md` are excluded from both Markdown tools.**
140
+ release-please regenerates the changelog from commit subjects, so a
141
+ reformat there is undone on the next release and can desync the manifest;
142
+ `CLAUDE.md` is a symlink to `AGENTS.md`, so linting it reports every line
143
+ twice and formatting it writes the same file twice. Both are named in
144
+ `.markdownlint-cli2.yaml` and `.prettierignore`.
145
+ - **`cli._now()` exists to be monkeypatched.** Tests that use the real clock rot
146
+ once the fixture's dates fall into the past.
147
+ - **`src/contrail/data/airline_codes.csv` is generated, never hand-edited.** Fix
148
+ it upstream in Wikidata and re-run the script, or the next refresh reverts you.
149
+ It must be written with LF: `.gitattributes` normalizes, and the csv module
150
+ defaults to CRLF.
151
+ - **`tests/fixtures/sample_flighty.csv` is synthetic.** This repo is public and
152
+ a real export is someone's entire travel history.
153
+
154
+ ## Related repos
155
+
156
+ - `atdr/contrail-gh` — public GitHub Actions template. Pins a contrail release
157
+ tag and ships a header-only CSV that has to match it. **A schema or output
158
+ change here needs a matching change there** — see
159
+ [docs/contrail-gh.md](docs/contrail-gh.md).
160
+ - A **private instance** created from that template (`octocat/my-contrail` in the
161
+ docs). Real data and secrets — never referenced by name in anything public.
@@ -0,0 +1,102 @@
1
+ # Changelog
2
+
3
+ ## [0.4.0](https://github.com/atdr/contrail/compare/v0.3.1...v0.4.0) (2026-09-05)
4
+
5
+
6
+ ### Features
7
+
8
+ * **release:** publish to PyPI as `contrails` ([#34](https://github.com/atdr/contrail/issues/34)) ([b34d65f](https://github.com/atdr/contrail/commit/b34d65ff050dee790bb416d272cc3dad0eb7be9f))
9
+
10
+ ## [0.3.1](https://github.com/atdr/contrail/compare/v0.3.0...v0.3.1) (2026-09-05)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **deps:** Update airportsdata requirement from &gt;=20260803 to &gt;=20260902 ([#31](https://github.com/atdr/contrail/issues/31)) ([24f6c2e](https://github.com/atdr/contrail/commit/24f6c2eac97357a3ae01a4887210f8032b4406d8))
16
+ * **deps:** Update icalendar requirement from &gt;=7.2.2 to &gt;=7.3.0 ([#29](https://github.com/atdr/contrail/issues/29)) ([640788f](https://github.com/atdr/contrail/commit/640788fd2720046957835ea4036d7ae94e8716fb))
17
+
18
+
19
+ ### Documentation
20
+
21
+ * a file-reading importer now needs its env var in two workflows ([#20](https://github.com/atdr/contrail/issues/20)) ([353b0c4](https://github.com/atdr/contrail/commit/353b0c4774e0e3cca444623bb437813eff13ad79))
22
+
23
+ ## [0.3.0](https://github.com/atdr/contrail/compare/v0.2.0...v0.3.0) (2026-08-16)
24
+
25
+
26
+ ### Features
27
+
28
+ * add a flighty_csv importer and match flights across sources ([ba0d292](https://github.com/atdr/contrail/commit/ba0d2921f04f6e3b92ec4f6c18c7024206cf9c54))
29
+
30
+
31
+ ### Bug Fixes
32
+
33
+ * stop version references drifting from the release ([429507c](https://github.com/atdr/contrail/commit/429507c8d3d121c20c62e67b883cdaeb44776ffb))
34
+
35
+
36
+ ### Documentation
37
+
38
+ * note the new way to reach the per-source identity gap ([941db88](https://github.com/atdr/contrail/commit/941db88d90a1b203aed0e5e7ce1905df389d4ef0))
39
+
40
+ ## [0.2.0](https://github.com/atdr/contrail/compare/v0.1.0...v0.2.0) (2026-08-15)
41
+
42
+
43
+ ### ⚠ BREAKING CHANGES
44
+
45
+ * Python 3.10 is no longer supported. requires-python is now >=3.11.
46
+
47
+ ### Features
48
+
49
+ * require Python 3.11 ([18cdc6e](https://github.com/atdr/contrail/commit/18cdc6ea6251f10870303a9d17fe20e7f7a26d64))
50
+
51
+
52
+ ### Bug Fixes
53
+
54
+ * **deps:** declare the pyyaml floor once and bound ruff ([8790755](https://github.com/atdr/contrail/commit/8790755521ace3cdbaf39e4a4e658762e6a28a0a))
55
+ * **deps:** Update icalendar requirement from &gt;=5.0 to &gt;=7.2.2 ([ee39316](https://github.com/atdr/contrail/commit/ee393167748db7b2cabd5d3667eb50b0837ab6e5))
56
+ * **deps:** Update pyyaml requirement from &gt;=6 to &gt;=6.0.3 ([2ed4484](https://github.com/atdr/contrail/commit/2ed4484142c9ef200c743634d54e286a43ca12b9))
57
+ * release runtime dependency bumps instead of hiding them ([68ff550](https://github.com/atdr/contrail/commit/68ff550a4d14db328fe017c235d5e59539767445))
58
+
59
+
60
+ ### Documentation
61
+
62
+ * note the Actions setting release-please needs ([5e1778b](https://github.com/atdr/contrail/commit/5e1778b7c0ffde2475b83b00482d21ac8e843446))
63
+
64
+ ## 0.1.0 (2026-08-15)
65
+
66
+
67
+ ### Features
68
+
69
+ * add config resolution and CLI ([ca9f865](https://github.com/atdr/contrail/commit/ca9f86567ec0a223ea8823d75799f5fa79aaadc2))
70
+ * add core data model ([5c7b0b6](https://github.com/atdr/contrail/commit/5c7b0b639853c1f18d8a0f080a638baccd02d6d4))
71
+ * add CSV storage keyed on cumulative_kg_actual ([c633763](https://github.com/atdr/contrail/commit/c63376384d358b8024dd2eb7d6a842dab2695d54))
72
+ * add importer seam and tripit_ical importer ([e52aafd](https://github.com/atdr/contrail/commit/e52aafd810b73730ac003ea2224a39f9069bcddf))
73
+ * add TIM emissions provider with route-average fallback ([9badcc4](https://github.com/atdr/contrail/commit/9badcc4c990d618d6363dbc7b01cea2df9672605))
74
+ * drop the stored cumulative total from the CSV ([0ce92b6](https://github.com/atdr/contrail/commit/0ce92b6bd8f62dc5837c33dd68df0618f69ab4e3))
75
+ * price codeshares as the operating flight ([f456f73](https://github.com/atdr/contrail/commit/f456f73e912d4ae1e1bbac1e3eaaf5bfc5d1d23d))
76
+ * re-sync flights that have not yet departed ([372606f](https://github.com/atdr/contrail/commit/372606f365d19fc3ad2f99cd047c5de617000fb7))
77
+ * resolve departure dates in the origin airport's timezone ([b9032c9](https://github.com/atdr/contrail/commit/b9032c9c0a3572154cb2ddc20427b6d07c01ff40))
78
+ * ship example config files ([17214b8](https://github.com/atdr/contrail/commit/17214b8019f2f47e6e7356951bae029ea62dbf18))
79
+ * store departure times and keep everything TIM returns ([4f8840a](https://github.com/atdr/contrail/commit/4f8840a596dd5f3b65240edda8fa224add24bf89))
80
+
81
+
82
+ ### Bug Fixes
83
+
84
+ * **emissions:** keep the TIM key out of URLs and error messages ([6e90e96](https://github.com/atdr/contrail/commit/6e90e968dee6a7b335ca2da6dea7ff655326025a))
85
+ * gitignore flight data backups and variants ([7b61808](https://github.com/atdr/contrail/commit/7b618087d6ecb473b3d998dfec7ebacc5f363ad5))
86
+ * harden re-sync against silent source failures and data loss ([25073ec](https://github.com/atdr/contrail/commit/25073ec4fad7473bed5e58ebb66596fcb67e4f30))
87
+ * **importers:** give UID-less events distinct dedup keys ([4b01c8c](https://github.com/atdr/contrail/commit/4b01c8c8bde1c1cf0353a60edb4586d9e92a5980))
88
+ * stop an upgrade downgrading every open exact figure ([2e618eb](https://github.com/atdr/contrail/commit/2e618eb8cd9094a0f48e8bf318aed2c512d75bc5))
89
+ * **storage:** count hand-edited rows and write the CSV atomically ([625c8a9](https://github.com/atdr/contrail/commit/625c8a9eefc8cab9bd2edd6225351a7e87e40f95))
90
+
91
+
92
+ ### Documentation
93
+
94
+ * add README ([f43641d](https://github.com/atdr/contrail/commit/f43641d7f8bdd766dd8398c6e1c148d40df7c8a8))
95
+ * add working notes, status and build plan ([84c42bb](https://github.com/atdr/contrail/commit/84c42bb3f2608c00def9c9b293c0b66493a6fd9d))
96
+ * document the repair workflow and departure-date caveat ([211e7a3](https://github.com/atdr/contrail/commit/211e7a3e263022fffd1c05a01f2582260b6dcb89))
97
+ * record findings from the first real-feed run ([28bebe0](https://github.com/atdr/contrail/commit/28bebe08435d66c21fcc853238a056d31ee921af))
98
+ * record the changed-itinerary and UTC-date open questions ([d01b09a](https://github.com/atdr/contrail/commit/d01b09ae59d939ab0fd270834a50a6c292d37752))
99
+ * record what a change here obliges in contrail-gh ([44eb9e1](https://github.com/atdr/contrail/commit/44eb9e14b09c329c1fbd270814af063c41e023ef))
100
+ * split working notes into AGENTS.md and docs/ ([cdb85f2](https://github.com/atdr/contrail/commit/cdb85f2d49569c503fe14095952111317a6da050))
101
+ * state what is verified, drop the archaeology ([e2b4d49](https://github.com/atdr/contrail/commit/e2b4d496b047773acb646552b570e688dbd46573))
102
+ * use a generic owner for instance repos ([cbfbffd](https://github.com/atdr/contrail/commit/cbfbffde322b37a6a89932c4c258af8e9db414f8))
@@ -0,0 +1 @@
1
+ AGENTS.md
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andreas Richardson
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.