From a00fa2434a43703eb72a1d87c6ebdcf62ea50308 Mon Sep 17 00:00:00 2001 From: Hansen Fan Date: Mon, 13 Jul 2026 13:55:16 +0100 Subject: [PATCH 1/7] fix: populating fixtures data --- test_fixtures_bug.py | 58 +++++++++++++++++++ vortexasdk/api/entity_flattening.py | 22 ++++++++ vortexasdk/api/fixture.py | 2 +- vortexasdk/endpoints/fixtures.py | 2 +- vortexasdk/endpoints/fixtures_result.py | 74 ++++++++----------------- 5 files changed, 104 insertions(+), 54 deletions(-) create mode 100644 test_fixtures_bug.py diff --git a/test_fixtures_bug.py b/test_fixtures_bug.py new file mode 100644 index 000000000..1835a6e59 --- /dev/null +++ b/test_fixtures_bug.py @@ -0,0 +1,58 @@ +from dotenv import load_dotenv +load_dotenv() + +from datetime import datetime +from vortexasdk import Fixtures +from vortexasdk.api.entity_flattening import convert_to_flat_dict, convert_fixture_to_flat_dict + +if __name__ == '__main__': + print("Fetching fixtures from API...") + result = Fixtures().search( + filter_time_min=datetime(2024, 1, 1), + filter_time_max=datetime(2024, 1, 7), + ) + raw_records = result.records + print(f"Got {len(raw_records)} fixtures") + + # Find a record with all three corporate entity layers + record = None + for r in raw_records: + ce = r.get("vessel", {}).get("corporate_entities", []) + layers = {e.get("layer") for e in ce} + if {"charterer", "effective_controller", "time_charterer"} <= layers: + record = r + break + + if not record: + # Fallback: find one with at least 2 + for r in raw_records: + ce = r.get("vessel", {}).get("corporate_entities", []) + if len(ce) > 1: + record = r + break + + if not record: + record = raw_records[0] + + ce = record.get("vessel", {}).get("corporate_entities", []) + layers = [e.get("layer") for e in ce] + print(f"Using fixture: {record['vessel']['name']} (layers: {layers})\n") + + expected = [ + "vessel.name", + "vessel.imo", + "tonnes", + "vessel.corporate_entities.charterer.label", + "vessel.corporate_entities.effective_controller.label", + "vessel.corporate_entities.time_charterer.label", + ] + + print("BEFORE FIX (convert_to_flat_dict):") + flat_before = convert_to_flat_dict(record, columns="all") + for col in expected: + print(f" {'✅' if col in flat_before else '❌'} {col} = {flat_before.get(col, 'MISSING')}") + + print("\nAFTER FIX (convert_fixture_to_flat_dict):") + flat_after = convert_fixture_to_flat_dict(record, columns="all") + for col in expected: + print(f" {'✅' if col in flat_after else '❌'} {col} = {flat_after.get(col, 'MISSING')}") diff --git a/vortexasdk/api/entity_flattening.py b/vortexasdk/api/entity_flattening.py index aa46fedc6..0cf9441d7 100644 --- a/vortexasdk/api/entity_flattening.py +++ b/vortexasdk/api/entity_flattening.py @@ -84,5 +84,27 @@ def _flatten_attributes(dictionary: Dict, key: str) -> Dict: return copied_dict +def convert_fixture_to_flat_dict( + fixture: Dict, columns: Union[Literal["all"], List[str]] = "all" +) -> Dict: + """Convert nested `Fixture` object to flat dictionary, keeping *cols*.""" + as_dict = _group_fixture_attributes_by_layer(fixture) + + formatted = flatten_dictionary(as_dict) + + if columns == "all": + return formatted + else: + return {k: v for k, v in formatted.items() if k in columns} + + +def _group_fixture_attributes_by_layer(fixture: Dict) -> Dict: + """Group relevant `Fixture` attributes by `Entity.layer`.""" + copied = copy.deepcopy(fixture) + if "vessel" in copied and isinstance(copied["vessel"], dict): + copied["vessel"] = _flatten_vessel_entity(copied["vessel"]) + return copied + + def _group_by_layer(entity_list: List[Dict]) -> Dict: return {e["layer"]: e for e in entity_list} diff --git a/vortexasdk/api/fixture.py b/vortexasdk/api/fixture.py index b201d28c7..10946d0e8 100644 --- a/vortexasdk/api/fixture.py +++ b/vortexasdk/api/fixture.py @@ -16,7 +16,7 @@ class Fixture(BaseModel): vessel: Optional[VesselEntity] = None laycan_from: Optional[str] = None laycan_to: Optional[str] = None - tones: Optional[int] = None + tonnes: Optional[int] = None fixing_timestamp: Optional[str] = None vtx_fulfilled: Optional[bool] = None origin: Optional[Entity] = None diff --git a/vortexasdk/endpoints/fixtures.py b/vortexasdk/endpoints/fixtures.py index e2d6f33f2..98a87ecf9 100644 --- a/vortexasdk/endpoints/fixtures.py +++ b/vortexasdk/endpoints/fixtures.py @@ -161,7 +161,7 @@ def search( returns - | vessel.name | tones | origin.label | product.label | + | vessel.name | tonnes | origin.label | product.label | | ------------------------ | ---------------- | ------------ | ------------- | | ALPINE EAGLE | 454.96048964485 | UK | Crude | diff --git a/vortexasdk/endpoints/fixtures_result.py b/vortexasdk/endpoints/fixtures_result.py index ce8f14bc3..90effbb44 100644 --- a/vortexasdk/endpoints/fixtures_result.py +++ b/vortexasdk/endpoints/fixtures_result.py @@ -7,7 +7,7 @@ import pandas as pd from vortexasdk.api.fixture import Fixture -from vortexasdk.api.entity_flattening import convert_to_flat_dict +from vortexasdk.api.entity_flattening import convert_fixture_to_flat_dict from vortexasdk.api.search_result import Result from vortexasdk.logger import get_logger from vortexasdk.result_conversions import create_dataframe, create_list @@ -20,7 +20,7 @@ "vessel.name", "laycan_from", "laycan_to", - "tones", + "tonnes", "fixing_timestamp", "vtx_fulfilled", "destination.label", @@ -47,69 +47,39 @@ def to_df( # Arguments columns: The Fixtures columns we want in the dataframe. - Defaults to `columns = [ - "id", - 'vessels.corporate_entities.charterer.id', - 'vessels.corporate_entities.charterer.label', - 'vessels.corporate_entities.charterer.layer', - 'vessels.corporate_entities.charterer.probability', - 'vessels.corporate_entities.charterer.source', - 'vessels.corporate_entities.effective_controller.id', - 'vessels.corporate_entities.effective_controller.label', - 'vessels.corporate_entities.effective_controller.layer', - 'vessels.corporate_entities.effective_controller.probability', - 'vessels.corporate_entities.effective_controller.source', - 'vessels.corporate_entities.time_charterer.end_timestamp', - 'vessels.corporate_entities.time_charterer.id', - 'vessels.corporate_entities.time_charterer.label', - 'vessels.corporate_entities.time_charterer.layer', - 'vessels.corporate_entities.time_charterer.probability', - 'vessels.corporate_entities.time_charterer.source', - 'vessels.corporate_entities.time_charterer.start_timestamp', - 'vessels.cubic_capacity', - 'vessels.dwt', - 'vessels.end_timestamp', - 'vessels.id', - 'vessels.imo', - 'vessels.mmsi', - 'vessels.name', - 'vessels.start_timestamp', - 'vessels.status', - 'vessels.tags.end_timestamp', - 'vessels.tags.start_timestamp', - 'vessels.tags.tag', - 'vessels.vessel_class', - 'vessels.voyage_id', - "laycan_from", - "laycan_to", - "tones", - "fixing_timestamp", - "vtx_fulfilled", - "destination.label", - "destination.id", - "origin.label", - "origin.id", - "product.label", - "product.id", - "charterer.label", - "charterer.id", - ]`. + Pass `columns="all"` to get all available columns. - A near complete list of columns is given below + A near complete list of available columns: ```python [ "id", "vessel.id", "vessel.name", + "vessel.imo", + "vessel.mmsi", + "vessel.dwt", + "vessel.cubic_capacity", + "vessel.vessel_class", + "vessel.year", + "vessel.corporate_entities.charterer.label", + "vessel.corporate_entities.charterer.id", + "vessel.corporate_entities.effective_controller.label", + "vessel.corporate_entities.effective_controller.id", + "vessel.corporate_entities.time_charterer.label", + "vessel.corporate_entities.time_charterer.id", "laycan_from", "laycan_to", - "tones", + "tonnes", "fixing_timestamp", "vtx_fulfilled", "destination.label", + "destination.id", "origin.label", + "origin.id", "product.label", + "product.id", "charterer.label", + "charterer.id", ] ``` @@ -117,7 +87,7 @@ def to_df( `pd.DataFrame` of Fixtures. """ - flatten = functools.partial(convert_to_flat_dict, columns=columns) + flatten = functools.partial(convert_fixture_to_flat_dict, columns=columns) with Pool(os.cpu_count()) as pool: records = pool.map(flatten, super().to_list()) From 1928359245c5f339baa00b3aee84cd085c005722 Mon Sep 17 00:00:00 2001 From: Hansen Fan Date: Mon, 13 Jul 2026 14:06:09 +0100 Subject: [PATCH 2/7] fix: aligned with pr conventions --- test_fixtures_bug.py | 58 ------------------- tests/api/test_fixture_flattening.py | 76 +++++++++++++++++++++++++ vortexasdk/endpoints/fixtures_result.py | 18 +----- vortexasdk/version.py | 2 +- 4 files changed, 78 insertions(+), 76 deletions(-) delete mode 100644 test_fixtures_bug.py create mode 100644 tests/api/test_fixture_flattening.py diff --git a/test_fixtures_bug.py b/test_fixtures_bug.py deleted file mode 100644 index 1835a6e59..000000000 --- a/test_fixtures_bug.py +++ /dev/null @@ -1,58 +0,0 @@ -from dotenv import load_dotenv -load_dotenv() - -from datetime import datetime -from vortexasdk import Fixtures -from vortexasdk.api.entity_flattening import convert_to_flat_dict, convert_fixture_to_flat_dict - -if __name__ == '__main__': - print("Fetching fixtures from API...") - result = Fixtures().search( - filter_time_min=datetime(2024, 1, 1), - filter_time_max=datetime(2024, 1, 7), - ) - raw_records = result.records - print(f"Got {len(raw_records)} fixtures") - - # Find a record with all three corporate entity layers - record = None - for r in raw_records: - ce = r.get("vessel", {}).get("corporate_entities", []) - layers = {e.get("layer") for e in ce} - if {"charterer", "effective_controller", "time_charterer"} <= layers: - record = r - break - - if not record: - # Fallback: find one with at least 2 - for r in raw_records: - ce = r.get("vessel", {}).get("corporate_entities", []) - if len(ce) > 1: - record = r - break - - if not record: - record = raw_records[0] - - ce = record.get("vessel", {}).get("corporate_entities", []) - layers = [e.get("layer") for e in ce] - print(f"Using fixture: {record['vessel']['name']} (layers: {layers})\n") - - expected = [ - "vessel.name", - "vessel.imo", - "tonnes", - "vessel.corporate_entities.charterer.label", - "vessel.corporate_entities.effective_controller.label", - "vessel.corporate_entities.time_charterer.label", - ] - - print("BEFORE FIX (convert_to_flat_dict):") - flat_before = convert_to_flat_dict(record, columns="all") - for col in expected: - print(f" {'✅' if col in flat_before else '❌'} {col} = {flat_before.get(col, 'MISSING')}") - - print("\nAFTER FIX (convert_fixture_to_flat_dict):") - flat_after = convert_fixture_to_flat_dict(record, columns="all") - for col in expected: - print(f" {'✅' if col in flat_after else '❌'} {col} = {flat_after.get(col, 'MISSING')}") diff --git a/tests/api/test_fixture_flattening.py b/tests/api/test_fixture_flattening.py new file mode 100644 index 000000000..623d431e2 --- /dev/null +++ b/tests/api/test_fixture_flattening.py @@ -0,0 +1,76 @@ +from unittest import TestCase + +from vortexasdk.api.entity_flattening import ( + convert_to_flat_dict, + convert_fixture_to_flat_dict, +) + + +class TestFixtureFlattening(TestCase): + """Test that convert_fixture_to_flat_dict groups corporate_entities by layer.""" + + FIXTURE = { + "id": "abc123", + "vessel": { + "id": "v1", + "name": "ALPINE EAGLE", + "imo": 9480980, + "mmsi": 255804460, + "dwt": 300000, + "cubic_capacity": 330000, + "vessel_class": "vlcc", + "classes": [], + "corporate_entities": [ + { + "id": "ec1", + "layer": "effective_controller", + "label": "Frontline Ltd", + "probability": 1, + "source": "external", + }, + { + "id": "tc1", + "layer": "time_charterer", + "label": "Shell", + "probability": 0.8, + "source": "model", + "start_timestamp": "2023-06-01", + "end_timestamp": "2024-06-01", + }, + ], + "tags": [], + "flag": [], + "scrubber": [], + }, + "tonnes": 270000, + "laycan_from": "2024-01-01T00:00:00+0000", + "laycan_to": "2024-01-03T00:00:00+0000", + "fixing_timestamp": "2023-12-28T14:30:00+0000", + "vtx_fulfilled": True, + "origin": {"id": "org1", "label": "Arabian Gulf"}, + "destination": {"id": "dst1", "label": "South Korea"}, + "product": {"id": "prd1", "label": "Crude"}, + "charterer": {"id": "c1", "label": "Trafigura"}, + } + + def test_generic_flattener_uses_numeric_indices(self): + flat = convert_to_flat_dict(self.FIXTURE, columns="all") + assert "vessel.corporate_entities.0.label" in flat + assert "vessel.corporate_entities.effective_controller.label" not in flat + + def test_fixture_flattener_groups_by_layer(self): + flat = convert_fixture_to_flat_dict(self.FIXTURE, columns="all") + assert flat["vessel.corporate_entities.effective_controller.label"] == "Frontline Ltd" + assert flat["vessel.corporate_entities.time_charterer.label"] == "Shell" + + def test_fixture_flattener_preserves_flat_fields(self): + flat = convert_fixture_to_flat_dict(self.FIXTURE, columns="all") + assert flat["vessel.name"] == "ALPINE EAGLE" + assert flat["vessel.imo"] == 9480980 + assert flat["tonnes"] == 270000 + assert flat["charterer.label"] == "Trafigura" + + def test_fixture_flattener_column_filter(self): + cols = ["vessel.name", "vessel.corporate_entities.effective_controller.label"] + flat = convert_fixture_to_flat_dict(self.FIXTURE, columns=cols) + assert set(flat.keys()) == set(cols) diff --git a/vortexasdk/endpoints/fixtures_result.py b/vortexasdk/endpoints/fixtures_result.py index 90effbb44..4ee7c0ad7 100644 --- a/vortexasdk/endpoints/fixtures_result.py +++ b/vortexasdk/endpoints/fixtures_result.py @@ -49,37 +49,21 @@ def to_df( columns: The Fixtures columns we want in the dataframe. Pass `columns="all"` to get all available columns. - A near complete list of available columns: + A near complete list of columns is given below ```python [ "id", "vessel.id", "vessel.name", - "vessel.imo", - "vessel.mmsi", - "vessel.dwt", - "vessel.cubic_capacity", - "vessel.vessel_class", - "vessel.year", - "vessel.corporate_entities.charterer.label", - "vessel.corporate_entities.charterer.id", - "vessel.corporate_entities.effective_controller.label", - "vessel.corporate_entities.effective_controller.id", - "vessel.corporate_entities.time_charterer.label", - "vessel.corporate_entities.time_charterer.id", "laycan_from", "laycan_to", "tonnes", "fixing_timestamp", "vtx_fulfilled", "destination.label", - "destination.id", "origin.label", - "origin.id", "product.label", - "product.id", "charterer.label", - "charterer.id", ] ``` diff --git a/vortexasdk/version.py b/vortexasdk/version.py index 7b64613a0..976a991dc 100644 --- a/vortexasdk/version.py +++ b/vortexasdk/version.py @@ -1 +1 @@ -__version__ = "1.0.29" +__version__ = "1.0.30" From 296f93bdc4006dfddff3ef992d5669644a386e69 Mon Sep 17 00:00:00 2001 From: Hansen Fan Date: Mon, 13 Jul 2026 14:07:42 +0100 Subject: [PATCH 3/7] fix: fixed docstring --- vortexasdk/endpoints/fixtures_result.py | 41 ++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/vortexasdk/endpoints/fixtures_result.py b/vortexasdk/endpoints/fixtures_result.py index 4ee7c0ad7..19cacf07e 100644 --- a/vortexasdk/endpoints/fixtures_result.py +++ b/vortexasdk/endpoints/fixtures_result.py @@ -47,7 +47,46 @@ def to_df( # Arguments columns: The Fixtures columns we want in the dataframe. - Pass `columns="all"` to get all available columns. + Defaults to `columns = [ + "id", + 'vessel.corporate_entities.charterer.id', + 'vessel.corporate_entities.charterer.label', + 'vessel.corporate_entities.charterer.layer', + 'vessel.corporate_entities.charterer.probability', + 'vessel.corporate_entities.charterer.source', + 'vessel.corporate_entities.effective_controller.id', + 'vessel.corporate_entities.effective_controller.label', + 'vessel.corporate_entities.effective_controller.layer', + 'vessel.corporate_entities.effective_controller.probability', + 'vessel.corporate_entities.effective_controller.source', + 'vessel.corporate_entities.time_charterer.end_timestamp', + 'vessel.corporate_entities.time_charterer.id', + 'vessel.corporate_entities.time_charterer.label', + 'vessel.corporate_entities.time_charterer.layer', + 'vessel.corporate_entities.time_charterer.probability', + 'vessel.corporate_entities.time_charterer.source', + 'vessel.corporate_entities.time_charterer.start_timestamp', + 'vessel.cubic_capacity', + 'vessel.dwt', + 'vessel.id', + 'vessel.imo', + 'vessel.mmsi', + 'vessel.name', + 'vessel.vessel_class', + "laycan_from", + "laycan_to", + "tonnes", + "fixing_timestamp", + "vtx_fulfilled", + "destination.label", + "destination.id", + "origin.label", + "origin.id", + "product.label", + "product.id", + "charterer.label", + "charterer.id", + ]`. A near complete list of columns is given below ```python From 7ce8889f0e5454fb5fc6c97c2655b5a146873d9d Mon Sep 17 00:00:00 2001 From: Hansen Fan Date: Mon, 13 Jul 2026 14:14:42 +0100 Subject: [PATCH 4/7] fix: updated documentation --- vortexasdk/endpoints/fixtures_result.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/vortexasdk/endpoints/fixtures_result.py b/vortexasdk/endpoints/fixtures_result.py index 19cacf07e..73e0a5b87 100644 --- a/vortexasdk/endpoints/fixtures_result.py +++ b/vortexasdk/endpoints/fixtures_result.py @@ -68,11 +68,18 @@ def to_df( 'vessel.corporate_entities.time_charterer.start_timestamp', 'vessel.cubic_capacity', 'vessel.dwt', + 'vessel.end_timestamp', 'vessel.id', 'vessel.imo', 'vessel.mmsi', 'vessel.name', + 'vessel.start_timestamp', + 'vessel.status', + 'vessel.tags.end_timestamp', + 'vessel.tags.start_timestamp', + 'vessel.tags.tag', 'vessel.vessel_class', + 'vessel.voyage_id', "laycan_from", "laycan_to", "tonnes", From f6d542816bdd9a15d16639fece92deed281e40d9 Mon Sep 17 00:00:00 2001 From: Hansen Fan Date: Mon, 13 Jul 2026 14:24:02 +0100 Subject: [PATCH 5/7] fix: deployment formatting --- tests/api/test_fixture_flattening.py | 18 ++++++++++++++---- vortexasdk/endpoints/fixtures_result.py | 4 +++- vortexasdk/version.py | 2 +- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/tests/api/test_fixture_flattening.py b/tests/api/test_fixture_flattening.py index 623d431e2..ca2435bb3 100644 --- a/tests/api/test_fixture_flattening.py +++ b/tests/api/test_fixture_flattening.py @@ -56,12 +56,19 @@ class TestFixtureFlattening(TestCase): def test_generic_flattener_uses_numeric_indices(self): flat = convert_to_flat_dict(self.FIXTURE, columns="all") assert "vessel.corporate_entities.0.label" in flat - assert "vessel.corporate_entities.effective_controller.label" not in flat + assert ( + "vessel.corporate_entities.effective_controller.label" not in flat + ) def test_fixture_flattener_groups_by_layer(self): flat = convert_fixture_to_flat_dict(self.FIXTURE, columns="all") - assert flat["vessel.corporate_entities.effective_controller.label"] == "Frontline Ltd" - assert flat["vessel.corporate_entities.time_charterer.label"] == "Shell" + assert ( + flat["vessel.corporate_entities.effective_controller.label"] + == "Frontline Ltd" + ) + assert ( + flat["vessel.corporate_entities.time_charterer.label"] == "Shell" + ) def test_fixture_flattener_preserves_flat_fields(self): flat = convert_fixture_to_flat_dict(self.FIXTURE, columns="all") @@ -71,6 +78,9 @@ def test_fixture_flattener_preserves_flat_fields(self): assert flat["charterer.label"] == "Trafigura" def test_fixture_flattener_column_filter(self): - cols = ["vessel.name", "vessel.corporate_entities.effective_controller.label"] + cols = [ + "vessel.name", + "vessel.corporate_entities.effective_controller.label", + ] flat = convert_fixture_to_flat_dict(self.FIXTURE, columns=cols) assert set(flat.keys()) == set(cols) diff --git a/vortexasdk/endpoints/fixtures_result.py b/vortexasdk/endpoints/fixtures_result.py index 73e0a5b87..f99486e54 100644 --- a/vortexasdk/endpoints/fixtures_result.py +++ b/vortexasdk/endpoints/fixtures_result.py @@ -117,7 +117,9 @@ def to_df( `pd.DataFrame` of Fixtures. """ - flatten = functools.partial(convert_fixture_to_flat_dict, columns=columns) + flatten = functools.partial( + convert_fixture_to_flat_dict, columns=columns + ) with Pool(os.cpu_count()) as pool: records = pool.map(flatten, super().to_list()) diff --git a/vortexasdk/version.py b/vortexasdk/version.py index 976a991dc..8873ad420 100644 --- a/vortexasdk/version.py +++ b/vortexasdk/version.py @@ -1 +1 @@ -__version__ = "1.0.30" +__version__ = "1.0.30a1" From 4e814e4dd6749594cfa6778cd3c9eb2e9ab934b9 Mon Sep 17 00:00:00 2001 From: Hansen Fan Date: Tue, 14 Jul 2026 10:27:19 +0100 Subject: [PATCH 6/7] fix: integration tests --- tests/endpoints/test_fixtures_real.py | 183 ++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 tests/endpoints/test_fixtures_real.py diff --git a/tests/endpoints/test_fixtures_real.py b/tests/endpoints/test_fixtures_real.py new file mode 100644 index 000000000..afcd06fa6 --- /dev/null +++ b/tests/endpoints/test_fixtures_real.py @@ -0,0 +1,183 @@ +from datetime import datetime + +from tests.testcases import TestCaseUsingRealAPI +from vortexasdk.endpoints.fixtures import Fixtures +from vortexasdk.api.entity_flattening import ( + convert_to_flat_dict, + convert_fixture_to_flat_dict, +) + + +class TestFixturesReal(TestCaseUsingRealAPI): + """Integration tests for Fixtures endpoint (RND-21448).""" + + def test_rnd_21448_old_flattener_broken_behavior(self): + """ + RND-21448: INITIAL BROKEN BEHAVIOR + + Before fix: When users tried to request layer-based columns like + "vessel.corporate_entities.effective_controller.label" using the old + convert_to_flat_dict, those columns would be EMPTY/MISSING because + the old flattener uses numeric indices. + + This reproduces the exact bug from the ticket. + """ + result = Fixtures().search( + filter_time_min=datetime(2020, 1, 1), + filter_time_max=datetime(2020, 1, 2), + ).to_list() + + assert len(result) > 0 + first_fixture = result[0].model_dump() + + # Simulate old to_df() behavior: request layer-based columns + old_flat = convert_to_flat_dict( + first_fixture, + columns=[ + "vessel.name", + "tonnes", + "vessel.corporate_entities.effective_controller.label", + "vessel.corporate_entities.time_charterer.label", + ] + ) + + # BROKEN: Requested columns would be missing or None + assert "vessel.corporate_entities.effective_controller.label" not in old_flat or \ + old_flat["vessel.corporate_entities.effective_controller.label"] is None, \ + "RND-21448 (BROKEN): Requesting layer-based corporate_entities returns empty/missing" + + assert "vessel.corporate_entities.time_charterer.label" not in old_flat or \ + old_flat["vessel.corporate_entities.time_charterer.label"] is None, \ + "RND-21448 (BROKEN): Requesting layer-based corporate_entities returns empty/missing" + + # OLD behavior: has numeric indices instead + old_flat_all = convert_to_flat_dict(first_fixture, columns="all") + numeric_keys = [k for k in old_flat_all.keys() if "corporate_entities.0." in k or + "corporate_entities.1." in k] + assert len(numeric_keys) > 0, \ + "RND-21448 (BROKEN): Old flattener produces numeric indices like corporate_entities.0.label" + + def test_rnd_21448_new_fixture_flattener_works(self): + """ + RND-21448: After fix, the new fixture flattener correctly groups corporate_entities + by layer name, making layer-based columns accessible and populated. + + This test verifies the FIX works: convert_fixture_to_flat_dict produces layer-based keys + with actual values matching what's in to_list(). + """ + result = Fixtures().search( + filter_time_min=datetime(2020, 1, 1), + filter_time_max=datetime(2020, 1, 2), + ).to_list() + + assert len(result) > 0 + first_fixture = result[0] + first_fixture_dict = first_fixture.model_dump() + + new_flat = convert_fixture_to_flat_dict(first_fixture_dict, columns="all") + + layer_based_keys = [k for k in new_flat.keys() if "corporate_entities.effective_controller." in k or + "corporate_entities.time_charterer." in k or + "corporate_entities.charterer." in k] + assert len(layer_based_keys) > 0, \ + "RND-21448 (fixed): new flattener should produce layer-based keys" + + for key in layer_based_keys: + value = new_flat.get(key) + if value is not None: + # At least some layer-based values should be populated + assert True + break + else: + assert len(layer_based_keys) > 0, \ + "RND-21448 (fixed): layer-based keys should be present" + + assert "tonnes" in new_flat + assert new_flat.get("tonnes") is not None + + def test_rnd_21448_to_df_integration(self): + """ + RND-21448: Verify that to_df() with the new fixture flattener returns + populated nested vessel fields (matching to_list() data). + """ + + list_result = Fixtures().search( + filter_time_field="fixing_timestamp", + filter_time_min=datetime(2020, 1, 1), + filter_time_max=datetime(2020, 1, 2), + ).to_list() + + assert len(list_result) > 0 + + df = Fixtures().search( + filter_time_min=datetime(2020, 1, 1), + filter_time_max=datetime(2020, 1, 2), + ).to_df( + columns=[ + "id", + "vessel.id", + "vessel.name", + "vessel.imo", + "tonnes", + "vessel.corporate_entities.effective_controller.label", + "vessel.corporate_entities.time_charterer.label", + "charterer.label", + ] + ) + + assert len(df) > 0 + + assert df["vessel.name"].notna().any() + assert df["vessel.imo"].notna().any() + assert df["tonnes"].notna().any() + + has_effective_controller = df["vessel.corporate_entities.effective_controller.label"].notna().any() + has_time_charterer = df["vessel.corporate_entities.time_charterer.label"].notna().any() + assert has_effective_controller or has_time_charterer + + def test_to_df_returns_data(self): + """Test that to_df() returns fixtures with default columns.""" + df = Fixtures().search( + filter_time_field="fixing_timestamp", + filter_time_min=datetime(2020, 1, 1), + filter_time_max=datetime(2020, 1, 2), + ).to_df() + + assert len(df) > 0 + assert "vessel.name" in df.columns + assert "tonnes" in df.columns + assert "charterer.label" in df.columns + + def test_to_df_nested_corporate_entities_populated(self): + """Test that nested vessel.corporate_entities fields are populated (RND-21448).""" + df = Fixtures().search( + filter_time_field="fixing_timestamp", + filter_time_min=datetime(2020, 1, 1), + filter_time_max=datetime(2020, 1, 2), + ).to_df( + columns=[ + "id", + "vessel.name", + "vessel.imo", + "vessel.corporate_entities.effective_controller.label", + "vessel.corporate_entities.time_charterer.label", + "tonnes", + ] + ) + + assert len(df) > 0 + assert "vessel.name" in df.columns + assert "vessel.corporate_entities.effective_controller.label" in df.columns + assert df["vessel.corporate_entities.effective_controller.label"].notna().any() + + def test_to_df_tonnes_column_populated(self): + """Test that tonnes column (fixed typo from tones) is populated.""" + df = Fixtures().search( + filter_time_field="fixing_timestamp", + filter_time_min=datetime(2020, 1, 1), + filter_time_max=datetime(2020, 1, 2), + ).to_df(columns=["id", "tonnes"]) + + assert len(df) > 0 + assert "tonnes" in df.columns + assert df["tonnes"].notna().sum() > len(df) * 0.5 From 09dcfd9a7e032b086973814c89a5ab1a5e173b96 Mon Sep 17 00:00:00 2001 From: Hansen Fan Date: Tue, 14 Jul 2026 10:31:36 +0100 Subject: [PATCH 7/7] fix: format integration tests with black --- tests/endpoints/test_fixtures_real.py | 206 +++++++++++++++++--------- 1 file changed, 132 insertions(+), 74 deletions(-) diff --git a/tests/endpoints/test_fixtures_real.py b/tests/endpoints/test_fixtures_real.py index afcd06fa6..32547faf2 100644 --- a/tests/endpoints/test_fixtures_real.py +++ b/tests/endpoints/test_fixtures_real.py @@ -22,10 +22,14 @@ def test_rnd_21448_old_flattener_broken_behavior(self): This reproduces the exact bug from the ticket. """ - result = Fixtures().search( - filter_time_min=datetime(2020, 1, 1), - filter_time_max=datetime(2020, 1, 2), - ).to_list() + result = ( + Fixtures() + .search( + filter_time_min=datetime(2020, 1, 1), + filter_time_max=datetime(2020, 1, 2), + ) + .to_list() + ) assert len(result) > 0 first_fixture = result[0].model_dump() @@ -38,24 +42,33 @@ def test_rnd_21448_old_flattener_broken_behavior(self): "tonnes", "vessel.corporate_entities.effective_controller.label", "vessel.corporate_entities.time_charterer.label", - ] + ], ) # BROKEN: Requested columns would be missing or None - assert "vessel.corporate_entities.effective_controller.label" not in old_flat or \ - old_flat["vessel.corporate_entities.effective_controller.label"] is None, \ - "RND-21448 (BROKEN): Requesting layer-based corporate_entities returns empty/missing" - - assert "vessel.corporate_entities.time_charterer.label" not in old_flat or \ - old_flat["vessel.corporate_entities.time_charterer.label"] is None, \ - "RND-21448 (BROKEN): Requesting layer-based corporate_entities returns empty/missing" + assert ( + "vessel.corporate_entities.effective_controller.label" + not in old_flat + or old_flat["vessel.corporate_entities.effective_controller.label"] + is None + ), "RND-21448 (BROKEN): Requesting layer-based corporate_entities returns empty/missing" + + assert ( + "vessel.corporate_entities.time_charterer.label" not in old_flat + or old_flat["vessel.corporate_entities.time_charterer.label"] + is None + ), "RND-21448 (BROKEN): Requesting layer-based corporate_entities returns empty/missing" # OLD behavior: has numeric indices instead old_flat_all = convert_to_flat_dict(first_fixture, columns="all") - numeric_keys = [k for k in old_flat_all.keys() if "corporate_entities.0." in k or - "corporate_entities.1." in k] - assert len(numeric_keys) > 0, \ - "RND-21448 (BROKEN): Old flattener produces numeric indices like corporate_entities.0.label" + numeric_keys = [ + k + for k in old_flat_all.keys() + if "corporate_entities.0." in k or "corporate_entities.1." in k + ] + assert ( + len(numeric_keys) > 0 + ), "RND-21448 (BROKEN): Old flattener produces numeric indices like corporate_entities.0.label" def test_rnd_21448_new_fixture_flattener_works(self): """ @@ -65,22 +78,33 @@ def test_rnd_21448_new_fixture_flattener_works(self): This test verifies the FIX works: convert_fixture_to_flat_dict produces layer-based keys with actual values matching what's in to_list(). """ - result = Fixtures().search( - filter_time_min=datetime(2020, 1, 1), - filter_time_max=datetime(2020, 1, 2), - ).to_list() + result = ( + Fixtures() + .search( + filter_time_min=datetime(2020, 1, 1), + filter_time_max=datetime(2020, 1, 2), + ) + .to_list() + ) assert len(result) > 0 first_fixture = result[0] first_fixture_dict = first_fixture.model_dump() - new_flat = convert_fixture_to_flat_dict(first_fixture_dict, columns="all") + new_flat = convert_fixture_to_flat_dict( + first_fixture_dict, columns="all" + ) - layer_based_keys = [k for k in new_flat.keys() if "corporate_entities.effective_controller." in k or - "corporate_entities.time_charterer." in k or - "corporate_entities.charterer." in k] - assert len(layer_based_keys) > 0, \ - "RND-21448 (fixed): new flattener should produce layer-based keys" + layer_based_keys = [ + k + for k in new_flat.keys() + if "corporate_entities.effective_controller." in k + or "corporate_entities.time_charterer." in k + or "corporate_entities.charterer." in k + ] + assert ( + len(layer_based_keys) > 0 + ), "RND-21448 (fixed): new flattener should produce layer-based keys" for key in layer_based_keys: value = new_flat.get(key) @@ -89,8 +113,9 @@ def test_rnd_21448_new_fixture_flattener_works(self): assert True break else: - assert len(layer_based_keys) > 0, \ - "RND-21448 (fixed): layer-based keys should be present" + assert ( + len(layer_based_keys) > 0 + ), "RND-21448 (fixed): layer-based keys should be present" assert "tonnes" in new_flat assert new_flat.get("tonnes") is not None @@ -101,28 +126,36 @@ def test_rnd_21448_to_df_integration(self): populated nested vessel fields (matching to_list() data). """ - list_result = Fixtures().search( - filter_time_field="fixing_timestamp", - filter_time_min=datetime(2020, 1, 1), - filter_time_max=datetime(2020, 1, 2), - ).to_list() + list_result = ( + Fixtures() + .search( + filter_time_field="fixing_timestamp", + filter_time_min=datetime(2020, 1, 1), + filter_time_max=datetime(2020, 1, 2), + ) + .to_list() + ) assert len(list_result) > 0 - df = Fixtures().search( - filter_time_min=datetime(2020, 1, 1), - filter_time_max=datetime(2020, 1, 2), - ).to_df( - columns=[ - "id", - "vessel.id", - "vessel.name", - "vessel.imo", - "tonnes", - "vessel.corporate_entities.effective_controller.label", - "vessel.corporate_entities.time_charterer.label", - "charterer.label", - ] + df = ( + Fixtures() + .search( + filter_time_min=datetime(2020, 1, 1), + filter_time_max=datetime(2020, 1, 2), + ) + .to_df( + columns=[ + "id", + "vessel.id", + "vessel.name", + "vessel.imo", + "tonnes", + "vessel.corporate_entities.effective_controller.label", + "vessel.corporate_entities.time_charterer.label", + "charterer.label", + ] + ) ) assert len(df) > 0 @@ -131,17 +164,27 @@ def test_rnd_21448_to_df_integration(self): assert df["vessel.imo"].notna().any() assert df["tonnes"].notna().any() - has_effective_controller = df["vessel.corporate_entities.effective_controller.label"].notna().any() - has_time_charterer = df["vessel.corporate_entities.time_charterer.label"].notna().any() + has_effective_controller = ( + df["vessel.corporate_entities.effective_controller.label"] + .notna() + .any() + ) + has_time_charterer = ( + df["vessel.corporate_entities.time_charterer.label"].notna().any() + ) assert has_effective_controller or has_time_charterer def test_to_df_returns_data(self): """Test that to_df() returns fixtures with default columns.""" - df = Fixtures().search( - filter_time_field="fixing_timestamp", - filter_time_min=datetime(2020, 1, 1), - filter_time_max=datetime(2020, 1, 2), - ).to_df() + df = ( + Fixtures() + .search( + filter_time_field="fixing_timestamp", + filter_time_min=datetime(2020, 1, 1), + filter_time_max=datetime(2020, 1, 2), + ) + .to_df() + ) assert len(df) > 0 assert "vessel.name" in df.columns @@ -150,33 +193,48 @@ def test_to_df_returns_data(self): def test_to_df_nested_corporate_entities_populated(self): """Test that nested vessel.corporate_entities fields are populated (RND-21448).""" - df = Fixtures().search( - filter_time_field="fixing_timestamp", - filter_time_min=datetime(2020, 1, 1), - filter_time_max=datetime(2020, 1, 2), - ).to_df( - columns=[ - "id", - "vessel.name", - "vessel.imo", - "vessel.corporate_entities.effective_controller.label", - "vessel.corporate_entities.time_charterer.label", - "tonnes", - ] + df = ( + Fixtures() + .search( + filter_time_field="fixing_timestamp", + filter_time_min=datetime(2020, 1, 1), + filter_time_max=datetime(2020, 1, 2), + ) + .to_df( + columns=[ + "id", + "vessel.name", + "vessel.imo", + "vessel.corporate_entities.effective_controller.label", + "vessel.corporate_entities.time_charterer.label", + "tonnes", + ] + ) ) assert len(df) > 0 assert "vessel.name" in df.columns - assert "vessel.corporate_entities.effective_controller.label" in df.columns - assert df["vessel.corporate_entities.effective_controller.label"].notna().any() + assert ( + "vessel.corporate_entities.effective_controller.label" + in df.columns + ) + assert ( + df["vessel.corporate_entities.effective_controller.label"] + .notna() + .any() + ) def test_to_df_tonnes_column_populated(self): """Test that tonnes column (fixed typo from tones) is populated.""" - df = Fixtures().search( - filter_time_field="fixing_timestamp", - filter_time_min=datetime(2020, 1, 1), - filter_time_max=datetime(2020, 1, 2), - ).to_df(columns=["id", "tonnes"]) + df = ( + Fixtures() + .search( + filter_time_field="fixing_timestamp", + filter_time_min=datetime(2020, 1, 1), + filter_time_max=datetime(2020, 1, 2), + ) + .to_df(columns=["id", "tonnes"]) + ) assert len(df) > 0 assert "tonnes" in df.columns