flightbyte 1.0.0__py3-none-any.whl
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.
- flightbyte/__init__.py +136 -0
- flightbyte/_client.py +157 -0
- flightbyte/_countries.py +57 -0
- flightbyte/_errors.py +311 -0
- flightbyte/_flights.py +84 -0
- flightbyte/_models.py +644 -0
- flightbyte/_rate_limit.py +75 -0
- flightbyte/_subscriptions.py +317 -0
- flightbyte/_transport.py +429 -0
- flightbyte/_util.py +28 -0
- flightbyte/_version.py +3 -0
- flightbyte/_webhook.py +233 -0
- flightbyte/py.typed +0 -0
- flightbyte-1.0.0.dist-info/METADATA +423 -0
- flightbyte-1.0.0.dist-info/RECORD +17 -0
- flightbyte-1.0.0.dist-info/WHEEL +4 -0
- flightbyte-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: flightbyte
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python client for the FlightByte flight status API: look flights up, watch them with signed webhooks and verify every delivery. No dependencies.
|
|
5
|
+
Project-URL: Homepage, https://flightbyte.dev
|
|
6
|
+
Project-URL: Documentation, https://flightbyte.dev/docs
|
|
7
|
+
Project-URL: Source, https://github.com/FlightByte-dev/flightbyte-python
|
|
8
|
+
Project-URL: Issues, https://github.com/FlightByte-dev/flightbyte-python/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/FlightByte-dev/flightbyte-python/blob/main/CHANGELOG.md
|
|
10
|
+
Author-email: FlightByte <support@flightbyte.dev>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: airline,airport,api client,aviation,delay,flight status,flight tracking,flightbyte,flights,gate,sdk,webhooks
|
|
14
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
25
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
26
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
27
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
28
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
29
|
+
Classifier: Typing :: Typed
|
|
30
|
+
Requires-Python: >=3.10
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
<p align="center">
|
|
34
|
+
<a href="https://flightbyte.dev">
|
|
35
|
+
<picture>
|
|
36
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/FlightByte-dev/flightbyte-python/main/assets/logo-dark.svg">
|
|
37
|
+
<img src="https://raw.githubusercontent.com/FlightByte-dev/flightbyte-python/main/assets/logo-light.svg" alt="FlightByte" width="340">
|
|
38
|
+
</picture>
|
|
39
|
+
</a>
|
|
40
|
+
</p>
|
|
41
|
+
|
|
42
|
+
<h3 align="center">FlightByte for Python</h3>
|
|
43
|
+
|
|
44
|
+
<p align="center">
|
|
45
|
+
The official Python client for the FlightByte flight status API.<br>
|
|
46
|
+
Look flights up, watch them with signed webhooks and verify every delivery,<br>
|
|
47
|
+
from a web service, a worker or a script.
|
|
48
|
+
</p>
|
|
49
|
+
|
|
50
|
+
<p align="center">
|
|
51
|
+
<a href="https://pypi.org/project/flightbyte/"><img src="https://img.shields.io/pypi/v/flightbyte?style=flat-square&color=EF6A1A&label=pypi" alt="PyPI version"></a>
|
|
52
|
+
<img src="https://img.shields.io/badge/Python-3.10%2B-121212?style=flat-square&logo=python&logoColor=white" alt="Python 3.10+">
|
|
53
|
+
<img src="https://img.shields.io/badge/dependencies-none-EF6A1A?style=flat-square" alt="No dependencies">
|
|
54
|
+
<a href="https://github.com/FlightByte-dev/flightbyte-python/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-121212?style=flat-square" alt="MIT license"></a>
|
|
55
|
+
</p>
|
|
56
|
+
|
|
57
|
+
<p align="center">
|
|
58
|
+
<a href="https://flightbyte.dev/docs"><b>Docs</b></a> ·
|
|
59
|
+
<a href="https://app.flightbyte.dev/signup?utm_source=pypi&utm_medium=readme&utm_campaign=python-sdk"><b>Get a free key</b></a> ·
|
|
60
|
+
<a href="https://flightbyte.dev/docs/reference"><b>API reference</b></a> ·
|
|
61
|
+
<a href="https://flightbyte.dev/docs/webhooks"><b>Webhook docs</b></a> ·
|
|
62
|
+
<a href="https://t.me/flightbyte"><b>Support</b></a>
|
|
63
|
+
</p>
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Quick start
|
|
68
|
+
|
|
69
|
+
1. Get a free API key at [app.flightbyte.dev/signup](https://app.flightbyte.dev/signup?utm_source=pypi&utm_medium=readme&utm_campaign=python-sdk).
|
|
70
|
+
2. Add the package:
|
|
71
|
+
|
|
72
|
+
| | Command |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| **pip** | `pip install flightbyte` |
|
|
75
|
+
| **uv** | `uv add flightbyte` |
|
|
76
|
+
| **Poetry** | `poetry add flightbyte` |
|
|
77
|
+
| **pipenv** | `pipenv install flightbyte` |
|
|
78
|
+
|
|
79
|
+
3. Look a flight up:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
import os
|
|
83
|
+
|
|
84
|
+
from flightbyte import FlightByte
|
|
85
|
+
|
|
86
|
+
client = FlightByte(api_key=os.environ["FLIGHTBYTE_API_KEY"])
|
|
87
|
+
|
|
88
|
+
flight = client.flights.get(
|
|
89
|
+
carrier="TK",
|
|
90
|
+
flight_number="1848",
|
|
91
|
+
date="2026-09-08", # local departure date at the origin
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
print(flight["status"]["label"]) # Scheduled
|
|
95
|
+
print(flight["departure"].get("gate")) # A11
|
|
96
|
+
print(flight["departure"].get("delayMinutes")) # 15
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Responses are plain dicts typed with `TypedDict`, so mypy, Pyright and your editor know every key. `FlightByte.format_date(datetime.now(timezone.utc), "Europe/Istanbul")` gives the `YYYY-MM-DD` the API expects, in the origin airport's time zone. To try it from a terminal, run the example in this repository:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
pip install -e .
|
|
103
|
+
FLIGHTBYTE_API_KEY=frk_live_... python examples/lookup.py TK 1 2026-11-15
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```text
|
|
107
|
+
TK1:2026-11-15@IST-JFK IST → JFK
|
|
108
|
+
Status Scheduled (scheduled)
|
|
109
|
+
Departure 2026-11-15T15:20:00.000 +03, gate D15
|
|
110
|
+
Arrival 2026-11-15T18:20:00.000 EST, baggage -
|
|
111
|
+
Quota 23 of 30 left this minute
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## What you can do
|
|
115
|
+
|
|
116
|
+
| You want to | You call |
|
|
117
|
+
| --- | --- |
|
|
118
|
+
| *Show a passenger their gate and delay* | `client.flights.get(...)` returns the live snapshot: phase, gate, terminal, baggage belt, scheduled, estimated and actual times, delay, aircraft and codeshares. |
|
|
119
|
+
| *Look up TK1 tomorrow, which flies two legs* | Pass `origin="IST"`. Without it the snapshot lists every leg in `alternates`, and a subscription refuses to guess: the `BadRequestError` carries the `legs` to choose from. |
|
|
120
|
+
| *Be told when anything changes* | `client.subscriptions.create(...)` starts the watch and returns the signing secret, shown once. |
|
|
121
|
+
| *Find out why webhooks stopped arriving* | `client.subscriptions.get(id)` and `deliveries(id)` show the status and the HTTP code and error of every recent attempt. |
|
|
122
|
+
| *Trust a webhook on your server* | `Webhook.construct_event(body, signature, secret)` verifies the signature against the raw body and decodes the event. |
|
|
123
|
+
|
|
124
|
+
## API
|
|
125
|
+
|
|
126
|
+
**Flights**: read-only.
|
|
127
|
+
|
|
128
|
+
| Method | What it does |
|
|
129
|
+
| --- | --- |
|
|
130
|
+
| `flights.get` | Live status of one flight leg, and every leg the number flies that day.<br><sub>`GET /v1/flights/{carrier}/{flightNumber}/{date}`</sub> |
|
|
131
|
+
| `flights.timeline` | The airline's own change history for a leg, newest first.<br><sub>`GET /v1/flights/{carrier}/{flightNumber}/{date}/timeline`</sub> |
|
|
132
|
+
|
|
133
|
+
**Subscriptions**
|
|
134
|
+
|
|
135
|
+
| Method | What it does | Access |
|
|
136
|
+
| --- | --- | --- |
|
|
137
|
+
| `subscriptions.create` | Watch a flight and get a signed webhook for every change. Counts as a watched flight on your plan; pass `idempotency_key` so a retry never counts twice.<br><sub>`POST /v1/subscriptions`</sub> | writes |
|
|
138
|
+
| `subscriptions.list` | One page of your subscriptions, filtered by status, carrier, number or date.<br><sub>`GET /v1/subscriptions`</sub> | read-only |
|
|
139
|
+
| `subscriptions.list_all` | Every matching subscription, fetched page by page as you iterate over it with `for`.<br><sub>`GET /v1/subscriptions`</sub> | read-only |
|
|
140
|
+
| `subscriptions.get` | One subscription with its status and delivery counters.<br><sub>`GET /v1/subscriptions/{id}`</sub> | read-only |
|
|
141
|
+
| `subscriptions.update` | Change the webhook URL, event filter, delay threshold, headers or metadata.<br><sub>`PATCH /v1/subscriptions/{id}`</sub> | writes |
|
|
142
|
+
| `subscriptions.pause` | Stop polling and deliveries without losing anything.<br><sub>`POST /v1/subscriptions/{id}/pause`</sub> | writes |
|
|
143
|
+
| `subscriptions.resume` | Resume a paused or auto-suspended subscription.<br><sub>`POST /v1/subscriptions/{id}/resume`</sub> | writes |
|
|
144
|
+
| `subscriptions.delete` | Delete a subscription for good.<br><sub>`DELETE /v1/subscriptions/{id}`</sub> | **destructive** |
|
|
145
|
+
| `subscriptions.send_test` | Send a signed `subscription.test` webhook to the subscription's URL.<br><sub>`POST /v1/subscriptions/{id}/test`</sub> | writes |
|
|
146
|
+
| `subscriptions.deliveries` | Recent delivery attempts with HTTP status and errors.<br><sub>`GET /v1/subscriptions/{id}/deliveries`</sub> | read-only |
|
|
147
|
+
| `subscriptions.events` | Recent events with the exact payloads that were sent.<br><sub>`GET /v1/subscriptions/{id}/events`</sub> | read-only |
|
|
148
|
+
| `subscriptions.secret` | Reveal the signing secret again.<br><sub>`GET /v1/subscriptions/{id}/secret`</sub> | read-only |
|
|
149
|
+
| `subscriptions.rotate_secret` | Generate a new signing secret. Previous signatures stop validating.<br><sub>`POST /v1/subscriptions/{id}/secret/rotate`</sub> | writes |
|
|
150
|
+
|
|
151
|
+
**Countries**: read-only.
|
|
152
|
+
|
|
153
|
+
| Method | What it does |
|
|
154
|
+
| --- | --- |
|
|
155
|
+
| `countries.list` | Every country with its name in any language, emoji and flag URLs.<br><sub>`GET /v1/countries`</sub> |
|
|
156
|
+
| `countries.get` | One country by its ISO 3166-1 alpha-2 code.<br><sub>`GET /v1/countries/{code}`</sub> |
|
|
157
|
+
| `countries.flag_url` | The URL of a country's SVG flag, 4:3 or square. Needs no key, so it can go straight into an `<img>`.<br><sub>`GET /v1/flags/{code}.svg`</sub> |
|
|
158
|
+
|
|
159
|
+
Every method takes a keyword-only `timeout` of its own, in seconds. Arguments carry the API's own names in Python form (`carrier`, `flight_number`, `date`, `origin`, `webhook_url`, `idempotency_key`, …), and responses come back exactly as the API sent them: dicts with the API's own keys, timestamps as ISO 8601 strings, and no field renamed or dropped. The full snapshot is documented under [Flights](https://flightbyte.dev/docs/flights).
|
|
160
|
+
|
|
161
|
+
## Watch a flight
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
created = client.subscriptions.create(
|
|
165
|
+
carrier="TK",
|
|
166
|
+
flight_number="1848",
|
|
167
|
+
date="2026-09-08",
|
|
168
|
+
webhook_url="https://api.example.com/hooks/flights",
|
|
169
|
+
events=["departure.gate.changed", "delay.increased", "flight.departed"],
|
|
170
|
+
metadata={"bookingId": "BK-1042"},
|
|
171
|
+
idempotency_key="BK-1042", # safe to retry
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Shown in full only now: store it where your webhook receiver can read it.
|
|
175
|
+
signing_secret = created["secret"]
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
FlightByte only delivers to public HTTPS addresses. To receive on your machine, expose the port with a tunnel (`cloudflared tunnel --url http://localhost:8080` or `ngrok http 8080`) and use that URL. `subscriptions.send_test(id)` sends a signed `subscription.test` whenever you want to see the path work.
|
|
179
|
+
|
|
180
|
+
## Receive webhooks
|
|
181
|
+
|
|
182
|
+
A receiver has to do four things, in this order: verify the signature against the **raw** body, deduplicate on the event id, answer `2xx` before doing the work, and branch on the event type.
|
|
183
|
+
|
|
184
|
+
`Webhook.construct_event` does the first: it checks the signature against the body and decodes the event. It raises `WebhookSignatureError` for a bad signature (answer `401`) and `FlightByteError` for a signed body that is not an event (answer `400`). An empty secret raises a `ValueError` right away, because anyone can sign with an empty key: an unset environment variable fails loudly instead of accepting forged events. Give it the body as bytes, before any JSON parsing. With Flask, that is `request.get_data()`, not `request.json`:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
import os
|
|
188
|
+
|
|
189
|
+
from flask import Flask, request
|
|
190
|
+
from flightbyte import FlightByteError, Webhook, WebhookSignatureError
|
|
191
|
+
|
|
192
|
+
app = Flask(__name__)
|
|
193
|
+
signing_secret = os.environ["FLIGHTBYTE_WEBHOOK_SECRET"]
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@app.post("/hooks/flightbyte")
|
|
197
|
+
def flightbyte_hook():
|
|
198
|
+
try:
|
|
199
|
+
event = Webhook.construct_event(
|
|
200
|
+
request.get_data(), # the raw body
|
|
201
|
+
request.headers.get(Webhook.SIGNATURE_HEADER),
|
|
202
|
+
signing_secret,
|
|
203
|
+
)
|
|
204
|
+
except WebhookSignatureError:
|
|
205
|
+
return "invalid signature", 401
|
|
206
|
+
except FlightByteError:
|
|
207
|
+
return "invalid event", 400
|
|
208
|
+
|
|
209
|
+
# A retry carries the same id: answer it, but don't handle it twice.
|
|
210
|
+
if mark_handled(event["id"]):
|
|
211
|
+
# Answer first: FlightByte treats a delivery as failed after 10 seconds.
|
|
212
|
+
jobs.put(event)
|
|
213
|
+
return "ok", 200
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
<details>
|
|
217
|
+
<summary><b>Django and FastAPI</b></summary>
|
|
218
|
+
|
|
219
|
+
<br>
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
# Django: the raw body is request.body. The signature is the check, so no CSRF token.
|
|
223
|
+
from django.http import HttpResponse
|
|
224
|
+
from django.views.decorators.csrf import csrf_exempt
|
|
225
|
+
from django.views.decorators.http import require_POST
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@csrf_exempt
|
|
229
|
+
@require_POST
|
|
230
|
+
def flightbyte_hook(request):
|
|
231
|
+
try:
|
|
232
|
+
event = Webhook.construct_event(
|
|
233
|
+
request.body, request.headers.get(Webhook.SIGNATURE_HEADER), signing_secret
|
|
234
|
+
)
|
|
235
|
+
except WebhookSignatureError:
|
|
236
|
+
return HttpResponse("invalid signature", status=401)
|
|
237
|
+
except FlightByteError:
|
|
238
|
+
return HttpResponse("invalid event", status=400)
|
|
239
|
+
jobs.put(event)
|
|
240
|
+
return HttpResponse("ok")
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# FastAPI and Starlette: read the body yourself, not through a Pydantic model.
|
|
244
|
+
from fastapi import FastAPI, Request, Response
|
|
245
|
+
|
|
246
|
+
app = FastAPI()
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@app.post("/hooks/flightbyte")
|
|
250
|
+
async def flightbyte_hook(request: Request) -> Response:
|
|
251
|
+
try:
|
|
252
|
+
event = Webhook.construct_event(
|
|
253
|
+
await request.body(), request.headers.get(Webhook.SIGNATURE_HEADER), signing_secret
|
|
254
|
+
)
|
|
255
|
+
except WebhookSignatureError:
|
|
256
|
+
return Response("invalid signature", status_code=401)
|
|
257
|
+
except FlightByteError:
|
|
258
|
+
return Response("invalid event", status_code=400)
|
|
259
|
+
jobs.put_nowait(event)
|
|
260
|
+
return Response("ok")
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
</details>
|
|
264
|
+
|
|
265
|
+
Then, off the request path:
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
match event["type"]:
|
|
269
|
+
case "flight.updated":
|
|
270
|
+
for change in event["changes"]:
|
|
271
|
+
print(f"{event['flightKey']} {change['type']}: {change['message']}")
|
|
272
|
+
case "subscription.completed":
|
|
273
|
+
print(f"{event['flightKey']} finished")
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
A receiver on the standard library's `http.server`, with no framework, is in [examples/webhook_server.py](https://github.com/FlightByte-dev/flightbyte-python/blob/main/examples/webhook_server.py).
|
|
277
|
+
|
|
278
|
+
<details>
|
|
279
|
+
<summary><b>How the signature works</b></summary>
|
|
280
|
+
|
|
281
|
+
<br>
|
|
282
|
+
|
|
283
|
+
```text
|
|
284
|
+
x-flightbyte-signature: t=1789305971,v1=5f1c9a...e3b2
|
|
285
|
+
v1 = HMAC-SHA256(secret, "<t>." + <raw body>)
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
`Webhook.verify` computes the HMAC over the bytes you received, not over JSON you parsed and serialised again, compares it in constant time with `hmac.compare_digest`, accepts any of several `v1` values, and rejects a `t` more than five minutes from now. `Webhook.sign` produces the same header, for testing your receiver. The test suite checks both against signatures made by the API's own signer, and throws thousands of random headers at the parser.
|
|
289
|
+
|
|
290
|
+
The reasons behind each step are in [Verifying webhook signatures](https://flightbyte.dev/guides/verifying-webhook-signatures), and reference receivers in Node.js, Python and Go are in [flightbyte-webhook-examples](https://github.com/FlightByte-dev/flightbyte-webhook-examples).
|
|
291
|
+
|
|
292
|
+
</details>
|
|
293
|
+
|
|
294
|
+
## Errors
|
|
295
|
+
|
|
296
|
+
Every failed request raises a subclass of `FlightByteError`, so you catch the ones you handle with `except`.
|
|
297
|
+
|
|
298
|
+
| Error | Status | When, and what to do |
|
|
299
|
+
| --- | --- | --- |
|
|
300
|
+
| `BadRequestError` | `400` | Invalid input; `messages` has one line per field. For a number that flies twice, `legs` lists them: pass one's `origin`. |
|
|
301
|
+
| `AuthenticationError` | `401` | Missing, invalid, revoked or expired API key. |
|
|
302
|
+
| `PaymentRequiredError` | `402` | No watched flights left. `code` says why: `watched_flight_limit`, `overage_cap_reached` (see `resets_at`) or `credits_exhausted` (see `balance` and `expires_at`). |
|
|
303
|
+
| `PermissionDeniedError` | `403` | The account is suspended, or a token lacks the scope (`code`). |
|
|
304
|
+
| `NotFoundError` | `404` | The flight did not operate that day, or no such subscription. |
|
|
305
|
+
| `RateLimitError` | `429` | A minute, day or month quota is used up. Wait `retry_after` seconds. |
|
|
306
|
+
| `UpstreamError` | `502` | The flight data provider failed. Retry shortly. It is also a `ServerError`. |
|
|
307
|
+
| `ServerError`, `UnexpectedStatusError` | other | Anything else. `request_id` is what support asks for. |
|
|
308
|
+
| `APIConnectionError` | none | No response at all: network or TLS error, or `timed_out` when the timeout ran out. |
|
|
309
|
+
|
|
310
|
+
```python
|
|
311
|
+
import time
|
|
312
|
+
|
|
313
|
+
from flightbyte import BadRequestError, PaymentRequiredError, RateLimitError
|
|
314
|
+
|
|
315
|
+
try:
|
|
316
|
+
client.subscriptions.create(**params)
|
|
317
|
+
except BadRequestError as err:
|
|
318
|
+
if not err.legs:
|
|
319
|
+
raise
|
|
320
|
+
print("Pick a leg:", ", ".join(leg["origin"] for leg in err.legs))
|
|
321
|
+
except PaymentRequiredError as err:
|
|
322
|
+
print(f"No watched flights left ({err.code}) until {err.resets_at}")
|
|
323
|
+
except RateLimitError as err:
|
|
324
|
+
time.sleep(err.retry_after or 5)
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
An `APIError` keeps the whole answer in `status`, `messages`, `body`, `raw_body` and `headers`, and every error pickles cleanly, so it crosses a process pool or a task queue intact. Every status is explained in the [errors docs](https://flightbyte.dev/docs/errors).
|
|
328
|
+
|
|
329
|
+
## Built for Python services
|
|
330
|
+
|
|
331
|
+
- **Web services, workers and scripts.** Flask, Django, FastAPI, Celery, cron jobs and notebooks, on Python 3.10 to 3.14, and CI runs it on every one of them. One client is safe to share across threads and keeps no connection open, so there is nothing to close. The client is synchronous: in an async app, call it from a plain `def` endpoint or through `asyncio.to_thread`. Proxies set in `HTTPS_PROXY` and `NO_PROXY` are honoured.
|
|
332
|
+
- **No dependencies.** Only the standard library: `urllib.request`, `json`, `hmac` and `hashlib`. Nothing to audit but this package.
|
|
333
|
+
- **Your key stays on your server.** An API key or a signing secret shipped inside a browser or mobile app can be pulled out of it by anyone who has the app. Flag images are the exception: `countries.flag_url` needs no key.
|
|
334
|
+
- **Your key stays with the API.** The client follows a redirect only for a read, and only on the API's own origin, so a misdirected answer cannot carry the key elsewhere and a write is never sent twice.
|
|
335
|
+
- **New API values don't break old code.** A phase, status or change type added after this release is still a valid `str`; `FLIGHT_PHASES`, `FLIGHT_CHANGE_TYPES` and the other tuples list the ones this version knows. The tests check every method and every type against the API's OpenAPI document, so a field the API adds shows up as a failing test here first, not as a missing key in your code.
|
|
336
|
+
- **Zero stays apart from unknown.** Keys the API may leave out are not required by the types, and values it may send as `null` include `None`, so a delay of `0` and no delay reading are different things.
|
|
337
|
+
- **Errors say what to do next.** A number that flies twice comes back with its legs, a `429` with how long to wait, a `402` with the reason and when the allowance resets.
|
|
338
|
+
- **Quotas are visible.** `client.last_rate_limit` holds the minute, day and month windows from the latest response's `X-RateLimit-*` headers.
|
|
339
|
+
|
|
340
|
+
## Configuration
|
|
341
|
+
|
|
342
|
+
```python
|
|
343
|
+
client = FlightByte(
|
|
344
|
+
api_key=os.environ["FLIGHTBYTE_API_KEY"],
|
|
345
|
+
base_url="http://localhost:3000/v1",
|
|
346
|
+
timeout=10.0,
|
|
347
|
+
user_agent="my-app/1.2",
|
|
348
|
+
)
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
| Option | Default | Meaning |
|
|
352
|
+
| --- | --- | --- |
|
|
353
|
+
| `api_key` | required | Your API key, sent as `x-api-key`. |
|
|
354
|
+
| `base_url` | `https://api.flightbyte.dev/v1` | API base URL, e.g. `http://localhost:3000/v1` for a local API. |
|
|
355
|
+
| `timeout` | `30.0` | Bounds each request, response body included, in seconds. `None` turns it off. Every method takes a `timeout` of its own too. |
|
|
356
|
+
| `user_agent` | none | Put in front of `flightbyte-python/<version>` in the `user-agent` header. |
|
|
357
|
+
|
|
358
|
+
## Development
|
|
359
|
+
|
|
360
|
+
```bash
|
|
361
|
+
python -m venv .venv && source .venv/bin/activate
|
|
362
|
+
pip install -e . -r requirements-dev.txt
|
|
363
|
+
python -m unittest discover -s tests -t . # every test, against local servers
|
|
364
|
+
ruff check . && ruff format --check .
|
|
365
|
+
mypy --strict src tests
|
|
366
|
+
python -m build && twine check --strict dist/*
|
|
367
|
+
python scripts/sync_openapi.py # refresh spec/openapi.json, then run the tests against it
|
|
368
|
+
node tool/webhook_vectors.mjs
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
The tests never touch the real API and need no key: they connect only to servers they start on `127.0.0.1`, and fail if anything tries to go elsewhere. They cover every method's request mapping, the error class for each status, redirects and timeouts against real local servers, the types against fixtures shaped like the API's JSON and against the API's OpenAPI document, and webhook verification against signatures made with Node's `createHmac` exactly as the API signs them. CI runs them on Python 3.10, 3.11, 3.12, 3.13 and 3.14, and again against the built wheel installed in a clean environment.
|
|
372
|
+
|
|
373
|
+
How to add a field, an enum value or an endpoint is in [CONTRIBUTING.md](https://github.com/FlightByte-dev/flightbyte-python/blob/main/CONTRIBUTING.md). Report security issues by email, as [SECURITY.md](https://github.com/FlightByte-dev/flightbyte-python/blob/main/SECURITY.md) explains, never in a public issue.
|
|
374
|
+
|
|
375
|
+
<details>
|
|
376
|
+
<summary><b>Project layout</b></summary>
|
|
377
|
+
|
|
378
|
+
```text
|
|
379
|
+
src/flightbyte/
|
|
380
|
+
__init__.py public exports
|
|
381
|
+
_client.py FlightByte and its options
|
|
382
|
+
_transport.py requests, headers, timeouts, redirects, error mapping
|
|
383
|
+
_flights.py flights
|
|
384
|
+
_subscriptions.py subscriptions, list_all and mask_secret
|
|
385
|
+
_countries.py countries and flag URLs
|
|
386
|
+
_models.py the API's TypedDicts and value lists
|
|
387
|
+
_errors.py FlightByteError and its subclasses
|
|
388
|
+
_webhook.py Webhook.verify, is_valid, construct_event and sign
|
|
389
|
+
_rate_limit.py X-RateLimit-* headers
|
|
390
|
+
_util.py unwrapping {"data": [...]} answers
|
|
391
|
+
py.typed marks the package as typed
|
|
392
|
+
spec/openapi.json the API document the tests check against
|
|
393
|
+
tests/fixtures/ API-shaped JSON and signed webhook vectors
|
|
394
|
+
examples/ a lookup from the terminal and a receiver on http.server
|
|
395
|
+
scripts/sync_openapi.py refreshes spec/openapi.json
|
|
396
|
+
tool/webhook_vectors.mjs regenerates the signed vectors with Node, as the API signs
|
|
397
|
+
tool/release_notes.sh a version's CHANGELOG.md section, for the GitHub release
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
</details>
|
|
401
|
+
|
|
402
|
+
<details>
|
|
403
|
+
<summary><b>Releasing</b></summary>
|
|
404
|
+
|
|
405
|
+
1. Set `version` in `pyproject.toml` and `__version__` in `src/flightbyte/_version.py`; a test fails if they differ.
|
|
406
|
+
2. Give the version its own `## <version>` section in `CHANGELOG.md`.
|
|
407
|
+
3. Push a tag such as `v1.0.1`.
|
|
408
|
+
|
|
409
|
+
The release workflow checks that the tag, `pyproject.toml` and `_version.py` agree and that the changelog has the section, runs every check, builds the sdist and wheel once, publishes them to PyPI with trusted publishing unless that version is already there, and creates the GitHub release from the changelog section with the files attached. Run it by hand from the Actions tab to rehearse: it builds and checks everything but publishes nothing.
|
|
410
|
+
|
|
411
|
+
</details>
|
|
412
|
+
|
|
413
|
+
## License
|
|
414
|
+
|
|
415
|
+
[MIT](https://github.com/FlightByte-dev/flightbyte-python/blob/main/LICENSE)
|
|
416
|
+
|
|
417
|
+
<br>
|
|
418
|
+
|
|
419
|
+
<p align="center">
|
|
420
|
+
<a href="https://flightbyte.dev"><img src="https://raw.githubusercontent.com/FlightByte-dev/flightbyte-python/main/assets/icon.svg" alt="FlightByte" width="36"></a>
|
|
421
|
+
<br>
|
|
422
|
+
<sub><b>FlightByte</b> · Flight intelligence, byte by byte · <a href="https://flightbyte.dev">flightbyte.dev</a></sub>
|
|
423
|
+
</p>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
flightbyte/__init__.py,sha256=-9XuByHyZLqMCpnQhaNFlxpxBbZTsP9pvOg5ZUln-rY,3127
|
|
2
|
+
flightbyte/_client.py,sha256=l9xVEKf_N-VizQ5C_fNLYvNk7koL3wfSnFQJ62YB6Rk,5724
|
|
3
|
+
flightbyte/_countries.py,sha256=MD5-JRwqFa4Z9RXmidHTJyz5DiJ0bKiMc6NohktvzFU,1766
|
|
4
|
+
flightbyte/_errors.py,sha256=qxxZCCoBiHquANc1Rz-QwsV06KV7hESElQasJfkG1eE,9887
|
|
5
|
+
flightbyte/_flights.py,sha256=pMNWpJ3hyW670qE6PArWfNPl_GHEUz3XEpCoVe0D8sE,2594
|
|
6
|
+
flightbyte/_models.py,sha256=90rfJiOktvNiw8n2KIBdhXFGE7QziuIjQVRMy1WigXQ,19098
|
|
7
|
+
flightbyte/_rate_limit.py,sha256=R1w6mnSGkFF55wzfYoh0hK16C3GFkC_GMqphRgYnxWk,2596
|
|
8
|
+
flightbyte/_subscriptions.py,sha256=0eL6l7TsMPoBzlql7kzrVyMJ3btdXwdrTsCsN7sfZgM,12386
|
|
9
|
+
flightbyte/_transport.py,sha256=dzRsqUbPHxNr11ebe-jddEzwFvx3yo7fkPU6sdkZvGk,15711
|
|
10
|
+
flightbyte/_util.py,sha256=Kt6SAN9tQor0GGBLcwmSGbo-_mggj8VKUcgN59gFsGo,983
|
|
11
|
+
flightbyte/_version.py,sha256=zHgC4no0tk853bN0bz7PfT65TRNddbAcm7s4Pb1tRy4,111
|
|
12
|
+
flightbyte/_webhook.py,sha256=CixwYkDo1NaKRqK-C535h6BLqnrfY50-xiisIkBafi8,9029
|
|
13
|
+
flightbyte/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
flightbyte-1.0.0.dist-info/METADATA,sha256=5grMn5o3Bd4Ca0CYhUmdoSIMp-NrTBxFcCc9AC2pivA,22698
|
|
15
|
+
flightbyte-1.0.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
16
|
+
flightbyte-1.0.0.dist-info/licenses/LICENSE,sha256=T5wB4TNRY8Cj6SJHULO7dO_9TSgeRhsWtsQy7iGShsk,1070
|
|
17
|
+
flightbyte-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Batuhan ERKAN
|
|
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.
|