vessel-api-python 1.1.0__tar.gz → 1.2.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.
- {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/PKG-INFO +22 -5
- {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/README.md +21 -4
- {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/examples/async_basic.py +19 -1
- {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/examples/basic.py +20 -1
- {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/pyproject.toml +1 -1
- {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/src/vessel_api_python/__init__.py +4 -0
- {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/src/vessel_api_python/_models.py +28 -0
- {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/src/vessel_api_python/_services.py +34 -0
- {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/tests/test_services.py +52 -0
- {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/tests/test_smoke.py +28 -1
- {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/.github/workflows/ci.yml +0 -0
- {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/.github/workflows/publish.yml +0 -0
- {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/.gitignore +0 -0
- {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/LICENSE +0 -0
- {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/Makefile +0 -0
- {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/SECURITY.md +0 -0
- {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/openapi/swagger.json +0 -0
- {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/src/vessel_api_python/_client.py +0 -0
- {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/src/vessel_api_python/_constants.py +0 -0
- {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/src/vessel_api_python/_errors.py +0 -0
- {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/src/vessel_api_python/_iterator.py +0 -0
- {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/src/vessel_api_python/_transport.py +0 -0
- {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/src/vessel_api_python/py.typed +0 -0
- {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/tests/__init__.py +0 -0
- {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/tests/conftest.py +0 -0
- {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/tests/test_client.py +0 -0
- {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/tests/test_errors.py +0 -0
- {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/tests/test_iterator.py +0 -0
- {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/tests/test_transport.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vessel-api-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Python client for the Vessel Tracking API — maritime vessel tracking, port events, emissions, and navigation data.
|
|
5
5
|
Project-URL: Documentation, https://vesselapi.com/docs
|
|
6
6
|
Project-URL: Repository, https://github.com/vessel-api/vesselapi-python
|
|
@@ -101,6 +101,23 @@ asyncio.run(main())
|
|
|
101
101
|
|
|
102
102
|
**37 methods total.**
|
|
103
103
|
|
|
104
|
+
## Vessel Lookup & Location
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
# Get vessel details by IMO number (defaults to IMO; pass filter_id_type="mmsi" for MMSI).
|
|
108
|
+
vessel = client.vessels.get("9811000")
|
|
109
|
+
print(f"{vessel.vessel.name} ({vessel.vessel.vessel_type})")
|
|
110
|
+
|
|
111
|
+
# Get the vessel's latest AIS position.
|
|
112
|
+
pos = client.vessels.position("9811000")
|
|
113
|
+
print(f"Position: {pos.vessel_position.latitude}, {pos.vessel_position.longitude}")
|
|
114
|
+
|
|
115
|
+
# Find all vessels within 10 km of Rotterdam.
|
|
116
|
+
nearby = client.location.vessels_radius(latitude=51.9225, longitude=4.47917, radius=10000)
|
|
117
|
+
for v in nearby.vessels or []:
|
|
118
|
+
print(f"{v.vessel_name} at {v.latitude}, {v.longitude}")
|
|
119
|
+
```
|
|
120
|
+
|
|
104
121
|
## Error Handling
|
|
105
122
|
|
|
106
123
|
All methods raise specific exception types on non-2xx responses:
|
|
@@ -126,15 +143,15 @@ Every list endpoint has an `all_*` / `list_all` variant returning an iterator:
|
|
|
126
143
|
|
|
127
144
|
```python
|
|
128
145
|
# Sync
|
|
129
|
-
for vessel in client.search.all_vessels(
|
|
146
|
+
for vessel in client.search.all_vessels(filter_vessel_type="Tanker"):
|
|
130
147
|
print(vessel.name)
|
|
131
148
|
|
|
132
149
|
# Async
|
|
133
|
-
async for vessel in client.search.all_vessels(
|
|
150
|
+
async for vessel in client.search.all_vessels(filter_vessel_type="Tanker"):
|
|
134
151
|
print(vessel.name)
|
|
135
152
|
|
|
136
|
-
# Collect
|
|
137
|
-
vessels = client.search.all_vessels(
|
|
153
|
+
# Collect a bounded set at once
|
|
154
|
+
vessels = client.search.all_vessels(filter_vessel_type="Tanker", pagination_limit=50).collect()
|
|
138
155
|
```
|
|
139
156
|
|
|
140
157
|
## Configuration
|
|
@@ -67,6 +67,23 @@ asyncio.run(main())
|
|
|
67
67
|
|
|
68
68
|
**37 methods total.**
|
|
69
69
|
|
|
70
|
+
## Vessel Lookup & Location
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
# Get vessel details by IMO number (defaults to IMO; pass filter_id_type="mmsi" for MMSI).
|
|
74
|
+
vessel = client.vessels.get("9811000")
|
|
75
|
+
print(f"{vessel.vessel.name} ({vessel.vessel.vessel_type})")
|
|
76
|
+
|
|
77
|
+
# Get the vessel's latest AIS position.
|
|
78
|
+
pos = client.vessels.position("9811000")
|
|
79
|
+
print(f"Position: {pos.vessel_position.latitude}, {pos.vessel_position.longitude}")
|
|
80
|
+
|
|
81
|
+
# Find all vessels within 10 km of Rotterdam.
|
|
82
|
+
nearby = client.location.vessels_radius(latitude=51.9225, longitude=4.47917, radius=10000)
|
|
83
|
+
for v in nearby.vessels or []:
|
|
84
|
+
print(f"{v.vessel_name} at {v.latitude}, {v.longitude}")
|
|
85
|
+
```
|
|
86
|
+
|
|
70
87
|
## Error Handling
|
|
71
88
|
|
|
72
89
|
All methods raise specific exception types on non-2xx responses:
|
|
@@ -92,15 +109,15 @@ Every list endpoint has an `all_*` / `list_all` variant returning an iterator:
|
|
|
92
109
|
|
|
93
110
|
```python
|
|
94
111
|
# Sync
|
|
95
|
-
for vessel in client.search.all_vessels(
|
|
112
|
+
for vessel in client.search.all_vessels(filter_vessel_type="Tanker"):
|
|
96
113
|
print(vessel.name)
|
|
97
114
|
|
|
98
115
|
# Async
|
|
99
|
-
async for vessel in client.search.all_vessels(
|
|
116
|
+
async for vessel in client.search.all_vessels(filter_vessel_type="Tanker"):
|
|
100
117
|
print(vessel.name)
|
|
101
118
|
|
|
102
|
-
# Collect
|
|
103
|
-
vessels = client.search.all_vessels(
|
|
119
|
+
# Collect a bounded set at once
|
|
120
|
+
vessels = client.search.all_vessels(filter_vessel_type="Tanker", pagination_limit=50).collect()
|
|
104
121
|
```
|
|
105
122
|
|
|
106
123
|
## Configuration
|
|
@@ -4,7 +4,7 @@ import asyncio
|
|
|
4
4
|
import os
|
|
5
5
|
import sys
|
|
6
6
|
|
|
7
|
-
from
|
|
7
|
+
from vessel_api_python import AsyncVesselClient, VesselAPIError
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
async def main() -> None:
|
|
@@ -26,6 +26,24 @@ async def main() -> None:
|
|
|
26
26
|
if port.port:
|
|
27
27
|
print(f"Port: {port.port.name} ({port.port.unlo_code})")
|
|
28
28
|
|
|
29
|
+
# Get vessel details by IMO number (defaults to IMO; pass filter_id_type="mmsi" for MMSI).
|
|
30
|
+
print("\n--- Vessel by IMO ---")
|
|
31
|
+
vessel = await client.vessels.get("9811000")
|
|
32
|
+
if vessel.vessel:
|
|
33
|
+
print(f"Vessel: {vessel.vessel.name} (Type: {vessel.vessel.vessel_type})")
|
|
34
|
+
|
|
35
|
+
# Get the vessel's latest AIS position.
|
|
36
|
+
print("\n--- Vessel Position ---")
|
|
37
|
+
pos = await client.vessels.position("9811000")
|
|
38
|
+
if pos.vessel_position:
|
|
39
|
+
print(f"Position: {pos.vessel_position.latitude}, {pos.vessel_position.longitude}")
|
|
40
|
+
|
|
41
|
+
# Find vessels within 10 km of Rotterdam.
|
|
42
|
+
print("\n--- Vessels Near Rotterdam ---")
|
|
43
|
+
nearby = await client.location.vessels_radius(latitude=51.9225, longitude=4.47917, radius=10000)
|
|
44
|
+
for v in nearby.vessels or []:
|
|
45
|
+
print(f"{v.vessel_name} (IMO: {v.imo}) at {v.latitude}, {v.longitude}")
|
|
46
|
+
|
|
29
47
|
# Error handling.
|
|
30
48
|
print("\n--- Error Handling ---")
|
|
31
49
|
try:
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import os
|
|
4
4
|
import sys
|
|
5
5
|
|
|
6
|
-
from
|
|
6
|
+
from vessel_api_python import VesselClient, VesselAPIError
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def main() -> None:
|
|
@@ -38,6 +38,25 @@ def main() -> None:
|
|
|
38
38
|
if port.port:
|
|
39
39
|
print(f"Port: {port.port.name} ({port.port.unlo_code})")
|
|
40
40
|
|
|
41
|
+
# Get vessel details by IMO number (defaults to IMO; pass filter_id_type="mmsi" for MMSI).
|
|
42
|
+
print("\n--- Vessel by IMO ---")
|
|
43
|
+
vessel = client.vessels.get("9811000")
|
|
44
|
+
if vessel.vessel:
|
|
45
|
+
print(f"Vessel: {vessel.vessel.name} (Type: {vessel.vessel.vessel_type})")
|
|
46
|
+
|
|
47
|
+
# Get the vessel's latest AIS position.
|
|
48
|
+
print("\n--- Vessel Position ---")
|
|
49
|
+
pos = client.vessels.position("9811000")
|
|
50
|
+
if pos.vessel_position:
|
|
51
|
+
print(f"Position: {pos.vessel_position.latitude}, {pos.vessel_position.longitude}")
|
|
52
|
+
print(f"Speed: {pos.vessel_position.sog} knots, Heading: {pos.vessel_position.heading}")
|
|
53
|
+
|
|
54
|
+
# Find vessels within 10 km of Rotterdam.
|
|
55
|
+
print("\n--- Vessels Near Rotterdam ---")
|
|
56
|
+
nearby = client.location.vessels_radius(latitude=51.9225, longitude=4.47917, radius=10000)
|
|
57
|
+
for v in nearby.vessels or []:
|
|
58
|
+
print(f"{v.vessel_name} (IMO: {v.imo}) at {v.latitude}, {v.longitude}")
|
|
59
|
+
|
|
41
60
|
# Handle a not-found port gracefully.
|
|
42
61
|
print("\n--- Not Found Handling ---")
|
|
43
62
|
try:
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vessel-api-python"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.2.0"
|
|
8
8
|
description = "Python client for the Vessel Tracking API — maritime vessel tracking, port events, emissions, and navigation data."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -65,11 +65,13 @@ from ._models import (
|
|
|
65
65
|
PortEvent,
|
|
66
66
|
PortEventResponse,
|
|
67
67
|
PortEventsResponse,
|
|
68
|
+
PortInboundResponse,
|
|
68
69
|
PortReference,
|
|
69
70
|
PortResponse,
|
|
70
71
|
PortsWithinLocationResponse,
|
|
71
72
|
RadioBeacon,
|
|
72
73
|
RadioBeaconsWithinLocationResponse,
|
|
74
|
+
ResolutionMeta,
|
|
73
75
|
TypesInspectionDetailResponse,
|
|
74
76
|
TypesInspectionsResponse,
|
|
75
77
|
TypesOwnershipResponse,
|
|
@@ -111,6 +113,7 @@ __all__ = [
|
|
|
111
113
|
"VesselReference",
|
|
112
114
|
"VesselFormerName",
|
|
113
115
|
"BroadcastStation",
|
|
116
|
+
"ResolutionMeta",
|
|
114
117
|
# Models — Classification sub-models
|
|
115
118
|
"ClassificationCertificate",
|
|
116
119
|
"ClassificationCondition",
|
|
@@ -159,6 +162,7 @@ __all__ = [
|
|
|
159
162
|
"PortEvent",
|
|
160
163
|
"PortEventResponse",
|
|
161
164
|
"PortEventsResponse",
|
|
165
|
+
"PortInboundResponse",
|
|
162
166
|
# Models — Search
|
|
163
167
|
"FindVesselsResponse",
|
|
164
168
|
"FindPortsResponse",
|
|
@@ -224,6 +224,18 @@ class ClassificationYard(_Base):
|
|
|
224
224
|
keel_date: str | None = Field(default=None, alias="keelDate")
|
|
225
225
|
|
|
226
226
|
|
|
227
|
+
# ---------------------------------------------------------------------------
|
|
228
|
+
# Resolution metadata
|
|
229
|
+
# ---------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class ResolutionMeta(_Base):
|
|
233
|
+
"""Metadata about ID resolution fallback. Present when the API resolved using a different ID type."""
|
|
234
|
+
requested_id_type: str | None = Field(default=None, alias="requestedIdType")
|
|
235
|
+
resolved_id_type: str | None = Field(default=None, alias="resolvedIdType")
|
|
236
|
+
resolved_id: int | None = Field(default=None, alias="resolvedId")
|
|
237
|
+
|
|
238
|
+
|
|
227
239
|
# ---------------------------------------------------------------------------
|
|
228
240
|
# Vessel models
|
|
229
241
|
# ---------------------------------------------------------------------------
|
|
@@ -264,6 +276,7 @@ class VesselResponse(_Base):
|
|
|
264
276
|
"""Response for a single vessel lookup."""
|
|
265
277
|
|
|
266
278
|
vessel: Vessel | None = None
|
|
279
|
+
meta: ResolutionMeta | None = Field(default=None, alias="_meta")
|
|
267
280
|
|
|
268
281
|
|
|
269
282
|
class VesselPosition(_Base):
|
|
@@ -290,6 +303,7 @@ class VesselPositionResponse(_Base):
|
|
|
290
303
|
vessel_position: VesselPosition | None = Field(
|
|
291
304
|
default=None, alias="vesselPosition"
|
|
292
305
|
)
|
|
306
|
+
meta: ResolutionMeta | None = Field(default=None, alias="_meta")
|
|
293
307
|
|
|
294
308
|
|
|
295
309
|
class VesselPositionsResponse(_Base):
|
|
@@ -299,6 +313,7 @@ class VesselPositionsResponse(_Base):
|
|
|
299
313
|
default=None, alias="vesselPositions"
|
|
300
314
|
)
|
|
301
315
|
next_token: str | None = Field(default=None, alias="nextToken")
|
|
316
|
+
meta: ResolutionMeta | None = Field(default=None, alias="_meta")
|
|
302
317
|
|
|
303
318
|
|
|
304
319
|
# ---------------------------------------------------------------------------
|
|
@@ -352,6 +367,7 @@ class MarineCasualtiesResponse(_Base):
|
|
|
352
367
|
|
|
353
368
|
casualties: list[MarineCasualty] | None = None
|
|
354
369
|
next_token: str | None = Field(default=None, alias="nextToken")
|
|
370
|
+
meta: ResolutionMeta | None = Field(default=None, alias="_meta")
|
|
355
371
|
|
|
356
372
|
|
|
357
373
|
class ClassificationVessel(_Base):
|
|
@@ -379,6 +395,7 @@ class ClassificationResponse(_Base):
|
|
|
379
395
|
"""Response for vessel classification."""
|
|
380
396
|
|
|
381
397
|
classification: ClassificationVessel | None = None
|
|
398
|
+
meta: ResolutionMeta | None = Field(default=None, alias="_meta")
|
|
382
399
|
|
|
383
400
|
|
|
384
401
|
class VesselEmission(_Base):
|
|
@@ -432,12 +449,14 @@ class VesselEmissionsResponse(_Base):
|
|
|
432
449
|
|
|
433
450
|
emissions: list[VesselEmission] | None = None
|
|
434
451
|
next_token: str | None = Field(default=None, alias="nextToken")
|
|
452
|
+
meta: ResolutionMeta | None = Field(default=None, alias="_meta")
|
|
435
453
|
|
|
436
454
|
|
|
437
455
|
class VesselETA(_Base):
|
|
438
456
|
"""Vessel Estimated Time of Arrival information."""
|
|
439
457
|
|
|
440
458
|
destination: str | None = None
|
|
459
|
+
destination_port: str | None = None
|
|
441
460
|
draught: float | None = None
|
|
442
461
|
eta: str | None = None
|
|
443
462
|
imo: int | None = None
|
|
@@ -450,6 +469,13 @@ class VesselETAResponse(_Base):
|
|
|
450
469
|
"""Response for vessel ETA."""
|
|
451
470
|
|
|
452
471
|
vessel_eta: VesselETA | None = Field(default=None, alias="vesselEta")
|
|
472
|
+
meta: ResolutionMeta | None = Field(default=None, alias="_meta")
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
class PortInboundResponse(_Base):
|
|
476
|
+
"""Response containing vessels heading to a port."""
|
|
477
|
+
vessel_etas: list[VesselETA] | None = Field(default=None, alias="vesselETAs")
|
|
478
|
+
next_token: str | None = Field(default=None, alias="nextToken")
|
|
453
479
|
|
|
454
480
|
|
|
455
481
|
# ---------------------------------------------------------------------------
|
|
@@ -652,12 +678,14 @@ class PortEventsResponse(_Base):
|
|
|
652
678
|
default=None, alias="portEvents"
|
|
653
679
|
)
|
|
654
680
|
next_token: str | None = Field(default=None, alias="nextToken")
|
|
681
|
+
meta: ResolutionMeta | None = Field(default=None, alias="_meta")
|
|
655
682
|
|
|
656
683
|
|
|
657
684
|
class PortEventResponse(_Base):
|
|
658
685
|
"""Response for a single port event (e.g. last by vessel)."""
|
|
659
686
|
|
|
660
687
|
port_event: PortEvent | None = Field(default=None, alias="portEvent")
|
|
688
|
+
meta: ResolutionMeta | None = Field(default=None, alias="_meta")
|
|
661
689
|
|
|
662
690
|
|
|
663
691
|
# ---------------------------------------------------------------------------
|
|
@@ -36,6 +36,7 @@ from ._models import (
|
|
|
36
36
|
PortEvent,
|
|
37
37
|
PortEventResponse,
|
|
38
38
|
PortEventsResponse,
|
|
39
|
+
PortInboundResponse,
|
|
39
40
|
PortResponse,
|
|
40
41
|
PortsWithinLocationResponse,
|
|
41
42
|
RadioBeacon,
|
|
@@ -46,6 +47,7 @@ from ._models import (
|
|
|
46
47
|
Vessel,
|
|
47
48
|
VesselEmission,
|
|
48
49
|
VesselEmissionsResponse,
|
|
50
|
+
VesselETA,
|
|
49
51
|
VesselETAResponse,
|
|
50
52
|
VesselPosition,
|
|
51
53
|
VesselPositionResponse,
|
|
@@ -179,6 +181,22 @@ class PortsService:
|
|
|
179
181
|
error_from_response(r.status_code, r.content)
|
|
180
182
|
return PortResponse.model_validate(r.json())
|
|
181
183
|
|
|
184
|
+
def inbound(self, unlocode: str, *, eta_from: str, eta_to: str, time_from: str | None = None, time_to: str | None = None, pagination_limit: int | None = None, pagination_next_token: str | None = None) -> PortInboundResponse:
|
|
185
|
+
"""Retrieve vessels heading to a port."""
|
|
186
|
+
r = self._client.get(f"/port/{unlocode}/inbound", params=_strip_none({"filter.etaFrom": eta_from, "filter.etaTo": eta_to, "time.from": time_from, "time.to": time_to, "pagination.limit": pagination_limit, "pagination.nextToken": pagination_next_token}))
|
|
187
|
+
error_from_response(r.status_code, r.content)
|
|
188
|
+
return PortInboundResponse.model_validate(r.json())
|
|
189
|
+
|
|
190
|
+
def inbound_all(self, unlocode: str, *, eta_from: str, eta_to: str, time_from: str | None = None, time_to: str | None = None, pagination_limit: int | None = None) -> SyncIterator[VesselETA]:
|
|
191
|
+
"""Iterate over all inbound vessels for a port across pages."""
|
|
192
|
+
token: str | None = None
|
|
193
|
+
def fetch() -> tuple[list[VesselETA], str | None]:
|
|
194
|
+
nonlocal token
|
|
195
|
+
resp = self.inbound(unlocode, eta_from=eta_from, eta_to=eta_to, time_from=time_from, time_to=time_to, pagination_limit=pagination_limit, pagination_next_token=token)
|
|
196
|
+
token = resp.next_token
|
|
197
|
+
return resp.vessel_etas or [], token
|
|
198
|
+
return SyncIterator(fetch)
|
|
199
|
+
|
|
182
200
|
|
|
183
201
|
class PortEventsService:
|
|
184
202
|
"""Port event endpoints (sync)."""
|
|
@@ -743,6 +761,22 @@ class AsyncPortsService:
|
|
|
743
761
|
error_from_response(r.status_code, r.content)
|
|
744
762
|
return PortResponse.model_validate(r.json())
|
|
745
763
|
|
|
764
|
+
async def inbound(self, unlocode: str, *, eta_from: str, eta_to: str, time_from: str | None = None, time_to: str | None = None, pagination_limit: int | None = None, pagination_next_token: str | None = None) -> PortInboundResponse:
|
|
765
|
+
"""Retrieve vessels heading to a port."""
|
|
766
|
+
r = await self._client.get(f"/port/{unlocode}/inbound", params=_strip_none({"filter.etaFrom": eta_from, "filter.etaTo": eta_to, "time.from": time_from, "time.to": time_to, "pagination.limit": pagination_limit, "pagination.nextToken": pagination_next_token}))
|
|
767
|
+
error_from_response(r.status_code, r.content)
|
|
768
|
+
return PortInboundResponse.model_validate(r.json())
|
|
769
|
+
|
|
770
|
+
def inbound_all(self, unlocode: str, *, eta_from: str, eta_to: str, time_from: str | None = None, time_to: str | None = None, pagination_limit: int | None = None) -> AsyncIterator[VesselETA]:
|
|
771
|
+
"""Iterate over all inbound vessels for a port across pages."""
|
|
772
|
+
token: str | None = None
|
|
773
|
+
async def fetch() -> tuple[list[VesselETA], str | None]:
|
|
774
|
+
nonlocal token
|
|
775
|
+
resp = await self.inbound(unlocode, eta_from=eta_from, eta_to=eta_to, time_from=time_from, time_to=time_to, pagination_limit=pagination_limit, pagination_next_token=token)
|
|
776
|
+
token = resp.next_token
|
|
777
|
+
return resp.vessel_etas or [], token
|
|
778
|
+
return AsyncIterator(fetch)
|
|
779
|
+
|
|
746
780
|
|
|
747
781
|
class AsyncPortEventsService:
|
|
748
782
|
"""Port event endpoints (async)."""
|
|
@@ -62,6 +62,58 @@ class TestPortsService:
|
|
|
62
62
|
assert result.port.name == "Rotterdam"
|
|
63
63
|
client.close()
|
|
64
64
|
|
|
65
|
+
def test_inbound(self) -> None:
|
|
66
|
+
with respx.mock() as mock:
|
|
67
|
+
route = mock.get("https://api.vesselapi.com/v1/port/NLRTM/inbound").mock(
|
|
68
|
+
return_value=httpx.Response(200, json={
|
|
69
|
+
"vesselETAs": [
|
|
70
|
+
{"imo": 9363728, "vessel_name": "Ever Given", "eta": "2026-03-10T12:00:00Z", "destination_port": "NLRTM"},
|
|
71
|
+
],
|
|
72
|
+
"nextToken": None,
|
|
73
|
+
})
|
|
74
|
+
)
|
|
75
|
+
client = VesselClient(api_key="key", max_retries=0)
|
|
76
|
+
result = client.ports.inbound("NLRTM", eta_from="2026-03-07T00:00:00Z", eta_to="2026-03-14T00:00:00Z")
|
|
77
|
+
assert result.vessel_etas is not None
|
|
78
|
+
assert len(result.vessel_etas) == 1
|
|
79
|
+
assert result.vessel_etas[0].imo == 9363728
|
|
80
|
+
assert result.vessel_etas[0].vessel_name == "Ever Given"
|
|
81
|
+
# Verify filter params are sent.
|
|
82
|
+
url = str(route.calls[0].request.url)
|
|
83
|
+
assert "filter.etaFrom=2026-03-07" in url
|
|
84
|
+
assert "filter.etaTo=2026-03-14" in url
|
|
85
|
+
client.close()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class TestResolutionMeta:
|
|
89
|
+
"""Tests for _meta deserialization on response models."""
|
|
90
|
+
|
|
91
|
+
def test_meta_on_vessel_response(self) -> None:
|
|
92
|
+
with respx.mock() as mock:
|
|
93
|
+
mock.get("https://api.vesselapi.com/v1/vessel/477045900").mock(
|
|
94
|
+
return_value=httpx.Response(200, json={
|
|
95
|
+
"vessel": {"imo": 9363728, "mmsi": 477045900},
|
|
96
|
+
"_meta": {"requestedIdType": "imo", "resolvedIdType": "mmsi", "resolvedId": 477045900},
|
|
97
|
+
})
|
|
98
|
+
)
|
|
99
|
+
client = VesselClient(api_key="key", max_retries=0)
|
|
100
|
+
result = client.vessels.get("477045900", filter_id_type="imo")
|
|
101
|
+
assert result.meta is not None
|
|
102
|
+
assert result.meta.requested_id_type == "imo"
|
|
103
|
+
assert result.meta.resolved_id_type == "mmsi"
|
|
104
|
+
assert result.meta.resolved_id == 477045900
|
|
105
|
+
client.close()
|
|
106
|
+
|
|
107
|
+
def test_meta_absent_when_no_fallback(self) -> None:
|
|
108
|
+
with respx.mock() as mock:
|
|
109
|
+
mock.get("https://api.vesselapi.com/v1/vessel/9363728").mock(
|
|
110
|
+
return_value=httpx.Response(200, json={"vessel": {"imo": 9363728}})
|
|
111
|
+
)
|
|
112
|
+
client = VesselClient(api_key="key", max_retries=0)
|
|
113
|
+
result = client.vessels.get("9363728")
|
|
114
|
+
assert result.meta is None
|
|
115
|
+
client.close()
|
|
116
|
+
|
|
65
117
|
|
|
66
118
|
class TestSearchService:
|
|
67
119
|
"""Tests for the SearchService."""
|
|
@@ -95,7 +95,7 @@ class TestSmoke_Vessels:
|
|
|
95
95
|
|
|
96
96
|
|
|
97
97
|
# ---------------------------------------------------------------------------
|
|
98
|
-
# Ports (
|
|
98
|
+
# Ports (2 subtests)
|
|
99
99
|
# ---------------------------------------------------------------------------
|
|
100
100
|
|
|
101
101
|
|
|
@@ -105,6 +105,33 @@ class TestSmoke_Ports:
|
|
|
105
105
|
assert resp.port is not None
|
|
106
106
|
assert resp.port.unlo_code == "NLRTM"
|
|
107
107
|
|
|
108
|
+
def test_inbound(self, client: VesselClient) -> None:
|
|
109
|
+
now = datetime.now(timezone.utc)
|
|
110
|
+
eta_from = now.isoformat()
|
|
111
|
+
eta_to = (now + timedelta(days=7)).isoformat()
|
|
112
|
+
resp = client.ports.inbound("NLRTM", eta_from=eta_from, eta_to=eta_to, pagination_limit=5)
|
|
113
|
+
assert resp is not None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class TestSmoke_VesselETA_DestinationPort:
|
|
117
|
+
def test_destination_port(self, client: VesselClient) -> None:
|
|
118
|
+
# CMA CGM KHAO SOK — known to have active ETA with destination_port
|
|
119
|
+
resp = client.vessels.eta("9925837")
|
|
120
|
+
assert resp.vessel_eta is not None
|
|
121
|
+
assert resp.vessel_eta.destination_port is not None
|
|
122
|
+
assert len(resp.vessel_eta.destination_port) > 0
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class TestSmoke_Meta_Fallback:
|
|
126
|
+
def test_meta_fallback(self, client: VesselClient) -> None:
|
|
127
|
+
# Use an MMSI value but claim idType=imo to trigger fallback
|
|
128
|
+
resp = client.vessels.get("477045900", filter_id_type="imo")
|
|
129
|
+
assert resp.vessel is not None
|
|
130
|
+
assert resp.meta is not None
|
|
131
|
+
assert resp.meta.requested_id_type == "imo"
|
|
132
|
+
assert resp.meta.resolved_id_type == "mmsi"
|
|
133
|
+
assert resp.meta.resolved_id == 477045900
|
|
134
|
+
|
|
108
135
|
|
|
109
136
|
# ---------------------------------------------------------------------------
|
|
110
137
|
# PortEvents (9 subtests)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|