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.
- vessel_api_python/__init__.py +183 -0
- vessel_api_python/_client.py +163 -0
- vessel_api_python/_constants.py +8 -0
- vessel_api_python/_errors.py +137 -0
- vessel_api_python/_iterator.py +120 -0
- vessel_api_python/_models.py +887 -0
- vessel_api_python/_services.py +1180 -0
- vessel_api_python/_transport.py +185 -0
- vessel_api_python/py.typed +0 -0
- vessel_api_python-1.0.0.dist-info/METADATA +168 -0
- vessel_api_python-1.0.0.dist-info/RECORD +13 -0
- vessel_api_python-1.0.0.dist-info/WHEEL +4 -0
- vessel_api_python-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Vessel Tracking API Python SDK.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
from vessel_api_python import VesselClient
|
|
6
|
+
|
|
7
|
+
client = VesselClient(api_key="your-api-key")
|
|
8
|
+
result = client.search.vessels(filter_name="Ever Given")
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from ._client import AsyncVesselClient, VesselClient
|
|
12
|
+
from ._constants import DEFAULT_BASE_URL, VERSION
|
|
13
|
+
from ._errors import (
|
|
14
|
+
VesselAPIError,
|
|
15
|
+
VesselAuthError,
|
|
16
|
+
VesselNotFoundError,
|
|
17
|
+
VesselRateLimitError,
|
|
18
|
+
VesselServerError,
|
|
19
|
+
)
|
|
20
|
+
from ._iterator import AsyncIterator, SyncIterator
|
|
21
|
+
from ._models import (
|
|
22
|
+
MODU,
|
|
23
|
+
BroadcastStation,
|
|
24
|
+
Classification,
|
|
25
|
+
ClassificationCertificate,
|
|
26
|
+
ClassificationCondition,
|
|
27
|
+
ClassificationDimensions,
|
|
28
|
+
ClassificationHull,
|
|
29
|
+
ClassificationIdentification,
|
|
30
|
+
ClassificationInfo,
|
|
31
|
+
ClassificationMachinery,
|
|
32
|
+
ClassificationOwner,
|
|
33
|
+
ClassificationPurpose,
|
|
34
|
+
ClassificationResponse,
|
|
35
|
+
ClassificationSurvey,
|
|
36
|
+
ClassificationVessel,
|
|
37
|
+
ClassificationYard,
|
|
38
|
+
DGPSStation,
|
|
39
|
+
DGPSStationsWithinLocationResponse,
|
|
40
|
+
FindDGPSStationsResponse,
|
|
41
|
+
FindLightAidsResponse,
|
|
42
|
+
FindMODUsResponse,
|
|
43
|
+
FindPortsResponse,
|
|
44
|
+
FindRadioBeaconsResponse,
|
|
45
|
+
FindVesselsResponse,
|
|
46
|
+
GeoJSON,
|
|
47
|
+
Inspection,
|
|
48
|
+
InspectionDeficiency,
|
|
49
|
+
InspectionDetail,
|
|
50
|
+
InspectionDetailRecord,
|
|
51
|
+
InspectionDetailResponseData,
|
|
52
|
+
InspectionRecord,
|
|
53
|
+
InspectionsResponseData,
|
|
54
|
+
LightAid,
|
|
55
|
+
LightAidsWithinLocationResponse,
|
|
56
|
+
MarineCasualtiesResponse,
|
|
57
|
+
MarineCasualty,
|
|
58
|
+
MODUsWithinLocationResponse,
|
|
59
|
+
Navtex,
|
|
60
|
+
NavtexMessagesResponse,
|
|
61
|
+
Ownership,
|
|
62
|
+
OwnershipResponseData,
|
|
63
|
+
Port,
|
|
64
|
+
PortCountry,
|
|
65
|
+
PortEvent,
|
|
66
|
+
PortEventResponse,
|
|
67
|
+
PortEventsResponse,
|
|
68
|
+
PortReference,
|
|
69
|
+
PortResponse,
|
|
70
|
+
PortsWithinLocationResponse,
|
|
71
|
+
RadioBeacon,
|
|
72
|
+
RadioBeaconsWithinLocationResponse,
|
|
73
|
+
TypesInspectionDetailResponse,
|
|
74
|
+
TypesInspectionsResponse,
|
|
75
|
+
TypesOwnershipResponse,
|
|
76
|
+
Vessel,
|
|
77
|
+
VesselEmission,
|
|
78
|
+
VesselEmissionsResponse,
|
|
79
|
+
VesselETA,
|
|
80
|
+
VesselETAResponse,
|
|
81
|
+
VesselFormerName,
|
|
82
|
+
VesselOwnership,
|
|
83
|
+
VesselPosition,
|
|
84
|
+
VesselPositionResponse,
|
|
85
|
+
VesselPositionsResponse,
|
|
86
|
+
VesselReference,
|
|
87
|
+
VesselResponse,
|
|
88
|
+
VesselsWithinLocationResponse,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
__all__ = [
|
|
92
|
+
# Clients
|
|
93
|
+
"VesselClient",
|
|
94
|
+
"AsyncVesselClient",
|
|
95
|
+
# Errors
|
|
96
|
+
"VesselAPIError",
|
|
97
|
+
"VesselAuthError",
|
|
98
|
+
"VesselNotFoundError",
|
|
99
|
+
"VesselRateLimitError",
|
|
100
|
+
"VesselServerError",
|
|
101
|
+
# Iterators
|
|
102
|
+
"SyncIterator",
|
|
103
|
+
"AsyncIterator",
|
|
104
|
+
# Constants
|
|
105
|
+
"VERSION",
|
|
106
|
+
"DEFAULT_BASE_URL",
|
|
107
|
+
# Models — Shared / helper
|
|
108
|
+
"GeoJSON",
|
|
109
|
+
"PortCountry",
|
|
110
|
+
"PortReference",
|
|
111
|
+
"VesselReference",
|
|
112
|
+
"VesselFormerName",
|
|
113
|
+
"BroadcastStation",
|
|
114
|
+
# Models — Classification sub-models
|
|
115
|
+
"ClassificationCertificate",
|
|
116
|
+
"ClassificationCondition",
|
|
117
|
+
"ClassificationDimensions",
|
|
118
|
+
"ClassificationHull",
|
|
119
|
+
"ClassificationIdentification",
|
|
120
|
+
"ClassificationInfo",
|
|
121
|
+
"ClassificationMachinery",
|
|
122
|
+
"ClassificationOwner",
|
|
123
|
+
"ClassificationPurpose",
|
|
124
|
+
"ClassificationSurvey",
|
|
125
|
+
"ClassificationVessel",
|
|
126
|
+
"ClassificationYard",
|
|
127
|
+
# Models — Vessel
|
|
128
|
+
"Vessel",
|
|
129
|
+
"VesselResponse",
|
|
130
|
+
"VesselPosition",
|
|
131
|
+
"VesselPositionResponse",
|
|
132
|
+
"VesselPositionsResponse",
|
|
133
|
+
"MarineCasualty",
|
|
134
|
+
"MarineCasualtiesResponse",
|
|
135
|
+
"Classification",
|
|
136
|
+
"ClassificationResponse",
|
|
137
|
+
"VesselEmission",
|
|
138
|
+
"VesselEmissionsResponse",
|
|
139
|
+
"VesselETA",
|
|
140
|
+
"VesselETAResponse",
|
|
141
|
+
# Models — Inspection
|
|
142
|
+
"InspectionRecord",
|
|
143
|
+
"Inspection",
|
|
144
|
+
"InspectionDeficiency",
|
|
145
|
+
"InspectionDetailRecord",
|
|
146
|
+
"InspectionDetail",
|
|
147
|
+
"InspectionsResponseData",
|
|
148
|
+
"InspectionDetailResponseData",
|
|
149
|
+
"TypesInspectionsResponse",
|
|
150
|
+
"TypesInspectionDetailResponse",
|
|
151
|
+
# Models — Ownership
|
|
152
|
+
"VesselOwnership",
|
|
153
|
+
"Ownership",
|
|
154
|
+
"OwnershipResponseData",
|
|
155
|
+
"TypesOwnershipResponse",
|
|
156
|
+
# Models — Port
|
|
157
|
+
"Port",
|
|
158
|
+
"PortResponse",
|
|
159
|
+
"PortEvent",
|
|
160
|
+
"PortEventResponse",
|
|
161
|
+
"PortEventsResponse",
|
|
162
|
+
# Models — Search
|
|
163
|
+
"FindVesselsResponse",
|
|
164
|
+
"FindPortsResponse",
|
|
165
|
+
"DGPSStation",
|
|
166
|
+
"FindDGPSStationsResponse",
|
|
167
|
+
"LightAid",
|
|
168
|
+
"FindLightAidsResponse",
|
|
169
|
+
"MODU",
|
|
170
|
+
"FindMODUsResponse",
|
|
171
|
+
"RadioBeacon",
|
|
172
|
+
"FindRadioBeaconsResponse",
|
|
173
|
+
# Models — Location
|
|
174
|
+
"VesselsWithinLocationResponse",
|
|
175
|
+
"PortsWithinLocationResponse",
|
|
176
|
+
"DGPSStationsWithinLocationResponse",
|
|
177
|
+
"LightAidsWithinLocationResponse",
|
|
178
|
+
"MODUsWithinLocationResponse",
|
|
179
|
+
"RadioBeaconsWithinLocationResponse",
|
|
180
|
+
# Models — Navtex
|
|
181
|
+
"Navtex",
|
|
182
|
+
"NavtexMessagesResponse",
|
|
183
|
+
]
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""High-level Vessel API clients (sync and async).
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
# Sync
|
|
6
|
+
client = VesselClient(api_key="your-api-key")
|
|
7
|
+
vessel = client.vessels.get("9363728")
|
|
8
|
+
|
|
9
|
+
# Async
|
|
10
|
+
async with AsyncVesselClient(api_key="your-api-key") as client:
|
|
11
|
+
vessel = await client.vessels.get("9363728")
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import httpx
|
|
17
|
+
|
|
18
|
+
from ._constants import (
|
|
19
|
+
DEFAULT_BASE_URL,
|
|
20
|
+
DEFAULT_MAX_RETRIES,
|
|
21
|
+
DEFAULT_TIMEOUT,
|
|
22
|
+
DEFAULT_USER_AGENT,
|
|
23
|
+
)
|
|
24
|
+
from ._services import (
|
|
25
|
+
AsyncEmissionsService,
|
|
26
|
+
AsyncLocationService,
|
|
27
|
+
AsyncNavtexService,
|
|
28
|
+
AsyncPortEventsService,
|
|
29
|
+
AsyncPortsService,
|
|
30
|
+
AsyncSearchService,
|
|
31
|
+
AsyncVesselsService,
|
|
32
|
+
EmissionsService,
|
|
33
|
+
LocationService,
|
|
34
|
+
NavtexService,
|
|
35
|
+
PortEventsService,
|
|
36
|
+
PortsService,
|
|
37
|
+
SearchService,
|
|
38
|
+
VesselsService,
|
|
39
|
+
)
|
|
40
|
+
from ._transport import (
|
|
41
|
+
AsyncAuthTransport,
|
|
42
|
+
AsyncRetryTransport,
|
|
43
|
+
AuthTransport,
|
|
44
|
+
RetryTransport,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class VesselClient:
|
|
49
|
+
"""Synchronous client for the Vessel Tracking API.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
api_key: Bearer token for authentication. Required, must not be empty.
|
|
53
|
+
base_url: API base URL. Defaults to ``https://api.vesselapi.com/v1``.
|
|
54
|
+
timeout: Request timeout in seconds. Defaults to 30.
|
|
55
|
+
max_retries: Maximum retries on 429/5xx. Defaults to 3.
|
|
56
|
+
user_agent: User-Agent header value.
|
|
57
|
+
transport: Custom base ``httpx.BaseTransport`` (auth + retry wrap on top).
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
ValueError: If ``api_key`` is empty.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
api_key: str,
|
|
66
|
+
*,
|
|
67
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
68
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
69
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
70
|
+
user_agent: str = DEFAULT_USER_AGENT,
|
|
71
|
+
transport: httpx.BaseTransport | None = None,
|
|
72
|
+
) -> None:
|
|
73
|
+
if not api_key:
|
|
74
|
+
raise ValueError("vesselapi: api_key must not be empty")
|
|
75
|
+
if max_retries < 0:
|
|
76
|
+
max_retries = 0
|
|
77
|
+
|
|
78
|
+
base_transport = transport or httpx.HTTPTransport()
|
|
79
|
+
auth_transport = AuthTransport(base_transport, api_key, user_agent)
|
|
80
|
+
retry_transport = RetryTransport(auth_transport, max_retries)
|
|
81
|
+
|
|
82
|
+
self._client = httpx.Client(
|
|
83
|
+
base_url=base_url,
|
|
84
|
+
timeout=timeout,
|
|
85
|
+
transport=retry_transport,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
self.vessels = VesselsService(self._client)
|
|
89
|
+
self.ports = PortsService(self._client)
|
|
90
|
+
self.port_events = PortEventsService(self._client)
|
|
91
|
+
self.emissions = EmissionsService(self._client)
|
|
92
|
+
self.search = SearchService(self._client)
|
|
93
|
+
self.location = LocationService(self._client)
|
|
94
|
+
self.navtex = NavtexService(self._client)
|
|
95
|
+
|
|
96
|
+
def close(self) -> None:
|
|
97
|
+
"""Close the underlying HTTP client and release resources."""
|
|
98
|
+
self._client.close()
|
|
99
|
+
|
|
100
|
+
def __enter__(self) -> VesselClient:
|
|
101
|
+
return self
|
|
102
|
+
|
|
103
|
+
def __exit__(self, *args: object) -> None:
|
|
104
|
+
self.close()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class AsyncVesselClient:
|
|
108
|
+
"""Asynchronous client for the Vessel Tracking API.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
api_key: Bearer token for authentication. Required, must not be empty.
|
|
112
|
+
base_url: API base URL. Defaults to ``https://api.vesselapi.com/v1``.
|
|
113
|
+
timeout: Request timeout in seconds. Defaults to 30.
|
|
114
|
+
max_retries: Maximum retries on 429/5xx. Defaults to 3.
|
|
115
|
+
user_agent: User-Agent header value.
|
|
116
|
+
transport: Custom base ``httpx.AsyncBaseTransport`` (auth + retry wrap on top).
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
ValueError: If ``api_key`` is empty.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
def __init__(
|
|
123
|
+
self,
|
|
124
|
+
api_key: str,
|
|
125
|
+
*,
|
|
126
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
127
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
128
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
129
|
+
user_agent: str = DEFAULT_USER_AGENT,
|
|
130
|
+
transport: httpx.AsyncBaseTransport | None = None,
|
|
131
|
+
) -> None:
|
|
132
|
+
if not api_key:
|
|
133
|
+
raise ValueError("vesselapi: api_key must not be empty")
|
|
134
|
+
if max_retries < 0:
|
|
135
|
+
max_retries = 0
|
|
136
|
+
|
|
137
|
+
base_transport = transport or httpx.AsyncHTTPTransport()
|
|
138
|
+
auth_transport = AsyncAuthTransport(base_transport, api_key, user_agent)
|
|
139
|
+
retry_transport = AsyncRetryTransport(auth_transport, max_retries)
|
|
140
|
+
|
|
141
|
+
self._client = httpx.AsyncClient(
|
|
142
|
+
base_url=base_url,
|
|
143
|
+
timeout=timeout,
|
|
144
|
+
transport=retry_transport,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
self.vessels = AsyncVesselsService(self._client)
|
|
148
|
+
self.ports = AsyncPortsService(self._client)
|
|
149
|
+
self.port_events = AsyncPortEventsService(self._client)
|
|
150
|
+
self.emissions = AsyncEmissionsService(self._client)
|
|
151
|
+
self.search = AsyncSearchService(self._client)
|
|
152
|
+
self.location = AsyncLocationService(self._client)
|
|
153
|
+
self.navtex = AsyncNavtexService(self._client)
|
|
154
|
+
|
|
155
|
+
async def aclose(self) -> None:
|
|
156
|
+
"""Close the underlying HTTP client and release resources."""
|
|
157
|
+
await self._client.aclose()
|
|
158
|
+
|
|
159
|
+
async def __aenter__(self) -> AsyncVesselClient:
|
|
160
|
+
return self
|
|
161
|
+
|
|
162
|
+
async def __aexit__(self, *args: object) -> None:
|
|
163
|
+
await self.aclose()
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Error types for the Vessel API Python SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import http
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class VesselAPIError(Exception):
|
|
10
|
+
"""Base error for all Vessel API errors.
|
|
11
|
+
|
|
12
|
+
Attributes:
|
|
13
|
+
status_code: The HTTP status code from the response.
|
|
14
|
+
message: A human-readable error message.
|
|
15
|
+
body: The raw response body bytes, available for re-parsing.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
status_code: int,
|
|
21
|
+
message: str,
|
|
22
|
+
body: bytes = b"",
|
|
23
|
+
) -> None:
|
|
24
|
+
self.status_code = status_code
|
|
25
|
+
self.message = message
|
|
26
|
+
self.body = body
|
|
27
|
+
super().__init__(f"vesselapi: {message} (status {status_code})")
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def is_not_found(self) -> bool:
|
|
31
|
+
"""True if the error is a 404 Not Found response."""
|
|
32
|
+
return self.status_code == 404
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def is_rate_limited(self) -> bool:
|
|
36
|
+
"""True if the error is a 429 Too Many Requests response."""
|
|
37
|
+
return self.status_code == 429
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def is_auth_error(self) -> bool:
|
|
41
|
+
"""True if the error is a 401 Unauthorized response."""
|
|
42
|
+
return self.status_code == 401
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class VesselAuthError(VesselAPIError):
|
|
46
|
+
"""Raised on 401 Unauthorized responses."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class VesselNotFoundError(VesselAPIError):
|
|
50
|
+
"""Raised on 404 Not Found responses."""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class VesselRateLimitError(VesselAPIError):
|
|
54
|
+
"""Raised on 429 Too Many Requests responses."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class VesselServerError(VesselAPIError):
|
|
58
|
+
"""Raised on 5xx server error responses."""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def error_from_response(
|
|
62
|
+
status_code: int,
|
|
63
|
+
body: bytes,
|
|
64
|
+
headers: dict[str, str] | None = None,
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Raise a VesselAPIError if the response indicates an error.
|
|
67
|
+
|
|
68
|
+
Checks for success using ``200 <= status_code < 300``. On error,
|
|
69
|
+
attempts to parse a human-readable message from the JSON body using
|
|
70
|
+
multiple known shapes, falling back to the HTTP status text.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
status_code: The HTTP response status code.
|
|
74
|
+
body: The raw response body bytes.
|
|
75
|
+
headers: Optional response headers (unused currently, reserved).
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
VesselAuthError: On 401 responses.
|
|
79
|
+
VesselNotFoundError: On 404 responses.
|
|
80
|
+
VesselRateLimitError: On 429 responses.
|
|
81
|
+
VesselServerError: On 5xx responses.
|
|
82
|
+
VesselAPIError: On all other non-2xx responses.
|
|
83
|
+
"""
|
|
84
|
+
if 200 <= status_code < 300:
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
# Try to extract a human-readable message from the response body.
|
|
88
|
+
msg = _status_text(status_code)
|
|
89
|
+
if body:
|
|
90
|
+
try:
|
|
91
|
+
data = json.loads(body)
|
|
92
|
+
# Try {"error": {"message": "..."}} (Vessel API standard shape).
|
|
93
|
+
nested_msg = _get_nested(data, "error", "message")
|
|
94
|
+
if nested_msg:
|
|
95
|
+
msg = nested_msg
|
|
96
|
+
else:
|
|
97
|
+
# Try {"message": "..."} (common alternative shape).
|
|
98
|
+
flat_msg = data.get("message") if isinstance(data, dict) else None
|
|
99
|
+
if flat_msg and isinstance(flat_msg, str):
|
|
100
|
+
msg = flat_msg
|
|
101
|
+
# If both fail, msg stays as HTTP status text.
|
|
102
|
+
# Raw body is always available in VesselAPIError.body.
|
|
103
|
+
except (json.JSONDecodeError, TypeError, AttributeError):
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
# Return the appropriate subclass based on status code.
|
|
107
|
+
error_cls: type[VesselAPIError]
|
|
108
|
+
if status_code == 401:
|
|
109
|
+
error_cls = VesselAuthError
|
|
110
|
+
elif status_code == 404:
|
|
111
|
+
error_cls = VesselNotFoundError
|
|
112
|
+
elif status_code == 429:
|
|
113
|
+
error_cls = VesselRateLimitError
|
|
114
|
+
elif status_code >= 500:
|
|
115
|
+
error_cls = VesselServerError
|
|
116
|
+
else:
|
|
117
|
+
error_cls = VesselAPIError
|
|
118
|
+
|
|
119
|
+
raise error_cls(status_code=status_code, message=msg, body=body)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _get_nested(data: dict[str, object] | list[object] | None, *keys: str) -> str | None:
|
|
123
|
+
"""Safely traverse nested dicts to extract a string value."""
|
|
124
|
+
current: object = data
|
|
125
|
+
for key in keys:
|
|
126
|
+
if not isinstance(current, dict):
|
|
127
|
+
return None
|
|
128
|
+
current = current.get(key)
|
|
129
|
+
return current if isinstance(current, str) else None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _status_text(status_code: int) -> str:
|
|
133
|
+
"""Return the HTTP status text for a given code."""
|
|
134
|
+
try:
|
|
135
|
+
return http.HTTPStatus(status_code).phrase
|
|
136
|
+
except ValueError:
|
|
137
|
+
return f"HTTP {status_code}"
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Pagination iterators for the Vessel API Python SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Callable, Generic, Optional, TypeVar
|
|
6
|
+
|
|
7
|
+
T = TypeVar("T")
|
|
8
|
+
|
|
9
|
+
# A fetch function returns (items, next_token_or_None).
|
|
10
|
+
FetchFunc = Callable[[], tuple[list[T], Optional[str]]]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SyncIterator(Generic[T]):
|
|
14
|
+
"""Lazy, sequential iterator over paginated API results (sync).
|
|
15
|
+
|
|
16
|
+
Implements the ``__iter__``/``__next__`` protocol so it can be used
|
|
17
|
+
directly in ``for`` loops.
|
|
18
|
+
|
|
19
|
+
Example::
|
|
20
|
+
|
|
21
|
+
for vessel in client.search.all_vessels(filter_name="tanker"):
|
|
22
|
+
print(vessel.name)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, fetch: FetchFunc[T]) -> None:
|
|
26
|
+
self._fetch = fetch
|
|
27
|
+
self._items: list[T] = []
|
|
28
|
+
self._index = 0
|
|
29
|
+
self._done = False
|
|
30
|
+
self._started = False
|
|
31
|
+
|
|
32
|
+
def __iter__(self) -> SyncIterator[T]:
|
|
33
|
+
return self
|
|
34
|
+
|
|
35
|
+
def __next__(self) -> T:
|
|
36
|
+
# Advance index if we've already yielded at least one item.
|
|
37
|
+
if self._started:
|
|
38
|
+
self._index += 1
|
|
39
|
+
self._started = True
|
|
40
|
+
|
|
41
|
+
# Return buffered item if available.
|
|
42
|
+
if self._index < len(self._items):
|
|
43
|
+
return self._items[self._index]
|
|
44
|
+
|
|
45
|
+
# No more pages.
|
|
46
|
+
if self._done:
|
|
47
|
+
raise StopIteration
|
|
48
|
+
|
|
49
|
+
# Fetch next page.
|
|
50
|
+
items, next_token = self._fetch()
|
|
51
|
+
self._items = items
|
|
52
|
+
self._index = 0
|
|
53
|
+
|
|
54
|
+
if not items:
|
|
55
|
+
self._done = True
|
|
56
|
+
raise StopIteration
|
|
57
|
+
|
|
58
|
+
if not next_token:
|
|
59
|
+
self._done = True
|
|
60
|
+
|
|
61
|
+
return self._items[self._index]
|
|
62
|
+
|
|
63
|
+
def collect(self) -> list[T]:
|
|
64
|
+
"""Consume the iterator and return all remaining items as a list."""
|
|
65
|
+
return list(self)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class AsyncIterator(Generic[T]):
|
|
69
|
+
"""Lazy, sequential iterator over paginated API results (async).
|
|
70
|
+
|
|
71
|
+
Implements the ``__aiter__``/``__anext__`` protocol for use in
|
|
72
|
+
``async for`` loops.
|
|
73
|
+
|
|
74
|
+
Example::
|
|
75
|
+
|
|
76
|
+
async for vessel in client.search.all_vessels(filter_name="tanker"):
|
|
77
|
+
print(vessel.name)
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(self, fetch: Callable[[], object]) -> None:
|
|
81
|
+
# fetch is an async callable returning (items, next_token).
|
|
82
|
+
self._fetch = fetch
|
|
83
|
+
self._items: list[T] = []
|
|
84
|
+
self._index = 0
|
|
85
|
+
self._done = False
|
|
86
|
+
self._started = False
|
|
87
|
+
|
|
88
|
+
def __aiter__(self) -> AsyncIterator[T]:
|
|
89
|
+
return self
|
|
90
|
+
|
|
91
|
+
async def __anext__(self) -> T:
|
|
92
|
+
if self._started:
|
|
93
|
+
self._index += 1
|
|
94
|
+
self._started = True
|
|
95
|
+
|
|
96
|
+
if self._index < len(self._items):
|
|
97
|
+
return self._items[self._index]
|
|
98
|
+
|
|
99
|
+
if self._done:
|
|
100
|
+
raise StopAsyncIteration
|
|
101
|
+
|
|
102
|
+
items, next_token = await self._fetch() # type: ignore[misc]
|
|
103
|
+
self._items = items
|
|
104
|
+
self._index = 0
|
|
105
|
+
|
|
106
|
+
if not items:
|
|
107
|
+
self._done = True
|
|
108
|
+
raise StopAsyncIteration
|
|
109
|
+
|
|
110
|
+
if not next_token:
|
|
111
|
+
self._done = True
|
|
112
|
+
|
|
113
|
+
return self._items[self._index]
|
|
114
|
+
|
|
115
|
+
async def collect(self) -> list[T]:
|
|
116
|
+
"""Consume the iterator and return all remaining items as a list."""
|
|
117
|
+
result: list[T] = []
|
|
118
|
+
async for item in self:
|
|
119
|
+
result.append(item)
|
|
120
|
+
return result
|