pymapflights 0.2.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,11 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ build/
6
+ .coverage
7
+ coverage.xml
8
+ htmlcov/
9
+ .mypy_cache/
10
+ .pytest_cache/
11
+ .ruff_cache/
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0
4
+
5
+ First release.
6
+
7
+ ### Added
8
+
9
+ - `MapFlights` client with an externally owned `aiohttp` session
10
+ - Stable endpoints: `near()`, `aircraft_flights()`, `airlines()`
11
+ - Experimental endpoints: `me()`, `day_board()`, `flight_by_callsign()`
12
+ - `key_prefix()` to get the non-secret key prefix without a request
13
+ - Per-key rate limiting shared by all clients of the process: 5 s live
14
+ slot, 60 requests per minute, blocking per 429 scope until `Retry-After`
15
+ - `live_rate_limit`, `daily_rate_limit`, `remaining_today` and
16
+ `attribution` from the response headers
17
+ - Exceptions for 401, every 403 tariff code, 404 (with
18
+ `MapFlightsNoCurrentFlightError` for `flight_by_callsign`), 429,
19
+ timeouts and network errors
20
+ - Typed models (`py.typed`), timestamps as aware UTC `datetime`
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Olli
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.
@@ -0,0 +1,148 @@
1
+ Metadata-Version: 2.5
2
+ Name: pymapflights
3
+ Version: 0.2.0
4
+ Summary: Asynchronous Python client for the map.flights API (unofficial)
5
+ Project-URL: Homepage, https://github.com/olli-dot-dev/pymapflights
6
+ Project-URL: Issues, https://github.com/olli-dot-dev/pymapflights/issues
7
+ Author-email: Olli <1@olli.dev>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: ads-b,aircraft,async,flights,home-assistant,map.flights
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Framework :: AsyncIO
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.12
19
+ Requires-Dist: aiohttp>=3.9.0
20
+ Requires-Dist: mashumaro>=3.13
21
+ Requires-Dist: yarl>=1.9.0
22
+ Description-Content-Type: text/markdown
23
+
24
+ # pymapflights
25
+
26
+ Asynchronous Python client for the [map.flights](https://map.flights) API.
27
+
28
+ > **Unofficial.** This library is not made or endorsed by map.flights. It is the
29
+ > API layer of the Home Assistant integration
30
+ > [ha-mapflights](https://github.com/olli-dot-dev/ha-mapflights).
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ pip install pymapflights
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ ```python
41
+ import asyncio
42
+
43
+ from aiohttp import ClientSession
44
+
45
+ from pymapflights import MapFlights
46
+
47
+
48
+ async def main() -> None:
49
+ async with ClientSession() as session:
50
+ client = MapFlights(
51
+ "YOUR_API_KEY",
52
+ session=session,
53
+ user_agent="my-app/1.0 (+https://example.com)",
54
+ )
55
+ result = await client.near(48.11, 16.57, 25, limit=100)
56
+ print(f"{result.total} aircraft within 25 km")
57
+ for aircraft in result.aircraft:
58
+ print(aircraft.callsign, aircraft.distance_km, aircraft.alt_baro)
59
+ print(client.attribution)
60
+
61
+
62
+ asyncio.run(main())
63
+ ```
64
+
65
+ Notes:
66
+
67
+ - `total` is the number of aircraft in the radius; `aircraft` is capped by
68
+ `limit` (API default 50, max 500). `result.truncated` tells you when the list
69
+ is incomplete.
70
+ - When `result.ready` is `False`, a delayed tariff is still filling its buffer
71
+ and the empty list does not mean there is no traffic.
72
+ - The session belongs to you; the client never closes it.
73
+
74
+ ## Endpoints
75
+
76
+ | Method | Endpoint | Stability | Quota |
77
+ |---|---|---|---|
78
+ | `near(lat, lon, radius_km, limit=)` | `/near/{lat}/{lon}/{radius}` | stable | live (1 per 5 s) |
79
+ | `aircraft_flights(hex_id, limit=)` | `/aircraft/{hex}/flights` | stable | daily |
80
+ | `airlines(query=, limit=, offset=)` | `/airlines` | stable | daily |
81
+ | `me()` | `/me` | experimental | daily |
82
+ | `day_board(airport, "arr" \| "dep", day=)` | `/airports/{icao}/day-board` | experimental | daily |
83
+ | `flight_by_callsign(callsign)` | `/flights/by-callsign/{callsign}` | experimental | daily |
84
+
85
+ Experimental endpoints may change within `/api/v1`; their models make every
86
+ field optional. `key_prefix(api_key)` returns the non-secret key prefix
87
+ (`mf_live_` + 8 hex) without a request.
88
+
89
+ `flight_by_callsign()` raises `MapFlightsNoCurrentFlightError` when there is
90
+ no current flight (`reason` is `no current flight` with `last_flight`, or
91
+ `unknown flight`). That is a normal state, but it still counts against the
92
+ daily quota. A landing appears there only 20 to 35 minutes after touchdown.
93
+
94
+ ## API keys and terms
95
+
96
+ Keys are issued by map.flights (operators of a receiving station can create a
97
+ free key at <https://map.flights/account/api-key>). Use of the API is subject to
98
+ the [map.flights API terms](https://map.flights/api-terms). Attribution is
99
+ mandatory: show at least "Data: map.flights"; the full text of the last response
100
+ is in `client.attribution`.
101
+
102
+ ### Rate limits
103
+
104
+ The client enforces the limits per key, shared by every client in the process
105
+ that uses the same key:
106
+
107
+ - **Live** (`near`): one request every 5 s. A call waits for its slot. Every
108
+ call uses the slot, even one that fails (a `near` outside the key's area too).
109
+ - **Minute**: at most 60 requests per minute over all endpoints.
110
+ - **After a 429** the client remembers `Retry-After` for that scope (`live`,
111
+ `minute` or `day`) and refuses further requests of that scope until then.
112
+
113
+ A refused request raises `MapFlightsRateLimitError` with `local=True` and is
114
+ never sent. `retry_after` and `scope` are set either way.
115
+
116
+ `client.live_rate_limit` and `client.daily_rate_limit` hold the
117
+ `X-RateLimit-*` headers of the last live and the last other response. For live
118
+ requests they describe the live bucket, not the daily quota, so
119
+ `client.remaining_today` only comes from non-live responses.
120
+
121
+ ## Errors
122
+
123
+ | Exception | When |
124
+ |---|---|
125
+ | `MapFlightsAuthError` | 401, unknown or revoked key |
126
+ | `MapFlightsOutOfAreaError` | 403 `out_of_area` |
127
+ | `MapFlightsKeyDormantError` | 403 `key_dormant` |
128
+ | `MapFlightsNotInTariffError` | 403 `group_not_in_tariff`, `endpoint_not_in_tariff`, `format_not_in_tariff` |
129
+ | `MapFlightsTariffError` | base class of the 403 errors above, also for unknown codes |
130
+ | `MapFlightsNotFoundError` | 404 (parsed body in `body`) |
131
+ | `MapFlightsNoCurrentFlightError` | 404 of `flight_by_callsign`, a normal state |
132
+ | `MapFlightsRateLimitError` | 429, or refused locally (`local=True`) |
133
+ | `MapFlightsConnectionError` | timeout or network error |
134
+ | `MapFlightsError` | base class of everything above |
135
+
136
+ ## Development
137
+
138
+ ```bash
139
+ uv sync
140
+ uv run pytest
141
+ uv run ruff check .
142
+ uv run ruff format --check .
143
+ uv run mypy
144
+ ```
145
+
146
+ ## License
147
+
148
+ MIT
@@ -0,0 +1,125 @@
1
+ # pymapflights
2
+
3
+ Asynchronous Python client for the [map.flights](https://map.flights) API.
4
+
5
+ > **Unofficial.** This library is not made or endorsed by map.flights. It is the
6
+ > API layer of the Home Assistant integration
7
+ > [ha-mapflights](https://github.com/olli-dot-dev/ha-mapflights).
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pip install pymapflights
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```python
18
+ import asyncio
19
+
20
+ from aiohttp import ClientSession
21
+
22
+ from pymapflights import MapFlights
23
+
24
+
25
+ async def main() -> None:
26
+ async with ClientSession() as session:
27
+ client = MapFlights(
28
+ "YOUR_API_KEY",
29
+ session=session,
30
+ user_agent="my-app/1.0 (+https://example.com)",
31
+ )
32
+ result = await client.near(48.11, 16.57, 25, limit=100)
33
+ print(f"{result.total} aircraft within 25 km")
34
+ for aircraft in result.aircraft:
35
+ print(aircraft.callsign, aircraft.distance_km, aircraft.alt_baro)
36
+ print(client.attribution)
37
+
38
+
39
+ asyncio.run(main())
40
+ ```
41
+
42
+ Notes:
43
+
44
+ - `total` is the number of aircraft in the radius; `aircraft` is capped by
45
+ `limit` (API default 50, max 500). `result.truncated` tells you when the list
46
+ is incomplete.
47
+ - When `result.ready` is `False`, a delayed tariff is still filling its buffer
48
+ and the empty list does not mean there is no traffic.
49
+ - The session belongs to you; the client never closes it.
50
+
51
+ ## Endpoints
52
+
53
+ | Method | Endpoint | Stability | Quota |
54
+ |---|---|---|---|
55
+ | `near(lat, lon, radius_km, limit=)` | `/near/{lat}/{lon}/{radius}` | stable | live (1 per 5 s) |
56
+ | `aircraft_flights(hex_id, limit=)` | `/aircraft/{hex}/flights` | stable | daily |
57
+ | `airlines(query=, limit=, offset=)` | `/airlines` | stable | daily |
58
+ | `me()` | `/me` | experimental | daily |
59
+ | `day_board(airport, "arr" \| "dep", day=)` | `/airports/{icao}/day-board` | experimental | daily |
60
+ | `flight_by_callsign(callsign)` | `/flights/by-callsign/{callsign}` | experimental | daily |
61
+
62
+ Experimental endpoints may change within `/api/v1`; their models make every
63
+ field optional. `key_prefix(api_key)` returns the non-secret key prefix
64
+ (`mf_live_` + 8 hex) without a request.
65
+
66
+ `flight_by_callsign()` raises `MapFlightsNoCurrentFlightError` when there is
67
+ no current flight (`reason` is `no current flight` with `last_flight`, or
68
+ `unknown flight`). That is a normal state, but it still counts against the
69
+ daily quota. A landing appears there only 20 to 35 minutes after touchdown.
70
+
71
+ ## API keys and terms
72
+
73
+ Keys are issued by map.flights (operators of a receiving station can create a
74
+ free key at <https://map.flights/account/api-key>). Use of the API is subject to
75
+ the [map.flights API terms](https://map.flights/api-terms). Attribution is
76
+ mandatory: show at least "Data: map.flights"; the full text of the last response
77
+ is in `client.attribution`.
78
+
79
+ ### Rate limits
80
+
81
+ The client enforces the limits per key, shared by every client in the process
82
+ that uses the same key:
83
+
84
+ - **Live** (`near`): one request every 5 s. A call waits for its slot. Every
85
+ call uses the slot, even one that fails (a `near` outside the key's area too).
86
+ - **Minute**: at most 60 requests per minute over all endpoints.
87
+ - **After a 429** the client remembers `Retry-After` for that scope (`live`,
88
+ `minute` or `day`) and refuses further requests of that scope until then.
89
+
90
+ A refused request raises `MapFlightsRateLimitError` with `local=True` and is
91
+ never sent. `retry_after` and `scope` are set either way.
92
+
93
+ `client.live_rate_limit` and `client.daily_rate_limit` hold the
94
+ `X-RateLimit-*` headers of the last live and the last other response. For live
95
+ requests they describe the live bucket, not the daily quota, so
96
+ `client.remaining_today` only comes from non-live responses.
97
+
98
+ ## Errors
99
+
100
+ | Exception | When |
101
+ |---|---|
102
+ | `MapFlightsAuthError` | 401, unknown or revoked key |
103
+ | `MapFlightsOutOfAreaError` | 403 `out_of_area` |
104
+ | `MapFlightsKeyDormantError` | 403 `key_dormant` |
105
+ | `MapFlightsNotInTariffError` | 403 `group_not_in_tariff`, `endpoint_not_in_tariff`, `format_not_in_tariff` |
106
+ | `MapFlightsTariffError` | base class of the 403 errors above, also for unknown codes |
107
+ | `MapFlightsNotFoundError` | 404 (parsed body in `body`) |
108
+ | `MapFlightsNoCurrentFlightError` | 404 of `flight_by_callsign`, a normal state |
109
+ | `MapFlightsRateLimitError` | 429, or refused locally (`local=True`) |
110
+ | `MapFlightsConnectionError` | timeout or network error |
111
+ | `MapFlightsError` | base class of everything above |
112
+
113
+ ## Development
114
+
115
+ ```bash
116
+ uv sync
117
+ uv run pytest
118
+ uv run ruff check .
119
+ uv run ruff format --check .
120
+ uv run mypy
121
+ ```
122
+
123
+ ## License
124
+
125
+ MIT
@@ -0,0 +1,82 @@
1
+ [project]
2
+ name = "pymapflights"
3
+ description = "Asynchronous Python client for the map.flights API (unofficial)"
4
+ readme = "README.md"
5
+ license = "MIT"
6
+ license-files = ["LICENSE"]
7
+ authors = [{ name = "Olli", email = "1@olli.dev" }]
8
+ requires-python = ">=3.12"
9
+ dynamic = ["version"]
10
+ keywords = ["map.flights", "ads-b", "aircraft", "flights", "home-assistant", "async"]
11
+ classifiers = [
12
+ "Development Status :: 3 - Alpha",
13
+ "Framework :: AsyncIO",
14
+ "Intended Audience :: Developers",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Programming Language :: Python :: 3.13",
18
+ "Typing :: Typed",
19
+ ]
20
+ dependencies = [
21
+ "aiohttp>=3.9.0",
22
+ "mashumaro>=3.13",
23
+ "yarl>=1.9.0",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/olli-dot-dev/pymapflights"
28
+ Issues = "https://github.com/olli-dot-dev/pymapflights/issues"
29
+
30
+ [dependency-groups]
31
+ dev = [
32
+ "mypy>=1.10",
33
+ "pytest>=8.0",
34
+ "pytest-asyncio>=0.23",
35
+ "pytest-cov>=5.0",
36
+ "ruff>=0.6",
37
+ ]
38
+
39
+ [build-system]
40
+ requires = ["hatchling"]
41
+ build-backend = "hatchling.build"
42
+
43
+ [tool.hatch.build.targets.sdist]
44
+ exclude = [".github", "uv.lock"]
45
+
46
+ [tool.hatch.version]
47
+ path = "src/pymapflights/const.py"
48
+ pattern = 'VERSION: Final = "(?P<version>[^"]+)"'
49
+
50
+ [tool.pytest.ini_options]
51
+ asyncio_mode = "auto"
52
+ asyncio_default_fixture_loop_scope = "function"
53
+ addopts = "--cov=pymapflights --cov-report=term-missing"
54
+ testpaths = ["tests"]
55
+
56
+ [tool.coverage.report]
57
+ fail_under = 95
58
+ show_missing = true
59
+
60
+ [tool.mypy]
61
+ strict = true
62
+ python_version = "3.12"
63
+ files = ["src", "tests"]
64
+
65
+ [tool.ruff]
66
+ target-version = "py312"
67
+ line-length = 88
68
+
69
+ [tool.ruff.lint]
70
+ select = ["ALL"]
71
+ ignore = [
72
+ "COM812", # conflicts with the formatter
73
+ "CPY001", # no per-file copyright headers, see LICENSE
74
+ "D203", # one-blank-line-before-class (incompatible with D211)
75
+ "D213", # multi-line-summary-second-line (incompatible with D212)
76
+ "EM101",
77
+ "EM102",
78
+ "TRY003",
79
+ ]
80
+
81
+ [tool.ruff.lint.per-file-ignores]
82
+ "tests/**" = ["S101", "PLR2004", "D", "ANN401", "SLF001"]
@@ -0,0 +1,76 @@
1
+ """Asynchronous Python client for the map.flights API (unofficial)."""
2
+
3
+ from .client import MapFlights, key_prefix
4
+ from .const import VERSION
5
+ from .exceptions import (
6
+ MapFlightsAuthError,
7
+ MapFlightsConnectionError,
8
+ MapFlightsError,
9
+ MapFlightsKeyDormantError,
10
+ MapFlightsNoCurrentFlightError,
11
+ MapFlightsNotFoundError,
12
+ MapFlightsNotInTariffError,
13
+ MapFlightsOutOfAreaError,
14
+ MapFlightsRateLimitError,
15
+ MapFlightsTariffError,
16
+ )
17
+ from .models import (
18
+ AircraftFlights,
19
+ Airline,
20
+ AirlinePage,
21
+ AirportRef,
22
+ Attribution,
23
+ DayBoard,
24
+ DayBoardRow,
25
+ Delay,
26
+ FlightDelay,
27
+ FlightPage,
28
+ FlightSummary,
29
+ LiveState,
30
+ Me,
31
+ MeKey,
32
+ MeLimits,
33
+ MeTariff,
34
+ MeUsage,
35
+ NearAircraft,
36
+ NearResponse,
37
+ RateLimitInfo,
38
+ )
39
+
40
+ __version__ = VERSION
41
+
42
+ __all__ = [
43
+ "AircraftFlights",
44
+ "Airline",
45
+ "AirlinePage",
46
+ "AirportRef",
47
+ "Attribution",
48
+ "DayBoard",
49
+ "DayBoardRow",
50
+ "Delay",
51
+ "FlightDelay",
52
+ "FlightPage",
53
+ "FlightSummary",
54
+ "LiveState",
55
+ "MapFlights",
56
+ "MapFlightsAuthError",
57
+ "MapFlightsConnectionError",
58
+ "MapFlightsError",
59
+ "MapFlightsKeyDormantError",
60
+ "MapFlightsNoCurrentFlightError",
61
+ "MapFlightsNotFoundError",
62
+ "MapFlightsNotInTariffError",
63
+ "MapFlightsOutOfAreaError",
64
+ "MapFlightsRateLimitError",
65
+ "MapFlightsTariffError",
66
+ "Me",
67
+ "MeKey",
68
+ "MeLimits",
69
+ "MeTariff",
70
+ "MeUsage",
71
+ "NearAircraft",
72
+ "NearResponse",
73
+ "RateLimitInfo",
74
+ "__version__",
75
+ "key_prefix",
76
+ ]