vessel-api-python 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.
@@ -0,0 +1,185 @@
1
+ """HTTP transport middleware for auth and retry logic."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import datetime
7
+ import math
8
+ import random
9
+ import time
10
+ from email.utils import parsedate_to_datetime
11
+
12
+ import httpx
13
+
14
+ from ._constants import MAX_BACKOFF
15
+
16
+ # ---------------------------------------------------------------------------
17
+ # Auth transports
18
+ # ---------------------------------------------------------------------------
19
+
20
+
21
+ class AuthTransport(httpx.BaseTransport):
22
+ """Sync transport that adds Bearer token auth and User-Agent headers."""
23
+
24
+ def __init__(self, base: httpx.BaseTransport, api_key: str, user_agent: str) -> None:
25
+ self._base = base
26
+ self._api_key = api_key
27
+ self._user_agent = user_agent
28
+
29
+ def handle_request(self, request: httpx.Request) -> httpx.Response:
30
+ request.headers["Authorization"] = f"Bearer {self._api_key}"
31
+ request.headers["User-Agent"] = self._user_agent
32
+ return self._base.handle_request(request)
33
+
34
+ def close(self) -> None:
35
+ self._base.close()
36
+
37
+
38
+ class AsyncAuthTransport(httpx.AsyncBaseTransport):
39
+ """Async transport that adds Bearer token auth and User-Agent headers."""
40
+
41
+ def __init__(self, base: httpx.AsyncBaseTransport, api_key: str, user_agent: str) -> None:
42
+ self._base = base
43
+ self._api_key = api_key
44
+ self._user_agent = user_agent
45
+
46
+ async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
47
+ request.headers["Authorization"] = f"Bearer {self._api_key}"
48
+ request.headers["User-Agent"] = self._user_agent
49
+ return await self._base.handle_async_request(request)
50
+
51
+ async def aclose(self) -> None:
52
+ await self._base.aclose()
53
+
54
+
55
+ # ---------------------------------------------------------------------------
56
+ # Retry transports
57
+ # ---------------------------------------------------------------------------
58
+
59
+
60
+ class RetryTransport(httpx.BaseTransport):
61
+ """Sync transport with retry logic for 429, 5xx, and transient errors.
62
+
63
+ Implements exponential backoff with jitter, respects Retry-After headers
64
+ (both seconds and HTTP-date formats), and caps backoff at 30 seconds.
65
+ Only retries non-idempotent methods (POST/PATCH) on 429.
66
+ """
67
+
68
+ def __init__(self, base: httpx.BaseTransport, max_retries: int = 3) -> None:
69
+ self._base = base
70
+ self._max_retries = max(max_retries, 0)
71
+
72
+ def handle_request(self, request: httpx.Request) -> httpx.Response:
73
+ for attempt in range(self._max_retries + 1):
74
+ try:
75
+ response = self._base.handle_request(request)
76
+ except httpx.TransportError:
77
+ # Retry transient network errors for idempotent methods only.
78
+ if attempt >= self._max_retries or not _is_idempotent(request.method):
79
+ raise
80
+ time.sleep(_calc_exp_backoff(attempt))
81
+ continue
82
+
83
+ if not _is_retryable(response.status_code) or attempt >= self._max_retries:
84
+ return response
85
+
86
+ # Don't retry non-idempotent methods on 5xx.
87
+ if response.status_code != 429 and not _is_idempotent(request.method):
88
+ return response
89
+
90
+ wait = _calc_backoff(attempt, response)
91
+ # Read and discard the body to free the connection.
92
+ response.read()
93
+ response.close()
94
+ time.sleep(wait)
95
+
96
+ # Unreachable — the loop always returns.
97
+ raise RuntimeError("vesselapi: retry loop exited unexpectedly") # pragma: no cover
98
+
99
+ def close(self) -> None:
100
+ self._base.close()
101
+
102
+
103
+ class AsyncRetryTransport(httpx.AsyncBaseTransport):
104
+ """Async transport with retry logic for 429, 5xx, and transient errors.
105
+
106
+ Same logic as RetryTransport but using asyncio.sleep for async contexts.
107
+ """
108
+
109
+ def __init__(self, base: httpx.AsyncBaseTransport, max_retries: int = 3) -> None:
110
+ self._base = base
111
+ self._max_retries = max(max_retries, 0)
112
+
113
+ async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
114
+ for attempt in range(self._max_retries + 1):
115
+ try:
116
+ response = await self._base.handle_async_request(request)
117
+ except httpx.TransportError:
118
+ if attempt >= self._max_retries or not _is_idempotent(request.method):
119
+ raise
120
+ await asyncio.sleep(_calc_exp_backoff(attempt))
121
+ continue
122
+
123
+ if not _is_retryable(response.status_code) or attempt >= self._max_retries:
124
+ return response
125
+
126
+ if response.status_code != 429 and not _is_idempotent(request.method):
127
+ return response
128
+
129
+ wait = _calc_backoff(attempt, response)
130
+ await response.aread()
131
+ await response.aclose()
132
+ await asyncio.sleep(wait)
133
+
134
+ raise RuntimeError("vesselapi: retry loop exited unexpectedly") # pragma: no cover
135
+
136
+ async def aclose(self) -> None:
137
+ await self._base.aclose()
138
+
139
+
140
+ # ---------------------------------------------------------------------------
141
+ # Helpers
142
+ # ---------------------------------------------------------------------------
143
+
144
+
145
+ def _is_retryable(status_code: int) -> bool:
146
+ """Return True if the status code warrants a retry."""
147
+ return status_code == 429 or status_code >= 500
148
+
149
+
150
+ def _is_idempotent(method: str) -> bool:
151
+ """Return True for HTTP methods that are safe to retry."""
152
+ return method.upper() in {"GET", "HEAD", "OPTIONS", "PUT", "DELETE"}
153
+
154
+
155
+ def _calc_backoff(attempt: int, response: httpx.Response) -> float:
156
+ """Calculate retry wait time, respecting Retry-After header."""
157
+ retry_after = response.headers.get("Retry-After", "")
158
+ if retry_after:
159
+ # Try integer seconds first.
160
+ try:
161
+ seconds = int(retry_after)
162
+ return max(0.0, min(float(seconds), MAX_BACKOFF))
163
+ except ValueError:
164
+ pass
165
+ # Try HTTP-date format (RFC 7231 section 7.1.3).
166
+ try:
167
+ dt = parsedate_to_datetime(retry_after)
168
+ delta = (dt - _utcnow()).total_seconds()
169
+ return max(0.0, min(delta, MAX_BACKOFF))
170
+ except (ValueError, TypeError):
171
+ pass
172
+ return _calc_exp_backoff(attempt)
173
+
174
+
175
+ def _calc_exp_backoff(attempt: int) -> float:
176
+ """Exponential backoff with jitter, capped at MAX_BACKOFF."""
177
+ base = math.pow(2, attempt)
178
+ jitter = random.random() * base # noqa: S311
179
+ duration = (base + jitter) * 0.5
180
+ return min(duration, MAX_BACKOFF)
181
+
182
+
183
+ def _utcnow() -> datetime.datetime:
184
+ """Return current UTC datetime. Extracted for test patching."""
185
+ return datetime.datetime.now(datetime.timezone.utc)
File without changes
@@ -0,0 +1,168 @@
1
+ Metadata-Version: 2.4
2
+ Name: vessel-api-python
3
+ Version: 1.0.0
4
+ Summary: Python client for the Vessel Tracking API — maritime vessel tracking, port events, emissions, and navigation data.
5
+ Project-URL: Documentation, https://vesselapi.com/docs
6
+ Project-URL: Repository, https://github.com/vessel-api/vesselapi-python
7
+ Project-URL: Issues, https://github.com/vessel-api/vesselapi-python/issues
8
+ Project-URL: Changelog, https://github.com/vessel-api/vesselapi-python/releases
9
+ Author-email: Vessel API <support@vesselapi.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: ais,maritime,sdk,tracking,vessel,vesselapi
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.9
24
+ Requires-Dist: eval-type-backport>=0.2.0; python_version < '3.10'
25
+ Requires-Dist: httpx>=0.27
26
+ Requires-Dist: pydantic>=2.0
27
+ Provides-Extra: dev
28
+ Requires-Dist: mypy>=1.10; extra == 'dev'
29
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
30
+ Requires-Dist: pytest>=8.0; extra == 'dev'
31
+ Requires-Dist: respx>=0.21; extra == 'dev'
32
+ Requires-Dist: ruff>=0.4; extra == 'dev'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # vesselapi-python
36
+
37
+ [![CI](https://github.com/vessel-api/vesselapi-python/actions/workflows/ci.yml/badge.svg)](https://github.com/vessel-api/vesselapi-python/actions/workflows/ci.yml)
38
+ [![PyPI](https://img.shields.io/pypi/v/vessel-api-python.svg)](https://pypi.org/project/vessel-api-python/)
39
+ [![Python](https://img.shields.io/pypi/pyversions/vessel-api-python.svg)](https://pypi.org/project/vessel-api-python/)
40
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
41
+
42
+ Python client for the [Vessel Tracking API](https://vesselapi.com) — maritime vessel tracking, port events, emissions, and navigation data.
43
+
44
+ **Resources**: [Documentation](https://vesselapi.com/docs) | [API Explorer](https://vesselapi.com/api-reference) | [Dashboard](https://dashboard.vesselapi.com) | [Contact Support](mailto:support@vesselapi.com)
45
+
46
+ ## Install
47
+
48
+ ```bash
49
+ pip install vessel-api-python
50
+ ```
51
+
52
+ Requires Python 3.9+.
53
+
54
+ ## Quick Start
55
+
56
+ ```python
57
+ from vessel_api_python import VesselClient
58
+
59
+ client = VesselClient(api_key="your-api-key")
60
+
61
+ # Search for a vessel by name.
62
+ result = client.search.vessels(filter_name="Ever Given")
63
+ for v in result.vessels or []:
64
+ print(f"{v.name} (IMO {v.imo})")
65
+
66
+ # Get a port by UN/LOCODE.
67
+ port = client.ports.get("NLRTM")
68
+ print(port.port.name)
69
+
70
+ # Auto-paginate through port events.
71
+ for event in client.port_events.list_all(pagination_limit=10):
72
+ print(f"{event.event} at {event.timestamp}")
73
+ ```
74
+
75
+ ### Async
76
+
77
+ ```python
78
+ import asyncio
79
+ from vessel_api_python import AsyncVesselClient
80
+
81
+ async def main():
82
+ async with AsyncVesselClient(api_key="your-api-key") as client:
83
+ result = await client.search.vessels(filter_name="Ever Given")
84
+ async for event in client.port_events.list_all(pagination_limit=10):
85
+ print(f"{event.event} at {event.timestamp}")
86
+
87
+ asyncio.run(main())
88
+ ```
89
+
90
+ ## Available Services
91
+
92
+ | Service | Methods | Description |
93
+ |---------|---------|-------------|
94
+ | `vessels` | `get`, `position`, `casualties`, `classification`, `emissions`, `eta`, `inspections`, `inspection_detail`, `ownership`, `positions` | Vessel details, positions, and records |
95
+ | `ports` | `get` | Port lookup by UN/LOCODE |
96
+ | `port_events` | `list`, `by_port`, `by_ports`, `by_vessel`, `last_by_vessel`, `by_vessels` | Vessel arrival/departure events |
97
+ | `emissions` | `list` | EU MRV emissions data |
98
+ | `search` | `vessels`, `ports`, `dgps`, `light_aids`, `modus`, `radio_beacons` | Full-text search across entity types |
99
+ | `location` | `vessels_bounding_box`, `vessels_radius`, `ports_bounding_box`, `ports_radius`, `dgps_bounding_box`, `dgps_radius`, `light_aids_bounding_box`, `light_aids_radius`, `modus_bounding_box`, `modus_radius`, `radio_beacons_bounding_box`, `radio_beacons_radius` | Geo queries by bounding box or radius |
100
+ | `navtex` | `list` | NAVTEX maritime safety messages |
101
+
102
+ **37 methods total.**
103
+
104
+ ## Error Handling
105
+
106
+ All methods raise specific exception types on non-2xx responses:
107
+
108
+ ```python
109
+ from vessel_api_python import VesselAPIError
110
+
111
+ try:
112
+ client.ports.get("ZZZZZ")
113
+ except VesselAPIError as err:
114
+ if err.is_not_found:
115
+ print("Port not found")
116
+ elif err.is_rate_limited:
117
+ print("Rate limited — back off")
118
+ elif err.is_auth_error:
119
+ print("Check API key")
120
+ print(err.status_code, err.message)
121
+ ```
122
+
123
+ ## Auto-Pagination
124
+
125
+ Every list endpoint has an `all_*` / `list_all` variant returning an iterator:
126
+
127
+ ```python
128
+ # Sync
129
+ for vessel in client.search.all_vessels(filter_name="tanker"):
130
+ print(vessel.name)
131
+
132
+ # Async
133
+ async for vessel in client.search.all_vessels(filter_name="tanker"):
134
+ print(vessel.name)
135
+
136
+ # Collect all at once
137
+ vessels = client.search.all_vessels(filter_name="tanker").collect()
138
+ ```
139
+
140
+ ## Configuration
141
+
142
+ ```python
143
+ client = VesselClient(
144
+ api_key="your-api-key",
145
+ base_url="https://custom-endpoint.example.com/v1",
146
+ timeout=60.0,
147
+ max_retries=5, # default: 3
148
+ user_agent="my-app/1.0",
149
+ )
150
+ ```
151
+
152
+ Retries use exponential backoff with jitter on 429 and 5xx responses. The `Retry-After` header is respected.
153
+
154
+ ## Documentation
155
+
156
+ - [API Documentation](https://vesselapi.com/docs) — endpoint guides, request/response schemas, and usage examples
157
+ - [API Explorer](https://vesselapi.com/api-reference) — interactive API reference
158
+ - [Dashboard](https://dashboard.vesselapi.com) — manage API keys and monitor usage
159
+
160
+ ## Contributing & Support
161
+
162
+ Found a bug, have a feature request, or need help? You're welcome to [open an issue](https://github.com/vessel-api/vesselapi-python/issues). For API-level bugs and feature requests, please use the [main VesselAPI repository](https://github.com/vessel-api/VesselApi/issues).
163
+
164
+ For security vulnerabilities, **do not** open a public issue — email security@vesselapi.com instead. See [SECURITY.md](SECURITY.md).
165
+
166
+ ## License
167
+
168
+ [MIT](LICENSE)
@@ -0,0 +1,13 @@
1
+ vessel_api_python/__init__.py,sha256=K4ob0XECb6ur-lKl_h0YAJQAhQ9HO1JSTv9fCshj9gQ,4505
2
+ vessel_api_python/_client.py,sha256=39yKlQ4W2TnE85sTtK7KlBzFF385Pl52I5ghyq575EA,5067
3
+ vessel_api_python/_constants.py,sha256=wQeOziPys14EdD21f2vBgM8BKrO3WeHIUeOTQAG3BAI,244
4
+ vessel_api_python/_errors.py,sha256=pxDm36mbrviOoDRmJ-ogtlUIsyu-dHpC0jcs5f34WDU,4310
5
+ vessel_api_python/_iterator.py,sha256=SkgsGSdxLrKtw-ad8fSUPaEDDJCamAKK0PrG_P8HX-4,3198
6
+ vessel_api_python/_models.py,sha256=g1c--6UUhtfDSfTv4G6wDmYxGdURN8KASNNAXXQvibE,29124
7
+ vessel_api_python/_services.py,sha256=-3FFJo3C_5fXZDnPc8gcgXQ472BC1hScaQWIOX6qQiU,73801
8
+ vessel_api_python/_transport.py,sha256=_4s-65JMRwD2S435u0iuoLGH_GyLNaX8cP_uSh_u80I,6692
9
+ vessel_api_python/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ vessel_api_python-1.0.0.dist-info/METADATA,sha256=Pc35iFq8GJ5FywrNWOJKGwcQ2VS8Miv7L8ubW3T3P0s,6309
11
+ vessel_api_python-1.0.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
12
+ vessel_api_python-1.0.0.dist-info/licenses/LICENSE,sha256=zoj_abq99mj8cz-Bz71XCnwB-Qv3kU1VzVXeKEntuQU,1067
13
+ vessel_api_python-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Vessel API
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.