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.
Files changed (29) hide show
  1. {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/PKG-INFO +22 -5
  2. {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/README.md +21 -4
  3. {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/examples/async_basic.py +19 -1
  4. {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/examples/basic.py +20 -1
  5. {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/pyproject.toml +1 -1
  6. {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/src/vessel_api_python/__init__.py +4 -0
  7. {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/src/vessel_api_python/_models.py +28 -0
  8. {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/src/vessel_api_python/_services.py +34 -0
  9. {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/tests/test_services.py +52 -0
  10. {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/tests/test_smoke.py +28 -1
  11. {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/.github/workflows/ci.yml +0 -0
  12. {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/.github/workflows/publish.yml +0 -0
  13. {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/.gitignore +0 -0
  14. {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/LICENSE +0 -0
  15. {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/Makefile +0 -0
  16. {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/SECURITY.md +0 -0
  17. {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/openapi/swagger.json +0 -0
  18. {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/src/vessel_api_python/_client.py +0 -0
  19. {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/src/vessel_api_python/_constants.py +0 -0
  20. {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/src/vessel_api_python/_errors.py +0 -0
  21. {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/src/vessel_api_python/_iterator.py +0 -0
  22. {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/src/vessel_api_python/_transport.py +0 -0
  23. {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/src/vessel_api_python/py.typed +0 -0
  24. {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/tests/__init__.py +0 -0
  25. {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/tests/conftest.py +0 -0
  26. {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/tests/test_client.py +0 -0
  27. {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/tests/test_errors.py +0 -0
  28. {vessel_api_python-1.1.0 → vessel_api_python-1.2.0}/tests/test_iterator.py +0 -0
  29. {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.1.0
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(filter_name="tanker"):
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(filter_name="tanker"):
150
+ async for vessel in client.search.all_vessels(filter_vessel_type="Tanker"):
134
151
  print(vessel.name)
135
152
 
136
- # Collect all at once
137
- vessels = client.search.all_vessels(filter_name="tanker").collect()
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(filter_name="tanker"):
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(filter_name="tanker"):
116
+ async for vessel in client.search.all_vessels(filter_vessel_type="Tanker"):
100
117
  print(vessel.name)
101
118
 
102
- # Collect all at once
103
- vessels = client.search.all_vessels(filter_name="tanker").collect()
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 vesselapi import AsyncVesselClient, VesselAPIError
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 vesselapi import VesselClient, VesselAPIError
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.1.0"
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 (1 subtest)
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)