From b668ddc7130e429e7e0f2d4899887eb434af6a85 Mon Sep 17 00:00:00 2001 From: SpliiT Date: Thu, 29 Jan 2026 14:03:21 +0100 Subject: [PATCH 1/5] fix(AttributeMetadata): metadata extraction for multi-valued and invalid attributes --- .../routes/blueprint_routes.py | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/opengeodeweb_back/routes/blueprint_routes.py b/src/opengeodeweb_back/routes/blueprint_routes.py index 88ab768..df90b71 100644 --- a/src/opengeodeweb_back/routes/blueprint_routes.py +++ b/src/opengeodeweb_back/routes/blueprint_routes.py @@ -2,6 +2,7 @@ import os import time import shutil +import math from typing import Any # Third party imports @@ -267,6 +268,7 @@ def texture_coordinates() -> flask.Response: def attributes_metadata(manager: og.AttributeManager) -> dict[str, list[float]]: metadata: dict[str, list[float]] = {} + nb_elements = manager.nb_elements() for name in manager.attribute_names(): attribute = manager.find_generic_attribute(name) if not attribute.is_genericable(): @@ -274,13 +276,22 @@ def attributes_metadata(manager: og.AttributeManager) -> dict[str, list[float]]: continue min_value = None max_value = None - nb_items = attribute.nb_items() - for i in range(nb_items): - generic_value = attribute.generic_value(i) - if min_value is None or generic_value < min_value: - min_value = generic_value - if max_value is None or generic_value > max_value: - max_value = generic_value + for i in range(nb_elements): + if hasattr(attribute, "value"): + val = attribute.value(i) + if isinstance(val, list): + values_to_check = val + else: + values_to_check = [val] + else: + values_to_check = [attribute.generic_value(i)] + + for v in values_to_check: + if v is not None and not math.isnan(v): + if min_value is None or v < min_value: + min_value = v + if max_value is None or v > max_value: + max_value = v metadata[name] = ( [min_value, max_value] if min_value is not None and max_value is not None From 8b99f69796902125ed9bbb34610e3ecd6c882e08 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Fri, 30 Jan 2026 17:16:40 +0100 Subject: [PATCH 2/5] attribute responses : name, min, max --- .../routes/blueprint_routes.py | 65 +++++++---------- tests/test_routes.py | 71 +++++++++---------- 2 files changed, 60 insertions(+), 76 deletions(-) diff --git a/src/opengeodeweb_back/routes/blueprint_routes.py b/src/opengeodeweb_back/routes/blueprint_routes.py index df90b71..50d8ab6 100644 --- a/src/opengeodeweb_back/routes/blueprint_routes.py +++ b/src/opengeodeweb_back/routes/blueprint_routes.py @@ -266,38 +266,42 @@ def texture_coordinates() -> flask.Response: return flask.make_response({"texture_coordinates": texture_coordinates}, 200) -def attributes_metadata(manager: og.AttributeManager) -> dict[str, list[float]]: - metadata: dict[str, list[float]] = {} +def attributes_metadata(manager: og.AttributeManager) -> list[dict[str, Any]]: + attributes: list[dict[str, Any]] = [] nb_elements = manager.nb_elements() for name in manager.attribute_names(): attribute = manager.find_generic_attribute(name) if not attribute.is_genericable(): - metadata[name] = [-1.0, -1.0] + attributes.append( + {"attribute_name": name, "min_value": -1.0, "max_value": -1.0} + ) continue min_value = None max_value = None - for i in range(nb_elements): + for index in range(nb_elements): if hasattr(attribute, "value"): - val = attribute.value(i) + val = attribute.value(index) if isinstance(val, list): values_to_check = val else: values_to_check = [val] else: - values_to_check = [attribute.generic_value(i)] - - for v in values_to_check: - if v is not None and not math.isnan(v): - if min_value is None or v < min_value: - min_value = v - if max_value is None or v > max_value: - max_value = v - metadata[name] = ( - [min_value, max_value] - if min_value is not None and max_value is not None - else [-1.0, -1.0] + values_to_check = [attribute.generic_value(index)] + + for value in values_to_check: + if value is not None and not math.isnan(value): + if min_value is None or value < min_value: + min_value = value + if max_value is None or value > max_value: + max_value = value + attributes.append( + { + "attribute_name": name, + "min_value": min_value if min_value is not None else -1.0, + "max_value": max_value if max_value is not None else -1.0, + } ) - return metadata + return attributes @routes.route( @@ -314,10 +318,7 @@ def vertex_attribute_names() -> flask.Response: flask.abort(400, f"{params.id} is not a GeodeMesh") attribute_manager = geode_object.vertex_attribute_manager() return flask.make_response( - { - "vertex_attribute_names": attribute_manager.attribute_names(), - "vertex_attribute_metadata": attributes_metadata(attribute_manager), - }, + {"attributes": attributes_metadata(attribute_manager)}, 200, ) @@ -336,10 +337,7 @@ def cell_attribute_names() -> flask.Response: flask.abort(400, f"{params.id} is not a GeodeGrid") attribute_manager = geode_object.cell_attribute_manager() return flask.make_response( - { - "cell_attribute_names": attribute_manager.attribute_names(), - "cell_attribute_metadata": attributes_metadata(attribute_manager), - }, + {"attributes": attributes_metadata(attribute_manager)}, 200, ) @@ -358,10 +356,7 @@ def polygon_attribute_names() -> flask.Response: flask.abort(400, f"{params.id} is not a GeodeSurfaceMesh") attribute_manager = geode_object.polygon_attribute_manager() return flask.make_response( - { - "polygon_attribute_names": attribute_manager.attribute_names(), - "polygon_attribute_metadata": attributes_metadata(attribute_manager), - }, + {"attributes": attributes_metadata(attribute_manager)}, 200, ) @@ -380,10 +375,7 @@ def polyhedron_attribute_names() -> flask.Response: flask.abort(400, f"{params.id} is not a GeodeSolidMesh") attribute_manager = geode_object.polyhedron_attribute_manager() return flask.make_response( - { - "polyhedron_attribute_names": attribute_manager.attribute_names(), - "polyhedron_attribute_metadata": attributes_metadata(attribute_manager), - }, + {"attributes": attributes_metadata(attribute_manager)}, 200, ) @@ -402,10 +394,7 @@ def edge_attribute_names() -> flask.Response: flask.abort(400, f"{params.id} does not have edges") attribute_manager = geode_object.edge_attribute_manager() return flask.make_response( - { - "edge_attribute_names": attribute_manager.attribute_names(), - "edge_attribute_metadata": attributes_metadata(attribute_manager), - }, + {"attributes": attributes_metadata(attribute_manager)}, 200, ) diff --git a/tests/test_routes.py b/tests/test_routes.py index d40c68a..4d42c4b 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -244,13 +244,12 @@ def test_vertex_attribute_names(client: FlaskClient, test_id: str) -> None: assert os.path.exists(data_path), f"File not found at {data_path}" response = client.post(route, json={"id": data.id}) assert response.status_code == 200 - vertex_attribute_names = response.get_json()["vertex_attribute_names"] - vertex_attribute_metadata = response.get_json()["vertex_attribute_metadata"] - assert type(vertex_attribute_names) is list - assert type(vertex_attribute_metadata) is dict - for vertex_attribute_name in vertex_attribute_names: - assert type(vertex_attribute_name) is str - assert vertex_attribute_name in vertex_attribute_metadata + attributes = response.get_json()["attributes"] + assert type(attributes) is list + for attribute in attributes: + assert "attribute_name" in attribute + assert "min_value" in attribute + assert "max_value" in attribute def test_cell_attribute_names(client: FlaskClient, test_id: str) -> None: @@ -273,13 +272,12 @@ def test_cell_attribute_names(client: FlaskClient, test_id: str) -> None: assert os.path.exists(data_path), f"File not found at {data_path}" response = client.post(route, json={"id": data.id}) assert response.status_code == 200 - cell_attribute_names = response.get_json()["cell_attribute_names"] - cell_attribute_metadata = response.get_json()["cell_attribute_metadata"] - assert type(cell_attribute_names) is list - assert type(cell_attribute_metadata) is dict - for cell_attribute_name in cell_attribute_names: - assert type(cell_attribute_name) is str - assert cell_attribute_name in cell_attribute_metadata + attributes = response.get_json()["attributes"] + assert type(attributes) is list + for attribute in attributes: + assert "attribute_name" in attribute + assert "min_value" in attribute + assert "max_value" in attribute def test_polygon_attribute_names(client: FlaskClient, test_id: str) -> None: @@ -302,13 +300,12 @@ def test_polygon_attribute_names(client: FlaskClient, test_id: str) -> None: assert os.path.exists(data_path), f"File not found at {data_path}" response = client.post(route, json={"id": data.id}) assert response.status_code == 200 - polygon_attribute_names = response.get_json()["polygon_attribute_names"] - polygon_attribute_metadata = response.get_json()["polygon_attribute_metadata"] - assert type(polygon_attribute_names) is list - assert type(polygon_attribute_metadata) is dict - for polygon_attribute_name in polygon_attribute_names: - assert type(polygon_attribute_name) is str - assert polygon_attribute_name in polygon_attribute_metadata + attributes = response.get_json()["attributes"] + assert type(attributes) is list + for attribute in attributes: + assert "attribute_name" in attribute + assert "min_value" in attribute + assert "max_value" in attribute def test_polyhedron_attribute_names(client: FlaskClient, test_id: str) -> None: @@ -332,16 +329,15 @@ def test_polyhedron_attribute_names(client: FlaskClient, test_id: str) -> None: response = client.post(route, json={"id": data.id}) print(response.get_json()) assert response.status_code == 200 - polyhedron_attribute_names = response.get_json()["polyhedron_attribute_names"] - polyhedron_attribute_metadata = response.get_json()["polyhedron_attribute_metadata"] - assert type(polyhedron_attribute_names) is list - assert type(polyhedron_attribute_metadata) is dict - for polyhedron_attribute_name in polyhedron_attribute_names: - assert type(polyhedron_attribute_name) is str - assert polyhedron_attribute_name in polyhedron_attribute_metadata - - if "Range" in polyhedron_attribute_metadata: - assert polyhedron_attribute_metadata["Range"] == [0.0, 579.0] + attributes = response.get_json()["attributes"] + assert type(attributes) is list + for attribute in attributes: + assert "attribute_name" in attribute + assert "min_value" in attribute + assert "max_value" in attribute + if attribute["attribute_name"] == "Range": + assert attribute["min_value"] == 0.0 + assert attribute["max_value"] == 579.0 def test_edge_attribute_names(client: FlaskClient, test_id: str) -> None: @@ -365,13 +361,12 @@ def test_edge_attribute_names(client: FlaskClient, test_id: str) -> None: response = client.post(route, json={"id": data.id}) print(response.get_json()) assert response.status_code == 200 - edge_attribute_names = response.get_json()["edge_attribute_names"] - edge_attribute_metadata = response.get_json()["edge_attribute_metadata"] - assert type(edge_attribute_names) is list - assert type(edge_attribute_metadata) is dict - for edge_attribute_name in edge_attribute_names: - assert type(edge_attribute_name) is str - assert edge_attribute_name in edge_attribute_metadata + attributes = response.get_json()["attributes"] + assert type(attributes) is list + for attribute in attributes: + assert "attribute_name" in attribute + assert "min_value" in attribute + assert "max_value" in attribute def test_database_uri_path(client: FlaskClient) -> None: From 43a865d0cf34c5faad16db88ebd93ff8ffaa41dc Mon Sep 17 00:00:00 2001 From: MaxNumerique <144453705+MaxNumerique@users.noreply.github.com> Date: Fri, 30 Jan 2026 16:33:05 +0000 Subject: [PATCH 3/5] Apply prepare changes --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b150412..08d64bd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -60,4 +60,3 @@ werkzeug==3.1.2 # flask # flask-cors -opengeodeweb-microservice==1.*,>=1.0.13 From 63142e7c726920b3088672d198df13fc97cb994a Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Fri, 30 Jan 2026 17:35:04 +0100 Subject: [PATCH 4/5] attribute responses : name, min, max --- .../routes/blueprint_routes.py | 45 ++++++++----------- 1 file changed, 18 insertions(+), 27 deletions(-) diff --git a/src/opengeodeweb_back/routes/blueprint_routes.py b/src/opengeodeweb_back/routes/blueprint_routes.py index 50d8ab6..64bd3b5 100644 --- a/src/opengeodeweb_back/routes/blueprint_routes.py +++ b/src/opengeodeweb_back/routes/blueprint_routes.py @@ -271,35 +271,26 @@ def attributes_metadata(manager: og.AttributeManager) -> list[dict[str, Any]]: nb_elements = manager.nb_elements() for name in manager.attribute_names(): attribute = manager.find_generic_attribute(name) - if not attribute.is_genericable(): - attributes.append( - {"attribute_name": name, "min_value": -1.0, "max_value": -1.0} + min_value, max_value = -1.0, -1.0 + if attribute.is_genericable(): + get_val = ( + attribute.value + if hasattr(attribute, "value") + else attribute.generic_value ) - continue - min_value = None - max_value = None - for index in range(nb_elements): - if hasattr(attribute, "value"): - val = attribute.value(index) - if isinstance(val, list): - values_to_check = val - else: - values_to_check = [val] - else: - values_to_check = [attribute.generic_value(index)] - - for value in values_to_check: - if value is not None and not math.isnan(value): - if min_value is None or value < min_value: - min_value = value - if max_value is None or value > max_value: - max_value = value + values = [] + for index in range(nb_elements): + value = get_val(index) + values.extend(value if isinstance(value, list) else [value]) + + valid_values = [ + value for value in values if value is not None and not math.isnan(value) + ] + if valid_values: + min_value, max_value = min(valid_values), max(valid_values) + attributes.append( - { - "attribute_name": name, - "min_value": min_value if min_value is not None else -1.0, - "max_value": max_value if max_value is not None else -1.0, - } + {"attribute_name": name, "min_value": min_value, "max_value": max_value} ) return attributes From d7e5ac49b984c29bc0e853902730c425815fc316 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Mon, 2 Feb 2026 09:21:39 +0100 Subject: [PATCH 5/5] fixing attributes_metadata type, strict function --- src/opengeodeweb_back/routes/blueprint_routes.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/opengeodeweb_back/routes/blueprint_routes.py b/src/opengeodeweb_back/routes/blueprint_routes.py index 64bd3b5..f8d46d4 100644 --- a/src/opengeodeweb_back/routes/blueprint_routes.py +++ b/src/opengeodeweb_back/routes/blueprint_routes.py @@ -266,22 +266,19 @@ def texture_coordinates() -> flask.Response: return flask.make_response({"texture_coordinates": texture_coordinates}, 200) -def attributes_metadata(manager: og.AttributeManager) -> list[dict[str, Any]]: - attributes: list[dict[str, Any]] = [] +def attributes_metadata(manager: og.AttributeManager) -> list[dict[str, str | float]]: + attributes: list[dict[str, str | float]] = [] nb_elements = manager.nb_elements() for name in manager.attribute_names(): attribute = manager.find_generic_attribute(name) min_value, max_value = -1.0, -1.0 if attribute.is_genericable(): - get_val = ( - attribute.value - if hasattr(attribute, "value") - else attribute.generic_value - ) values = [] + nb_items = attribute.nb_items() for index in range(nb_elements): - value = get_val(index) - values.extend(value if isinstance(value, list) else [value]) + for item in range(nb_items): + value = attribute.generic_item_value(index, item) + values.append(value) valid_values = [ value for value in values if value is not None and not math.isnan(value)