aioabrp 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.
@@ -0,0 +1,36 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ concurrency:
9
+ group: ci-${{ github.ref }}
10
+ cancel-in-progress: true
11
+
12
+ jobs:
13
+ ci:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v5
17
+
18
+ - name: Install uv
19
+ uses: astral-sh/setup-uv@v7
20
+ with:
21
+ python-version: "3.14"
22
+
23
+ - name: Install dependencies
24
+ run: uv sync --locked
25
+
26
+ - name: Check formatting
27
+ run: uv run ruff format --check .
28
+
29
+ - name: Lint
30
+ run: uv run ruff check .
31
+
32
+ - name: Type check
33
+ run: uv run mypy
34
+
35
+ - name: Test
36
+ run: uv run pytest
@@ -0,0 +1,63 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ release:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ # Required for PyPI Trusted Publishing (OIDC).
13
+ id-token: write
14
+ # Required to create the GitHub release.
15
+ contents: write
16
+ steps:
17
+ - uses: actions/checkout@v5
18
+ with:
19
+ persist-credentials: false
20
+ # Full history + tags so git-cliff can compute release notes.
21
+ fetch-depth: 0
22
+
23
+ - name: Install uv
24
+ uses: astral-sh/setup-uv@v7
25
+ with:
26
+ python-version: "3.14"
27
+
28
+ - name: Install dependencies
29
+ run: uv sync --locked
30
+
31
+ - name: Check formatting
32
+ run: uv run ruff format --check .
33
+
34
+ - name: Lint
35
+ run: uv run ruff check .
36
+
37
+ - name: Type check
38
+ run: uv run mypy
39
+
40
+ - name: Test
41
+ run: uv run pytest
42
+
43
+ - name: Build sdist and wheel
44
+ run: uv build
45
+
46
+ # `uv version --short` cannot read hatchling dynamic versions, so ask
47
+ # the package itself (src/aioabrp/__init__.py is the single source).
48
+ - name: Check tag matches project version
49
+ run: test "v$(uv run python -c 'import aioabrp; print(aioabrp.__version__)')" = "${GITHUB_REF_NAME}"
50
+
51
+ - name: Publish to PyPI
52
+ uses: pypa/gh-action-pypi-publish@release/v1
53
+
54
+ - name: Generate release notes from conventional commits
55
+ run: uv tool run git-cliff --latest --strip all -o "${RUNNER_TEMP}/RELEASE_NOTES.md"
56
+
57
+ - name: Create GitHub release
58
+ env:
59
+ GH_TOKEN: ${{ github.token }}
60
+ run: >-
61
+ gh release create "${GITHUB_REF_NAME}" dist/*
62
+ --title "${GITHUB_REF_NAME}"
63
+ --notes-file "${RUNNER_TEMP}/RELEASE_NOTES.md"
@@ -0,0 +1,25 @@
1
+ # Byte-compiled / caches
2
+ __pycache__/
3
+ *.py[cod]
4
+ .pytest_cache/
5
+ .mypy_cache/
6
+ .ruff_cache/
7
+
8
+ # Virtual environments
9
+ .venv/
10
+ venv/
11
+
12
+ # Build artifacts
13
+ build/
14
+ dist/
15
+ *.egg-info/
16
+
17
+ # Coverage
18
+ .coverage
19
+ .coverage.*
20
+ htmlcov/
21
+
22
+ # Editors / OS
23
+ .DS_Store
24
+ .idea/
25
+ .vscode/
aioabrp-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Martin Andersson
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.
aioabrp-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,218 @@
1
+ Metadata-Version: 2.4
2
+ Name: aioabrp
3
+ Version: 0.1.0
4
+ Summary: Async Python client for the A Better Routeplanner (ABRP) / Iternio telemetry API
5
+ Project-URL: Homepage, https://github.com/mtandersson/aioabrp
6
+ Project-URL: Repository, https://github.com/mtandersson/aioabrp
7
+ Project-URL: Changelog, https://github.com/mtandersson/aioabrp/releases
8
+ Project-URL: Issues, https://github.com/mtandersson/aioabrp/issues
9
+ Author: Martin Andersson
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.14
20
+ Requires-Dist: aiohttp
21
+ Description-Content-Type: text/markdown
22
+
23
+ # aioabrp
24
+
25
+ Async Python client for the [A Better Routeplanner](https://abetterrouteplanner.com)
26
+ (ABRP) / Iternio telemetry API. The library mirrors ABRP's API points 1:1 and
27
+ has no Home Assistant dependency: a stateless request/response `AbrpClient`
28
+ (garage, vehicle catalog, one-shot telemetry snapshot) and a resilient
29
+ `TelemetryStream` (server-sent events with reconnect/backoff/watchdog) that
30
+ delivers extracted, typed metric values — never raw wire dicts — to consumer
31
+ callbacks. Authentication is injected: the consumer owns the token lifecycle
32
+ and hands the library a fresh access token via `AbstractAuth` (or the
33
+ fixed-token `StaticAuth` convenience).
34
+
35
+ ## Installation
36
+
37
+ ```sh
38
+ pip install aioabrp
39
+ ```
40
+
41
+ Requires Python ≥ 3.14. The only runtime dependency is `aiohttp`.
42
+
43
+ ## Usage
44
+
45
+ A runnable standalone example (fill in a real partner API key and access
46
+ token before running — the calls below hit the live API):
47
+
48
+ ```python
49
+ import asyncio
50
+
51
+ import aiohttp
52
+
53
+ from aioabrp import (
54
+ AbrpClient,
55
+ ConnectionEvent,
56
+ StaticAuth,
57
+ Telemetry,
58
+ TelemetryStream,
59
+ )
60
+
61
+ API_KEY = "your-iternio-partner-api-key"
62
+ ACCESS_TOKEN = "your-abrp-access-token"
63
+
64
+
65
+ def on_update(vehicle_id: int, telemetry: Telemetry) -> None:
66
+ for metric, mv in telemetry.items():
67
+ print(f"vehicle {vehicle_id}: {metric} = {mv.value!r} (time={mv.time})")
68
+
69
+
70
+ def on_connection_change(event: ConnectionEvent) -> None:
71
+ print(f"connection: {event.state.name} (reason={event.reason})")
72
+
73
+
74
+ async def main() -> None:
75
+ async with aiohttp.ClientSession() as session:
76
+ auth = StaticAuth(ACCESS_TOKEN)
77
+ client = AbrpClient(session, API_KEY, auth)
78
+
79
+ vehicles = await client.async_get_vehicles()
80
+ for vehicle in vehicles:
81
+ print(f"{vehicle.vehicle_id}: {vehicle.name or vehicle.vehicle_model}")
82
+
83
+ stream = TelemetryStream(
84
+ session,
85
+ API_KEY,
86
+ auth,
87
+ vehicle_ids=[v.vehicle_id for v in vehicles],
88
+ on_update=on_update,
89
+ on_connection_change=on_connection_change,
90
+ )
91
+ await stream.start()
92
+ try:
93
+ await asyncio.sleep(600) # stream telemetry for ten minutes
94
+ finally:
95
+ await stream.stop()
96
+
97
+
98
+ if __name__ == "__main__":
99
+ asyncio.run(main())
100
+ ```
101
+
102
+ ### Get your own API key
103
+
104
+ The `api_key` constructor argument is an Iternio **partner API key**, not a
105
+ per-user credential. If you are building your own consumer, obtain your own
106
+ key from Iternio — see the
107
+ [Iternio Telemetry API documentation](https://documenter.getpostman.com/view/7396339/SWTK5a8w)
108
+ or contact Iternio for a partner API key. The per-user access token comes
109
+ from your own auth flow and is handed to the library through `AbstractAuth`.
110
+
111
+ ## Consumer contracts
112
+
113
+ These behaviors are pinned by the test suite; consumers may rely on them.
114
+
115
+ ### Callbacks
116
+
117
+ - `on_update` and `on_connection_change` are **synchronous** callbacks,
118
+ delivered on the event loop that ran `start()`. They must be
119
+ non-blocking — a slow callback stalls the stream (and every other stream
120
+ on the same loop).
121
+ - A raising callback is logged with its traceback and swallowed; the stream
122
+ continues (frame loss beats stream death).
123
+
124
+ ### Connection events
125
+
126
+ - `CONNECTED` fires on the **first frame** of a connection, not on the HTTP
127
+ connect — a connection that opens but never produces a frame keeps
128
+ reading as down until proven healthy.
129
+ - `DISCONNECTED` is **steady-state, not exceptional**: the ABRP server
130
+ unilaterally closes idle streams at roughly 200 s and the stream
131
+ reconnects with backoff. Do not treat a `DISCONNECTED` event as an
132
+ outage. Events are status reports, not strict state transitions — a
133
+ `DISCONNECTED` MAY arrive before the first `CONNECTED` (for example when
134
+ the very first connection attempt fails).
135
+ - `AUTH_FAILED` is **terminal**: the stream stops itself and will not
136
+ retry. The consumer decides whether/when to restart with fresh
137
+ credentials.
138
+ - A transient (non-`AbrpAuthError`) failure from the token getter emits
139
+ **no** `ConnectionEvent` at all (debug log only) — no connection attempt
140
+ was made, so consumers keep seeing the last known state during a
141
+ token-endpoint outage.
142
+
143
+ ### Lifecycle
144
+
145
+ - `stop()` is idempotent and cancel-based (never a graceful join). No
146
+ callbacks fire after `stop()` returns.
147
+ - `stop()` propagates the **caller's own** cancellation: if you wrap it in
148
+ `asyncio.wait_for(stream.stop(), timeout)` and that times out, the
149
+ resulting `TimeoutError` means the stream **may still be running** — the
150
+ stop was interrupted, not completed.
151
+ - `start()` after `stop()` restarts the stream.
152
+
153
+ ### Monotonicity gate
154
+
155
+ Each stream keeps one piece of state: a per-`(vehicle_id, Metric)` map of
156
+ the last adopted block timestamp.
157
+
158
+ - A block whose `time` is **strictly older** than the last adopted time for
159
+ that `(vehicle, metric)` is dropped.
160
+ - An **equal-time** block re-emits **by design**: every reconnect
161
+ re-delivers a full-state snapshot with unchanged block times, and
162
+ consumers rely on that backfill.
163
+ - A **time-less** block (missing/malformed/naive `time`) is adopted and
164
+ **clears** the gate for that metric — it carries no ordering claim, so it
165
+ also stops gating subsequent values.
166
+ - A block whose `time` is in the **future** is rewritten to "now" before
167
+ delivery (and before gating), so a clock-skewed upstream stamp can neither
168
+ be delivered to the consumer nor become an unreachable high-water mark that
169
+ silently stalls the metric. `AbrpClient.async_get_current_telemetry` applies
170
+ the same clamp (it does not gate).
171
+ - **Known limitation:** a legitimately backdated server correction (an
172
+ older timestamp that really is a newer truth) is suppressed for the
173
+ stream's lifetime.
174
+
175
+ The gate can be **pre-warmed** across process restarts: pass
176
+ `TelemetryStream(..., seed=Mapping[int, Mapping[Metric, datetime]])` — a
177
+ per-vehicle map of metric to its last wire-block time (e.g. derived from the
178
+ consumer's last persisted snapshot) — and the stream seeds its high-water marks
179
+ from those times, each clamped not-future. Only times are needed; no typed
180
+ values. The clock is the `aioabrp._clock._now` seam (the monkeypatch target in
181
+ tests).
182
+
183
+ ### Logging
184
+
185
+ Enable the `aioabrp` logger at `DEBUG` for triage. Connect/disconnect and
186
+ reasons log at `INFO`, watchdog stalls at `WARNING`, per-frame activity at
187
+ `DEBUG`. Frames are logged as keys and sizes only — frame bodies, header
188
+ values, and tokens (including GPS coordinates and other PII) are never
189
+ logged. Pass `name=` to `TelemetryStream` to prefix its log lines when
190
+ running multiple streams.
191
+
192
+ ## Development
193
+
194
+ This project uses [uv](https://docs.astral.sh/uv/):
195
+
196
+ ```sh
197
+ uv sync
198
+ uv run ruff format . && uv run ruff check . && uv run mypy && uv run pytest
199
+ ```
200
+
201
+ Note: the locked dev environment constrains `aiohttp<3.14` because the
202
+ latest `aioresponses` release is incompatible with aiohttp ≥ 3.14
203
+ ([aioresponses#289](https://github.com/pnuckowski/aioresponses/issues/289));
204
+ the published runtime dependency stays unpinned.
205
+
206
+ ### Releases
207
+
208
+ Commits follow [Conventional Commits](https://www.conventionalcommits.org/);
209
+ there is no CHANGELOG file — release notes are generated from the commit
210
+ history by [git-cliff](https://git-cliff.org/) (`cliff.toml`) and published
211
+ as GitHub Releases. Pushing a `v*` tag runs the quality gate, builds the
212
+ sdist + wheel, verifies the tag matches `aioabrp.__version__`, publishes to
213
+ PyPI via [Trusted Publishing](https://docs.pypi.org/trusted-publishers/)
214
+ (OIDC), and creates the GitHub Release.
215
+
216
+ ## License
217
+
218
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,196 @@
1
+ # aioabrp
2
+
3
+ Async Python client for the [A Better Routeplanner](https://abetterrouteplanner.com)
4
+ (ABRP) / Iternio telemetry API. The library mirrors ABRP's API points 1:1 and
5
+ has no Home Assistant dependency: a stateless request/response `AbrpClient`
6
+ (garage, vehicle catalog, one-shot telemetry snapshot) and a resilient
7
+ `TelemetryStream` (server-sent events with reconnect/backoff/watchdog) that
8
+ delivers extracted, typed metric values — never raw wire dicts — to consumer
9
+ callbacks. Authentication is injected: the consumer owns the token lifecycle
10
+ and hands the library a fresh access token via `AbstractAuth` (or the
11
+ fixed-token `StaticAuth` convenience).
12
+
13
+ ## Installation
14
+
15
+ ```sh
16
+ pip install aioabrp
17
+ ```
18
+
19
+ Requires Python ≥ 3.14. The only runtime dependency is `aiohttp`.
20
+
21
+ ## Usage
22
+
23
+ A runnable standalone example (fill in a real partner API key and access
24
+ token before running — the calls below hit the live API):
25
+
26
+ ```python
27
+ import asyncio
28
+
29
+ import aiohttp
30
+
31
+ from aioabrp import (
32
+ AbrpClient,
33
+ ConnectionEvent,
34
+ StaticAuth,
35
+ Telemetry,
36
+ TelemetryStream,
37
+ )
38
+
39
+ API_KEY = "your-iternio-partner-api-key"
40
+ ACCESS_TOKEN = "your-abrp-access-token"
41
+
42
+
43
+ def on_update(vehicle_id: int, telemetry: Telemetry) -> None:
44
+ for metric, mv in telemetry.items():
45
+ print(f"vehicle {vehicle_id}: {metric} = {mv.value!r} (time={mv.time})")
46
+
47
+
48
+ def on_connection_change(event: ConnectionEvent) -> None:
49
+ print(f"connection: {event.state.name} (reason={event.reason})")
50
+
51
+
52
+ async def main() -> None:
53
+ async with aiohttp.ClientSession() as session:
54
+ auth = StaticAuth(ACCESS_TOKEN)
55
+ client = AbrpClient(session, API_KEY, auth)
56
+
57
+ vehicles = await client.async_get_vehicles()
58
+ for vehicle in vehicles:
59
+ print(f"{vehicle.vehicle_id}: {vehicle.name or vehicle.vehicle_model}")
60
+
61
+ stream = TelemetryStream(
62
+ session,
63
+ API_KEY,
64
+ auth,
65
+ vehicle_ids=[v.vehicle_id for v in vehicles],
66
+ on_update=on_update,
67
+ on_connection_change=on_connection_change,
68
+ )
69
+ await stream.start()
70
+ try:
71
+ await asyncio.sleep(600) # stream telemetry for ten minutes
72
+ finally:
73
+ await stream.stop()
74
+
75
+
76
+ if __name__ == "__main__":
77
+ asyncio.run(main())
78
+ ```
79
+
80
+ ### Get your own API key
81
+
82
+ The `api_key` constructor argument is an Iternio **partner API key**, not a
83
+ per-user credential. If you are building your own consumer, obtain your own
84
+ key from Iternio — see the
85
+ [Iternio Telemetry API documentation](https://documenter.getpostman.com/view/7396339/SWTK5a8w)
86
+ or contact Iternio for a partner API key. The per-user access token comes
87
+ from your own auth flow and is handed to the library through `AbstractAuth`.
88
+
89
+ ## Consumer contracts
90
+
91
+ These behaviors are pinned by the test suite; consumers may rely on them.
92
+
93
+ ### Callbacks
94
+
95
+ - `on_update` and `on_connection_change` are **synchronous** callbacks,
96
+ delivered on the event loop that ran `start()`. They must be
97
+ non-blocking — a slow callback stalls the stream (and every other stream
98
+ on the same loop).
99
+ - A raising callback is logged with its traceback and swallowed; the stream
100
+ continues (frame loss beats stream death).
101
+
102
+ ### Connection events
103
+
104
+ - `CONNECTED` fires on the **first frame** of a connection, not on the HTTP
105
+ connect — a connection that opens but never produces a frame keeps
106
+ reading as down until proven healthy.
107
+ - `DISCONNECTED` is **steady-state, not exceptional**: the ABRP server
108
+ unilaterally closes idle streams at roughly 200 s and the stream
109
+ reconnects with backoff. Do not treat a `DISCONNECTED` event as an
110
+ outage. Events are status reports, not strict state transitions — a
111
+ `DISCONNECTED` MAY arrive before the first `CONNECTED` (for example when
112
+ the very first connection attempt fails).
113
+ - `AUTH_FAILED` is **terminal**: the stream stops itself and will not
114
+ retry. The consumer decides whether/when to restart with fresh
115
+ credentials.
116
+ - A transient (non-`AbrpAuthError`) failure from the token getter emits
117
+ **no** `ConnectionEvent` at all (debug log only) — no connection attempt
118
+ was made, so consumers keep seeing the last known state during a
119
+ token-endpoint outage.
120
+
121
+ ### Lifecycle
122
+
123
+ - `stop()` is idempotent and cancel-based (never a graceful join). No
124
+ callbacks fire after `stop()` returns.
125
+ - `stop()` propagates the **caller's own** cancellation: if you wrap it in
126
+ `asyncio.wait_for(stream.stop(), timeout)` and that times out, the
127
+ resulting `TimeoutError` means the stream **may still be running** — the
128
+ stop was interrupted, not completed.
129
+ - `start()` after `stop()` restarts the stream.
130
+
131
+ ### Monotonicity gate
132
+
133
+ Each stream keeps one piece of state: a per-`(vehicle_id, Metric)` map of
134
+ the last adopted block timestamp.
135
+
136
+ - A block whose `time` is **strictly older** than the last adopted time for
137
+ that `(vehicle, metric)` is dropped.
138
+ - An **equal-time** block re-emits **by design**: every reconnect
139
+ re-delivers a full-state snapshot with unchanged block times, and
140
+ consumers rely on that backfill.
141
+ - A **time-less** block (missing/malformed/naive `time`) is adopted and
142
+ **clears** the gate for that metric — it carries no ordering claim, so it
143
+ also stops gating subsequent values.
144
+ - A block whose `time` is in the **future** is rewritten to "now" before
145
+ delivery (and before gating), so a clock-skewed upstream stamp can neither
146
+ be delivered to the consumer nor become an unreachable high-water mark that
147
+ silently stalls the metric. `AbrpClient.async_get_current_telemetry` applies
148
+ the same clamp (it does not gate).
149
+ - **Known limitation:** a legitimately backdated server correction (an
150
+ older timestamp that really is a newer truth) is suppressed for the
151
+ stream's lifetime.
152
+
153
+ The gate can be **pre-warmed** across process restarts: pass
154
+ `TelemetryStream(..., seed=Mapping[int, Mapping[Metric, datetime]])` — a
155
+ per-vehicle map of metric to its last wire-block time (e.g. derived from the
156
+ consumer's last persisted snapshot) — and the stream seeds its high-water marks
157
+ from those times, each clamped not-future. Only times are needed; no typed
158
+ values. The clock is the `aioabrp._clock._now` seam (the monkeypatch target in
159
+ tests).
160
+
161
+ ### Logging
162
+
163
+ Enable the `aioabrp` logger at `DEBUG` for triage. Connect/disconnect and
164
+ reasons log at `INFO`, watchdog stalls at `WARNING`, per-frame activity at
165
+ `DEBUG`. Frames are logged as keys and sizes only — frame bodies, header
166
+ values, and tokens (including GPS coordinates and other PII) are never
167
+ logged. Pass `name=` to `TelemetryStream` to prefix its log lines when
168
+ running multiple streams.
169
+
170
+ ## Development
171
+
172
+ This project uses [uv](https://docs.astral.sh/uv/):
173
+
174
+ ```sh
175
+ uv sync
176
+ uv run ruff format . && uv run ruff check . && uv run mypy && uv run pytest
177
+ ```
178
+
179
+ Note: the locked dev environment constrains `aiohttp<3.14` because the
180
+ latest `aioresponses` release is incompatible with aiohttp ≥ 3.14
181
+ ([aioresponses#289](https://github.com/pnuckowski/aioresponses/issues/289));
182
+ the published runtime dependency stays unpinned.
183
+
184
+ ### Releases
185
+
186
+ Commits follow [Conventional Commits](https://www.conventionalcommits.org/);
187
+ there is no CHANGELOG file — release notes are generated from the commit
188
+ history by [git-cliff](https://git-cliff.org/) (`cliff.toml`) and published
189
+ as GitHub Releases. Pushing a `v*` tag runs the quality gate, builds the
190
+ sdist + wheel, verifies the tag matches `aioabrp.__version__`, publishes to
191
+ PyPI via [Trusted Publishing](https://docs.pypi.org/trusted-publishers/)
192
+ (OIDC), and creates the GitHub Release.
193
+
194
+ ## License
195
+
196
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,35 @@
1
+ # git-cliff configuration — release notes are generated from Conventional
2
+ # Commits (https://www.conventionalcommits.org) by the release workflow.
3
+ # https://git-cliff.org/docs/configuration
4
+
5
+ [changelog]
6
+ header = ""
7
+ body = """
8
+ {% for group, commits in commits | group_by(attribute="group") %}
9
+ ### {{ group | striptags | trim | upper_first }}
10
+ {% for commit in commits %}
11
+ - {{ commit.message | split(pat="\n") | first | upper_first }} ({{ commit.id | truncate(length=7, end="") }})
12
+ {%- endfor %}
13
+ {% endfor %}
14
+ """
15
+ footer = ""
16
+ trim = true
17
+
18
+ [git]
19
+ conventional_commits = true
20
+ # Unconventional commits still land in release notes (under "Other") rather
21
+ # than vanishing silently.
22
+ filter_unconventional = false
23
+ protect_breaking_commits = true
24
+ tag_pattern = "v[0-9].*"
25
+ sort_commits = "oldest"
26
+ commit_parsers = [
27
+ { message = "^feat", group = "<!-- 0 -->Features" },
28
+ { message = "^fix", group = "<!-- 1 -->Bug fixes" },
29
+ { message = "^perf", group = "<!-- 2 -->Performance" },
30
+ { message = "^docs?", group = "<!-- 3 -->Documentation" },
31
+ { message = "^refactor", group = "<!-- 4 -->Refactoring" },
32
+ { message = "^test", group = "<!-- 5 -->Testing" },
33
+ { message = "^(build|ci|chore)", group = "<!-- 6 -->Maintenance" },
34
+ { message = ".*", group = "<!-- 7 -->Other" },
35
+ ]
@@ -0,0 +1,100 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "aioabrp"
7
+ dynamic = ["version"]
8
+ description = "Async Python client for the A Better Routeplanner (ABRP) / Iternio telemetry API"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [{ name = "Martin Andersson" }]
13
+ requires-python = ">=3.14"
14
+ dependencies = ["aiohttp"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.14",
21
+ "Topic :: Software Development :: Libraries :: Python Modules",
22
+ "Typing :: Typed",
23
+ ]
24
+
25
+ [project.urls]
26
+ Homepage = "https://github.com/mtandersson/aioabrp"
27
+ Repository = "https://github.com/mtandersson/aioabrp"
28
+ Changelog = "https://github.com/mtandersson/aioabrp/releases"
29
+ Issues = "https://github.com/mtandersson/aioabrp/issues"
30
+
31
+ [dependency-groups]
32
+ dev = [
33
+ "aioresponses",
34
+ "mypy",
35
+ "pytest",
36
+ "pytest-asyncio",
37
+ "ruff",
38
+ ]
39
+
40
+ [tool.uv]
41
+ # aioresponses 0.7.8 (latest) is incompatible with aiohttp >= 3.14, which made
42
+ # ClientResponse.__init__'s `stream_writer` argument required
43
+ # (https://github.com/pnuckowski/aioresponses/issues/289). Constrain the locked
44
+ # dev environment until an aioresponses release supports aiohttp 3.14; the
45
+ # published runtime dependency stays unpinned.
46
+ constraint-dependencies = ["aiohttp<3.14"]
47
+
48
+ [tool.hatch.version]
49
+ path = "src/aioabrp/__init__.py"
50
+
51
+ [tool.hatch.build.targets.wheel]
52
+ packages = ["src/aioabrp"]
53
+
54
+ [tool.hatch.build.targets.sdist]
55
+ # Local-only planning files are untracked via .git/info/exclude, which
56
+ # hatchling (unlike .gitignore) does not honor — exclude them explicitly
57
+ # so they never ship in the sdist.
58
+ exclude = ["PLAN.md", "docs/"]
59
+
60
+ [tool.ruff]
61
+ line-length = 88
62
+ target-version = "py314"
63
+
64
+ [tool.ruff.lint]
65
+ select = [
66
+ "A", # flake8-builtins
67
+ "ASYNC", # flake8-async
68
+ "B", # flake8-bugbear
69
+ "C4", # flake8-comprehensions
70
+ "D", # pydocstyle
71
+ "E", # pycodestyle errors
72
+ "F", # pyflakes
73
+ "I", # isort
74
+ "ISC", # flake8-implicit-str-concat
75
+ "N", # pep8-naming
76
+ "PIE", # flake8-pie
77
+ "PT", # flake8-pytest-style
78
+ "RET", # flake8-return
79
+ "RUF", # ruff-specific rules
80
+ "SIM", # flake8-simplify
81
+ "T20", # flake8-print
82
+ "UP", # pyupgrade
83
+ "W", # pycodestyle warnings
84
+ ]
85
+
86
+ [tool.ruff.lint.per-file-ignores]
87
+ "tests/**" = ["D1"]
88
+
89
+ [tool.ruff.lint.pydocstyle]
90
+ convention = "pep257"
91
+
92
+ [tool.mypy]
93
+ python_version = "3.14"
94
+ strict = true
95
+ files = ["src", "tests"]
96
+
97
+ [tool.pytest.ini_options]
98
+ testpaths = ["tests"]
99
+ asyncio_mode = "auto"
100
+ asyncio_default_fixture_loop_scope = "function"