eolas-data 1.7.0__tar.gz → 1.9.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 (58) hide show
  1. {eolas_data-1.7.0 → eolas_data-1.9.0}/.github/workflows/catalog-drift.yml +1 -1
  2. eolas_data-1.9.0/.github/workflows/pre-release.yml +50 -0
  3. eolas_data-1.9.0/.github/workflows/publish.yml +143 -0
  4. eolas_data-1.9.0/.github/workflows/smoke.yml +39 -0
  5. eolas_data-1.9.0/.github/workflows/test.yml +84 -0
  6. eolas_data-1.9.0/CHANGELOG.md +105 -0
  7. eolas_data-1.9.0/LICENSE +21 -0
  8. eolas_data-1.9.0/PKG-INFO +364 -0
  9. eolas_data-1.9.0/README.md +312 -0
  10. eolas_data-1.9.0/eolas_data/__init__.py +47 -0
  11. {eolas_data-1.7.0 → eolas_data-1.9.0}/eolas_data/_dataset_names.py +299 -43
  12. eolas_data-1.9.0/eolas_data/_regen_names.py +58 -0
  13. eolas_data-1.9.0/eolas_data/cdc.py +233 -0
  14. eolas_data-1.9.0/eolas_data/cli.py +1583 -0
  15. eolas_data-1.9.0/eolas_data/client.py +2796 -0
  16. eolas_data-1.9.0/eolas_data/console.py +25 -0
  17. {eolas_data-1.7.0 → eolas_data-1.9.0}/eolas_data/dataset.py +17 -3
  18. eolas_data-1.9.0/eolas_data/exceptions.py +94 -0
  19. eolas_data-1.9.0/eolas_data/library.py +408 -0
  20. eolas_data-1.9.0/eolas_data/meta.py +232 -0
  21. eolas_data-1.9.0/eolas_data/rows.py +36 -0
  22. {eolas_data-1.7.0 → eolas_data-1.9.0}/eolas_data/schedule.py +19 -8
  23. eolas_data-1.9.0/eolas_data/search.py +133 -0
  24. {eolas_data-1.7.0 → eolas_data-1.9.0}/pyproject.toml +23 -3
  25. eolas_data-1.9.0/scripts/check_client_contract.py +61 -0
  26. eolas_data-1.9.0/scripts/preflight.sh +43 -0
  27. eolas_data-1.9.0/tests/fixtures/source_helpers.json +37 -0
  28. eolas_data-1.9.0/tests/test_as_arrow.py +51 -0
  29. eolas_data-1.9.0/tests/test_cdc_roundtrip.py +166 -0
  30. eolas_data-1.9.0/tests/test_changes_http.py +74 -0
  31. {eolas_data-1.7.0 → eolas_data-1.9.0}/tests/test_cli.py +184 -3
  32. eolas_data-1.9.0/tests/test_client.py +692 -0
  33. eolas_data-1.9.0/tests/test_date_filter.py +81 -0
  34. eolas_data-1.9.0/tests/test_geometry_param.py +235 -0
  35. eolas_data-1.9.0/tests/test_get_local.py +276 -0
  36. eolas_data-1.9.0/tests/test_keyring.py +266 -0
  37. eolas_data-1.9.0/tests/test_library.py +235 -0
  38. eolas_data-1.9.0/tests/test_meta.py +101 -0
  39. eolas_data-1.9.0/tests/test_preview_dimensions.py +82 -0
  40. eolas_data-1.9.0/tests/test_progress.py +518 -0
  41. eolas_data-1.9.0/tests/test_resilience.py +95 -0
  42. eolas_data-1.9.0/tests/test_rows.py +24 -0
  43. {eolas_data-1.7.0 → eolas_data-1.9.0}/tests/test_schedule.py +10 -1
  44. eolas_data-1.9.0/tests/test_search.py +77 -0
  45. eolas_data-1.9.0/tests/test_smoke_live.py +85 -0
  46. eolas_data-1.9.0/tests/test_source_helpers.py +36 -0
  47. eolas_data-1.9.0/tests/test_sync_bulk.py +499 -0
  48. eolas_data-1.9.0/tests/test_sync_changes.py +791 -0
  49. eolas_data-1.7.0/.github/workflows/publish.yml +0 -31
  50. eolas_data-1.7.0/PKG-INFO +0 -216
  51. eolas_data-1.7.0/README.md +0 -174
  52. eolas_data-1.7.0/eolas_data/__init__.py +0 -16
  53. eolas_data-1.7.0/eolas_data/_regen_names.py +0 -57
  54. eolas_data-1.7.0/eolas_data/cli.py +0 -639
  55. eolas_data-1.7.0/eolas_data/client.py +0 -400
  56. eolas_data-1.7.0/eolas_data/exceptions.py +0 -20
  57. eolas_data-1.7.0/tests/test_client.py +0 -337
  58. {eolas_data-1.7.0 → eolas_data-1.9.0}/.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,105 @@
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.0
7
+
8
+ > Version jumps 1.4.0 -> 1.9.0. PyPI versions 1.5.0-1.8.0 were uploaded in May
9
+ > 2026 and later yanked; yanked filenames are permanently reserved, so those
10
+ > numbers can never be published again. The 1.9.0 jump clears the band for good.
11
+ > The R client moves to 1.9.0 at the same time so the two stay aligned.
12
+
13
+
14
+ ### Added
15
+
16
+ - **`geometry=False` on `get()`** — omit the `geometry_wkt` column. Two-thirds of
17
+ eolas datasets (1017/1536) carry geometry, and on TA/RC boundary tables the WKT
18
+ dwarfs the attributes. The column is projected away at the API's storage layer,
19
+ so it is never read from S3 or transferred — this cuts I/O, not just payload.
20
+ Responses carry `X-Eolas-Geometry-Omitted: true`.
21
+
22
+ ### Changed
23
+
24
+ - **Whole-dataset pulls of small spatial tables stay on the live path when
25
+ `geometry=False`.** Geometry was one of the two triggers for the API's
26
+ large-dataset (413) guard, so `get("some_boundary_table")` was transparently
27
+ diverted to a bulk download. The client now mirrors the server's relaxed guard.
28
+ The row-count trigger is unchanged — dropping a column doesn't reduce row count.
29
+
30
+ ### Fixed
31
+
32
+ - **`geometry=False` is now honoured on the bulk-routed path.** A spatial table
33
+ over the 100k-row threshold stays "blocked" even with `geometry=False`, so
34
+ `get()` routes it to the bulk cache — which has no server-side projection. The
35
+ flag was dropped at that hand-off, so the caller silently received the full
36
+ geometry-bearing file and, with `as_geo=None`, an auto-converted GeoDataFrame.
37
+ Found in peer review.
38
+ - **The in-memory response cache now keys on `geometry`.** Without it,
39
+ `get(x)` and `get(x, geometry=False)` shared a cache key, so whichever ran
40
+ second silently returned the other's shape (a whole column different). Only
41
+ reachable with `Client(cache=True)`.
42
+
43
+ ## 1.4.0
44
+
45
+ ### Added
46
+
47
+ - **`Client.preview(name)`** — up to 10 sample rows via the unauthenticated
48
+ `/preview` endpoint. No API key required, no rate-limit cost, geometry hidden.
49
+ `eolas datasets preview` now uses it instead of burning an authenticated,
50
+ rate-limited `/data` call (DRIFT-5).
51
+ - **`dimensions=` filter on `get()`** and `eolas get --dimensions` — a
52
+ case-insensitive substring filter on dimension columns, applied server-side on
53
+ the live `/data` path (the filter was live in prod but exposed by no client;
54
+ DRIFT-4).
55
+
56
+ ## 1.3.22
57
+
58
+ Network-hardening + correctness release, addressing findings from the
59
+ 2026-07-05 client-library audit.
60
+
61
+ ### Fixed
62
+
63
+ - **Request timeouts everywhere (EH-1).** Every HTTP call now carries a default
64
+ `(10, 300)` second (connect, read) timeout via a session subclass. A
65
+ black-holed connection raises instead of hanging the caller forever. Override
66
+ with `Client(timeout=...)`.
67
+ - **Clean errors on transport failures (EH-2).** Connection refused / DNS /
68
+ timeout / reset now surface as `EolasError("Network error talking to
69
+ api.eolas.fyi: ...")` instead of a raw urllib3 traceback. The CLI's existing
70
+ error handler renders them cleanly.
71
+ - **Atomic downloads (EH-5).** `download()` / `download_bulk()` write to a temp
72
+ file and atomically rename on success, and verify bytes received against
73
+ `Content-Length`. An interrupted or truncated download now leaves **no file**
74
+ at the final path instead of a silent partial.
75
+ - **`get()` no longer degrades a bulk failure to a misleading 413 (EH-3).** The
76
+ auto-route swallow is narrowed to the routing decision; a real failure in the
77
+ bulk path now propagates instead of falling through to "use bulk download".
78
+ - **Arrow negotiation no longer masks real errors (EH-7).** An auth/404/429
79
+ during the Arrow attempt now propagates immediately, without the bogus "Arrow
80
+ IPC unavailable" nag and without re-sending the failed request as JSON. Only a
81
+ non-Arrow 200 or a genuine pyarrow/IPC decode error downgrades the session.
82
+ - **Watermark floor parsed correctly (DRIFT-2).** The HTTP 410 re-baseline
83
+ response nests its fields under `detail`; `WatermarkExpired.min_available_seq`
84
+ was always 0. Now read from the correct level with a top-level fallback.
85
+ - **No more pandas-3 warning on every `get()` (PY-3).** Metadata accessors are
86
+ set via `object.__setattr__`, bypassing the pandas 3 `UserWarning`.
87
+ - **Orphaned partial downloads are swept (PY-5).** `*.eolas-tmp-*` files >24h
88
+ old are cleaned on download start; a full `cache_clear()` removes them all.
89
+ - **`download()` docstring corrected (PY-1).** It no longer claims to work for
90
+ "all datasets" — it is the live path and 413s on whole-dataset pulls of
91
+ large/geo tables; use `get()` or `download_bulk()` there.
92
+ - **`eolas datasets list` no longer renders a blank ghost column (CLI-1).** On
93
+ narrow (<100 col) terminals the title column is dropped with a hint instead of
94
+ being squeezed to zero width.
95
+ - **Bulk-download docs corrected (DRIFT-3).** `download_bulk()` / the CLI
96
+ `--freshness` help no longer claim Free plans get monthly bulk — bulk is
97
+ Pro/Enterprise and Free keys receive HTTP 402.
98
+
99
+ ### Changed
100
+
101
+ - **Single-sourced version (REL-1).** `__version__` is read from installed
102
+ distribution metadata, so it can never drift from `pyproject.toml` again
103
+ (shipped 1.3.21 previously self-reported 1.3.20). The publish workflow asserts
104
+ the installed `__version__` equals the release tag.
105
+ - **`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.