eolas-data 1.8.0__tar.gz → 1.9.1__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 (58) hide show
  1. {eolas_data-1.8.0 → eolas_data-1.9.1}/.github/workflows/catalog-drift.yml +1 -1
  2. eolas_data-1.9.1/.github/workflows/pre-release.yml +50 -0
  3. eolas_data-1.9.1/.github/workflows/publish.yml +143 -0
  4. eolas_data-1.9.1/.github/workflows/smoke.yml +39 -0
  5. eolas_data-1.9.1/.github/workflows/test.yml +84 -0
  6. eolas_data-1.9.1/CHANGELOG.md +130 -0
  7. eolas_data-1.9.1/LICENSE +21 -0
  8. eolas_data-1.9.1/PKG-INFO +364 -0
  9. eolas_data-1.9.1/README.md +312 -0
  10. eolas_data-1.9.1/eolas_data/__init__.py +47 -0
  11. {eolas_data-1.8.0 → eolas_data-1.9.1}/eolas_data/_dataset_names.py +299 -43
  12. eolas_data-1.9.1/eolas_data/_regen_names.py +58 -0
  13. eolas_data-1.9.1/eolas_data/cdc.py +233 -0
  14. eolas_data-1.9.1/eolas_data/cli.py +1583 -0
  15. eolas_data-1.9.1/eolas_data/client.py +2840 -0
  16. eolas_data-1.9.1/eolas_data/console.py +25 -0
  17. {eolas_data-1.8.0 → eolas_data-1.9.1}/eolas_data/dataset.py +17 -3
  18. eolas_data-1.9.1/eolas_data/exceptions.py +94 -0
  19. eolas_data-1.9.1/eolas_data/library.py +408 -0
  20. eolas_data-1.9.1/eolas_data/meta.py +232 -0
  21. eolas_data-1.9.1/eolas_data/rows.py +36 -0
  22. {eolas_data-1.8.0 → eolas_data-1.9.1}/eolas_data/schedule.py +19 -8
  23. eolas_data-1.9.1/eolas_data/search.py +133 -0
  24. {eolas_data-1.8.0 → eolas_data-1.9.1}/pyproject.toml +23 -3
  25. eolas_data-1.9.1/scripts/check_client_contract.py +61 -0
  26. eolas_data-1.9.1/scripts/preflight.sh +43 -0
  27. eolas_data-1.9.1/tests/fixtures/source_helpers.json +37 -0
  28. eolas_data-1.9.1/tests/test_as_arrow.py +51 -0
  29. eolas_data-1.9.1/tests/test_cdc_roundtrip.py +166 -0
  30. eolas_data-1.9.1/tests/test_changes_http.py +74 -0
  31. {eolas_data-1.8.0 → eolas_data-1.9.1}/tests/test_cli.py +184 -3
  32. eolas_data-1.9.1/tests/test_client.py +692 -0
  33. eolas_data-1.9.1/tests/test_date_filter.py +81 -0
  34. eolas_data-1.9.1/tests/test_geometry_param.py +299 -0
  35. eolas_data-1.9.1/tests/test_get_local.py +276 -0
  36. eolas_data-1.9.1/tests/test_keyring.py +266 -0
  37. eolas_data-1.9.1/tests/test_library.py +235 -0
  38. eolas_data-1.9.1/tests/test_meta.py +101 -0
  39. eolas_data-1.9.1/tests/test_preview_dimensions.py +82 -0
  40. eolas_data-1.9.1/tests/test_progress.py +518 -0
  41. eolas_data-1.9.1/tests/test_resilience.py +95 -0
  42. eolas_data-1.9.1/tests/test_rows.py +24 -0
  43. {eolas_data-1.8.0 → eolas_data-1.9.1}/tests/test_schedule.py +10 -1
  44. eolas_data-1.9.1/tests/test_search.py +77 -0
  45. eolas_data-1.9.1/tests/test_smoke_live.py +85 -0
  46. eolas_data-1.9.1/tests/test_source_helpers.py +36 -0
  47. eolas_data-1.9.1/tests/test_sync_bulk.py +499 -0
  48. eolas_data-1.9.1/tests/test_sync_changes.py +791 -0
  49. eolas_data-1.8.0/.github/workflows/publish.yml +0 -31
  50. eolas_data-1.8.0/PKG-INFO +0 -216
  51. eolas_data-1.8.0/README.md +0 -174
  52. eolas_data-1.8.0/eolas_data/__init__.py +0 -16
  53. eolas_data-1.8.0/eolas_data/_regen_names.py +0 -57
  54. eolas_data-1.8.0/eolas_data/cli.py +0 -639
  55. eolas_data-1.8.0/eolas_data/client.py +0 -681
  56. eolas_data-1.8.0/eolas_data/exceptions.py +0 -20
  57. eolas_data-1.8.0/tests/test_client.py +0 -337
  58. {eolas_data-1.8.0 → eolas_data-1.9.1}/.gitignore +0 -0
