Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions tests/api/examples/cargo_movements.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,17 @@
"type": "consignee",
"id": "5b3fcbc1bd2efec8999bd37d128a7e2132ebd9acf5a1a6fd8d6d2ba234c13938",
"label": "RELIANCE"
},
{
"type": "load",
"buyer_id": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
"buyer_label": "BUYER CORP",
"seller_id": "f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5",
"seller_label": "SELLER CORP",
"contract_type": "spot",
"delivery_method": "FOB",
"buyer_reason": "ais_history",
"seller_reason": "ais_history"
}
]
}
Expand Down
41 changes: 41 additions & 0 deletions tests/api/test_cargo_movement.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,17 @@ class TestCargoMovement(TestCase):
"id": "5b3fcbc1bd2efec8999bd37d128a7e2132ebd9acf5a1a6fd8d6d2ba234c13938",
"label": "RELIANCE",
},
{
"type": "load",
"buyer_id": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
"buyer_label": "BUYER CORP",
"seller_id": "f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5",
"seller_label": "SELLER CORP",
"contract_type": "spot",
"delivery_method": "FOB",
"buyer_reason": "ais_history",
"seller_reason": "ais_history",
},
],
}

Expand Down Expand Up @@ -191,9 +202,39 @@ def test_convert_to_flat_dict(self) -> None:
"trades.0.type": "shipper",
"trades.0.id": "c9f70607e743e82428ea24cd32f6e403c6ced09078eacfe3d6c175347a9ab508",
"trades.0.label": "NAYARA ENERGY",
"trades.0.label_keyword": None,
"trades.0.buyer_id": None,
"trades.0.buyer_label": None,
"trades.0.seller_id": None,
"trades.0.seller_label": None,
"trades.0.contract_type": None,
"trades.0.delivery_method": None,
"trades.0.buyer_reason": None,
"trades.0.seller_reason": None,
"trades.1.type": "consignee",
"trades.1.id": "5b3fcbc1bd2efec8999bd37d128a7e2132ebd9acf5a1a6fd8d6d2ba234c13938",
"trades.1.label": "RELIANCE",
"trades.1.label_keyword": None,
"trades.1.buyer_id": None,
"trades.1.buyer_label": None,
"trades.1.seller_id": None,
"trades.1.seller_label": None,
"trades.1.contract_type": None,
"trades.1.delivery_method": None,
"trades.1.buyer_reason": None,
"trades.1.seller_reason": None,
"trades.2.type": "load",
"trades.2.id": None,
"trades.2.label": None,
"trades.2.label_keyword": None,
"trades.2.buyer_id": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
"trades.2.buyer_label": "BUYER CORP",
"trades.2.seller_id": "f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5",
"trades.2.seller_label": "SELLER CORP",
"trades.2.contract_type": "spot",
"trades.2.delivery_method": "FOB",
"trades.2.buyer_reason": "ais_history",
"trades.2.seller_reason": "ais_history",
}

assert flat == expected
87 changes: 87 additions & 0 deletions tests/endpoints/test_cargo_movements_real.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,3 +374,90 @@ def test_filter_exclude_shipper_and_consignee(self):
trade["id"]
!= "5b3fcbc1bd2efec8999bd37d128a7e2132ebd9acf5a1a6fd8d6d2ba234c13938"
)

def test_filter_by_buyer_and_seller(self):
# BP as buyer, Atlantic LNG as seller
buyer_id = (
"409dd25aeff466583e8d738f2972ab7d6032622b07c2722f6459edd1be32e1de"
)
seller_id = (
"273f7718a1739b407893709d5738992f5c5653bb98460db69295ced21bb38c72"
)

cms = CargoMovements().search(
filter_activity="loading_start",
filter_time_min=datetime(2025, 1, 1),
filter_time_max=datetime(2025, 1, 14),
filter_buyer=buyer_id,
filter_seller=seller_id,
)

results = list(cms)
assert len(results) > 0, "Expected at least one cargo movement"

found_matching_trade = False
for cm in results:
if "trades" in cm and cm["trades"]:
# At least one trade should have the filtered buyer_id
buyer_ids = [
t.get("buyer_id")
for t in cm["trades"]
if t.get("buyer_id")
]
if buyer_id in buyer_ids:
found_matching_trade = True

# At least one trade should have the filtered seller_id
seller_ids = [
t.get("seller_id")
for t in cm["trades"]
if t.get("seller_id")
]
if seller_id in seller_ids:
found_matching_trade = True

assert (
found_matching_trade
), "Expected at least one trade with matching buyer_id or seller_id"

def test_filter_exclude_buyer_and_seller(self):
# Exclude BP as buyer, Atlantic LNG as seller
buyer_id = (
"409dd25aeff466583e8d738f2972ab7d6032622b07c2722f6459edd1be32e1de"
)
seller_id = (
"273f7718a1739b407893709d5738992f5c5653bb98460db69295ced21bb38c72"
)

