diff --git a/tests/api/test_cargo_movement.py b/tests/api/test_cargo_movement.py index ea4f90e3..5839feb1 100644 --- a/tests/api/test_cargo_movement.py +++ b/tests/api/test_cargo_movement.py @@ -162,6 +162,11 @@ def test_convert_to_flat_dict(self): "vessels.0.imo": 9480980, "vessels.0.mmsi": 255804460, "vessels.0.name": "JOHANN ESSBERGER", + "vessels.0.year": None, + "vessels.0.scrubber": None, + "vessels.0.flag": None, + "vessels.0.ice_class": None, + "vessels.0.propulsion": None, "vessels.0.start_timestamp": "2019-10-18T21:38:34+0000", "vessels.0.status": "vessel_status_laden_known", "vessels.0.vessel_class": "tiny_tanker", diff --git a/tests/api/test_vessel_movement.py b/tests/api/test_vessel_movement.py index 822203c6..5053ae2e 100644 --- a/tests/api/test_vessel_movement.py +++ b/tests/api/test_vessel_movement.py @@ -357,6 +357,11 @@ def test_flatten(self): "vessel.status": "vessel_status_ballast", "vessel.vessel_class": "suezmax", "vessel.voyage_id": None, + "vessel.year": None, + "vessel.flag": None, + "vessel.scrubber": None, + "vessel.ice_class": None, + "vessel.propulsion": None, } assert expected == flat diff --git a/tests/endpoints/test_cargo_movements_real.py b/tests/endpoints/test_cargo_movements_real.py index 79bf77df..d61e6df8 100644 --- a/tests/endpoints/test_cargo_movements_real.py +++ b/tests/endpoints/test_cargo_movements_real.py @@ -48,11 +48,6 @@ def test_search_single_filter_id(self): assert len(df) == 2 def test_exlusion_filter(self): - crude = [ - p.id - for p in Products().search("Crude/Condensates").to_list() - if p.layer == ["group"] - ] arab_medium = [ p.id for p in Products().search("Arab Medium").to_list() diff --git a/tests/endpoints/test_vessel_movements_real.py b/tests/endpoints/test_vessel_movements_real.py index 05e765d9..6c4d412c 100644 --- a/tests/endpoints/test_vessel_movements_real.py +++ b/tests/endpoints/test_vessel_movements_real.py @@ -1,7 +1,7 @@ from datetime import datetime from tests.testcases import TestCaseUsingRealAPI -from vortexasdk import VesselMovements, Geographies, Corporations +from vortexasdk import VesselMovements, Geographies, Corporations, Attributes from vortexasdk.endpoints import vessel_movements_result @@ -151,3 +151,51 @@ def test_filter_activity(self): print(df.head()) assert len(df) == 2 + + def test_age_flag_scrubbers_filters(self): + panama = [ + g.id + for g in Geographies().search("panama").to_list() + if "country" in g.layer + ] + + df = ( + VesselMovements() + .search( + filter_vessel_scrubbers="inc", + filter_vessel_age_min=2, + filter_vessel_age_max=15, + filter_vessel_flags=panama, + filter_time_min=datetime(2017, 10, 1), + filter_time_max=datetime(2017, 10, 1), + ) + .to_df() + .head(2) + ) + + print(df.head()) + assert len(df) == 2 + + def test_ice_class_filters(self): + ice_classes = [ + g.id for g in Attributes().search(type="ice_class").to_list() + ] + + propulsion = [ + g.id for g in Attributes().search(type="propulsion").to_list() + ] + + df = ( + VesselMovements() + .search( + filter_vessel_ice_class=ice_classes, + filter_vessel_propulsion=propulsion, + filter_time_min=datetime(2017, 10, 1), + filter_time_max=datetime(2017, 10, 1), + ) + .to_df() + .head(2) + ) + + print(df.head()) + assert len(df) == 2 diff --git a/vortexasdk/api/shared_types.py b/vortexasdk/api/shared_types.py index 3b8ccd74..6f0bc305 100644 --- a/vortexasdk/api/shared_types.py +++ b/vortexasdk/api/shared_types.py @@ -94,3 +94,39 @@ class Tag: tag: str start_timestamp: Optional[ISODate] = None end_timestamp: Optional[ISODate] = None + + +@dataclass(frozen=True) +class Flag: + """ + + Represents a property that is associated with a vessel's flag. + + - `flag` key will be a Geography Entity ID. + - `flag_country` key will be the ISO code for the country + + [Geography Entity Further Documentation](https://docs.vortexa.com/reference/intro-geography-entries) + + """ + + tag: str + flag: str + flag_country: str + + +@dataclass(frozen=True) +class Scrubber: + """ + + Represents information about scrubbers fitted to a vessel. + + - `scrubber` key will be the type of scrubber. + - `planned` key is if this scrubber has not yet been fitted but is planned. + + An empty `scrubber` List may mean the scrubber status is unknown or a vessel has none fitted. + + """ + + tag: str + scrubber: str + planned: bool diff --git a/vortexasdk/api/vessel.py b/vortexasdk/api/vessel.py index 20e1f5d3..c553ffd5 100644 --- a/vortexasdk/api/vessel.py +++ b/vortexasdk/api/vessel.py @@ -4,7 +4,14 @@ from vortexasdk.api.corporation import CorporateEntity from vortexasdk.api.id import ID from vortexasdk.api.serdes import FromDictMixin -from vortexasdk.api.shared_types import IDName, ISODate, Node, Tag +from vortexasdk.api.shared_types import ( + IDName, + ISODate, + Node, + Tag, + Scrubber, + Flag, +) @dataclass(frozen=True,) @@ -34,6 +41,11 @@ class Vessel(Node, FromDictMixin): imo: Optional[int] = None gross_tonnage: Optional[int] = None + scrubber: Optional[Scrubber] = None + flag: Optional[Flag] = None + ice_class: Optional[str] = None + propulsion: Optional[str] = None + @dataclass(frozen=True) class VesselEntity(IDName): @@ -54,6 +66,7 @@ class VesselEntity(IDName): corporate_entities: List[CorporateEntity] tags: List[Tag] status: str + year: Optional[int] = None start_timestamp: Optional[ISODate] = None @@ -62,3 +75,8 @@ class VesselEntity(IDName): fixture_fulfilled: Optional[bool] = None end_timestamp: Optional[ISODate] = None fixture_id: Optional[str] = None + + scrubber: Optional[Scrubber] = None + flag: Optional[Flag] = None + ice_class: Optional[str] = None + propulsion: Optional[str] = None diff --git a/vortexasdk/endpoints/cargo_movements.py b/vortexasdk/endpoints/cargo_movements.py index 63ddd0d3..58fdf4a0 100644 --- a/vortexasdk/endpoints/cargo_movements.py +++ b/vortexasdk/endpoints/cargo_movements.py @@ -40,12 +40,21 @@ def search( filter_storage_locations: Union[ID, List[ID]] = None, filter_ship_to_ship_locations: Union[ID, List[ID]] = None, filter_waypoints: Union[ID, List[ID]] = None, + filter_vessel_age_min: int = None, + filter_vessel_age_max: int = None, + filter_vessel_scrubbers: str = "disabled", + filter_vessel_flags: Union[ID, List[ID]] = None, + filter_vessel_ice_class: Union[ID, List[ID]] = None, + filter_vessel_propulsion: Union[ID, List[ID]] = None, exclude_origins: Union[ID, List[ID]] = None, exclude_destinations: Union[ID, List[ID]] = None, exclude_products: Union[ID, List[ID]] = None, exclude_vessels: Union[ID, List[ID]] = None, exclude_charterers: Union[ID, List[ID]] = None, exclude_owners: Union[ID, List[ID]] = None, + exclude_vessel_flags: Union[ID, List[ID]] = None, + exclude_vessel_ice_class: Union[ID, List[ID]] = None, + exclude_vessel_propulsion: Union[ID, List[ID]] = None, disable_geographic_exclusion_rules: bool = None, timeseries_activity_time_span_min: int = None, timeseries_activity_time_span_max: int = None, @@ -84,6 +93,18 @@ def search( filter_waypoints: A geography ID, or list of geography IDs to filter on. + filter_vessel_age_min: A number between 1 and 100 (representing years). + + filter_vessel_age_max: A number between 1 and 100 (representing years). + + filter_vessel_scrubbers: Either inactive 'disabled', or included 'inc' or excluded 'exc'. + + filter_vessel_flags: A geography ID, or list of geography IDs to filter on. + + filter_vessel_ice_class: An attribute ID, or list of attribute IDs to filter on. + + filter_vessel_propulsion: An attribute ID, or list of attribute IDs to filter on. + exclude_origins: A geography ID, or list of geography IDs to exclude. exclude_destinations: A geography ID, or list of geography IDs to exclude. @@ -96,6 +117,12 @@ def search( exclude_owners: An owner ID, or list of owner IDs to exclude. + exclude_vessel_flags: A geography ID, or list of geography IDs to exclude. + + exclude_vessel_ice_class: An attribute ID, or list of attribute IDs to exclude. + + exclude_vessel_propulsion: An attribute ID, or list of attribute IDs to exclude. + disable_geographic_exclusion_rules: This controls a popular industry term "intra-movements" and determines the filter behaviour for cargo leaving then entering the same geographic area. @@ -183,6 +210,13 @@ def search( "filter_vessels": convert_to_list(exclude_vessels), "filter_charterers": convert_to_list(exclude_charterers), "filter_owners": convert_to_list(exclude_owners), + "filter_vessel_flags": convert_to_list(exclude_vessel_flags), + "filter_vessel_ice_class": convert_to_list( + exclude_vessel_ice_class + ), + "filter_vessel_propulsion": convert_to_list( + exclude_vessel_propulsion + ), } params = { @@ -205,6 +239,16 @@ def search( filter_ship_to_ship_locations ), "filter_waypoints": convert_to_list(filter_waypoints), + "filter_vessel_age_min": filter_vessel_age_min, + "filter_vessel_age_max": filter_vessel_age_max, + "filter_vessel_scrubbers": filter_vessel_scrubbers, + "filter_vessel_flags": convert_to_list(filter_vessel_flags), + "filter_vessel_ice_class": convert_to_list( + filter_vessel_ice_class + ), + "filter_vessel_propulsion": convert_to_list( + filter_vessel_propulsion + ), "exclude": exclude_params, "disable_geographic_exclusion_rules": disable_geographic_exclusion_rules, "size": self._MAX_PAGE_RESULT_SIZE, diff --git a/vortexasdk/endpoints/vessel_movements.py b/vortexasdk/endpoints/vessel_movements.py index 1c289a32..e6fc830e 100644 --- a/vortexasdk/endpoints/vessel_movements.py +++ b/vortexasdk/endpoints/vessel_movements.py @@ -2,6 +2,7 @@ from datetime import datetime from typing import List, Union +from vortexasdk.api import ID from vortexasdk.api.shared_types import to_ISODate from vortexasdk.endpoints.endpoints import VESSEL_MOVEMENTS_RESOURCE from vortexasdk.endpoints.vessel_movements_result import VesselMovementsResult @@ -36,21 +37,30 @@ def search( filter_time_max: datetime = datetime(2019, 10, 1, 1), unit: str = "b", filter_activity: str = None, - filter_charterers: Union[str, List[str]] = None, - filter_destinations: Union[str, List[str]] = None, - filter_origins: Union[str, List[str]] = None, - filter_owners: Union[str, List[str]] = None, - filter_products: Union[str, List[str]] = None, - filter_vessels: Union[str, List[str]] = None, - filter_vessel_classes: Union[str, List[str]] = None, + filter_charterers: Union[ID, List[ID]] = None, + filter_destinations: Union[ID, List[ID]] = None, + filter_origins: Union[ID, List[ID]] = None, + filter_owners: Union[ID, List[ID]] = None, + filter_products: Union[ID, List[ID]] = None, + filter_vessels: Union[ID, List[ID]] = None, + filter_vessel_classes: Union[ID, List[ID]] = None, filter_vessel_status: str = None, - exclude_origins: Union[str, List[str]] = None, - exclude_destinations: Union[str, List[str]] = None, - exclude_products: Union[str, List[str]] = None, - exclude_vessels: Union[str, List[str]] = None, - exclude_vessel_classes: Union[str, List[str]] = None, - exclude_charterers: Union[str, List[str]] = None, - exclude_owners: Union[str, List[str]] = None, + filter_vessel_age_min: int = None, + filter_vessel_age_max: int = None, + filter_vessel_scrubbers: str = "disabled", + filter_vessel_flags: Union[ID, List[ID]] = None, + filter_vessel_ice_class: Union[ID, List[ID]] = None, + filter_vessel_propulsion: Union[ID, List[ID]] = None, + exclude_origins: Union[ID, List[ID]] = None, + exclude_destinations: Union[ID, List[ID]] = None, + exclude_products: Union[ID, List[ID]] = None, + exclude_vessels: Union[ID, List[ID]] = None, + exclude_vessel_classes: Union[ID, List[ID]] = None, + exclude_charterers: Union[ID, List[ID]] = None, + exclude_owners: Union[ID, List[ID]] = None, + exclude_vessel_flags: Union[ID, List[ID]] = None, + exclude_vessel_ice_class: Union[ID, List[ID]] = None, + exclude_vessel_propulsion: Union[ID, List[ID]] = None, ) -> VesselMovementsResult: """ Find VesselMovements matching the given search parameters. @@ -83,6 +93,18 @@ def search( filter_vessel_status: The vessel status on which to base the filter. Enter 'vessel_status_ballast' for ballast vessels, 'vessel_status_laden_known' for laden vessels with known cargo (i.e. a type of cargo that Vortexa currently tracks) or 'vessel_status_laden_unknown' for laden vessels with unknown cargo (i.e. a type of cargo that Vortexa currently does not track). + filter_vessel_age_min: A number between 1 and 100 (representing years). + + filter_vessel_age_max: A number between 1 and 100 (representing years). + + filter_vessel_scrubbers: Either inactive 'disabled', or included 'inc' or excluded 'exc'. + + filter_vessel_flags: A geography ID, or list of geography IDs to filter on. + + filter_vessel_ice_class: An attribute ID, or list of attribute IDs to filter on. + + filter_vessel_propulsion: An attribute ID, or list of attribute IDs to filter on. + exclude_origins: A geography ID, or list of geography IDs to exclude. exclude_destinations: A geography ID, or list of geography IDs to exclude. @@ -97,6 +119,12 @@ def search( exclude_owners: An owner ID, or list of owner IDs to exclude. + exclude_vessel_flags: A geography ID, or list of geography IDs to exclude. + + exclude_vessel_ice_class: An attribute ID, or list of attribute IDs to exclude. + + exclude_vessel_propulsion: An attribute ID, or list of attribute IDs to exclude. + # Returns `VesselMovementsResult`, containing all the vessel movements matching the given search terms. @@ -132,6 +160,13 @@ def search( "filter_vessel_classes": convert_to_list(exclude_vessel_classes), "filter_charterers": convert_to_list(exclude_charterers), "filter_owners": convert_to_list(exclude_owners), + "filter_vessel_flags": convert_to_list(exclude_vessel_flags), + "filter_vessel_ice_class": convert_to_list( + exclude_vessel_ice_class + ), + "filter_vessel_propulsion": convert_to_list( + exclude_vessel_propulsion + ), } params = { @@ -147,6 +182,16 @@ def search( "filter_vessels": convert_to_list(filter_vessels), "filter_vessel_classes": convert_to_list(filter_vessel_classes), "filter_vessel_status": filter_vessel_status, + "filter_vessel_age_min": filter_vessel_age_min, + "filter_vessel_age_max": filter_vessel_age_max, + "filter_vessel_scrubbers": filter_vessel_scrubbers, + "filter_vessel_flags": convert_to_list(filter_vessel_flags), + "filter_vessel_ice_class": convert_to_list( + filter_vessel_ice_class + ), + "filter_vessel_propulsion": convert_to_list( + filter_vessel_propulsion + ), "exclude": exclude_params, "size": self._MAX_PAGE_RESULT_SIZE, } diff --git a/vortexasdk/endpoints/vessels.py b/vortexasdk/endpoints/vessels.py index 5f9c551c..115cf040 100644 --- a/vortexasdk/endpoints/vessels.py +++ b/vortexasdk/endpoints/vessels.py @@ -26,6 +26,7 @@ def search( ids: Union[str, List[str]] = None, vessel_classes: Union[str, List[str]] = None, vessel_product_types: Union[ID, List[ID]] = None, + vessel_scrubbers: str = "disabled", ) -> VesselsResult: """ Find all vessels matching given search arguments. Search arguments are combined in an AND manner. @@ -39,6 +40,8 @@ def search( vessel_product_types: A product ID, or list of product IDs to filter on, searching vessels currently carrying these products. + vessel_scrubbers: Either inactive 'disabled', included 'inc' or excluded 'exc'. + # Returns List of vessels matching the search arguments. @@ -87,6 +90,7 @@ def search( "vessel_classes": [ v.lower() for v in (convert_to_list(vessel_classes)) ], + "vessel_scrubbers": vessel_scrubbers, } return VesselsResult(super().search(**search_params))