@@ -17,7 +17,7 @@ jobs:
17
17
  check:
18
18
  runs-on: ubuntu-latest
19
19
  steps:
20
- - uses: actions/checkout@v4
20
+ - uses: actions/checkout@v5
21
21
 
22
22
  - uses: actions/setup-python@v5
23
23
  with:
@@ -0,0 +1,50 @@
1
+ name: Pre-release gate
2
+
3
+ on:
4
+ workflow_dispatch:
5
+
6
+ jobs:
7
+ gate:
8
+ runs-on: ubuntu-latest
9
+ env:
10
+ PYTHON_KEYRING_BACKEND: keyring.backends.null.Keyring
11
+ steps:
12
+ - uses: actions/checkout@v5
13
+ - uses: actions/setup-python@v5
14
+ with:
15
+ python-version: "3.12"
16
+
17
+ - name: Client contract matrix
18
+ run: python scripts/check_client_contract.py
19
+
20
+ - name: Build + install wheel
21
+ run: |
22
+ pip install build
23
+ python -m build
24
+ WHL=$(ls -1 dist/*.whl | head -1)
25
+ pip install "$WHL"
26
+ pip install pytest responses geopandas shapely pandas pyarrow typer rich keyring
27
+
28
+ - name: Unit tests (installed package)
29
+ run: pytest -q -m "not integration"
30
+
31
+ - name: Live smoke
32
+ env:
33
+ EOLAS_API_KEY: ${{ secrets.EOLAS_API_KEY }}
34
+ run: |
35
+ if [ -z "$EOLAS_API_KEY" ]; then
36
+ echo "::error::EOLAS_API_KEY secret required for pre-release smoke."
37
+ exit 1
38
+ fi
39
+ pytest -q -m integration tests/test_smoke_live.py
40
+
41
+ - name: Version parity with eolas-r
42
+ run: |
43
+ set -euo pipefail
44
+ py_ver=$(grep -E '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/')
45
+ r_ver=$(curl -fsSL "https://raw.githubusercontent.com/phildonovan/eolas-r/master/DESCRIPTION" \
46
+ | awk -F': ' '/^Version:/{print $2; exit}')
47
+ echo "Python: $py_ver | R (master): $r_ver"
48
+ if [ "$py_ver" != "$r_ver" ]; then
49
+ echo "::warning::Client versions differ — bump both before tagging if shipping related fixes"
50
+ fi
@@ -0,0 +1,143 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ version-parity:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v5
13
+ - name: Compare tag to eolas-r DESCRIPTION
14
+ env:
15
+ TAG: ${{ github.ref_name }}
16
+ run: |
17
+ set -euo pipefail
18
+ expected="${TAG#v}"
19
+ r_ver=$(curl -fsSL "https://raw.githubusercontent.com/phildonovan/eolas-r/master/DESCRIPTION" \
20
+ | awk -F': ' '/^Version:/{print $2; exit}')
21
+ py_ver=$(grep -E '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/')
22
+ echo "Tag: $expected | R: $r_ver | Python (pyproject): $py_ver"
23
+ if [ "$expected" != "$py_ver" ]; then
24
+ echo "::error::Tag $TAG does not match pyproject.toml version $py_ver"
25
+ exit 1
26
+ fi
27
+ if [ "$expected" != "$r_ver" ]; then
28
+ echo "::warning::R package version ($r_ver) differs from tag ($expected) — align releases when possible"
29
+ fi
30
+
31
+ - name: Guard — version must not already exist on PyPI
32
+ env:
33
+ TAG: ${{ github.ref_name }}
34
+ run: |
35
+ # PyPI permanently reserves a filename once uploaded, INCLUDING for
36
+ # yanked releases — a yanked version can never be re-published. This
37
+ # repo has a burned band at 1.5.0-1.8.0 (uploaded May 2026, yanked),
38
+ # and development later resumed BELOW it on the 1.3.x line, so a naive
39
+ # minor bump can walk straight back into it. Without this guard that
40
+ # failure surfaces only at the upload step, after a full build+test,
41
+ # as an opaque "400 File already exists".
42
+ set -euo pipefail
43
+ expected="${TAG#v}"
44
+ taken=$(curl -fsSL "https://pypi.org/pypi/eolas-data/json" \
45
+ | python3 -c "import json,sys; print('yes' if '$expected' in json.load(sys.stdin)['releases'] else 'no')")
46
+ if [ "$taken" = "yes" ]; then
47
+ echo "::error::Version $expected already exists on PyPI (possibly yanked)."
48
+ echo "::error::Yanked versions permanently reserve their filenames and cannot be re-published."
49
+ echo "::error::Pick a version above every existing release and re-tag."
50
+ exit 1
51
+ fi
52
+ echo "Version $expected is free on PyPI."
53
+
54
+ test:
55
+ runs-on: ubuntu-latest
56
+ needs: version-parity
57
+ env:
58
+ PYTHON_KEYRING_BACKEND: keyring.backends.null.Keyring
59
+ steps:
60
+ - uses: actions/checkout@v5
61
+
62
+ - uses: actions/setup-python@v5
63
+ with:
64
+ python-version: "3.12"
65
+
66
+ - name: Build wheel
67
+ run: pip install build && python -m build
68
+
69
+ - name: Install wheel + test deps
70
+ run: |
71
+ WHL=$(ls -1 dist/*.whl | head -1)
72
+ pip install "$WHL"
73
+ pip install pytest responses geopandas shapely pandas pyarrow typer rich keyring
74
+
75
+ - name: Version self-report parity (REL-1)
76
+ env:
77
+ TAG: ${{ github.ref_name }}
78
+ run: |
79
+ set -euo pipefail
80
+ expected="${TAG#v}"
81
+ # __version__ is read from installed distribution metadata, so this
82
+ # asserts the wheel the user pip-installs self-reports the tag — the
83
+ # exact drift (shipped 1.3.21 said 1.3.20) that this gate structurally
84
+ # closes. Run from /tmp so the source tree's __init__ can't shadow.
85
+ reported=$(cd /tmp && python -c "import eolas_data; print(eolas_data.__version__)")
86
+ echo "Tag: $expected | __version__: $reported"
87
+ if [ "$reported" != "$expected" ]; then
88
+ echo "::error::Installed eolas_data.__version__ ($reported) != tag ($expected)"
89
+ exit 1
90
+ fi
91
+
92
+ - name: Client contract matrix
93
+ run: python scripts/check_client_contract.py
94
+
95
+ - name: pytest
96
+ run: pytest -q -m "not integration"
97
+
98
+ smoke:
99
+ runs-on: ubuntu-latest
100
+ needs: test
101
+ steps:
102
+ - uses: actions/checkout@v5
103
+ - uses: actions/setup-python@v5
104
+ with:
105
+ python-version: "3.12"
106
+ - name: Install package from tag
107
+ run: pip install ".[dev,geo]"
108
+ - name: Live smoke (pre-publish gate)
109
+ env:
110
+ EOLAS_API_KEY: ${{ secrets.EOLAS_API_KEY }}
111
+ run: |
112
+ if [ -z "$EOLAS_API_KEY" ]; then
113
+ echo "::warning::EOLAS_API_KEY not set — skipping publish smoke"
114
+ exit 0
115
+ fi
116
+ pytest -q -m integration tests/test_smoke_live.py
117
+
118
+ publish:
119
+ needs: [test, smoke]
120
+ if: needs.test.result == 'success'
121
+ runs-on: ubuntu-latest
122
+ environment: pypi
123
+ permissions:
124
+ id-token: write
125
+
126
+ steps:
127
+ - uses: actions/checkout@v5
128
+
129
+ - uses: actions/setup-python@v5
130
+ with:
131
+ python-version: "3.12"
132
+
133
+ - name: Install build tools
134
+ run: pip install build
135
+
136
+ - name: Build
137
+ run: python -m build
138
+
139
+ - name: Publish to PyPI
140
+ uses: pypa/gh-action-pypi-publish@release/v1
141
+ with:
142
+ # Uses PYPI_API_TOKEN when set; otherwise OIDC trusted publishing.
143
+ password: ${{ secrets.PYPI_API_TOKEN }}
@@ -0,0 +1,39 @@
1
+ name: Live API smoke
2
+
3
+ on:
4
+ schedule:
5
+ - cron: "0 8 * * 1" # Mondays 08:00 UTC
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ smoke:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v5
13
+
14
+ - uses: actions/setup-python@v5
15
+ with:
16
+ python-version: "3.12"
17
+
18
+ - name: Require API key on manual runs
19
+ if: github.event_name == 'workflow_dispatch'
20
+ env:
21
+ EOLAS_API_KEY: ${{ secrets.EOLAS_API_KEY }}
22
+ run: |
23
+ if [ -z "$EOLAS_API_KEY" ]; then
24
+ echo "::error::EOLAS_API_KEY secret is required for workflow_dispatch smoke runs."
25
+ exit 1
26
+ fi
27
+
28
+ - name: Install package + dev/geo deps (as users would)
29
+ run: pip install ".[dev,geo]"
30
+
31
+ - name: Live smoke tests
32
+ env:
33
+ EOLAS_API_KEY: ${{ secrets.EOLAS_API_KEY }}
34
+ run: |
35
+ if [ -z "$EOLAS_API_KEY" ]; then
36
+ echo "EOLAS_API_KEY secret not configured — skipping scheduled smoke tests."
37
+ exit 0
38
+ fi
39
+ pytest -q -m integration tests/test_smoke_live.py
@@ -0,0 +1,84 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ pull_request:
7
+
8
+ jobs:
9
+ contract:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v5
13
+ - uses: actions/setup-python@v5
14
+ with:
15
+ python-version: "3.12"
16
+ - name: Client contract matrix
17
+ run: python scripts/check_client_contract.py
18
+
19
+ test:
20
+ runs-on: ubuntu-latest
21
+ needs: contract
22
+ strategy:
23
+ fail-fast: false
24
+ matrix:
25
+ python-version: ["3.10", "3.12", "3.13"]
26
+ env:
27
+ PYTHON_KEYRING_BACKEND: keyring.backends.null.Keyring
28
+ steps:
29
+ - uses: actions/checkout@v5
30
+
31
+ - uses: actions/setup-python@v5
32
+ with:
33
+ python-version: ${{ matrix.python-version }}
34
+
35
+ - name: Install package + dev + geo + secure (editable)
36
+ run: pip install -e ".[dev,geo,secure]"
37
+
38
+ - name: pytest (unit suite)
39
+ run: pytest -q -m "not integration"
40
+
41
+ test-installed:
42
+ runs-on: ubuntu-latest
43
+ needs: contract
44
+ env:
45
+ PYTHON_KEYRING_BACKEND: keyring.backends.null.Keyring
46
+ steps:
47
+ - uses: actions/checkout@v5
48
+
49
+ - uses: actions/setup-python@v5
50
+ with:
51
+ python-version: "3.12"
52
+
53
+ - name: Build wheel
54
+ run: pip install build && python -m build
55
+
56
+ - name: Install built wheel + test deps (as users would)
57
+ run: |
58
+ WHL=$(ls -1 dist/*.whl | head -1)
59
+ pip install "$WHL"
60
+ pip install pytest responses geopandas shapely pandas pyarrow typer rich keyring
61
+
62
+ - name: pytest on installed package
63
+ run: pytest -q -m "not integration"
64
+
65
+ coverage:
66
+ runs-on: ubuntu-latest
67
+ needs: contract
68
+ permissions:
69
+ id-token: write
70
+ env:
71
+ PYTHON_KEYRING_BACKEND: keyring.backends.null.Keyring
72
+ steps:
73
+ - uses: actions/checkout@v5
74
+ - uses: actions/setup-python@v5
75
+ with:
76
+ python-version: "3.12"
77
+ - run: pip install -e ".[dev,geo,secure]" pytest-cov
78
+ - run: pytest -q -m "not integration" --cov=eolas_data --cov-report=xml
79
+ - name: Upload coverage
80
+ uses: codecov/codecov-action@v5
81
+ with:
82
+ files: coverage.xml
83
+ token: ${{ secrets.CODECOV_TOKEN }}
84
+ fail_ci_if_error: false
@@ -0,0 +1,130 @@
1
+ # Changelog
2
+
3
+ All notable changes to `eolas-data` are recorded here. This project follows
4
+ [Semantic Versioning](https://semver.org/).
5
+
6
+ ## 1.9.1
7
+
8
+ ### Changed
9
+
10
+ - **`geometry=False` now skips the column at READ time, not after.** 1.9.0 read
11
+ the whole Parquet — WKT included — and dropped the column afterwards, paying
12
+ the full parse and peak memory for data discarded a line later. `get_local()`
13
+ now passes `columns=` to `pd.read_parquet()`, so those column chunks are never
14
+ decoded, and it deliberately avoids `gpd.read_parquet()` (which would
15
+ materialise shapely geometry we are about to throw away). `get()` passes the
16
+ flag down rather than applying it to the result.
17
+
18
+ Geometry is typically ~95% of a spatial layer's bytes: `geometry_wkt` is 262 of
19
+ 275 MB in `linz.nz_building_outlines`.
20
+
21
+ - `get_local()` gains a `geometry` argument, mirroring `get()`.
22
+
23
+ ### Unchanged (deliberately)
24
+
25
+ - What gets downloaded. One cached artifact still serves both variants, so
26
+ upgrading a non-spatial read back to spatial re-reads the same local file with
27
+ no re-download. A test asserts the cache file is byte-identical after a
28
+ `geometry=False` read. Reducing the *download* is a separate design question —
29
+ see `docs/geometry-fetch-design-2026-07-22.md` in the eolas repo.
30
+
31
+ ## 1.9.0
32
+
33
+ > Version jumps 1.4.0 -> 1.9.0. PyPI versions 1.5.0-1.8.0 were uploaded in May
34
+ > 2026 and later yanked; yanked filenames are permanently reserved, so those
35
+ > numbers can never be published again. The 1.9.0 jump clears the band for good.
36
+ > The R client moves to 1.9.0 at the same time so the two stay aligned.
37
+
38
+
39
+ ### Added
40
+
41
+ - **`geometry=False` on `get()`** — omit the `geometry_wkt` column. Two-thirds of
42
+ eolas datasets (1017/1536) carry geometry, and on TA/RC boundary tables the WKT
43
+ dwarfs the attributes. The column is projected away at the API's storage layer,
44
+ so it is never read from S3 or transferred — this cuts I/O, not just payload.
45
+ Responses carry `X-Eolas-Geometry-Omitted: true`.
46
+
47
+ ### Changed
48
+
49
+ - **Whole-dataset pulls of small spatial tables stay on the live path when
50
+ `geometry=False`.** Geometry was one of the two triggers for the API's
51
+ large-dataset (413) guard, so `get("some_boundary_table")` was transparently
52
+ diverted to a bulk download. The client now mirrors the server's relaxed guard.
53
+ The row-count trigger is unchanged — dropping a column doesn't reduce row count.
54
+
55
+ ### Fixed
56
+
57
+ - **`geometry=False` is now honoured on the bulk-routed path.** A spatial table
58
+ over the 100k-row threshold stays "blocked" even with `geometry=False`, so
59
+ `get()` routes it to the bulk cache — which has no server-side projection. The
60
+ flag was dropped at that hand-off, so the caller silently received the full
61
+ geometry-bearing file and, with `as_geo=None`, an auto-converted GeoDataFrame.
62
+ Found in peer review.
63
+ - **The in-memory response cache now keys on `geometry`.** Without it,
64
+ `get(x)` and `get(x, geometry=False)` shared a cache key, so whichever ran
65
+ second silently returned the other's shape (a whole column different). Only
66
+ reachable with `Client(cache=True)`.
67
+
68
+ ## 1.4.0
69
+
70
+ ### Added
71
+
72
+ - **`Client.preview(name)`** — up to 10 sample rows via the unauthenticated
73
+ `/preview` endpoint. No API key required, no rate-limit cost, geometry hidden.
74
+ `eolas datasets preview` now uses it instead of burning an authenticated,
75
+ rate-limited `/data` call (DRIFT-5).
76
+ - **`dimensions=` filter on `get()`** and `eolas get --dimensions` — a
77
+ case-insensitive substring filter on dimension columns, applied server-side on
78
+ the live `/data` path (the filter was live in prod but exposed by no client;
79
+ DRIFT-4).
80
+
81
+ ## 1.3.22
82
+
83
+ Network-hardening + correctness release, addressing findings from the
84
+ 2026-07-05 client-library audit.
85
+
86
+ ### Fixed
87
+
88
+ - **Request timeouts everywhere (EH-1).** Every HTTP call now carries a default
89
+ `(10, 300)` second (connect, read) timeout via a session subclass. A
90
+ black-holed connection raises instead of hanging the caller forever. Override
91
+ with `Client(timeout=...)`.
92
+ - **Clean errors on transport failures (EH-2).** Connection refused / DNS /
93
+ timeout / reset now surface as `EolasError("Network error talking to
94
+ api.eolas.fyi: ...")` instead of a raw urllib3 traceback. The CLI's existing
95
+ error handler renders them cleanly.
96
+ - **Atomic downloads (EH-5).** `download()` / `download_bulk()` write to a temp
97
+ file and atomically rename on success, and verify bytes received against
98
+ `Content-Length`. An interrupted or truncated download now leaves **no file**
99
+ at the final path instead of a silent partial.
100
+ - **`get()` no longer degrades a bulk failure to a misleading 413 (EH-3).** The
101
+ auto-route swallow is narrowed to the routing decision; a real failure in the
102
+ bulk path now propagates instead of falling through to "use bulk download".
103
+ - **Arrow negotiation no longer masks real errors (EH-7).** An auth/404/429
104
+ during the Arrow attempt now propagates immediately, without the bogus "Arrow
105
+ IPC unavailable" nag and without re-sending the failed request as JSON. Only a
106
+ non-Arrow 200 or a genuine pyarrow/IPC decode error downgrades the session.
107
+ - **Watermark floor parsed correctly (DRIFT-2).** The HTTP 410 re-baseline
108
+ response nests its fields under `detail`; `WatermarkExpired.min_available_seq`
109
+ was always 0. Now read from the correct level with a top-level fallback.
110
+ - **No more pandas-3 warning on every `get()` (PY-3).** Metadata accessors are
111
+ set via `object.__setattr__`, bypassing the pandas 3 `UserWarning`.
112
+ - **Orphaned partial downloads are swept (PY-5).** `*.eolas-tmp-*` files >24h
113
+ old are cleaned on download start; a full `cache_clear()` removes them all.
114
+ - **`download()` docstring corrected (PY-1).** It no longer claims to work for
115
+ "all datasets" — it is the live path and 413s on whole-dataset pulls of
116
+ large/geo tables; use `get()` or `download_bulk()` there.
117
+ - **`eolas datasets list` no longer renders a blank ghost column (CLI-1).** On
118
+ narrow (<100 col) terminals the title column is dropped with a hint instead of
119
+ being squeezed to zero width.
120
+ - **Bulk-download docs corrected (DRIFT-3).** `download_bulk()` / the CLI
121
+ `--freshness` help no longer claim Free plans get monthly bulk — bulk is
122
+ Pro/Enterprise and Free keys receive HTTP 402.
123
+
124
+ ### Changed
125
+
126
+ - **Single-sourced version (REL-1).** `__version__` is read from installed
127
+ distribution metadata, so it can never drift from `pyproject.toml` again
128
+ (shipped 1.3.21 previously self-reported 1.3.20). The publish workflow asserts
129
+ the installed `__version__` equals the release tag.
130
+ - **`merge_changes` is now a public export (DRIFT-6)** — added to `__all__`.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Virtus Solutions
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.