cms = CargoMovements().search(
filter_activity="loading_start",
filter_time_min=datetime(2025, 1, 1),
filter_time_max=datetime(2025, 1, 14),
exclude_buyer=buyer_id,
exclude_seller=seller_id,
)

results = list(cms)
assert len(results) > 0, "Expected at least one cargo movement"

for cm in results:
if "trades" in cm and cm["trades"]:
# No trade should have the excluded buyer_id
buyer_ids = [
t.get("buyer_id")
for t in cm["trades"]
if t.get("buyer_id")
]
assert (
buyer_id not in buyer_ids
), f"Did not expect {buyer_id} in trades"

# No trade should have the excluded seller_id
seller_ids = [
t.get("seller_id")
for t in cm["trades"]
if t.get("seller_id")
]
assert (
seller_id not in seller_ids
), f"Did not expect {seller_id} in trades"
14 changes: 14 additions & 0 deletions vortexasdk/api/cargo_movement.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,24 @@ class CargoMovementProductEntry(BaseModel):
label: Optional[str] = None


CargoMovementContractType = Literal["spot", "term"]

CargoMovementDeliveryMethodType = Literal["FOB", "DES", "CFR", "CIF"]


class CargoMovementTradeEntry(BaseModel):
type: Literal["load", "discharge", "shipper", "consignee"]
id: Optional[ID] = None
label: Optional[str] = None
label_keyword: Optional[str] = None
buyer_id: Optional[ID] = None
buyer_label: Optional[str] = None
seller_id: Optional[ID] = None
seller_label: Optional[str] = None
contract_type: Optional[CargoMovementContractType] = None
delivery_method: Optional[CargoMovementDeliveryMethodType] = None
buyer_reason: Optional[str] = None
seller_reason: Optional[str] = None


class CargoMovement(BaseModel):
Expand Down
23 changes: 18 additions & 5 deletions vortexasdk/endpoints/cargo_movements.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ def search(
disable_geographic_exclusion_rules: Optional[bool] = None,
intra_movements: Optional[str] = None,
quantity_at_time_of: str = "load",
filter_buyer: Optional[Union[ID, List[ID]]] = None,
exclude_buyer: Optional[Union[ID, List[ID]]] = None,
filter_seller: Optional[Union[ID, List[ID]]] = None,
exclude_seller: Optional[Union[ID, List[ID]]] = None,
) -> CargoMovementsResult:
"""

Expand Down Expand Up @@ -174,16 +178,21 @@ def search(
exclude_vessel_tags: A time bound vessel tag, or list of time bound vessel tags 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.
the filter behaviour for cargo leaving then entering the same geographic area.

intra_movements: This enum controls a popular industry term intra-movements and determines the filter behaviour for cargo leaving then entering the same geographic area.
One of `all`, `exclude_intra_country` or `exclude_intra_geography`
One of `all`, `exclude_intra_country` or `exclude_intra_geography`

quantity_at_time_of: This parameter is designed for LNG cargo and gives the user the freedom to choose whether to create the time series based on the load volume or discharged volumes, as we consider the discharge quantities to differ from load quantities due to boil-off gas.

One of:
`load` - represents the quantity of the selected unit at the time of the loading event.
`unload` - represents the quantity of the selected unit at the time of the unloading event.
One of:
`load` - represents the quantity of the selected unit at the time of the loading event.
`unload` - represents the quantity of the selected unit at the time of the unloading event.

filter_buyer: A buyer ID, or list of buyer IDs to filter on.
exclude_buyer: A buyer ID, or list of buyer IDs to exclude.
filter_seller: A seller ID, or list of seller IDs to filter on.
exclude_seller: A seller ID, or list of seller IDs to exclude.

# Returns
`CargoMovementsResult`, containing all the cargo movements matching the given search terms.
Expand Down Expand Up @@ -278,6 +287,8 @@ def search(
"filter_vessel_propulsion": convert_to_list(
exclude_vessel_propulsion
),
"filter_seller": convert_to_list(exclude_seller),
"filter_buyer": convert_to_list(exclude_buyer),
}

api_params: Dict[str, Any] = {
Expand Down Expand Up @@ -323,6 +334,8 @@ def search(
"intra_movements": intra_movements,
"size": self._MAX_PAGE_RESULT_SIZE,
"quantity_at_time_of": quantity_at_time_of,
"filter_buyer": convert_to_list(filter_buyer),
"filter_seller": convert_to_list(filter_seller),
}

response = super().search_with_client(**api_params)
Expand Down
2 changes: 1 addition & 1 deletion vortexasdk/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.0.20"
__version__ = "1.0.21"
Loading