From 5e0f628f6bab9bbe41d49e20182487f32bda8fe8 Mon Sep 17 00:00:00 2001 From: lykmapipo Date: Mon, 22 Jun 2026 18:41:12 +0300 Subject: [PATCH 1/3] feat(vessels): introduce VesselResult base class and vessel_ids property This: - Add `VesselResult` class as base `Result` for Vessesl API endpoints - Refactor `VesselDetailResult`, `VesselListResult`, and `VesselSearchResult` to inherit `VesselResult` - Add `_iter_matched_self_reported_info()` and `_iter_matched_vessel_ids()` to `VesselItem` --- .../resources/vessels/base/models/response.py | 97 ++++++++++++++++- .../vessels/detail/models/response.py | 7 +- .../resources/vessels/list/models/response.py | 7 +- .../vessels/search/models/response.py | 7 +- tests/resources/vessels/base/__init__.py | 1 + .../resources/vessels/base/models/__init__.py | 1 + .../base/models/test_response_models.py | 102 ++++++++++++++++++ .../detail/models/test_response_models.py | 23 ++++ .../list/models/test_response_models.py | 21 ++++ .../search/models/test_response_models.py | 23 +++- 10 files changed, 273 insertions(+), 16 deletions(-) create mode 100644 tests/resources/vessels/base/__init__.py create mode 100644 tests/resources/vessels/base/models/__init__.py create mode 100644 tests/resources/vessels/base/models/test_response_models.py diff --git a/src/gfwapiclient/resources/vessels/base/models/response.py b/src/gfwapiclient/resources/vessels/base/models/response.py index 6d73cac..6a8d879 100644 --- a/src/gfwapiclient/resources/vessels/base/models/response.py +++ b/src/gfwapiclient/resources/vessels/base/models/response.py @@ -6,15 +6,15 @@ import datetime -from typing import Any, List, Optional +from typing import Any, Iterator, List, Optional, Type, TypeVar, Union from pydantic import Field from gfwapiclient.base.models import BaseModel -from gfwapiclient.http.models import ResultItem +from gfwapiclient.http.models import Result, ResultItem -__all__ = ["VesselItem"] +__all__ = ["VesselItem", "VesselResult"] class ExtraField(BaseModel): @@ -350,3 +350,94 @@ class VesselItem(ResultItem): self_reported_info: Optional[List[SelfReportedInfo]] = Field( None, alias="selfReportedInfo" ) + + def _iter_matched_self_reported_info(self) -> Iterator[SelfReportedInfo]: + """Yields matched AIS self-reported vessel information. + + **Note:** A vessel is considered matched when it includes both `registry_info` + and `self_reported_info` (AIS), as this indicates a successful match between + registry data and AIS information. + + See how the Vessel API is used in the Vessel Viewer + here: https://globalfishingwatch.org/our-apis/assets/2024_Vessel_Viewer_and_APIs_behind_It.pdf + + Yields: + SelfReportedInfo: + Valid matched AIS self-reported vessel information. + """ + if ( + self.registry_info_total_records + and self.registry_info_total_records >= 1 + and self.self_reported_info + ): + yield from self.self_reported_info + + def _iter_matched_vessel_ids(self) -> Iterator[str]: + """Yields matched AIS self-reported vessel identifiers (IDs). + + **Note:** A vessel is considered matched when it includes both `registry_info` + and `self_reported_info` (AIS), as this indicates a successful match between + registry data and AIS information. + + See how the Vessel API is used in the Vessel Viewer + here: https://globalfishingwatch.org/our-apis/assets/2024_Vessel_Viewer_and_APIs_behind_It.pdf + + Yields: + str: + Valid matched AIS self-reported vessel identifier (ID). + """ + for self_reported_info in self._iter_matched_self_reported_info(): + if self_reported_info.id and self_reported_info.id.strip(): + yield self_reported_info.id.strip() + + +_VesselItemT = TypeVar("_VesselItemT", bound=VesselItem) + + +class VesselResult(Result[_VesselItemT]): + """Result for the Vessels API endpoints. + + This class extends :class:`Result` to provide a specialized result container + for the Vessels API endpoints. + """ + + _result_item_class: Type[_VesselItemT] + _data: Union[List[_VesselItemT], _VesselItemT] + + def __init__(self, *, data: Union[List[_VesselItemT], _VesselItemT]) -> None: + """Initializes a new `VesselResult`. + + Args: + data (Union[List[_VesselItemT], _VesselItemT]): + The response data from the Vessels API endpoint, which can + be either a single `ResultItem` or a list of `ResultItem` instances. + """ + super().__init__(data=data) + + @property + def vessel_ids(self) -> List[str]: + """Returns matched AIS self-reported vessel identifiers (IDs). + + **Note:** A vessel is considered matched when it includes both `registry_info` + and `self_reported_info` (AIS), as this indicates a successful match between + registry data and AIS information. + + See how the Vessel API is used in the Vessel Viewer + here: https://globalfishingwatch.org/our-apis/assets/2024_Vessel_Viewer_and_APIs_behind_It.pdf + + Returns: + List[str]: + Valid list of matched AIS self-reported vessel identifier (ID). + """ + + def iter_vessel_ids(item: _VesselItemT) -> Iterator[str]: + yield from item._iter_matched_vessel_ids() + + # USE: flat_map + mapped_vessel_ids: Iterator[Iterator[str]] = self.map(mapper=iter_vessel_ids) + matched_vessel_ids: List[str] = [] + for _vessel_ids in mapped_vessel_ids: + for _vessel_id in _vessel_ids: + matched_vessel_ids.append(_vessel_id.strip()) + + return matched_vessel_ids diff --git a/src/gfwapiclient/resources/vessels/detail/models/response.py b/src/gfwapiclient/resources/vessels/detail/models/response.py index fb10581..e499549 100644 --- a/src/gfwapiclient/resources/vessels/detail/models/response.py +++ b/src/gfwapiclient/resources/vessels/detail/models/response.py @@ -5,8 +5,7 @@ from typing import Type -from gfwapiclient.http.models import Result -from gfwapiclient.resources.vessels.base.models.response import VesselItem +from gfwapiclient.resources.vessels.base.models.response import VesselItem, VesselResult __all__ = ["VesselDetailItem", "VesselDetailResult"] @@ -22,10 +21,10 @@ class VesselDetailItem(VesselItem): pass -class VesselDetailResult(Result[VesselDetailItem]): +class VesselDetailResult(VesselResult[VesselDetailItem]): """Result for the get vessel by ID API endpoint. - This class extends :class:`Result` to provide a specialized result container + This class extends :class:`VesselResult` to provide a specialized result container for the vessel detail endpoint. """ diff --git a/src/gfwapiclient/resources/vessels/list/models/response.py b/src/gfwapiclient/resources/vessels/list/models/response.py index 054968e..92575b2 100644 --- a/src/gfwapiclient/resources/vessels/list/models/response.py +++ b/src/gfwapiclient/resources/vessels/list/models/response.py @@ -5,8 +5,7 @@ from typing import List, Type -from gfwapiclient.http.models import Result -from gfwapiclient.resources.vessels.base.models.response import VesselItem +from gfwapiclient.resources.vessels.base.models.response import VesselItem, VesselResult __all__ = ["VesselListItem", "VesselListResult"] @@ -22,10 +21,10 @@ class VesselListItem(VesselItem): pass -class VesselListResult(Result[VesselListItem]): +class VesselListResult(VesselResult[VesselListItem]): """Result for the get vessels by IDs API endpoint. - This class extends :class:`Result` to provide a specialized result container + This class extends :class:`VesselResult` to provide a specialized result container for the vessel list endpoint. """ diff --git a/src/gfwapiclient/resources/vessels/search/models/response.py b/src/gfwapiclient/resources/vessels/search/models/response.py index fb8eb3e..4702353 100644 --- a/src/gfwapiclient/resources/vessels/search/models/response.py +++ b/src/gfwapiclient/resources/vessels/search/models/response.py @@ -5,8 +5,7 @@ from typing import List, Type -from gfwapiclient.http.models import Result -from gfwapiclient.resources.vessels.base.models.response import VesselItem +from gfwapiclient.resources.vessels.base.models.response import VesselItem, VesselResult __all__ = ["VesselSearchItem", "VesselSearchResult"] @@ -22,10 +21,10 @@ class VesselSearchItem(VesselItem): pass -class VesselSearchResult(Result[VesselSearchItem]): +class VesselSearchResult(VesselResult[VesselSearchItem]): """Result for the vessels search API endpoint. - This class extends :class:`Result` to provide a specialized result container + This class extends :class:`VesselResult` to provide a specialized result container for the vessel search endpoint. """ diff --git a/tests/resources/vessels/base/__init__.py b/tests/resources/vessels/base/__init__.py new file mode 100644 index 0000000..f268c76 --- /dev/null +++ b/tests/resources/vessels/base/__init__.py @@ -0,0 +1 @@ +"""Tests for `gfwapiclient.resources.vessels.base`.""" diff --git a/tests/resources/vessels/base/models/__init__.py b/tests/resources/vessels/base/models/__init__.py new file mode 100644 index 0000000..66ae65c --- /dev/null +++ b/tests/resources/vessels/base/models/__init__.py @@ -0,0 +1 @@ +"""Tests for `gfwapiclient.resources.vessels.base.models`.""" diff --git a/tests/resources/vessels/base/models/test_response_models.py b/tests/resources/vessels/base/models/test_response_models.py new file mode 100644 index 0000000..bf25583 --- /dev/null +++ b/tests/resources/vessels/base/models/test_response_models.py @@ -0,0 +1,102 @@ +"""Tests for `gfwapiclient.resources.vessels.base.models.response`.""" + +from typing import Any, Dict, Iterator, List + +import pytest + +from gfwapiclient.resources.vessels.base.models.response import ( + SelfReportedInfo, + VesselItem, + VesselResult, +) + + +def test_vessel_item__iter_matched_self_reported_info_yields_correctly( + mock_raw_vessel_list_item: Dict[str, Any], +) -> None: + """Test that `VesselItem` iter matched self reported info yields correctly.""" + vessel_item: VesselItem = VesselItem(**mock_raw_vessel_list_item) + matched_self_reported_info: Iterator[SelfReportedInfo] = ( + vessel_item._iter_matched_self_reported_info() + ) + assert matched_self_reported_info is not None + assert isinstance(matched_self_reported_info, Iterator) + assert len(list(matched_self_reported_info)) >= 1 + + +@pytest.mark.parametrize( + "update", + [ + {"registryInfoTotalRecords": None}, + {"registryInfoTotalRecords": 0}, + {"selfReportedInfo": None}, + {"selfReportedInfo": []}, + ], +) +def test_vessel_item_self__iter_matched_self_reported_info_does_not_yields_when_registry_or_self_report_info_missing( + mock_raw_vessel_list_item: Dict[str, Any], + update: Dict[str, Any], +) -> None: + """Test that `VesselItem` iter matched self reported info does not yields when registry or self reported info missing.""" + mocked_raw_vessel_list_item: Dict[str, Any] = {**mock_raw_vessel_list_item} + for k, v in update.items(): + mocked_raw_vessel_list_item[k] = v + + vessel_item: VesselItem = VesselItem(**mocked_raw_vessel_list_item) + matched_self_reported_info: Iterator[SelfReportedInfo] = ( + vessel_item._iter_matched_self_reported_info() + ) + + assert matched_self_reported_info is not None + assert isinstance(matched_self_reported_info, Iterator) + assert len(list(matched_self_reported_info)) == 0 + + +def test_vessel_item__iter_matched_vessel_ids_yields_correctly( + mock_raw_vessel_list_item: Dict[str, Any], +) -> None: + """Test that `VesselItem` iter matched vessel ids yields correctly.""" + vessel_item: VesselItem = VesselItem(**mock_raw_vessel_list_item) + matched_vessel_ids: Iterator[str] = vessel_item._iter_matched_vessel_ids() + assert matched_vessel_ids is not None + assert isinstance(matched_vessel_ids, Iterator) + assert len(list(matched_vessel_ids)) >= 1 + + +@pytest.mark.parametrize( + "update", + [ + {"registryInfoTotalRecords": None}, + {"registryInfoTotalRecords": 0}, + {"selfReportedInfo": None}, + {"selfReportedInfo": []}, + {"selfReportedInfo": [{"id": None}]}, + {"selfReportedInfo": [{"id": ""}]}, + {"selfReportedInfo": [{"id": " "}]}, + ], +) +def test_vessel_item__iter_matched_vessel_ids_does_not_yields_when_registry_or_self_report_info_missing( + mock_raw_vessel_list_item: Dict[str, Any], + update: Dict[str, Any], +) -> None: + """Test that `VesselItem` iter matched vessel ids does not yields when registry or self reported info missing.""" + mocked_raw_vessel_list_item: Dict[str, Any] = {**mock_raw_vessel_list_item} + for k, v in update.items(): + mocked_raw_vessel_list_item[k] = v + + vessel_item: VesselItem = VesselItem(**mocked_raw_vessel_list_item) + matched_vessel_ids: Iterator[str] = vessel_item._iter_matched_vessel_ids() + assert matched_vessel_ids is not None + assert isinstance(matched_vessel_ids, Iterator) + assert len(list(matched_vessel_ids)) == 0 + + +def test_vessel_result_vessel_ids_returns_correctly( + mock_raw_vessel_list_item: Dict[str, Any], +) -> None: + """Test that `VesselResult` returns list of vessel ids correctly.""" + data: List[VesselItem] = [VesselItem(**mock_raw_vessel_list_item)] + result = VesselResult(data=data) + assert result.vessel_ids is not None + assert isinstance(result.vessel_ids, list) + assert len(result.vessel_ids) >= 1 diff --git a/tests/resources/vessels/detail/models/test_response_models.py b/tests/resources/vessels/detail/models/test_response_models.py index a8c8d53..15889a3 100644 --- a/tests/resources/vessels/detail/models/test_response_models.py +++ b/tests/resources/vessels/detail/models/test_response_models.py @@ -24,6 +24,18 @@ def test_vessel_detail_item_deserializes_all_fields( assert vessel_detail_item.dataset is not None +# def test_vessel_detail_item_vessel_ids_yields_correctly( +# mock_raw_vessel_detail_item: Dict[str, Any], +# ) -> None: +# """Test that `VesselDetailItem` yields vessel ids correctly.""" +# vessel_detail_item: VesselDetailItem = VesselDetailItem( +# **mock_raw_vessel_detail_item +# ) +# assert vessel_detail_item.vessel_ids is not None +# assert isinstance(vessel_detail_item.vessel_ids, Iterator) +# assert len(list(vessel_detail_item.vessel_ids)) >= 1 + + def test_vessel_detail_result_deserializes_all_fields( mock_raw_vessel_detail_item: Dict[str, Any], ) -> None: @@ -31,3 +43,14 @@ def test_vessel_detail_result_deserializes_all_fields( data: VesselDetailItem = VesselDetailItem(**mock_raw_vessel_detail_item) result = VesselDetailResult(data=data) assert cast(VesselDetailItem, result.data()) == data + + +def test_vessel_detail_result_vessel_ids_returns_correctly( + mock_raw_vessel_list_item: Dict[str, Any], +) -> None: + """Test that `VesselDetailResult` returns list of vessel ids correctly.""" + data: VesselDetailItem = VesselDetailItem(**mock_raw_vessel_list_item) + result = VesselDetailResult(data=data) + assert result.vessel_ids is not None + assert isinstance(result.vessel_ids, list) + assert len(result.vessel_ids) >= 1 diff --git a/tests/resources/vessels/list/models/test_response_models.py b/tests/resources/vessels/list/models/test_response_models.py index 4b83366..ab1c6c5 100644 --- a/tests/resources/vessels/list/models/test_response_models.py +++ b/tests/resources/vessels/list/models/test_response_models.py @@ -22,6 +22,16 @@ def test_vessel_list_item_deserializes_all_fields( assert vessel_list_item.dataset is not None +# def test_vessel_list_item_vessel_ids_yields_correctly( +# mock_raw_vessel_list_item: Dict[str, Any], +# ) -> None: +# """Test that `VesselListItem` yields vessel ids correctly.""" +# vessel_list_item: VesselListItem = VesselListItem(**mock_raw_vessel_list_item) +# assert vessel_list_item.vessel_ids is not None +# assert isinstance(vessel_list_item.vessel_ids, Iterator) +# assert len(list(vessel_list_item.vessel_ids)) >= 1 + + def test_vessel_list_result_deserializes_all_fields( mock_raw_vessel_list_item: Dict[str, Any], ) -> None: @@ -29,3 +39,14 @@ def test_vessel_list_result_deserializes_all_fields( data: List[VesselListItem] = [VesselListItem(**mock_raw_vessel_list_item)] result = VesselListResult(data=data) assert cast(List[VesselListItem], result.data()) == data + + +def test_vessel_list_result_vessel_ids_returns_correctly( + mock_raw_vessel_list_item: Dict[str, Any], +) -> None: + """Test that `VesselListResult` returns list of vessel ids correctly.""" + data: List[VesselListItem] = [VesselListItem(**mock_raw_vessel_list_item)] + result = VesselListResult(data=data) + assert result.vessel_ids is not None + assert isinstance(result.vessel_ids, list) + assert len(result.vessel_ids) >= 1 diff --git a/tests/resources/vessels/search/models/test_response_models.py b/tests/resources/vessels/search/models/test_response_models.py index 4edecb8..59cd56b 100644 --- a/tests/resources/vessels/search/models/test_response_models.py +++ b/tests/resources/vessels/search/models/test_response_models.py @@ -22,10 +22,31 @@ def test_vessel_search_item_deserializes_all_fields( assert vessel_search_item.dataset is not None +# def test_vessel_search_item_vessel_ids_yields_correctly( +# mock_raw_vessel_list_item: Dict[str, Any], +# ) -> None: +# """Test that `VesselSearchItem` yields vessel ids correctly.""" +# vessel_search_item: VesselSearchItem = VesselSearchItem(**mock_raw_vessel_list_item) +# assert vessel_search_item.vessel_ids is not None +# assert isinstance(vessel_search_item.vessel_ids, Iterator) +# assert len(list(vessel_search_item.vessel_ids)) >= 1 + + def test_vessel_search_result_deserializes_all_fields( mock_raw_vessel_list_item: Dict[str, Any], ) -> None: - """Test that `VesselListResult` deserializes all fields correctly.""" + """Test that `VesselSearchResult` deserializes all fields correctly.""" data: List[VesselSearchItem] = [VesselSearchItem(**mock_raw_vessel_list_item)] result = VesselSearchResult(data=data) assert cast(List[VesselSearchItem], result.data()) == data + + +def test_vessel_search_result_vessel_ids_returns_correctly( + mock_raw_vessel_list_item: Dict[str, Any], +) -> None: + """Test that `VesselSearchResult` returns list of vessel ids correctly.""" + data: List[VesselSearchItem] = [VesselSearchItem(**mock_raw_vessel_list_item)] + result = VesselSearchResult(data=data) + assert result.vessel_ids is not None + assert isinstance(result.vessel_ids, list) + assert len(result.vessel_ids) >= 1 From 3937b4f1207b6619ac4e5110b0d9450b423d241f Mon Sep 17 00:00:00 2001 From: lykmapipo Date: Mon, 22 Jun 2026 23:07:14 +0300 Subject: [PATCH 2/3] feat(vessels): add matched transmission dates and improve matched vessel ids to Result This: - Add `transmission_dates_from` and `transmission_dates_to` on `VesselResult` - Refactor and clean up matched `vessel_ids` - Rename `_iter_matched_self_reported_info` to `_iter_matched_self_reported_infos` - Remove `_iter_matched_vessel_ids` method from `VesselItem` --- .../resources/vessels/base/models/response.py | 111 ++++++++++++----- .../base/models/test_response_models.py | 113 ++++++++++++++---- .../detail/models/test_response_models.py | 34 ++++-- .../list/models/test_response_models.py | 32 +++-- .../search/models/test_response_models.py | 32 +++-- 5 files changed, 236 insertions(+), 86 deletions(-) diff --git a/src/gfwapiclient/resources/vessels/base/models/response.py b/src/gfwapiclient/resources/vessels/base/models/response.py index 6a8d879..cae35cd 100644 --- a/src/gfwapiclient/resources/vessels/base/models/response.py +++ b/src/gfwapiclient/resources/vessels/base/models/response.py @@ -284,10 +284,10 @@ class SelfReportedInfo(BaseModel): Matched fields. transmission_date_from (Optional[datetime.datetime]): - Transmission date from. + Transmission date from (start). transmission_date_to (Optional[datetime.datetime]): - Transmission date to. + Transmission date to (end). """ id: Optional[str] = Field(None, alias="id") @@ -351,7 +351,7 @@ class VesselItem(ResultItem): None, alias="selfReportedInfo" ) - def _iter_matched_self_reported_info(self) -> Iterator[SelfReportedInfo]: + def _iter_matched_self_reported_infos(self) -> Iterator[SelfReportedInfo]: """Yields matched AIS self-reported vessel information. **Note:** A vessel is considered matched when it includes both `registry_info` @@ -372,24 +372,6 @@ def _iter_matched_self_reported_info(self) -> Iterator[SelfReportedInfo]: ): yield from self.self_reported_info - def _iter_matched_vessel_ids(self) -> Iterator[str]: - """Yields matched AIS self-reported vessel identifiers (IDs). - - **Note:** A vessel is considered matched when it includes both `registry_info` - and `self_reported_info` (AIS), as this indicates a successful match between - registry data and AIS information. - - See how the Vessel API is used in the Vessel Viewer - here: https://globalfishingwatch.org/our-apis/assets/2024_Vessel_Viewer_and_APIs_behind_It.pdf - - Yields: - str: - Valid matched AIS self-reported vessel identifier (ID). - """ - for self_reported_info in self._iter_matched_self_reported_info(): - if self_reported_info.id and self_reported_info.id.strip(): - yield self_reported_info.id.strip() - _VesselItemT = TypeVar("_VesselItemT", bound=VesselItem) @@ -430,14 +412,85 @@ def vessel_ids(self) -> List[str]: Valid list of matched AIS self-reported vessel identifier (ID). """ - def iter_vessel_ids(item: _VesselItemT) -> Iterator[str]: - yield from item._iter_matched_vessel_ids() + def extract_vessel_ids(item: _VesselItemT) -> Iterator[Optional[str]]: + for self_reported_info in item._iter_matched_self_reported_infos(): + yield self_reported_info.id - # USE: flat_map - mapped_vessel_ids: Iterator[Iterator[str]] = self.map(mapper=iter_vessel_ids) - matched_vessel_ids: List[str] = [] - for _vessel_ids in mapped_vessel_ids: - for _vessel_id in _vessel_ids: - matched_vessel_ids.append(_vessel_id.strip()) + mapped_vessel_ids: Iterator[Optional[str]] = self.flat_map( + mapper=extract_vessel_ids + ) + matched_vessel_ids: List[str] = list( + {_vessel_id.strip() for _vessel_id in mapped_vessel_ids if _vessel_id} + ) return matched_vessel_ids + + @property + def transmission_dates_from(self) -> List[datetime.date]: + """Returns matched AIS transmission start dates. + + **Note:** A vessel is considered matched when it includes both `registry_info` + and `self_reported_info` (AIS), as this indicates a successful match between + registry data and AIS information. + + See how the Vessel API is used in the Vessel Viewer + here: https://globalfishingwatch.org/our-apis/assets/2024_Vessel_Viewer_and_APIs_behind_It.pdf + + Returns: + List[str]: + Valid list of matched AIS self-reported transmission date from. + """ + + def extract_transmission_date_from( + item: _VesselItemT, + ) -> Iterator[Optional[datetime.datetime]]: + for self_reported_info in item._iter_matched_self_reported_infos(): + yield self_reported_info.transmission_date_from + + mapped_transmission_dates_from: Iterator[Optional[datetime.datetime]] = ( + self.flat_map(mapper=extract_transmission_date_from) + ) + matched_transmission_dates_from: List[datetime.date] = list( + { + _transmission_date_from.date() + for _transmission_date_from in mapped_transmission_dates_from + if _transmission_date_from + } + ) + + return matched_transmission_dates_from + + @property + def transmission_dates_to(self) -> List[datetime.date]: + """Returns matched AIS transmission end dates. + + **Note:** A vessel is considered matched when it includes both `registry_info` + and `self_reported_info` (AIS), as this indicates a successful match between + registry data and AIS information. + + See how the Vessel API is used in the Vessel Viewer + here: https://globalfishingwatch.org/our-apis/assets/2024_Vessel_Viewer_and_APIs_behind_It.pdf + + Returns: + List[str]: + Valid list of matched AIS self-reported transmission date to. + """ + + def extract_transmission_date_to( + item: _VesselItemT, + ) -> Iterator[Optional[datetime.datetime]]: + for self_reported_info in item._iter_matched_self_reported_infos(): + yield self_reported_info.transmission_date_to + + mapped_transmission_dates_to: Iterator[Optional[datetime.datetime]] = ( + self.flat_map(mapper=extract_transmission_date_to) + ) + matched_transmission_dates_to: List[datetime.date] = list( + { + _transmission_date_to.date() + for _transmission_date_to in mapped_transmission_dates_to + if _transmission_date_to + } + ) + + return matched_transmission_dates_to diff --git a/tests/resources/vessels/base/models/test_response_models.py b/tests/resources/vessels/base/models/test_response_models.py index bf25583..3b836d4 100644 --- a/tests/resources/vessels/base/models/test_response_models.py +++ b/tests/resources/vessels/base/models/test_response_models.py @@ -11,13 +11,13 @@ ) -def test_vessel_item__iter_matched_self_reported_info_yields_correctly( +def test_vessel_item_iter_matched_self_reported_infos_yields_correctly( mock_raw_vessel_list_item: Dict[str, Any], ) -> None: - """Test that `VesselItem` iter matched self reported info yields correctly.""" + """Test that `VesselItem` iter matched self reported infos yields correctly.""" vessel_item: VesselItem = VesselItem(**mock_raw_vessel_list_item) matched_self_reported_info: Iterator[SelfReportedInfo] = ( - vessel_item._iter_matched_self_reported_info() + vessel_item._iter_matched_self_reported_infos() ) assert matched_self_reported_info is not None assert isinstance(matched_self_reported_info, Iterator) @@ -33,18 +33,18 @@ def test_vessel_item__iter_matched_self_reported_info_yields_correctly( {"selfReportedInfo": []}, ], ) -def test_vessel_item_self__iter_matched_self_reported_info_does_not_yields_when_registry_or_self_report_info_missing( +def test_vessel_item_iter_matched_self_reported_infos_does_not_yields_when_registry_or_self_report_info_missing( mock_raw_vessel_list_item: Dict[str, Any], update: Dict[str, Any], ) -> None: - """Test that `VesselItem` iter matched self reported info does not yields when registry or self reported info missing.""" + """Test that `VesselItem` iter matched self reported infos does not yields when registry or self reported info missing.""" mocked_raw_vessel_list_item: Dict[str, Any] = {**mock_raw_vessel_list_item} for k, v in update.items(): mocked_raw_vessel_list_item[k] = v vessel_item: VesselItem = VesselItem(**mocked_raw_vessel_list_item) matched_self_reported_info: Iterator[SelfReportedInfo] = ( - vessel_item._iter_matched_self_reported_info() + vessel_item._iter_matched_self_reported_infos() ) assert matched_self_reported_info is not None @@ -52,15 +52,15 @@ def test_vessel_item_self__iter_matched_self_reported_info_does_not_yields_when_ assert len(list(matched_self_reported_info)) == 0 -def test_vessel_item__iter_matched_vessel_ids_yields_correctly( +def test_vessel_result_vessel_ids_returns_correctly( mock_raw_vessel_list_item: Dict[str, Any], ) -> None: - """Test that `VesselItem` iter matched vessel ids yields correctly.""" - vessel_item: VesselItem = VesselItem(**mock_raw_vessel_list_item) - matched_vessel_ids: Iterator[str] = vessel_item._iter_matched_vessel_ids() - assert matched_vessel_ids is not None - assert isinstance(matched_vessel_ids, Iterator) - assert len(list(matched_vessel_ids)) >= 1 + """Test that `VesselResult` returns list of vessel ids correctly.""" + data: List[VesselItem] = [VesselItem(**mock_raw_vessel_list_item)] + result = VesselResult(data=data) + assert result.vessel_ids is not None + assert isinstance(result.vessel_ids, list) + assert len(result.vessel_ids) >= 1 @pytest.mark.parametrize( @@ -75,28 +75,91 @@ def test_vessel_item__iter_matched_vessel_ids_yields_correctly( {"selfReportedInfo": [{"id": " "}]}, ], ) -def test_vessel_item__iter_matched_vessel_ids_does_not_yields_when_registry_or_self_report_info_missing( +def test_vessel_item_vessel_ids_returns_empty_list_when_registry_or_self_report_info_missing( mock_raw_vessel_list_item: Dict[str, Any], update: Dict[str, Any], ) -> None: - """Test that `VesselItem` iter matched vessel ids does not yields when registry or self reported info missing.""" + """Test that `VesselItem` vessel ids returns empty list when registry or self reported info missing.""" mocked_raw_vessel_list_item: Dict[str, Any] = {**mock_raw_vessel_list_item} for k, v in update.items(): mocked_raw_vessel_list_item[k] = v - vessel_item: VesselItem = VesselItem(**mocked_raw_vessel_list_item) - matched_vessel_ids: Iterator[str] = vessel_item._iter_matched_vessel_ids() - assert matched_vessel_ids is not None - assert isinstance(matched_vessel_ids, Iterator) - assert len(list(matched_vessel_ids)) == 0 + data: List[VesselItem] = [VesselItem(**mocked_raw_vessel_list_item)] + result = VesselResult(data=data) + assert result.vessel_ids is not None + assert isinstance(result.vessel_ids, list) + assert len(result.vessel_ids) == 0 -def test_vessel_result_vessel_ids_returns_correctly( +def test_vessel_result_transmission_dates_from_returns_correctly( mock_raw_vessel_list_item: Dict[str, Any], ) -> None: - """Test that `VesselResult` returns list of vessel ids correctly.""" + """Test that `VesselResult` transmission dates from returns list of transmission start dates correctly.""" data: List[VesselItem] = [VesselItem(**mock_raw_vessel_list_item)] result = VesselResult(data=data) - assert result.vessel_ids is not None - assert isinstance(result.vessel_ids, list) - assert len(result.vessel_ids) >= 1 + assert result.transmission_dates_from is not None + assert isinstance(result.transmission_dates_from, list) + assert len(result.transmission_dates_from) >= 1 + + +@pytest.mark.parametrize( + "update", + [ + {"registryInfoTotalRecords": None}, + {"registryInfoTotalRecords": 0}, + {"selfReportedInfo": None}, + {"selfReportedInfo": []}, + {"selfReportedInfo": [{"transmissionDateFrom": None}]}, + ], +) +def test_vessel_item_vessel_transmission_dates_from_returns_empty_list_when_registry_or_self_report_info_missing( + mock_raw_vessel_list_item: Dict[str, Any], + update: Dict[str, Any], +) -> None: + """Test that `VesselItem` transmission dates from returns empty list when registry or self reported info missing.""" + mocked_raw_vessel_list_item: Dict[str, Any] = {**mock_raw_vessel_list_item} + for k, v in update.items(): + mocked_raw_vessel_list_item[k] = v + + data: List[VesselItem] = [VesselItem(**mocked_raw_vessel_list_item)] + result = VesselResult(data=data) + assert result.transmission_dates_from is not None + assert isinstance(result.transmission_dates_from, list) + assert len(result.transmission_dates_from) == 0 + + +def test_vessel_result_transmission_dates_to_returns_correctly( + mock_raw_vessel_list_item: Dict[str, Any], +) -> None: + """Test that `VesselResult` transmission dates to returns list of transmission end dates correctly.""" + data: List[VesselItem] = [VesselItem(**mock_raw_vessel_list_item)] + result = VesselResult(data=data) + assert result.transmission_dates_to is not None + assert isinstance(result.transmission_dates_to, list) + assert len(result.transmission_dates_to) >= 1 + + +@pytest.mark.parametrize( + "update", + [ + {"registryInfoTotalRecords": None}, + {"registryInfoTotalRecords": 0}, + {"selfReportedInfo": None}, + {"selfReportedInfo": []}, + {"selfReportedInfo": [{"transmissionDateTo": None}]}, + ], +) +def test_vessel_item_vessel_transmission_dates_to_returns_empty_list_when_registry_or_self_report_info_missing( + mock_raw_vessel_list_item: Dict[str, Any], + update: Dict[str, Any], +) -> None: + """Test that `VesselItem` transmission dates to returns empty list when registry or self reported info missing.""" + mocked_raw_vessel_list_item: Dict[str, Any] = {**mock_raw_vessel_list_item} + for k, v in update.items(): + mocked_raw_vessel_list_item[k] = v + + data: List[VesselItem] = [VesselItem(**mocked_raw_vessel_list_item)] + result = VesselResult(data=data) + assert result.transmission_dates_to is not None + assert isinstance(result.transmission_dates_to, list) + assert len(result.transmission_dates_to) == 0 diff --git a/tests/resources/vessels/detail/models/test_response_models.py b/tests/resources/vessels/detail/models/test_response_models.py index 15889a3..4aed564 100644 --- a/tests/resources/vessels/detail/models/test_response_models.py +++ b/tests/resources/vessels/detail/models/test_response_models.py @@ -24,18 +24,6 @@ def test_vessel_detail_item_deserializes_all_fields( assert vessel_detail_item.dataset is not None -# def test_vessel_detail_item_vessel_ids_yields_correctly( -# mock_raw_vessel_detail_item: Dict[str, Any], -# ) -> None: -# """Test that `VesselDetailItem` yields vessel ids correctly.""" -# vessel_detail_item: VesselDetailItem = VesselDetailItem( -# **mock_raw_vessel_detail_item -# ) -# assert vessel_detail_item.vessel_ids is not None -# assert isinstance(vessel_detail_item.vessel_ids, Iterator) -# assert len(list(vessel_detail_item.vessel_ids)) >= 1 - - def test_vessel_detail_result_deserializes_all_fields( mock_raw_vessel_detail_item: Dict[str, Any], ) -> None: @@ -54,3 +42,25 @@ def test_vessel_detail_result_vessel_ids_returns_correctly( assert result.vessel_ids is not None assert isinstance(result.vessel_ids, list) assert len(result.vessel_ids) >= 1 + + +def test_vessel_detail_result_transmission_dates_from_returns_correctly( + mock_raw_vessel_list_item: Dict[str, Any], +) -> None: + """Test that `VesselDetailResult` transmission dates from returns list of transmission start dates correctly.""" + data: VesselDetailItem = VesselDetailItem(**mock_raw_vessel_list_item) + result = VesselDetailResult(data=data) + assert result.transmission_dates_from is not None + assert isinstance(result.transmission_dates_from, list) + assert len(result.transmission_dates_from) >= 1 + + +def test_vessel_detail_result_transmission_dates_to_returns_correctly( + mock_raw_vessel_list_item: Dict[str, Any], +) -> None: + """Test that `VesselDetailResult` transmission dates to returns list of transmission end dates correctly.""" + data: VesselDetailItem = VesselDetailItem(**mock_raw_vessel_list_item) + result = VesselDetailResult(data=data) + assert result.transmission_dates_to is not None + assert isinstance(result.transmission_dates_to, list) + assert len(result.transmission_dates_to) >= 1 diff --git a/tests/resources/vessels/list/models/test_response_models.py b/tests/resources/vessels/list/models/test_response_models.py index ab1c6c5..7a05e86 100644 --- a/tests/resources/vessels/list/models/test_response_models.py +++ b/tests/resources/vessels/list/models/test_response_models.py @@ -22,16 +22,6 @@ def test_vessel_list_item_deserializes_all_fields( assert vessel_list_item.dataset is not None -# def test_vessel_list_item_vessel_ids_yields_correctly( -# mock_raw_vessel_list_item: Dict[str, Any], -# ) -> None: -# """Test that `VesselListItem` yields vessel ids correctly.""" -# vessel_list_item: VesselListItem = VesselListItem(**mock_raw_vessel_list_item) -# assert vessel_list_item.vessel_ids is not None -# assert isinstance(vessel_list_item.vessel_ids, Iterator) -# assert len(list(vessel_list_item.vessel_ids)) >= 1 - - def test_vessel_list_result_deserializes_all_fields( mock_raw_vessel_list_item: Dict[str, Any], ) -> None: @@ -50,3 +40,25 @@ def test_vessel_list_result_vessel_ids_returns_correctly( assert result.vessel_ids is not None assert isinstance(result.vessel_ids, list) assert len(result.vessel_ids) >= 1 + + +def test_vessel_list_result_transmission_dates_from_returns_correctly( + mock_raw_vessel_list_item: Dict[str, Any], +) -> None: + """Test that `VesselListResult` transmission dates from returns list of transmission start dates correctly.""" + data: List[VesselListItem] = [VesselListItem(**mock_raw_vessel_list_item)] + result = VesselListResult(data=data) + assert result.transmission_dates_from is not None + assert isinstance(result.transmission_dates_from, list) + assert len(result.transmission_dates_from) >= 1 + + +def test_vessel_list_result_transmission_dates_to_returns_correctly( + mock_raw_vessel_list_item: Dict[str, Any], +) -> None: + """Test that `VesselListResult` transmission dates to returns list of transmission end dates correctly.""" + data: List[VesselListItem] = [VesselListItem(**mock_raw_vessel_list_item)] + result = VesselListResult(data=data) + assert result.transmission_dates_to is not None + assert isinstance(result.transmission_dates_to, list) + assert len(result.transmission_dates_to) >= 1 diff --git a/tests/resources/vessels/search/models/test_response_models.py b/tests/resources/vessels/search/models/test_response_models.py index 59cd56b..aac21e5 100644 --- a/tests/resources/vessels/search/models/test_response_models.py +++ b/tests/resources/vessels/search/models/test_response_models.py @@ -22,16 +22,6 @@ def test_vessel_search_item_deserializes_all_fields( assert vessel_search_item.dataset is not None -# def test_vessel_search_item_vessel_ids_yields_correctly( -# mock_raw_vessel_list_item: Dict[str, Any], -# ) -> None: -# """Test that `VesselSearchItem` yields vessel ids correctly.""" -# vessel_search_item: VesselSearchItem = VesselSearchItem(**mock_raw_vessel_list_item) -# assert vessel_search_item.vessel_ids is not None -# assert isinstance(vessel_search_item.vessel_ids, Iterator) -# assert len(list(vessel_search_item.vessel_ids)) >= 1 - - def test_vessel_search_result_deserializes_all_fields( mock_raw_vessel_list_item: Dict[str, Any], ) -> None: @@ -50,3 +40,25 @@ def test_vessel_search_result_vessel_ids_returns_correctly( assert result.vessel_ids is not None assert isinstance(result.vessel_ids, list) assert len(result.vessel_ids) >= 1 + + +def test_vessel_search_result_transmission_dates_from_returns_correctly( + mock_raw_vessel_list_item: Dict[str, Any], +) -> None: + """Test that `VesselSearchResult` transmission dates from returns list of transmission start dates correctly.""" + data: List[VesselSearchItem] = [VesselSearchItem(**mock_raw_vessel_list_item)] + result = VesselSearchResult(data=data) + assert result.transmission_dates_from is not None + assert isinstance(result.transmission_dates_from, list) + assert len(result.transmission_dates_from) >= 1 + + +def test_vessel_search_result_transmission_dates_to_returns_correctly( + mock_raw_vessel_list_item: Dict[str, Any], +) -> None: + """Test that `VesselSearchResult` transmission dates to returns list of transmission end dates correctly.""" + data: List[VesselSearchItem] = [VesselSearchItem(**mock_raw_vessel_list_item)] + result = VesselSearchResult(data=data) + assert result.transmission_dates_to is not None + assert isinstance(result.transmission_dates_to, list) + assert len(result.transmission_dates_to) >= 1 From 0553147f72ddadfdd83e375efef54c45aab23493 Mon Sep 17 00:00:00 2001 From: lykmapipo Date: Tue, 23 Jun 2026 01:33:03 +0300 Subject: [PATCH 3/3] test(vessels): fix typo and incorrect names docstrings --- .../vessels/base/models/test_response_models.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/resources/vessels/base/models/test_response_models.py b/tests/resources/vessels/base/models/test_response_models.py index 3b836d4..971e03e 100644 --- a/tests/resources/vessels/base/models/test_response_models.py +++ b/tests/resources/vessels/base/models/test_response_models.py @@ -37,7 +37,7 @@ def test_vessel_item_iter_matched_self_reported_infos_does_not_yields_when_regis mock_raw_vessel_list_item: Dict[str, Any], update: Dict[str, Any], ) -> None: - """Test that `VesselItem` iter matched self reported infos does not yields when registry or self reported info missing.""" + """Test that `VesselItem` iter matched self reported infos does not yields when registry or self reported info missing.""" mocked_raw_vessel_list_item: Dict[str, Any] = {**mock_raw_vessel_list_item} for k, v in update.items(): mocked_raw_vessel_list_item[k] = v @@ -75,11 +75,11 @@ def test_vessel_result_vessel_ids_returns_correctly( {"selfReportedInfo": [{"id": " "}]}, ], ) -def test_vessel_item_vessel_ids_returns_empty_list_when_registry_or_self_report_info_missing( +def test_vessel_result_vessel_ids_returns_empty_list_when_registry_or_self_report_info_missing( mock_raw_vessel_list_item: Dict[str, Any], update: Dict[str, Any], ) -> None: - """Test that `VesselItem` vessel ids returns empty list when registry or self reported info missing.""" + """Test that `VesselResult` vessel ids returns empty list when registry or self reported info missing.""" mocked_raw_vessel_list_item: Dict[str, Any] = {**mock_raw_vessel_list_item} for k, v in update.items(): mocked_raw_vessel_list_item[k] = v @@ -112,11 +112,11 @@ def test_vessel_result_transmission_dates_from_returns_correctly( {"selfReportedInfo": [{"transmissionDateFrom": None}]}, ], ) -def test_vessel_item_vessel_transmission_dates_from_returns_empty_list_when_registry_or_self_report_info_missing( +def test_vessel_result_vessel_transmission_dates_from_returns_empty_list_when_registry_or_self_report_info_missing( mock_raw_vessel_list_item: Dict[str, Any], update: Dict[str, Any], ) -> None: - """Test that `VesselItem` transmission dates from returns empty list when registry or self reported info missing.""" + """Test that `VesselResult` transmission dates from returns empty list when registry or self reported info missing.""" mocked_raw_vessel_list_item: Dict[str, Any] = {**mock_raw_vessel_list_item} for k, v in update.items(): mocked_raw_vessel_list_item[k] = v @@ -149,11 +149,11 @@ def test_vessel_result_transmission_dates_to_returns_correctly( {"selfReportedInfo": [{"transmissionDateTo": None}]}, ], ) -def test_vessel_item_vessel_transmission_dates_to_returns_empty_list_when_registry_or_self_report_info_missing( +def test_vessel_result_vessel_transmission_dates_to_returns_empty_list_when_registry_or_self_report_info_missing( mock_raw_vessel_list_item: Dict[str, Any], update: Dict[str, Any], ) -> None: - """Test that `VesselItem` transmission dates to returns empty list when registry or self reported info missing.""" + """Test that `VesselResult` transmission dates to returns empty list when registry or self reported info missing.""" mocked_raw_vessel_list_item: Dict[str, Any] = {**mock_raw_vessel_list_item} for k, v in update.items(): mocked_raw_vessel_list_item[k] = v