avia-api 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.
avia_api-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ivan Sladkov
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,172 @@
1
+ Metadata-Version: 2.4
2
+ Name: avia-api
3
+ Version: 0.1.0
4
+ Summary: Async Python client for the Travelpayouts / Aviasales Data API
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Keywords: travelpayouts,aviasales,flights,airline-tickets,prices,api-client,async,httpx,pydantic
8
+ Author: Ivan Sladkov
9
+ Author-email: ivan.sladkov@yandex.ru
10
+ Requires-Python: >=3.11
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Typing :: Typed
20
+ Classifier: Framework :: AsyncIO
21
+ Classifier: Topic :: Internet :: WWW/HTTP
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Requires-Dist: hishel[httpx] (>=1.3.1,<2.0.0)
24
+ Requires-Dist: httpx (>=0.28.1,<0.29.0)
25
+ Requires-Dist: pydantic (>=2.13.5,<3.0.0)
26
+ Requires-Dist: pyrate-limiter (>=4.5.0,<5.0.0)
27
+ Requires-Dist: tenacity (>=9.1.4,<10.0.0)
28
+ Project-URL: Changelog, https://github.com/sliv2001/avia-api/blob/master/CHANGELOG.md
29
+ Project-URL: Homepage, https://github.com/sliv2001/avia-api
30
+ Project-URL: Issues, https://github.com/sliv2001/avia-api/issues
31
+ Project-URL: Repository, https://github.com/sliv2001/avia-api
32
+ Description-Content-Type: text/markdown
33
+
34
+ # avia-api
35
+
36
+ [![CI](https://github.com/sliv2001/avia-api/actions/workflows/ci.yml/badge.svg)](https://github.com/sliv2001/avia-api/actions/workflows/ci.yml)
37
+ [![coverage](https://img.shields.io/badge/coverage-99%25%2B-brightgreen)](https://github.com/sliv2001/avia-api/blob/master/pyproject.toml)
38
+ [![python](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue)](https://github.com/sliv2001/avia-api)
39
+ [![license](https://img.shields.io/badge/license-MIT-blue)](https://github.com/sliv2001/avia-api/blob/master/LICENSE)
40
+
41
+ Async Python client for the [Aviasales / Travelpayouts Data API](https://support.travelpayouts.com/hc/ru/sections/201008338-Aviasales-flight-data-API) - historical/cached ticket prices, price calendars, and reference data (countries, cities, airports, airlines, routes).
42
+
43
+ Not covered: real-time search (`Aviasales Flights Search API`) and the GraphQL API - these are separate products with a different interaction model.
44
+
45
+ ## Installation
46
+
47
+ The package is available through any standard package manager:
48
+
49
+ ```bash
50
+ pip install avia-api
51
+ # or
52
+ uv add avia-api
53
+ # or
54
+ poetry add avia-api
55
+ ```
56
+
57
+ Requires Python 3.11+.
58
+
59
+ ## Quick start
60
+
61
+ ```python
62
+ import asyncio
63
+ from avia_api import AviaApiClient
64
+
65
+ async def main() -> None:
66
+ async with AviaApiClient(token="YOUR_TOKEN") as client:
67
+ prices = await client.prices.cheap(origin="MOW", destination="LED")
68
+ for destination, by_index in prices.items():
69
+ for entry in by_index.values():
70
+ print(destination, entry.price, entry.airline, entry.departure_at)
71
+
72
+ asyncio.run(main())
73
+ ```
74
+
75
+ You can also skip passing the token explicitly and put it in the `TRAVELPAYOUTS_TOKEN` environment variable - the client will pick it up automatically. Get a token in your personal dashboard: https://www.travelpayouts.com/programs/100/tools/api
76
+
77
+ ## Resources and endpoints
78
+
79
+ All methods return models validated by [pydantic](https://docs.pydantic.dev/)
80
+
81
+ ### `client.prices` - prices
82
+
83
+ | Method | Endpoint | Description |
84
+ | ------------------------------------------------- | -------------------------------------- | ------------------------------------------------ |
85
+ | `cheap(origin, destination, ...)` | `GET /v1/prices/cheap` | Cheapest tickets for a route |
86
+ | `direct(origin, destination, ...)` | `GET /v1/prices/direct` | Same, but direct flights only |
87
+ | `calendar(origin, destination, depart_date, ...)` | `GET /v1/prices/calendar` | Price calendar for every day of the month |
88
+ | `monthly(origin, destination, ...)` | `GET /v1/prices/monthly` | Lowest price by month |
89
+ | `latest(...)` | `GET /v2/prices/latest` | Latest found prices across the whole search base |
90
+ | `month_matrix(origin, destination, ...)` | `GET /v2/prices/month-matrix` | Price calendar for a month (v2) |
91
+ | `week_matrix(origin, destination, ...)` | `GET /v2/prices/week-matrix` | Price calendar for a week |
92
+ | `nearest_places_matrix(origin, destination, ...)` | `GET /v2/prices/nearest-places-matrix` | Prices for nearby airports/cities |
93
+
94
+ ### `client.directions` - popular routes
95
+
96
+ | Method | Endpoint | Description |
97
+ | ---------------------------- | ---------------------------- | -------------------------------- |
98
+ | `airline(airline_code, ...)` | `GET /v1/airline-directions` | Popular routes for an airline |
99
+ | `city(origin, ...)` | `GET /v1/city-directions` | Popular destinations from a city |
100
+
101
+ ### `client.reference` - reference data
102
+
103
+ Public, rarely changing JSON files:
104
+
105
+ | Method | Endpoint |
106
+ | ---------------------------------- | ---------------------------------------------- |
107
+ | `countries(language="en")` | `GET /data/{language}/countries.json` |
108
+ | `cities(language="en")` | `GET /data/{language}/cities.json` |
109
+ | `airports(language="en")` | `GET /data/{language}/airports.json` |
110
+ | `airlines(language="en")` | `GET /data/{language}/airlines.json` |
111
+ | `airline_alliances(language="en")` | `GET /data/{language}/airlines_alliances.json` |
112
+ | `planes()` | `GET /data/planes.json` |
113
+ | `routes()` | `GET /data/routes.json` |
114
+
115
+ ## Client configuration
116
+
117
+ ```python
118
+ from avia_api import AviaApiClient
119
+ from pyrate_limiter import Rate, Duration
120
+
121
+ client = AviaApiClient(
122
+ token="...",
123
+ rate=Rate(5, Duration.SECOND), # outgoing request rate limit (pyrate-limiter)
124
+ max_retries=3, # retries on 429/5xx and connection drops
125
+ cache_ttl=1800, # seconds; None disables the response cache
126
+ cache_path="avia_api.db", # sqlite cache file (hishel), relative path
127
+ # goes under .cache/hishel/
128
+ timeout=10.0,
129
+ )
130
+ ```
131
+
132
+ - **Rate limiting** - [pyrate-limiter](https://github.com/vutran1710/PyrateLimiter), a single bucket per client. Limits the rate of outgoing requests before they're sent, to avoid getting a `429` from the API.
133
+ - **Retries** - [tenacity](https://github.com/jd/tenacity) with exponential backoff and jitter; on `429` the `Retry-After` header is honored if present, but capped at 60 seconds per attempt - an unusually large value from the server (e.g. during an incident on its side) can't stall a request indefinitely.
134
+ - **Cache** - [hishel](https://hishel.com) on top of sqlite. Travelpayouts responses don't send `Cache-Control`, so `FilterPolicy` is used (any successful `GET` is cached, with entry lifetime governed by `cache_ttl`) instead of RFC 9111.
135
+
136
+ For tests or non-standard scenarios you can pass `transport=...` - your own `httpx.AsyncBaseTransport`, which fully disables rate limiting/retry/cache, and requests go straight through it (see `respx` or `httpx.MockTransport`).
137
+
138
+ ## Error handling
139
+
140
+ All exceptions derive from `avia_api.AviaApiError`:
141
+
142
+ | Exception | When it occurs |
143
+ | ---------------------------- | -------------------------------------------------------------- |
144
+ | `AviaApiConnectionError` | Network unavailable / timeout - after retries are exhausted |
145
+ | `AviaApiAuthenticationError` | HTTP 401/403 - token missing or invalid |
146
+ | `AviaApiRateLimitError` | HTTP 429 - after retries are exhausted; has `.retry_after` |
147
+ | `AviaApiServerError` | HTTP 5xx - after retries are exhausted |
148
+ | `AviaApiHTTPStatusError` | Other HTTP errors |
149
+ | `AviaApiResponseError` | HTTP 200, but `{"success": false}` in the body; has `.payload` |
150
+ | `AviaApiValidationError` | Response doesn't match the expected schema (API changed) |
151
+
152
+ ## Logging
153
+
154
+ The library uses standard `logging`. To see logs, enable the desired level for the `avia_api` logger:
155
+
156
+ ```python
157
+ import logging
158
+
159
+ logging.basicConfig(level=logging.INFO)
160
+ logging.getLogger("avia_api").setLevel(logging.DEBUG)
161
+ ```
162
+
163
+ What is logged and at what level:
164
+
165
+ | Logger | Level | Event |
166
+ | --------------------- | --------- | -------------------------------------------------------------------------- |
167
+ | `avia_api._transport` | `DEBUG` | Delay in the rate limiter; successful request and its status |
168
+ | `avia_api._transport` | `WARNING` | Retry after a transient error/status; retry budget exhausted |
169
+ | `avia_api._client` | `DEBUG` | Outgoing request (path + query parameters) and response status |
170
+ | `avia_api._client` | `WARNING` | HTTP 401/403/429/4xx; `{"success": false}` in the response body |
171
+ | `avia_api._client` | `ERROR` | HTTP 5xx; network error after retries; response failed the pydantic schema |
172
+
@@ -0,0 +1,138 @@
1
+ # avia-api
2
+
3
+ [![CI](https://github.com/sliv2001/avia-api/actions/workflows/ci.yml/badge.svg)](https://github.com/sliv2001/avia-api/actions/workflows/ci.yml)
4
+ [![coverage](https://img.shields.io/badge/coverage-99%25%2B-brightgreen)](https://github.com/sliv2001/avia-api/blob/master/pyproject.toml)
5
+ [![python](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue)](https://github.com/sliv2001/avia-api)
6
+ [![license](https://img.shields.io/badge/license-MIT-blue)](https://github.com/sliv2001/avia-api/blob/master/LICENSE)
7
+
8
+ Async Python client for the [Aviasales / Travelpayouts Data API](https://support.travelpayouts.com/hc/ru/sections/201008338-Aviasales-flight-data-API) - historical/cached ticket prices, price calendars, and reference data (countries, cities, airports, airlines, routes).
9
+
10
+ Not covered: real-time search (`Aviasales Flights Search API`) and the GraphQL API - these are separate products with a different interaction model.
11
+
12
+ ## Installation
13
+
14
+ The package is available through any standard package manager:
15
+
16
+ ```bash
17
+ pip install avia-api
18
+ # or
19
+ uv add avia-api
20
+ # or
21
+ poetry add avia-api
22
+ ```
23
+
24
+ Requires Python 3.11+.
25
+
26
+ ## Quick start
27
+
28
+ ```python
29
+ import asyncio
30
+ from avia_api import AviaApiClient
31
+
32
+ async def main() -> None:
33
+ async with AviaApiClient(token="YOUR_TOKEN") as client:
34
+ prices = await client.prices.cheap(origin="MOW", destination="LED")
35
+ for destination, by_index in prices.items():
36
+ for entry in by_index.values():
37
+ print(destination, entry.price, entry.airline, entry.departure_at)
38
+
39
+ asyncio.run(main())
40
+ ```
41
+
42
+ You can also skip passing the token explicitly and put it in the `TRAVELPAYOUTS_TOKEN` environment variable - the client will pick it up automatically. Get a token in your personal dashboard: https://www.travelpayouts.com/programs/100/tools/api
43
+
44
+ ## Resources and endpoints
45
+
46
+ All methods return models validated by [pydantic](https://docs.pydantic.dev/)
47
+
48
+ ### `client.prices` - prices
49
+
50
+ | Method | Endpoint | Description |
51
+ | ------------------------------------------------- | -------------------------------------- | ------------------------------------------------ |
52
+ | `cheap(origin, destination, ...)` | `GET /v1/prices/cheap` | Cheapest tickets for a route |
53
+ | `direct(origin, destination, ...)` | `GET /v1/prices/direct` | Same, but direct flights only |
54
+ | `calendar(origin, destination, depart_date, ...)` | `GET /v1/prices/calendar` | Price calendar for every day of the month |
55
+ | `monthly(origin, destination, ...)` | `GET /v1/prices/monthly` | Lowest price by month |
56
+ | `latest(...)` | `GET /v2/prices/latest` | Latest found prices across the whole search base |
57
+ | `month_matrix(origin, destination, ...)` | `GET /v2/prices/month-matrix` | Price calendar for a month (v2) |
58
+ | `week_matrix(origin, destination, ...)` | `GET /v2/prices/week-matrix` | Price calendar for a week |
59
+ | `nearest_places_matrix(origin, destination, ...)` | `GET /v2/prices/nearest-places-matrix` | Prices for nearby airports/cities |
60
+
61
+ ### `client.directions` - popular routes
62
+
63
+ | Method | Endpoint | Description |
64
+ | ---------------------------- | ---------------------------- | -------------------------------- |
65
+ | `airline(airline_code, ...)` | `GET /v1/airline-directions` | Popular routes for an airline |
66
+ | `city(origin, ...)` | `GET /v1/city-directions` | Popular destinations from a city |
67
+
68
+ ### `client.reference` - reference data
69
+
70
+ Public, rarely changing JSON files:
71
+
72
+ | Method | Endpoint |
73
+ | ---------------------------------- | ---------------------------------------------- |
74
+ | `countries(language="en")` | `GET /data/{language}/countries.json` |
75
+ | `cities(language="en")` | `GET /data/{language}/cities.json` |
76
+ | `airports(language="en")` | `GET /data/{language}/airports.json` |
77
+ | `airlines(language="en")` | `GET /data/{language}/airlines.json` |
78
+ | `airline_alliances(language="en")` | `GET /data/{language}/airlines_alliances.json` |
79
+ | `planes()` | `GET /data/planes.json` |
80
+ | `routes()` | `GET /data/routes.json` |
81
+
82
+ ## Client configuration
83
+
84
+ ```python
85
+ from avia_api import AviaApiClient
86
+ from pyrate_limiter import Rate, Duration
87
+
88
+ client = AviaApiClient(
89
+ token="...",
90
+ rate=Rate(5, Duration.SECOND), # outgoing request rate limit (pyrate-limiter)
91
+ max_retries=3, # retries on 429/5xx and connection drops
92
+ cache_ttl=1800, # seconds; None disables the response cache
93
+ cache_path="avia_api.db", # sqlite cache file (hishel), relative path
94
+ # goes under .cache/hishel/
95
+ timeout=10.0,
96
+ )
97
+ ```
98
+
99
+ - **Rate limiting** - [pyrate-limiter](https://github.com/vutran1710/PyrateLimiter), a single bucket per client. Limits the rate of outgoing requests before they're sent, to avoid getting a `429` from the API.
100
+ - **Retries** - [tenacity](https://github.com/jd/tenacity) with exponential backoff and jitter; on `429` the `Retry-After` header is honored if present, but capped at 60 seconds per attempt - an unusually large value from the server (e.g. during an incident on its side) can't stall a request indefinitely.
101
+ - **Cache** - [hishel](https://hishel.com) on top of sqlite. Travelpayouts responses don't send `Cache-Control`, so `FilterPolicy` is used (any successful `GET` is cached, with entry lifetime governed by `cache_ttl`) instead of RFC 9111.
102
+
103
+ For tests or non-standard scenarios you can pass `transport=...` - your own `httpx.AsyncBaseTransport`, which fully disables rate limiting/retry/cache, and requests go straight through it (see `respx` or `httpx.MockTransport`).
104
+
105
+ ## Error handling
106
+
107
+ All exceptions derive from `avia_api.AviaApiError`:
108
+
109
+ | Exception | When it occurs |
110
+ | ---------------------------- | -------------------------------------------------------------- |
111
+ | `AviaApiConnectionError` | Network unavailable / timeout - after retries are exhausted |
112
+ | `AviaApiAuthenticationError` | HTTP 401/403 - token missing or invalid |
113
+ | `AviaApiRateLimitError` | HTTP 429 - after retries are exhausted; has `.retry_after` |
114
+ | `AviaApiServerError` | HTTP 5xx - after retries are exhausted |
115
+ | `AviaApiHTTPStatusError` | Other HTTP errors |
116
+ | `AviaApiResponseError` | HTTP 200, but `{"success": false}` in the body; has `.payload` |
117
+ | `AviaApiValidationError` | Response doesn't match the expected schema (API changed) |
118
+
119
+ ## Logging
120
+
121
+ The library uses standard `logging`. To see logs, enable the desired level for the `avia_api` logger:
122
+
123
+ ```python
124
+ import logging
125
+
126
+ logging.basicConfig(level=logging.INFO)
127
+ logging.getLogger("avia_api").setLevel(logging.DEBUG)
128
+ ```
129
+
130
+ What is logged and at what level:
131
+
132
+ | Logger | Level | Event |
133
+ | --------------------- | --------- | -------------------------------------------------------------------------- |
134
+ | `avia_api._transport` | `DEBUG` | Delay in the rate limiter; successful request and its status |
135
+ | `avia_api._transport` | `WARNING` | Retry after a transient error/status; retry budget exhausted |
136
+ | `avia_api._client` | `DEBUG` | Outgoing request (path + query parameters) and response status |
137
+ | `avia_api._client` | `WARNING` | HTTP 401/403/429/4xx; `{"success": false}` in the response body |
138
+ | `avia_api._client` | `ERROR` | HTTP 5xx; network error after retries; response failed the pydantic schema |
@@ -0,0 +1,101 @@
1
+ [project]
2
+ name = "avia-api"
3
+ version = "0.1.0"
4
+ description = "Async Python client for the Travelpayouts / Aviasales Data API"
5
+ authors = [
6
+ {name = "Ivan Sladkov",email = "ivan.sladkov@yandex.ru"}
7
+ ]
8
+ license = "MIT"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ keywords = [
12
+ "travelpayouts",
13
+ "aviasales",
14
+ "flights",
15
+ "airline-tickets",
16
+ "prices",
17
+ "api-client",
18
+ "async",
19
+ "httpx",
20
+ "pydantic",
21
+ ]
22
+ classifiers = [
23
+ "Development Status :: 4 - Beta",
24
+ "Intended Audience :: Developers",
25
+ "Operating System :: OS Independent",
26
+ "Programming Language :: Python :: 3 :: Only",
27
+ "Programming Language :: Python :: 3.11",
28
+ "Programming Language :: Python :: 3.12",
29
+ "Programming Language :: Python :: 3.13",
30
+ "Programming Language :: Python :: 3.14",
31
+ "Typing :: Typed",
32
+ "Framework :: AsyncIO",
33
+ "Topic :: Internet :: WWW/HTTP",
34
+ "Topic :: Software Development :: Libraries :: Python Modules",
35
+ ]
36
+ dependencies = [
37
+ "httpx (>=0.28.1,<0.29.0)",
38
+ "pyrate-limiter (>=4.5.0,<5.0.0)",
39
+ "tenacity (>=9.1.4,<10.0.0)",
40
+ "pydantic (>=2.13.5,<3.0.0)",
41
+ "hishel[httpx] (>=1.3.1,<2.0.0)"
42
+ ]
43
+
44
+ [project.urls]
45
+ Homepage = "https://github.com/sliv2001/avia-api"
46
+ Repository = "https://github.com/sliv2001/avia-api"
47
+ Issues = "https://github.com/sliv2001/avia-api/issues"
48
+ Changelog = "https://github.com/sliv2001/avia-api/blob/master/CHANGELOG.md"
49
+
50
+ [tool.poetry]
51
+ packages = [{include = "avia_api", from = "src"}]
52
+
53
+ [build-system]
54
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
55
+ build-backend = "poetry.core.masonry.api"
56
+
57
+ [dependency-groups]
58
+ dev = [
59
+ "pytest (>=9.1.1,<10.0.0)",
60
+ "pytest-asyncio (>=1.4.0,<2.0.0)",
61
+ "respx (>=0.23.1,<0.24.0)",
62
+ "pytest-cov (>=7.1.0,<8.0.0)",
63
+ "ruff (>=0.14.0,<0.15.0)",
64
+ "mypy (>=1.19.0,<2.0.0)"
65
+ ]
66
+
67
+ [tool.pytest.ini_options]
68
+ asyncio_mode = "auto"
69
+ testpaths = ["tests"]
70
+
71
+ [tool.coverage.run]
72
+ source = ["src/avia_api"]
73
+
74
+ [tool.coverage.report]
75
+ fail_under = 99
76
+ exclude_also = [
77
+ "if t\\.TYPE_CHECKING:",
78
+ ]
79
+
80
+ [tool.ruff]
81
+ line-length = 120
82
+ target-version = "py311"
83
+
84
+ [tool.ruff.lint]
85
+ select = [
86
+ "E", # pycodestyle
87
+ "F", # pyflakes
88
+ "I", # isort
89
+ "UP", # pyupgrade
90
+ "B", # flake8-bugbear
91
+ "SIM", # flake8-simplify
92
+ ]
93
+
94
+ [tool.mypy]
95
+ python_version = "3.11"
96
+ strict = true
97
+ files = ["src/avia_api"]
98
+
99
+ [[tool.mypy.overrides]]
100
+ module = "hishel.*"
101
+ ignore_missing_imports = true
@@ -0,0 +1,34 @@
1
+ """Async Python client for the Travelpayouts / Aviasales Data API."""
2
+
3
+ import logging
4
+
5
+ from ._client import AviaApiClient
6
+ from .exceptions import (
7
+ AviaApiAuthenticationError,
8
+ AviaApiConnectionError,
9
+ AviaApiError,
10
+ AviaApiHTTPStatusError,
11
+ AviaApiRateLimitError,
12
+ AviaApiResponseError,
13
+ AviaApiServerError,
14
+ AviaApiValidationError,
15
+ )
16
+
17
+ # Libraries should never configure handlers themselves - this only silences
18
+ # the "No handlers could be found" warning until the application attaches
19
+ # its own. See https://docs.python.org/3/howto/logging.html#library-config
20
+ logging.getLogger(__name__).addHandler(logging.NullHandler())
21
+
22
+ __version__ = "0.1.0"
23
+
24
+ __all__ = [
25
+ "AviaApiClient",
26
+ "AviaApiError",
27
+ "AviaApiConnectionError",
28
+ "AviaApiHTTPStatusError",
29
+ "AviaApiAuthenticationError",
30
+ "AviaApiRateLimitError",
31
+ "AviaApiServerError",
32
+ "AviaApiResponseError",
33
+ "AviaApiValidationError",
34
+ ]
@@ -0,0 +1,137 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Any, TypeVar
7
+
8
+ import httpx
9
+ from pydantic import TypeAdapter, ValidationError
10
+ from pyrate_limiter import Rate
11
+
12
+ from ._params import clean_params
13
+ from ._transport import build_transport
14
+ from ._utils import parse_retry_after
15
+ from .exceptions import (
16
+ AviaApiAuthenticationError,
17
+ AviaApiConnectionError,
18
+ AviaApiHTTPStatusError,
19
+ AviaApiRateLimitError,
20
+ AviaApiResponseError,
21
+ AviaApiServerError,
22
+ AviaApiValidationError,
23
+ )
24
+ from .resources import DirectionsResource, PricesResource, ReferenceResource
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ DEFAULT_BASE_URL = "https://api.travelpayouts.com"
29
+ TOKEN_ENV_VAR = "TRAVELPAYOUTS_TOKEN"
30
+
31
+ T = TypeVar("T")
32
+
33
+
34
+ class AviaApiClient:
35
+ """Async client for the Travelpayouts / Aviasales Data API.
36
+
37
+ Example:
38
+ async with AviaApiClient(token="...") as client:
39
+ prices = await client.prices.cheap(origin="MOW", destination="LED")
40
+
41
+ Args:
42
+ token: API token from the partner's Travelpayouts account. Falls
43
+ back to the ``TRAVELPAYOUTS_TOKEN`` environment variable. Most
44
+ endpoints work without one at a reduced quota, but reads always
45
+ attach it when available.
46
+ base_url: Overridable mainly for testing.
47
+ timeout: Passed straight to ``httpx``.
48
+ rate: A ``pyrate_limiter.Rate`` (or list of rates) capping outbound
49
+ request throughput. Defaults to 5 requests/second.
50
+ max_retries: Attempts for requests that fail with a connection error
51
+ or a 429/5xx status, with exponential backoff (honoring
52
+ ``Retry-After`` when present).
53
+ cache_ttl: How long a successful GET response is reused for, in
54
+ seconds. ``None`` disables the cache.
55
+ cache_path: SQLite file backing the cache (relative paths land under
56
+ ``.cache/hishel/``, matching hishel's own convention).
57
+ transport: Escape hatch for tests or advanced setups - supplying this
58
+ bypasses rate limiting/retries/caching entirely.
59
+ """
60
+
61
+ def __init__(
62
+ self,
63
+ token: str | None = None,
64
+ *,
65
+ base_url: str = DEFAULT_BASE_URL,
66
+ timeout: float | httpx.Timeout = 10.0,
67
+ rate: Rate | list[Rate] | None = None,
68
+ max_retries: int = 3,
69
+ cache_ttl: float | None = 1800.0,
70
+ cache_path: str | Path = "avia_api.db",
71
+ transport: httpx.AsyncBaseTransport | None = None,
72
+ ) -> None:
73
+ token = token or os.environ.get(TOKEN_ENV_VAR)
74
+ headers = {"X-Access-Token": token} if token else {}
75
+ if transport is None:
76
+ transport = build_transport(
77
+ rate=rate,
78
+ max_retries=max_retries,
79
+ cache_ttl=cache_ttl,
80
+ cache_path=cache_path,
81
+ )
82
+ self._http = httpx.AsyncClient(base_url=base_url, headers=headers, timeout=timeout, transport=transport)
83
+
84
+ self.prices = PricesResource(self)
85
+ self.directions = DirectionsResource(self)
86
+ self.reference = ReferenceResource(self)
87
+
88
+ async def __aenter__(self) -> AviaApiClient:
89
+ return self
90
+
91
+ async def __aexit__(self, *exc_info: object) -> None:
92
+ await self.aclose()
93
+
94
+ async def aclose(self) -> None:
95
+ await self._http.aclose()
96
+
97
+ async def _get_json(self, path: str, *, params: dict[str, Any], adapter: TypeAdapter[T]) -> T:
98
+ cleaned_params = clean_params(params)
99
+ logger.debug("GET %s params=%r", path, cleaned_params)
100
+ try:
101
+ response = await self._http.get(path, params=cleaned_params)
102
+ except httpx.TransportError as exc:
103
+ logger.error("GET %s failed: %s", path, exc)
104
+ raise AviaApiConnectionError(str(exc)) from exc
105
+
106
+ self._raise_for_status(response)
107
+
108
+ payload = response.json()
109
+ if isinstance(payload, dict) and payload.get("success") is False:
110
+ logger.warning("GET %s returned success=false: %s", path, payload.get("error"))
111
+ raise AviaApiResponseError(payload.get("error") or "Aviasales API returned an error", payload=payload)
112
+
113
+ try:
114
+ result = adapter.validate_python(payload)
115
+ except ValidationError as exc:
116
+ logger.error("GET %s response failed schema validation: %s", path, exc)
117
+ raise AviaApiValidationError(str(exc)) from exc
118
+
119
+ logger.debug("GET %s -> %d", path, response.status_code)
120
+ return result
121
+
122
+ @staticmethod
123
+ def _raise_for_status(response: httpx.Response) -> None:
124
+ if response.status_code < 400:
125
+ return
126
+ if response.status_code in (401, 403):
127
+ logger.warning("%s -> %d (authentication error)", response.request.url, response.status_code)
128
+ raise AviaApiAuthenticationError(response)
129
+ if response.status_code == 429:
130
+ retry_after = parse_retry_after(response.headers.get("retry-after"))
131
+ logger.warning("%s -> 429 (rate limited); retry_after=%s", response.request.url, retry_after)
132
+ raise AviaApiRateLimitError(response, retry_after=retry_after)
133
+ if response.status_code >= 500:
134
+ logger.error("%s -> %d (server error)", response.request.url, response.status_code)
135
+ raise AviaApiServerError(response)
136
+ logger.warning("%s -> %d", response.request.url, response.status_code)
137
+ raise AviaApiHTTPStatusError(response)
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from typing import Any
5
+
6
+
7
+ def clean_params(params: Mapping[str, Any]) -> dict[str, Any]:
8
+ """Drop ``None`` values for use as query params.
9
+
10
+ httpx serializes booleans as ``true``/``false`` on its own, but it has no
11
+ special handling for ``None`` - it would be sent as an empty string
12
+ otherwise - so that's filtered out here before the request is built.
13
+ """
14
+ return {key: value for key, value in params.items() if value is not None}