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,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,8 @@
1
+ """Constants for the Vessel API Python SDK."""
2
+
3
+ VERSION = "1.0.0"
4
+ DEFAULT_BASE_URL = "https://api.vesselapi.com/v1"
5
+ DEFAULT_USER_AGENT = f"vesselapi-python/{VERSION}"
6
+ DEFAULT_MAX_RETRIES = 3
7
+ DEFAULT_TIMEOUT = 30.0
8
+ MAX_BACKOFF = 30.0 # seconds
@@ -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