diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bcca56f1..f7bcaade 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,10 +46,10 @@ pip install -e '.[tests]' To run the live tests, you'll need to have the `VORTEXA_API_KEY` environment variable set - `export VORTEXA_API_KEY=xyz` -run tests +run all tests ```bash -python setup.py test +pytest ``` If you're just looking to run tests in a single module (`test_vessels` in this case), you can do like this: diff --git a/docs/autogen.py b/docs/autogen.py index adca3723..93cecc46 100644 --- a/docs/autogen.py +++ b/docs/autogen.py @@ -53,6 +53,20 @@ def copy_examples(examples_dir: str, destination_dir: str) -> None: f_out.write("```") +def copy_css(source_dir: str, destination_dir: str) -> None: + """Copy CSS files to the documentation build directory.""" + pathlib.Path(destination_dir).mkdir(parents=True, exist_ok=True) + for file in os.listdir(source_dir): + if file.endswith(".css"): + source_path = os.path.join(source_dir, file) + dest_path = os.path.join(destination_dir, file) + with open(source_path, "r", encoding="utf-8") as f_in: + content = f_in.read() + with open(dest_path, "w", encoding="utf-8") as f_out: + f_out.write(content) + + if __name__ == "__main__": print(os.getcwd()) copy_examples("./docs/examples", "./_build/pydocmd/examples") + copy_css("./docs/css", "./_build/pydocmd/css") diff --git a/docs/css/custom.css b/docs/css/custom.css new file mode 100644 index 00000000..5d01ba70 --- /dev/null +++ b/docs/css/custom.css @@ -0,0 +1,5 @@ +/* Fix long module names overflowing */ +h1, h2, h3, h4, h5, h6 { + word-wrap: break-word; + overflow-wrap: break-word; +} diff --git a/docs/scripts/serve_docs.sh b/docs/scripts/serve_docs.sh index f24229ac..a544addf 100755 --- a/docs/scripts/serve_docs.sh +++ b/docs/scripts/serve_docs.sh @@ -4,5 +4,7 @@ set -e . venv/bin/activate rm -rf ./_build mkdir -p ./_build/pydocmd/examples +mkdir -p ./_build/pydocmd/css +cp -r docs/css/* ./_build/pydocmd/css/ python docs/autogen.py pydocmd serve diff --git a/mkdocs.yml b/mkdocs.yml index c070a07d..78e50101 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -24,6 +24,16 @@ pages: Products: endpoints/products.md Vessels: endpoints/vessels.md Fixtures: endpoints/fixtures.md + Anywhere Freight Pricing: + Latest Update: endpoints/anywhere_freight_pricing_latest_update_timestamp.md + Time-series: endpoints/anywhere_freight_pricing_price_timeseries.md + Price: + Get Prices: endpoints/anywhere_freight_pricing_get_price_details.md + Post Prices: endpoints/anywhere_freight_pricing_post_price_details.md + Top Ports: + Origin: endpoints/anywhere_freight_pricing_top_ports_origin.md + Destination: endpoints/anywhere_freight_pricing_top_ports_destination.md + Vessel Classes: endpoints/anywhere_freight_pricing_vessel_classes_details.md Vessel Availability: Search: endpoints/vessel_availability_search.md Time Series: endpoints/vessel_availability_timeseries.md @@ -75,3 +85,5 @@ theme: custom_dir: custom_theme/ name: readthedocs search_index_only: false +extra_css: + - css/custom.css diff --git a/pydocmd.yml b/pydocmd.yml index 90f04d51..c59aeb07 100644 --- a/pydocmd.yml +++ b/pydocmd.yml @@ -93,8 +93,24 @@ generate: - endpoints/voyages_routes_breakdown.md: vortexasdk.endpoints.voyages_routes_breakdown++ - endpoints/voyages_top_hits.md: vortexasdk.endpoints.voyages_top_hits++ - endpoints/voyages_congestion_breakdown.md: vortexasdk.endpoints.voyages_congestion_breakdown++ - - endpoints/refineries.md: + - endpoints/refineries.md: - vortexasdk.endpoints.refineries++ + - endpoints/anywhere_freight_pricing_latest_update_timestamp.md: + - vortexasdk.endpoints.anywhere_freight_pricing_latest_update_timestamp++ + - endpoints/anywhere_freight_pricing_result.md: + - vortexasdk.endpoints.anywhere_freight_pricing_result++ + - endpoints/anywhere_freight_pricing_price_timeseries.md: + - vortexasdk.endpoints.anywhere_freight_pricing_price_timeseries++ + - endpoints/anywhere_freight_pricing_get_price_details.md: + - vortexasdk.endpoints.anywhere_freight_pricing_get_price_details++ + - endpoints/anywhere_freight_pricing_post_price_details.md: + - vortexasdk.endpoints.anywhere_freight_pricing_post_price_details++ + - endpoints/anywhere_freight_pricing_top_ports_origin.md: + - vortexasdk.endpoints.anywhere_freight_pricing_top_ports_origin++ + - endpoints/anywhere_freight_pricing_top_ports_destination.md: + - vortexasdk.endpoints.anywhere_freight_pricing_top_ports_destination++ + - endpoints/anywhere_freight_pricing_vessel_classes_details.md: + - vortexasdk.endpoints.anywhere_freight_pricing_vessel_classes_details++ pages: - Home: index.md << README.md - Endpoints: diff --git a/tests/endpoints/test_anywhere_freight_pricing_get_price_details.py b/tests/endpoints/test_anywhere_freight_pricing_get_price_details.py new file mode 100644 index 00000000..3905558d --- /dev/null +++ b/tests/endpoints/test_anywhere_freight_pricing_get_price_details.py @@ -0,0 +1,57 @@ +from datetime import datetime + +from tests.testcases import TestCaseUsingRealAPI +from vortexasdk import AnywhereFreightPricingGetPriceDetails + + +class TestAnywhereFreightPricingGetPriceDetails(TestCaseUsingRealAPI): + def test_search_returns_data(self): + result = AnywhereFreightPricingGetPriceDetails().search( + time_min=datetime(2024, 1, 1), + time_max=datetime(2024, 1, 31), + origin_port="7f314ba0a498c36359b1c88781e94a73e19dcc9bbb030ec6b82f944a73d4da2f", + destination_port="68faf65af1345067f11dc6723b8da32f00e304a6f33c000118fccd81947deb4e", + vessel_class="oil_aframax_lr2", + product="crude", + unit="usd_per_tonne", + ) + + result_list = result.to_list() + assert len(result_list) > 0 + assert "origin_port" in result_list[0] + assert "destination_port" in result_list[0] + assert "vessel_class" in result_list[0] + assert "rates" in result_list[0] + + def test_search_to_df(self): + result = AnywhereFreightPricingGetPriceDetails().search( + time_min=datetime(2024, 1, 1), + time_max=datetime(2024, 1, 31), + origin_port="7f314ba0a498c36359b1c88781e94a73e19dcc9bbb030ec6b82f944a73d4da2f", + destination_port="68faf65af1345067f11dc6723b8da32f00e304a6f33c000118fccd81947deb4e", + vessel_class="oil_aframax_lr2", + product="crude", + unit="usd_per_tonne", + ) + + df = result.to_df() + assert len(df) > 0 + # pd.json_normalize uses dot notation for nested keys + assert "origin_port.id" in df.columns + assert "destination_port.id" in df.columns + assert "vessel_class" in df.columns + + def test_search_with_avoid_zone(self): + result = AnywhereFreightPricingGetPriceDetails().search( + time_min=datetime(2024, 1, 1), + time_max=datetime(2024, 1, 31), + origin_port="7f314ba0a498c36359b1c88781e94a73e19dcc9bbb030ec6b82f944a73d4da2f", + destination_port="68faf65af1345067f11dc6723b8da32f00e304a6f33c000118fccd81947deb4e", + vessel_class="oil_aframax_lr2", + product="crude", + unit="usd_per_tonne", + avoid_zone=["Suez Canal"], + ) + + result_list = result.to_list() + assert len(result_list) > 0 diff --git a/tests/endpoints/test_anywhere_freight_pricing_latest_update_timestamp.py b/tests/endpoints/test_anywhere_freight_pricing_latest_update_timestamp.py new file mode 100644 index 00000000..03fae85e --- /dev/null +++ b/tests/endpoints/test_anywhere_freight_pricing_latest_update_timestamp.py @@ -0,0 +1,8 @@ +from tests.testcases import TestCaseUsingRealAPI +from vortexasdk import AnywhereFreightPricingLatestUpdateTimestamp + + +class TestAnywhereFreightPricingLatestUpdateTimestamp(TestCaseUsingRealAPI): + def test_search_returns_timestamp(self): + result = AnywhereFreightPricingLatestUpdateTimestamp().search() + assert "timestamp" in result diff --git a/tests/endpoints/test_anywhere_freight_pricing_post_price_details.py b/tests/endpoints/test_anywhere_freight_pricing_post_price_details.py new file mode 100644 index 00000000..20423acf --- /dev/null +++ b/tests/endpoints/test_anywhere_freight_pricing_post_price_details.py @@ -0,0 +1,101 @@ +from datetime import datetime + +from tests.testcases import TestCaseUsingRealAPI +from vortexasdk import AnywhereFreightPricingPostPriceDetails + + +class TestAnywhereFreightPricingPostPriceDetails(TestCaseUsingRealAPI): + def test_search_returns_data(self): + routes = [ + { + "origin_port": "7f314ba0a498c36359b1c88781e94a73e19dcc9bbb030ec6b82f944a73d4da2f", + "destination_port": "68faf65af1345067f11dc6723b8da32f00e304a6f33c000118fccd81947deb4e", + "product": "crude", + "vessel_class": "oil_aframax_lr2", + } + ] + + result = AnywhereFreightPricingPostPriceDetails().search( + routes=routes, + time_min=datetime(2024, 1, 1), + time_max=datetime(2024, 1, 31), + unit="usd_per_tonne", + ) + + result_list = result.to_list() + assert len(result_list) > 0 + assert "origin_port" in result_list[0] + assert "destination_port" in result_list[0] + assert "vessel_class" in result_list[0] + assert "rates" in result_list[0] + + def test_search_to_df(self): + routes = [ + { + "origin_port": "7f314ba0a498c36359b1c88781e94a73e19dcc9bbb030ec6b82f944a73d4da2f", + "destination_port": "68faf65af1345067f11dc6723b8da32f00e304a6f33c000118fccd81947deb4e", + "product": "crude", + "vessel_class": "oil_aframax_lr2", + } + ] + + result = AnywhereFreightPricingPostPriceDetails().search( + routes=routes, + time_min=datetime(2024, 1, 1), + time_max=datetime(2024, 1, 31), + unit="usd_per_tonne", + ) + + df = result.to_df() + assert len(df) > 0 + # pd.json_normalize uses dot notation for nested keys + assert "origin_port.id" in df.columns + assert "destination_port.id" in df.columns + assert "vessel_class" in df.columns + + def test_search_multiple_routes(self): + routes = [ + { + "origin_port": "7f314ba0a498c36359b1c88781e94a73e19dcc9bbb030ec6b82f944a73d4da2f", + "destination_port": "68faf65af1345067f11dc6723b8da32f00e304a6f33c000118fccd81947deb4e", + "product": "crude", + "vessel_class": "oil_aframax_lr2", + }, + { + "origin_port": "68faf65af1345067f11dc6723b8da32f00e304a6f33c000118fccd81947deb4e", + "destination_port": "ea4921c8ad4fddb5fe3e7a4f834c1aa5863e43283c73da5f02d93bbc5dba72eb", + "product": "clean", + "vessel_class": "oil_handymax_mr2", + }, + ] + + result = AnywhereFreightPricingPostPriceDetails().search( + routes=routes, + time_min=datetime(2024, 1, 1), + time_max=datetime(2024, 1, 31), + unit="usd_per_tonne", + ) + + result_list = result.to_list() + assert len(result_list) >= 2 + + def test_search_with_avoid_zone(self): + routes = [ + { + "origin_port": "7f314ba0a498c36359b1c88781e94a73e19dcc9bbb030ec6b82f944a73d4da2f", + "destination_port": "68faf65af1345067f11dc6723b8da32f00e304a6f33c000118fccd81947deb4e", + "product": "crude", + "vessel_class": "oil_aframax_lr2", + "avoid_zone": ["Suez Canal"], + } + ] + + result = AnywhereFreightPricingPostPriceDetails().search( + routes=routes, + time_min=datetime(2024, 1, 1), + time_max=datetime(2024, 1, 31), + unit="usd_per_tonne", + ) + + result_list = result.to_list() + assert len(result_list) > 0 diff --git a/tests/endpoints/test_anywhere_freight_pricing_price_timeseries.py b/tests/endpoints/test_anywhere_freight_pricing_price_timeseries.py new file mode 100644 index 00000000..6355ac53 --- /dev/null +++ b/tests/endpoints/test_anywhere_freight_pricing_price_timeseries.py @@ -0,0 +1,55 @@ +from datetime import datetime + +from tests.testcases import TestCaseUsingRealAPI +from vortexasdk import AnywhereFreightPricingPriceTimeseries + + +class TestAnywhereFreightPricingPriceTimeseries(TestCaseUsingRealAPI): + def test_search_returns_data(self): + routes = [ + { + "origin_port": "68faf65af1345067f11dc6723b8da32f00e304a6f33c000118fccd81947deb4e", + "destination_port": "ea4921c8ad4fddb5fe3e7a4f834c1aa5863e43283c73da5f02d93bbc5dba72eb", + "product": "clean", + "vessel_class": "oil_handymax_mr2", + } + ] + + result = AnywhereFreightPricingPriceTimeseries().search( + routes=routes, + time_min=datetime(2026, 2, 20), + time_max=datetime(2026, 5, 20), + frequency="month", + unit="usd_per_tonne", + ) + + result_list = result.to_list() + assert len(result_list) > 0 + assert "origin_port" in result_list[0] + assert "destination_port" in result_list[0] + assert "prices" in result_list[0] + + def test_search_to_df(self): + routes = [ + { + "origin_port": "68faf65af1345067f11dc6723b8da32f00e304a6f33c000118fccd81947deb4e", + "destination_port": "ea4921c8ad4fddb5fe3e7a4f834c1aa5863e43283c73da5f02d93bbc5dba72eb", + "product": "clean", + "vessel_class": "oil_handymax_mr2", + } + ] + + result = AnywhereFreightPricingPriceTimeseries().search( + routes=routes, + time_min=datetime(2026, 2, 20), + time_max=datetime(2026, 5, 20), + frequency="month", + unit="usd_per_tonne", + ) + + df = result.to_df() + assert len(df) > 0 + # Top-level keys preserved, nested arrays like prices stay as lists + assert "origin_port" in df.columns + assert "destination_port" in df.columns + assert "vessel_class" in df.columns diff --git a/tests/endpoints/test_anywhere_freight_pricing_top_ports_destination.py b/tests/endpoints/test_anywhere_freight_pricing_top_ports_destination.py new file mode 100644 index 00000000..2955f047 --- /dev/null +++ b/tests/endpoints/test_anywhere_freight_pricing_top_ports_destination.py @@ -0,0 +1,45 @@ +from tests.testcases import TestCaseUsingRealAPI +from vortexasdk import AnywhereFreightPricingTopPortsDestination + + +class TestAnywhereFreightPricingTopPortsDestination(TestCaseUsingRealAPI): + def test_search_returns_data(self): + result = AnywhereFreightPricingTopPortsDestination().search( + origin_id="7f314ba0a498c36359b1c88781e94a73e19dcc9bbb030ec6b82f944a73d4da2f", + vessel_class="oil_handymax_mr2", + product="clean", + unit="usd_per_tonne", + ) + + result_list = result.to_list() + assert len(result_list) > 0 + assert "geography" in result_list[0] + assert "price_details" in result_list[0] + assert "id" in result_list[0]["geography"] + assert "name" in result_list[0]["geography"] + + def test_search_to_df(self): + result = AnywhereFreightPricingTopPortsDestination().search( + origin_id="7f314ba0a498c36359b1c88781e94a73e19dcc9bbb030ec6b82f944a73d4da2f", + vessel_class="oil_handymax_mr2", + product="clean", + unit="usd_per_tonne", + ) + + df = result.to_df() + assert len(df) > 0 + # pd.json_normalize uses dot notation for nested keys + assert "geography.id" in df.columns + assert "geography.name" in df.columns + + def test_search_with_avoid_zone(self): + result = AnywhereFreightPricingTopPortsDestination().search( + origin_id="7f314ba0a498c36359b1c88781e94a73e19dcc9bbb030ec6b82f944a73d4da2f", + vessel_class="oil_handymax_mr2", + product="clean", + unit="usd_per_tonne", + avoid_zone=["Suez Canal"], + ) + + result_list = result.to_list() + assert len(result_list) > 0 diff --git a/tests/endpoints/test_anywhere_freight_pricing_top_ports_origin.py b/tests/endpoints/test_anywhere_freight_pricing_top_ports_origin.py new file mode 100644 index 00000000..7e698ab1 --- /dev/null +++ b/tests/endpoints/test_anywhere_freight_pricing_top_ports_origin.py @@ -0,0 +1,45 @@ +from tests.testcases import TestCaseUsingRealAPI +from vortexasdk import AnywhereFreightPricingTopPortsOrigin + + +class TestAnywhereFreightPricingTopPortsOrigin(TestCaseUsingRealAPI): + def test_search_returns_data(self): + result = AnywhereFreightPricingTopPortsOrigin().search( + destination_id="68faf65af1345067f11dc6723b8da32f00e304a6f33c000118fccd81947deb4e", + vessel_class="oil_handymax_mr2", + product="clean", + unit="usd_per_tonne", + ) + + result_list = result.to_list() + assert len(result_list) > 0 + assert "geography" in result_list[0] + assert "price_details" in result_list[0] + assert "id" in result_list[0]["geography"] + assert "name" in result_list[0]["geography"] + + def test_search_to_df(self): + result = AnywhereFreightPricingTopPortsOrigin().search( + destination_id="68faf65af1345067f11dc6723b8da32f00e304a6f33c000118fccd81947deb4e", + vessel_class="oil_handymax_mr2", + product="clean", + unit="usd_per_tonne", + ) + + df = result.to_df() + assert len(df) > 0 + # pd.json_normalize uses dot notation for nested keys + assert "geography.id" in df.columns + assert "geography.name" in df.columns + + def test_search_with_avoid_zone(self): + result = AnywhereFreightPricingTopPortsOrigin().search( + destination_id="68faf65af1345067f11dc6723b8da32f00e304a6f33c000118fccd81947deb4e", + vessel_class="oil_handymax_mr2", + product="clean", + unit="usd_per_tonne", + avoid_zone=["Suez Canal"], + ) + + result_list = result.to_list() + assert len(result_list) > 0 diff --git a/tests/endpoints/test_anywhere_freight_pricing_vessel_classes_details.py b/tests/endpoints/test_anywhere_freight_pricing_vessel_classes_details.py new file mode 100644 index 00000000..4e4dbc8e --- /dev/null +++ b/tests/endpoints/test_anywhere_freight_pricing_vessel_classes_details.py @@ -0,0 +1,24 @@ +from tests.testcases import TestCaseUsingRealAPI +from vortexasdk import AnywhereFreightPricingVesselClassesDetails + + +class TestAnywhereFreightPricingVesselClassesDetails(TestCaseUsingRealAPI): + def test_search_returns_data(self): + result = AnywhereFreightPricingVesselClassesDetails().search() + + result_list = result.to_list() + assert len(result_list) > 0 + assert "name" in result_list[0] + assert "suggested_tonnage" in result_list[0] + assert "min_tonnage" in result_list[0] + assert "max_tonnage" in result_list[0] + + def test_search_to_df(self): + result = AnywhereFreightPricingVesselClassesDetails().search() + + df = result.to_df() + assert len(df) > 0 + assert "name" in df.columns + assert "suggested_tonnage" in df.columns + assert "min_tonnage" in df.columns + assert "max_tonnage" in df.columns diff --git a/vortexasdk/__init__.py b/vortexasdk/__init__.py index fc28ab34..076409e6 100644 --- a/vortexasdk/__init__.py +++ b/vortexasdk/__init__.py @@ -40,6 +40,13 @@ VesselSummary, VesselPositions, Refineries, + AnywhereFreightPricingLatestUpdateTimestamp, + AnywhereFreightPricingPriceTimeseries, + AnywhereFreightPricingGetPriceDetails, + AnywhereFreightPricingPostPriceDetails, + AnywhereFreightPricingTopPortsDestination, + AnywhereFreightPricingTopPortsOrigin, + AnywhereFreightPricingVesselClassesDetails, ) # noinspection PyUnresolvedReferences @@ -88,6 +95,13 @@ "VesselSummary", "VesselPositions", "Refineries", + "AnywhereFreightPricingLatestUpdateTimestamp", + "AnywhereFreightPricingPriceTimeseries", + "AnywhereFreightPricingGetPriceDetails", + "AnywhereFreightPricingPostPriceDetails", + "AnywhereFreightPricingTopPortsDestination", + "AnywhereFreightPricingTopPortsOrigin", + "AnywhereFreightPricingVesselClassesDetails", "__version__", "run_all_checks", ] diff --git a/vortexasdk/endpoints/__init__.py b/vortexasdk/endpoints/__init__.py index 8db54fc8..e35aaca1 100644 --- a/vortexasdk/endpoints/__init__.py +++ b/vortexasdk/endpoints/__init__.py @@ -68,6 +68,27 @@ from vortexasdk.endpoints.vessel_positions import VesselPositions from vortexasdk.endpoints.refineries import Refineries +from vortexasdk.endpoints.anywhere_freight_pricing_latest_update_timestamp import ( + AnywhereFreightPricingLatestUpdateTimestamp, +) +from vortexasdk.endpoints.anywhere_freight_pricing_price_timeseries import ( + AnywhereFreightPricingPriceTimeseries, +) +from vortexasdk.endpoints.anywhere_freight_pricing_get_price_details import ( + AnywhereFreightPricingGetPriceDetails, +) +from vortexasdk.endpoints.anywhere_freight_pricing_post_price_details import ( + AnywhereFreightPricingPostPriceDetails, +) +from vortexasdk.endpoints.anywhere_freight_pricing_top_ports_destination import ( + AnywhereFreightPricingTopPortsDestination, +) +from vortexasdk.endpoints.anywhere_freight_pricing_top_ports_origin import ( + AnywhereFreightPricingTopPortsOrigin, +) +from vortexasdk.endpoints.anywhere_freight_pricing_vessel_classes_details import ( + AnywhereFreightPricingVesselClassesDetails, +) # Explicitly list all exported classes, to help MyPy know what is available __all__ = [ @@ -109,4 +130,11 @@ "VesselSummary", "VesselPositions", "Refineries", + "AnywhereFreightPricingLatestUpdateTimestamp", + "AnywhereFreightPricingPriceTimeseries", + "AnywhereFreightPricingGetPriceDetails", + "AnywhereFreightPricingPostPriceDetails", + "AnywhereFreightPricingTopPortsDestination", + "AnywhereFreightPricingTopPortsOrigin", + "AnywhereFreightPricingVesselClassesDetails", ] diff --git a/vortexasdk/endpoints/anywhere_freight_pricing_get_price_details.py b/vortexasdk/endpoints/anywhere_freight_pricing_get_price_details.py new file mode 100644 index 00000000..b6cb12aa --- /dev/null +++ b/vortexasdk/endpoints/anywhere_freight_pricing_get_price_details.py @@ -0,0 +1,141 @@ +from datetime import datetime +from typing import Any, Dict, List, Optional +from urllib.parse import urlencode + +from vortexasdk.client import default_client, _handle_response +from vortexasdk.endpoints.endpoints import ( + ANYWHERE_FREIGHT_PRICING_PRICE_DETAILS, +) +from vortexasdk.endpoints.anywhere_freight_pricing_result import ( + AnywhereFreightPricingResult, +) +from vortexasdk.logger import get_logger +from vortexasdk.retry_session import retry_get +from vortexasdk.utils import to_date_string + +logger = get_logger(__name__) + + +class AnywhereFreightPricingGetPriceDetails: + """ + Anywhere Freight Pricing Get Price Details endpoint. + + Given a set of details about a single route (origin, destination, etc), + this will find rates, lumpsums and prediction confidence of the route. + + Please note: you will require a subscription to our Anywhere Freight Pricing + module to access this endpoint. + """ + + def __init__(self) -> None: + self._resource = ANYWHERE_FREIGHT_PRICING_PRICE_DETAILS + + def search( + self, + time_min: datetime, + time_max: datetime, + origin_port: str, + destination_port: str, + vessel_class: str, + product: str, + unit: str = "usd_per_tonne", + avoid_zone: Optional[List[str]] = None, + suggested_tonnage: Optional[float] = None, + ) -> AnywhereFreightPricingResult: + """ + List prices of a route. + + Given a set of details about a single route (origin, destination, etc), + this will find rates, lumpsums and prediction confidence of the route. + + # Arguments + + time_min: The UTC start date of the time filter. + + time_max: The UTC end date of the time filter. + + origin_port: Geographic ID of the origin port. + + destination_port: Geographic ID of the destination port. + + vessel_class: The vessel class for the route. Must be one of: + `'oil_coastal'`, `'oil_specialised'`, `'oil_handysize_mr1'`, + `'oil_handymax_mr2'`, `'oil_panamax_lr1'`, `'oil_aframax_lr2'`, + `'oil_suezmax_lr3'`, `'oil_vlcc'`. + + product: The product type. Must be one of: `'clean'`, `'dirty'`, `'crude'`. + + unit: The unit for pricing. Must be one of: `'usd_per_tonne'`, `'usd_per_barrel'`. + Defaults to `'usd_per_tonne'`. + + avoid_zone: Routing zones to avoid for this route. Options: + `'Panama Canal'`, `'Suez Canal'`. + + suggested_tonnage: Override the default suggested tonnage Vortexa will + suggest based on the vessel class. + + # Returns + `AnywhereFreightPricingResult` + + # Example + _Get price details for an Aframax LR2 crude route from Houston to Rotterdam._ + + ```python + >>> from vortexasdk import AnywhereFreightPricingGetPriceDetails + >>> from datetime import datetime + >>> result = AnywhereFreightPricingGetPriceDetails().search( + ... time_min=datetime(2024, 1, 1), + ... time_max=datetime(2024, 12, 31), + ... origin_port="7f314ba0a498c36359b1c88781e94a73e19dcc9bbb030ec6b82f944a73d4da2f", + ... destination_port="68faf65af1345067f11dc6723b8da32f00e304a6f33c000118fccd81947deb4e", + ... vessel_class="oil_aframax_lr2", + ... product="crude", + ... unit="usd_per_tonne", + ... ) + >>> df = result.to_df() + + ``` + + Returns a DataFrame with columns including rates, lumpsums, and confidence values: + + | | date | rate | lumpsum | confidence | + |---:|:-----------|------:|----------:|-----------:| + | 0 | 2024-01-01 | 12.50 | 1250000.0 | 0.85 | + | 1 | 2024-01-02 | 12.75 | 1275000.0 | 0.87 | + + """ + logger.info( + f"Fetching Anywhere Freight Pricing price details for route " + f"{origin_port} -> {destination_port}" + ) + + params: Dict[str, Any] = { + "time_min": to_date_string(time_min), + "time_max": to_date_string(time_max), + "origin_port": origin_port, + "destination_port": destination_port, + "vessel_class": vessel_class, + "product": product, + "unit": unit, + } + + if avoid_zone: + params["avoid_zone"] = avoid_zone + + if suggested_tonnage is not None: + params["suggested_tonnage"] = suggested_tonnage + + client = default_client() + url = client._create_url(self._resource) + + # Use doseq=True to handle list parameters (avoid_zone) + query_string = urlencode(params, doseq=True) + url = f"{url}&{query_string}" + + response = retry_get(url) + + data = _handle_response(response) + return AnywhereFreightPricingResult( + records=data.get("data", []), + reference=data.get("metadata", {}), + ) diff --git a/vortexasdk/endpoints/anywhere_freight_pricing_latest_update_timestamp.py b/vortexasdk/endpoints/anywhere_freight_pricing_latest_update_timestamp.py new file mode 100644 index 00000000..fc3375dd --- /dev/null +++ b/vortexasdk/endpoints/anywhere_freight_pricing_latest_update_timestamp.py @@ -0,0 +1,58 @@ +from typing import Dict + +from vortexasdk.client import default_client, _handle_response +from vortexasdk.endpoints.endpoints import ( + ANYWHERE_FREIGHT_PRICING_LATEST_UPDATE, +) +from vortexasdk.logger import get_logger +from vortexasdk.retry_session import retry_get + +logger = get_logger(__name__) + + +class AnywhereFreightPricingLatestUpdateTimestamp: + """ + Anywhere Freight Pricing Latest Update Timestamp endpoint. + + Please note: you will require a subscription to our Anywhere Freight Pricing + module to access this endpoint. + """ + + def __init__(self) -> None: + self._resource = ANYWHERE_FREIGHT_PRICING_LATEST_UPDATE + + def search(self) -> Dict[str, str]: + """ + Get the time the predicted route prices were last updated. + + # Returns + A dictionary containing a `timestamp` key with the ISO 8601 formatted + date-time string of when the pricing predictions were last updated. + + # Example + _Get the latest update timestamp for Anywhere Freight Pricing._ + + ```python + >>> from vortexasdk import AnywhereFreightPricingLatestUpdateTimestamp + >>> result = AnywhereFreightPricingLatestUpdateTimestamp().search() + + ``` + + Returns: + + ``` + { + 'timestamp': '2025-01-30T14:40:06.803Z' + } + ``` + + """ + logger.info( + "Fetching Anywhere Freight Pricing latest update timestamp" + ) + + client = default_client() + url = client._create_url(self._resource) + response = retry_get(url) + + return _handle_response(response) diff --git a/vortexasdk/endpoints/anywhere_freight_pricing_post_price_details.py b/vortexasdk/endpoints/anywhere_freight_pricing_post_price_details.py new file mode 100644 index 00000000..a478af55 --- /dev/null +++ b/vortexasdk/endpoints/anywhere_freight_pricing_post_price_details.py @@ -0,0 +1,117 @@ +from datetime import datetime +from typing import Any, Dict, List + +from vortexasdk.endpoints.endpoints import ( + ANYWHERE_FREIGHT_PRICING_PRICE_DETAILS, +) +from vortexasdk.endpoints.anywhere_freight_pricing_result import ( + AnywhereFreightPricingResult, +) +from vortexasdk.logger import get_logger +from vortexasdk.operations import Search +from vortexasdk.utils import to_date_string + +logger = get_logger(__name__) + + +class AnywhereFreightPricingPostPriceDetails(Search): + """ + Anywhere Freight Pricing Post Price Details endpoint. + + Given a set of details about multiple routes (origin, destination, etc), + this will find rates, lumpsums and prediction confidence for each route. + + Please note: you will require a subscription to our Anywhere Freight Pricing + module to access this endpoint. + """ + + def __init__(self) -> None: + Search.__init__(self, ANYWHERE_FREIGHT_PRICING_PRICE_DETAILS) + + def search( + self, + routes: List[Dict[str, Any]], + time_min: datetime, + time_max: datetime, + unit: str = "usd_per_tonne", + ) -> AnywhereFreightPricingResult: + """ + List prices for multiple routes. + + Given a set of details about multiple routes (origin, destination, etc), + this will find rates, lumpsums and prediction confidence for each route. + + # Arguments + + routes: A list of route dictionaries. Each route must contain: + - `origin_port` (str, required): Geographical ID of the origin port. + - `destination_port` (str, required): Geographical ID of the destination port. + - `product` (str, required): One of `'clean'`, `'dirty'`, `'crude'`. + - `vessel_class` (str, required): One of `'oil_coastal'`, `'oil_specialised'`, + `'oil_handysize_mr1'`, `'oil_handymax_mr2'`, `'oil_panamax_lr1'`, + `'oil_aframax_lr2'`, `'oil_suezmax_lr3'`, `'oil_vlcc'`. + - `avoid_zone` (list, optional): Routing zones to avoid. Options: + `'Panama Canal'`, `'Suez Canal'`. + - `suggested_tonnage` (float, optional): Suggested tonnage for the route. + + time_min: The UTC start date of the time filter. + + time_max: The UTC end date of the time filter. + + unit: The unit for pricing. Must be one of: `'usd_per_tonne'`, `'usd_per_barrel'`. + + # Returns + `AnywhereFreightPricingResult` + + # Example + _Get price details for multiple routes._ + + ```python + >>> from vortexasdk import AnywhereFreightPricingPostPriceDetails + >>> from datetime import datetime + >>> routes = [ + ... { + ... "origin_port": "7f314ba0a498c36359b1c88781e94a73e19dcc9bbb030ec6b82f944a73d4da2f", + ... "destination_port": "68faf65af1345067f11dc6723b8da32f00e304a6f33c000118fccd81947deb4e", + ... "product": "crude", + ... "vessel_class": "oil_aframax_lr2", + ... }, + ... { + ... "origin_port": "68faf65af1345067f11dc6723b8da32f00e304a6f33c000118fccd81947deb4e", + ... "destination_port": "ea4921c8ad4fddb5fe3e7a4f834c1aa5863e43283c73da5f02d93bbc5dba72eb", + ... "product": "clean", + ... "vessel_class": "oil_handymax_mr2", + ... } + ... ] + >>> result = AnywhereFreightPricingPostPriceDetails().search( + ... routes=routes, + ... time_min=datetime(2024, 1, 1), + ... time_max=datetime(2024, 1, 31), + ... unit="usd_per_tonne", + ... ) + >>> df = result.to_df() + + ``` + + Returns a DataFrame with columns including rates, lumpsums, and confidence values: + + | | date | rate | lumpsum | confidence | + |---:|:-----------|------:|----------:|-----------:| + | 0 | 2024-01-01 | 12.50 | 1250000.0 | 0.85 | + | 1 | 2024-01-02 | 12.75 | 1275000.0 | 0.87 | + + """ + api_params: Dict[str, Any] = { + "routes": routes, + "time_min": to_date_string(time_min), + "time_max": to_date_string(time_max), + "unit": unit, + } + + response = super().search_with_client( + response_type="breakdown", **api_params + ) + + return AnywhereFreightPricingResult( + records=response["data"], reference=response.get("reference", {}) + ) diff --git a/vortexasdk/endpoints/anywhere_freight_pricing_price_timeseries.py b/vortexasdk/endpoints/anywhere_freight_pricing_price_timeseries.py new file mode 100644 index 00000000..4c88f6b7 --- /dev/null +++ b/vortexasdk/endpoints/anywhere_freight_pricing_price_timeseries.py @@ -0,0 +1,115 @@ +from datetime import datetime +from typing import Any, Dict, List + +from vortexasdk.endpoints.endpoints import ( + ANYWHERE_FREIGHT_PRICING_PRICE_TIMESERIES, +) +from vortexasdk.endpoints.anywhere_freight_pricing_result import ( + AnywhereFreightPricingResult, +) +from vortexasdk.logger import get_logger +from vortexasdk.operations import Search +from vortexasdk.utils import to_date_string + +logger = get_logger(__name__) + + +class AnywhereFreightPricingPriceTimeseries(Search): + """ + Anywhere Freight Pricing Price Timeseries endpoint. + + Please note: you will require a subscription to our Anywhere Freight Pricing + module to access this endpoint. + """ + + def __init__(self) -> None: + Search.__init__(self, ANYWHERE_FREIGHT_PRICING_PRICE_TIMESERIES) + + def search( + self, + routes: List[Dict[str, Any]], + time_min: datetime, + time_max: datetime, + frequency: str = "month", + unit: str = "usd_per_tonne", + ) -> AnywhereFreightPricingResult: + """ + Get historical pricing over time for multiple routes. + + Given a set of details about multiple routes (origin, destination, etc), + a time period and frequency, this returns historical pricing over time + bucketed by the chosen frequency. + + # Arguments + + routes: A list of route dictionaries. Each route must contain: + - `origin_port` (str, required): Geographical ID of the origin port. + - `destination_port` (str, required): Geographical ID of the destination port. + - `product` (str, required): One of `'clean'`, `'dirty'`, `'crude'`. + - `vessel_class` (str, required): One of `'oil_coastal'`, `'oil_specialised'`, + `'oil_handysize_mr1'`, `'oil_handymax_mr2'`, `'oil_panamax_lr1'`, + `'oil_aframax_lr2'`, `'oil_suezmax_lr3'`, `'oil_vlcc'`. + - `avoid_zone` (list, optional): Routing zones to avoid. Options: + `'Panama Canal'`, `'Suez Canal'`. + - `suggested_tonnage` (float, optional): Suggested tonnage for the route. + + time_min: The UTC start date of the time filter. + + time_max: The UTC end date of the time filter. + + frequency: Frequency denoting the granularity of the time series. + Must be one of: `'day'`, `'week'`, `'doe_week'`, `'month'`, `'quarter'`, `'year'`. + + unit: The unit for pricing. Must be one of: `'usd_per_tonne'`, `'usd_per_barrel'`. + + # Returns + `AnywhereFreightPricingResult` + + # Example + _Get daily pricing for a Handymax MR2 clean route from Rotterdam to New York._ + + ```python + >>> from vortexasdk import AnywhereFreightPricingPriceTimeseries + >>> from datetime import datetime + >>> routes = [ + ... { + ... "origin_port": "68faf65af1345067f11dc6723b8da32f00e304a6f33c000118fccd81947deb4e", + ... "destination_port": "ea4921c8ad4fddb5fe3e7a4f834c1aa5863e43283c73da5f02d93bbc5dba72eb", + ... "product": "clean", + ... "vessel_class": "oil_handymax_mr2", + ... } + ... ] + >>> result = AnywhereFreightPricingPriceTimeseries().search( + ... routes=routes, + ... time_min=datetime(2026, 2, 20), + ... time_max=datetime(2026, 5, 20), + ... frequency="day", + ... unit="usd_per_tonne", + ... ) + >>> df = result.to_df() + + ``` + + Returns a DataFrame with columns: + + | | origin_port | destination_port | vessel_class | product | date | price | price_lower | price_upper | voyage_price | + |---:|:------------|:-----------------|:---------------------|:--------|:-----------|------:|------------:|------------:|-------------- :| + | 0 | 68faf65a... | ea4921c8... | oil_handymax_mr2 | clean | 2026-02-20 | 15.50 | 14.00 | 17.00 | 16.223888 | + | 1 | 68faf65a... | ea4921c8... | oil_handymax_mr2 | clean | 2026-02-21 | 16.20 | 14.80 | 17.60 | 16.223888 | + + """ + api_params: Dict[str, Any] = { + "routes": routes, + "time_min": to_date_string(time_min), + "time_max": to_date_string(time_max), + "frequency": frequency, + "unit": unit, + } + + response = super().search_with_client( + response_type="breakdown", **api_params + ) + + return AnywhereFreightPricingResult( + records=response["data"], reference=response.get("reference", {}) + ) diff --git a/vortexasdk/endpoints/anywhere_freight_pricing_result.py b/vortexasdk/endpoints/anywhere_freight_pricing_result.py new file mode 100644 index 00000000..0771b6e5 --- /dev/null +++ b/vortexasdk/endpoints/anywhere_freight_pricing_result.py @@ -0,0 +1,30 @@ +from typing import Any, Dict, List + +import pandas as pd +from pydantic import BaseModel, Field + + +class AnywhereFreightPricingResult(BaseModel): + """Container class that holds results from Anywhere Freight Pricing endpoints.""" + + records: List = Field(default_factory=list) + reference: Dict[str, Any] = Field(default_factory=dict) + + def to_list(self) -> List[Dict[str, Any]]: + """ + Represent the results as a list of records. + + Returns the raw API response data. + """ + return self.records + + def to_df(self) -> pd.DataFrame: + """ + Represent the results as a DataFrame. + + Uses pd.json_normalize to flatten nested structures. + """ + if not self.records: + return pd.DataFrame() + + return pd.json_normalize(self.records) diff --git a/vortexasdk/endpoints/anywhere_freight_pricing_top_ports_destination.py b/vortexasdk/endpoints/anywhere_freight_pricing_top_ports_destination.py new file mode 100644 index 00000000..be0a0522 --- /dev/null +++ b/vortexasdk/endpoints/anywhere_freight_pricing_top_ports_destination.py @@ -0,0 +1,109 @@ +from typing import Any, Dict, List, Optional + +from vortexasdk.client import default_client, _handle_response +from vortexasdk.endpoints.endpoints import ( + ANYWHERE_FREIGHT_PRICING_TOP_PORTS_DESTINATION, +) +from vortexasdk.endpoints.anywhere_freight_pricing_result import ( + AnywhereFreightPricingResult, +) +from vortexasdk.logger import get_logger +from vortexasdk.retry_session import retry_post + +logger = get_logger(__name__) + + +class AnywhereFreightPricingTopPortsDestination: + """ + Anywhere Freight Pricing Top Ports Destination endpoint. + + List top destination ports. A top destination port refers to the port + with the greatest volume of incoming voyages from vessels in a specified class. + + Please note: you will require a subscription to our Anywhere Freight Pricing + module to access this endpoint. + """ + + def __init__(self) -> None: + self._resource = ANYWHERE_FREIGHT_PRICING_TOP_PORTS_DESTINATION + + def search( + self, + origin_id: str, + vessel_class: str, + product: str, + unit: str = "usd_per_tonne", + avoid_zone: Optional[List[str]] = None, + ) -> AnywhereFreightPricingResult: + """ + List top destination ports from a given origin. + + A top destination port refers to the port with the greatest volume of + incoming voyages from vessels in a specified class. + + # Arguments + + origin_id: Geographical ID of the origin port. + + vessel_class: The vessel class for the route. Must be one of: + `'oil_coastal'`, `'oil_specialised'`, `'oil_handysize_mr1'`, + `'oil_handymax_mr2'`, `'oil_panamax_lr1'`, `'oil_aframax_lr2'`, + `'oil_suezmax_lr3'`, `'oil_vlcc'`. + + product: The product type. Must be one of: `'clean'`, `'dirty'`, `'crude'`. + + unit: The unit for pricing. Must be one of: `'usd_per_tonne'`, `'usd_per_barrel'`. + Defaults to `'usd_per_tonne'`. + + avoid_zone: Routing zones to avoid. Options: `'Panama Canal'`, `'Suez Canal'`. + + # Returns + `AnywhereFreightPricingResult` + + # Example + _Get top destination ports for clean products from Houston using MR2 vessels._ + + ```python + >>> from vortexasdk import AnywhereFreightPricingTopPortsDestination + >>> result = AnywhereFreightPricingTopPortsDestination().search( + ... origin_id="7f314ba0a498c36359b1c88781e94a73e19dcc9bbb030ec6b82f944a73d4da2f", + ... vessel_class="oil_handymax_mr2", + ... product="clean", + ... unit="usd_per_tonne", + ... ) + >>> df = result.to_df() + + ``` + + Returns a DataFrame with columns including geography info, rates, lumpsums, + and confidence values: + + | | geography_name | date | rate | lumpsum | confidence | + |---:|:---------------|:-----------|------:|------------:|-----------:| + | 0 | Callao [PE] | 2024-01-01 | 63.55 | 2351511.83 | 2 | + + """ + logger.info( + f"Fetching Anywhere Freight Pricing top destination ports for origin {origin_id}" + ) + + payload: Dict[str, Any] = { + "origin_id": origin_id, + "vessel_class": vessel_class, + "product": product, + "unit": unit, + } + + if avoid_zone is not None: + payload["avoid_zone"] = avoid_zone + + client = default_client() + url = client._create_url(self._resource) + + response = retry_post(url, json=payload) + + data = _handle_response(response) + return AnywhereFreightPricingResult( + records=data.get("data", []), + reference=data.get("metadata", {}), + ) diff --git a/vortexasdk/endpoints/anywhere_freight_pricing_top_ports_origin.py b/vortexasdk/endpoints/anywhere_freight_pricing_top_ports_origin.py new file mode 100644 index 00000000..da7b12e0 --- /dev/null +++ b/vortexasdk/endpoints/anywhere_freight_pricing_top_ports_origin.py @@ -0,0 +1,109 @@ +from typing import Any, Dict, List, Optional + +from vortexasdk.client import default_client, _handle_response +from vortexasdk.endpoints.endpoints import ( + ANYWHERE_FREIGHT_PRICING_TOP_PORTS_ORIGIN, +) +from vortexasdk.endpoints.anywhere_freight_pricing_result import ( + AnywhereFreightPricingResult, +) +from vortexasdk.logger import get_logger +from vortexasdk.retry_session import retry_post + +logger = get_logger(__name__) + + +class AnywhereFreightPricingTopPortsOrigin: + """ + Anywhere Freight Pricing Top Ports Origin endpoint. + + List top origin ports. A top origin port refers to the port + with the greatest volume of outgoing voyages from vessels in a specified class. + + Please note: you will require a subscription to our Anywhere Freight Pricing + module to access this endpoint. + """ + + def __init__(self) -> None: + self._resource = ANYWHERE_FREIGHT_PRICING_TOP_PORTS_ORIGIN + + def search( + self, + destination_id: str, + vessel_class: str, + product: str, + unit: str = "usd_per_tonne", + avoid_zone: Optional[List[str]] = None, + ) -> AnywhereFreightPricingResult: + """ + List top origin ports for a given destination. + + A top origin port refers to the port with the greatest volume of + outgoing voyages from vessels in a specified class. + + # Arguments + + destination_id: Geographical ID of the destination port. + + vessel_class: The vessel class for the route. Must be one of: + `'oil_coastal'`, `'oil_specialised'`, `'oil_handysize_mr1'`, + `'oil_handymax_mr2'`, `'oil_panamax_lr1'`, `'oil_aframax_lr2'`, + `'oil_suezmax_lr3'`, `'oil_vlcc'`. + + product: The product type. Must be one of: `'clean'`, `'dirty'`, `'crude'`. + + unit: The unit for pricing. Must be one of: `'usd_per_tonne'`, `'usd_per_barrel'`. + Defaults to `'usd_per_tonne'`. + + avoid_zone: Routing zones to avoid. Options: `'Panama Canal'`, `'Suez Canal'`. + + # Returns + `AnywhereFreightPricingResult` + + # Example + _Get top origin ports for clean products to Rotterdam using MR2 vessels._ + + ```python + >>> from vortexasdk import AnywhereFreightPricingTopPortsOrigin + >>> result = AnywhereFreightPricingTopPortsOrigin().search( + ... destination_id="68faf65af1345067f11dc6723b8da32f00e304a6f33c000118fccd81947deb4e", + ... vessel_class="oil_handymax_mr2", + ... product="clean", + ... unit="usd_per_tonne", + ... ) + >>> df = result.to_df() + + ``` + + Returns a DataFrame with columns including geography info, rates, lumpsums, + and confidence values: + + | | geography_name | date | rate | lumpsum | confidence | + |---:|:---------------|:-----------|------:|------------:|-----------:| + | 0 | Houston [US] | 2024-01-01 | 63.55 | 2351511.83 | 2 | + + """ + logger.info( + f"Fetching Anywhere Freight Pricing top origin ports for destination {destination_id}" + ) + + payload: Dict[str, Any] = { + "destination_id": destination_id, + "vessel_class": vessel_class, + "product": product, + "unit": unit, + } + + if avoid_zone is not None: + payload["avoid_zone"] = avoid_zone + + client = default_client() + url = client._create_url(self._resource) + + response = retry_post(url, json=payload) + + data = _handle_response(response) + return AnywhereFreightPricingResult( + records=data.get("data", []), + reference=data.get("metadata", {}), + ) diff --git a/vortexasdk/endpoints/anywhere_freight_pricing_vessel_classes_details.py b/vortexasdk/endpoints/anywhere_freight_pricing_vessel_classes_details.py new file mode 100644 index 00000000..b74ed587 --- /dev/null +++ b/vortexasdk/endpoints/anywhere_freight_pricing_vessel_classes_details.py @@ -0,0 +1,68 @@ +from vortexasdk.client import default_client, _handle_response +from vortexasdk.endpoints.endpoints import ( + ANYWHERE_FREIGHT_PRICING_VESSEL_CLASSES_DETAILS, +) +from vortexasdk.endpoints.anywhere_freight_pricing_result import ( + AnywhereFreightPricingResult, +) +from vortexasdk.logger import get_logger +from vortexasdk.retry_session import retry_get + +logger = get_logger(__name__) + + +class AnywhereFreightPricingVesselClassesDetails: + """ + Anywhere Freight Pricing Vessel Classes Details endpoint. + + Lists all the vessel classes supported for Anywhere Freight Pricing + and the tonnages they can carry. + + Please note: you will require a subscription to our Anywhere Freight Pricing + module to access this endpoint. + """ + + def __init__(self) -> None: + self._resource = ANYWHERE_FREIGHT_PRICING_VESSEL_CLASSES_DETAILS + + def search(self) -> AnywhereFreightPricingResult: + """ + List vessel classes with tonnages. + + Lists all the vessel classes supported for Anywhere Freight Pricing + and the tonnages they can carry. + + # Returns + `AnywhereFreightPricingResult` + + # Example + _Get all supported vessel classes and their tonnage ranges._ + + ```python + >>> from vortexasdk import AnywhereFreightPricingVesselClassesDetails + >>> result = AnywhereFreightPricingVesselClassesDetails().search() + >>> df = result.to_df() + + ``` + + Returns a DataFrame with vessel class details: + + | | name | suggested_tonnage | min_tonnage | max_tonnage | + |---:|:------------------|------------------:|------------:|------------:| + | 0 | oil_handymax_mr2 | 37000.0 | 18000.0 | 54000.0 | + | 1 | oil_panamax_lr1 | 60000.0 | 30000.0 | 79000.0 | + | 2 | oil_aframax_lr2 | 85000.0 | 42000.0 | 119000.0 | + + """ + logger.info("Fetching Anywhere Freight Pricing vessel classes details") + + client = default_client() + url = client._create_url(self._resource) + + response = retry_get(url) + + data = _handle_response(response) + return AnywhereFreightPricingResult( + records=data.get("data", []), + reference=data.get("metadata", {}), + ) diff --git a/vortexasdk/endpoints/endpoints.py b/vortexasdk/endpoints/endpoints.py index 3a60a1c4..8d125c7b 100644 --- a/vortexasdk/endpoints/endpoints.py +++ b/vortexasdk/endpoints/endpoints.py @@ -33,6 +33,25 @@ FREIGHT_PRICING_SEARCH = "/v5/freight-outlook/rates" FREIGHT_PRICING_TIMESERIES = "/v5/freight-outlook/timeseries" +ANYWHERE_FREIGHT_PRICING_LATEST_UPDATE = ( + "/v5/anywhere-freight-pricing/latest-update-timestamp" +) +ANYWHERE_FREIGHT_PRICING_PRICE_TIMESERIES = ( + "/v5/anywhere-freight-pricing/price-timeseries" +) +ANYWHERE_FREIGHT_PRICING_PRICE_DETAILS = ( + "/v5/anywhere-freight-pricing/price/details" +) +ANYWHERE_FREIGHT_PRICING_TOP_PORTS_DESTINATION = ( + "/v5/anywhere-freight-pricing/top-ports/destination" +) +ANYWHERE_FREIGHT_PRICING_TOP_PORTS_ORIGIN = ( + "/v5/anywhere-freight-pricing/top-ports/origin" +) +ANYWHERE_FREIGHT_PRICING_VESSEL_CLASSES_DETAILS = ( + "/v5/anywhere-freight-pricing/vessel-classes-details" +) + VOYAGES_SEARCH_ENRICHED = "/v5/voyages/search-enriched" VOYAGES_TOP_HITS = "/v5/voyages/top-hits" VOYAGES_CONGESTION_BREAKDOWN = "/v5/voyages/congestion-breakdown" diff --git a/vortexasdk/utils.py b/vortexasdk/utils.py index 1bc00e3a..9b1c57e4 100644 --- a/vortexasdk/utils.py +++ b/vortexasdk/utils.py @@ -93,6 +93,11 @@ def filter_empty_values(data: Dict) -> Dict: } +def to_date_string(dt: datetime) -> str: + """Convert datetime to YYYY-MM-DD date string as required by AFP API.""" + return dt.strftime("%Y-%m-%d") + + def sts_param_value(param: Optional[bool]) -> Dict[str, bool]: """ If sts filter is True, apply cross filter.