From 50cf43d0a8dc12f9df42a84bdc2396e358a630ba Mon Sep 17 00:00:00 2001 From: SpliiT Date: Tue, 7 Oct 2025 14:00:39 +0200 Subject: [PATCH 1/8] Fix tests & other --- requirements-internal.txt | 54 +++++ requirements.in | 2 +- requirements.txt | 61 ++--- src/opengeodeweb_back/app.py | 7 + .../routes/blueprint_routes.py | 28 +-- .../routes/create/blueprint_create.py | 138 ++++++++++++ .../routes/create/schemas/create_aoi.json | 46 ++++ .../routes/create/schemas/create_point.json | 29 +++ src/opengeodeweb_back/utils_functions.py | 2 +- tests/conftest.py | 2 +- tests/test_create_routes.py | 208 ++++++++++++++++++ tests/test_routes.py | 16 -- 12 files changed, 506 insertions(+), 87 deletions(-) create mode 100644 requirements-internal.txt create mode 100644 src/opengeodeweb_back/routes/create/blueprint_create.py create mode 100644 src/opengeodeweb_back/routes/create/schemas/create_aoi.json create mode 100644 src/opengeodeweb_back/routes/create/schemas/create_point.json create mode 100644 tests/test_create_routes.py diff --git a/requirements-internal.txt b/requirements-internal.txt new file mode 100644 index 00000000..f4a5dd4b --- /dev/null +++ b/requirements-internal.txt @@ -0,0 +1,54 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile --cert=None --client-cert=None --index-url=None --pip-args=None requirements-internal.in +# +blinker==1.9.0 + # via + # flask + # opengeodeweb-microservice +click==8.3.0 + # via + # flask + # opengeodeweb-microservice +fastjsonschema==2.21.1 + # via opengeodeweb-microservice +flask==3.1.2 + # via + # flask-sqlalchemy + # opengeodeweb-microservice +flask-sqlalchemy==3.1.1 + # via opengeodeweb-microservice +greenlet==3.2.4 + # via + # opengeodeweb-microservice + # sqlalchemy +itsdangerous==2.2.0 + # via + # flask + # opengeodeweb-microservice +jinja2==3.1.6 + # via + # flask + # opengeodeweb-microservice +markupsafe==3.0.3 + # via + # flask + # jinja2 + # opengeodeweb-microservice + # werkzeug +opengeodeweb-microservice==1.0.3 + # via -r requirements-internal.in +sqlalchemy==2.0.43 + # via + # flask-sqlalchemy + # opengeodeweb-microservice +typing-extensions==4.15.0 + # via + # opengeodeweb-microservice + # sqlalchemy +werkzeug==3.1.3 + # via + # flask + # opengeodeweb-microservice diff --git a/requirements.in b/requirements.in index 1a55b764..80417d35 100644 --- a/requirements.in +++ b/requirements.in @@ -8,4 +8,4 @@ geode-viewables==3.3.0 flask[async]==3.1.2 flask-cors==6.0.1 werkzeug==3.1.2 -flask-sqlalchemy==3.1.1 \ No newline at end of file +flask-sqlalchemy==3.1.1 diff --git a/requirements.txt b/requirements.txt index b89a2a2c..c4d40f78 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,55 +2,39 @@ # This file is autogenerated by pip-compile with Python 3.12 # by the following command: # -# pip-compile --output-file=./requirements.txt --pre ./requirements-internal.in ./requirements.in +# pip-compile --cert=None --client-cert=None --index-url=None --pip-args=None requirements.in # -asgiref~=3.10 +asgiref==3.10.0 # via flask -blinker~=1.9 - # via - # flask - # opengeodeweb-microservice -click~=8.3 - # via - # flask - # opengeodeweb-microservice -fastjsonschema~=2.21 - # via opengeodeweb-microservice -flask[async]~=3.1 +blinker==1.9.0 + # via flask +click==8.3.0 + # via flask +flask[async]==3.1.2 # via # -r requirements.in # flask-cors # flask-sqlalchemy - # opengeodeweb-microservice flask-cors==6.0.1 # via -r requirements.in flask-sqlalchemy==3.1.1 - # via - # -r requirements.in - # opengeodeweb-microservice + # via -r requirements.in geode-common==33.11.0 # via # -r requirements.in # geode-viewables geode-viewables==3.3.0 # via -r requirements.in -greenlet~=3.2 - # via - # opengeodeweb-microservice - # sqlalchemy -itsdangerous~=2.2 - # via - # flask - # opengeodeweb-microservice -jinja2~=3.1 - # via - # flask - # opengeodeweb-microservice -markupsafe~=3.0 +greenlet==3.2.4 + # via sqlalchemy +itsdangerous==2.2.0 + # via flask +jinja2==3.1.6 + # via flask +markupsafe==3.0.3 # via # flask # jinja2 - # opengeodeweb-microservice # werkzeug opengeode-core==15.27.4 # via @@ -75,19 +59,12 @@ opengeode-io==7.4.0 # -r requirements.in # geode-viewables # opengeode-geosciencesio -opengeodeweb-microservice~=1.0,>=1.0.3 - # via -r requirements-internal.in -sqlalchemy~=2.0 - # via - # flask-sqlalchemy - # opengeodeweb-microservice -typing-extensions~=4.15 - # via - # opengeodeweb-microservice - # sqlalchemy +sqlalchemy==2.0.43 + # via flask-sqlalchemy +typing-extensions==4.15.0 + # via sqlalchemy werkzeug==3.1.2 # via # -r requirements.in # flask # flask-cors - # opengeodeweb-microservice diff --git a/src/opengeodeweb_back/app.py b/src/opengeodeweb_back/app.py index 48381159..79a32f02 100644 --- a/src/opengeodeweb_back/app.py +++ b/src/opengeodeweb_back/app.py @@ -14,6 +14,7 @@ from opengeodeweb_back import utils_functions, app_config from opengeodeweb_back.routes import blueprint_routes from opengeodeweb_back.routes.models import blueprint_models +from opengeodeweb_back.routes.create import blueprint_create from opengeodeweb_microservice.database.connection import init_database @@ -51,6 +52,12 @@ name="opengeodeweb_models", ) +app.register_blueprint( + blueprint_create.routes, + url_prefix="/opengeodeweb_back/create", + name="opengeodeweb_create", +) + if FLASK_DEBUG == False: utils_functions.set_interval( utils_functions.kill_task, SECONDS_BETWEEN_SHUTDOWNS, app diff --git a/src/opengeodeweb_back/routes/blueprint_routes.py b/src/opengeodeweb_back/routes/blueprint_routes.py index 7f965009..7682d904 100644 --- a/src/opengeodeweb_back/routes/blueprint_routes.py +++ b/src/opengeodeweb_back/routes/blueprint_routes.py @@ -10,7 +10,8 @@ # Local application imports from .. import geode_functions, utils_functions - +from opengeodeweb_microservice.database.data import Data +from opengeodeweb_microservice.database.connection import get_session from .models import blueprint_models routes = flask.Blueprint("routes", __name__, url_prefix="/opengeodeweb_back") @@ -257,31 +258,6 @@ def save_viewable_file(): 200, ) - -with open(os.path.join(schemas, "create_point.json"), "r") as file: - create_point_json = json.load(file) - - -@routes.route(create_point_json["route"], methods=create_point_json["methods"]) -def create_point(): - utils_functions.validate_request(flask.request, create_point_json) - title = flask.request.get_json()["title"] - x = flask.request.get_json()["x"] - y = flask.request.get_json()["y"] - z = flask.request.get_json()["z"] - class_ = geode_functions.geode_object_class("PointSet3D") - PointSet3D = class_.create() - builder = geode_functions.create_builder("PointSet3D", PointSet3D) - builder.create_point(opengeode.Point3D([x, y, z])) - builder.set_name(title) - return flask.make_response( - utils_functions.generate_native_viewable_and_light_viewable_from_object( - "PointSet3D", PointSet3D - ), - 200, - ) - - with open(os.path.join(schemas, "texture_coordinates.json"), "r") as file: texture_coordinates_json = json.load(file) diff --git a/src/opengeodeweb_back/routes/create/blueprint_create.py b/src/opengeodeweb_back/routes/create/blueprint_create.py new file mode 100644 index 00000000..ce88c3d7 --- /dev/null +++ b/src/opengeodeweb_back/routes/create/blueprint_create.py @@ -0,0 +1,138 @@ +# Standard library imports +import json +import os +import uuid + +# Third party imports +import flask +import opengeode +import werkzeug + +# Local application imports +from opengeodeweb_back import geode_functions, utils_functions +from opengeodeweb_microservice.database.data import Data +from opengeodeweb_microservice.database.connection import get_session +from opengeodeweb_back.utils_functions import save_all_viewables_and_return_info + +routes = flask.Blueprint("create", __name__, url_prefix="/opengeodeweb_back/create") + +schemas = os.path.join(os.path.dirname(__file__), "schemas") + +# Load schema for point creation +with open(os.path.join(schemas, "create_point.json"), "r") as file: + create_point_json = json.load(file) + +@routes.route( + create_point_json["route"], + methods=create_point_json["methods"] +) +def create_point(): + """Endpoint to create a single point in 3D space.""" + print(f"create_point : {flask.request=}", flush=True) + + utils_functions.validate_request(flask.request, create_point_json) + + # Extract data from request + title = flask.request.json["title"] + x = flask.request.json["x"] + y = flask.request.json["y"] + z = flask.request.json["z"] + + # Create the point set + class_ = geode_functions.geode_object_class("PointSet3D") + point_set = class_.create() + builder = geode_functions.create_builder("PointSet3D", point_set) + builder.create_point(opengeode.Point3D([x, y, z])) + builder.set_name(title) + + # Save and get info + result = save_all_viewables_and_return_info( + geode_object="PointSet3D", + data=point_set, + input_file=None, + additional_files=[] + ) + + # Prepare response with the title + response = { + "viewable_file_name": result["viewable_file_name"], + "id": result["id"], + "name": title, + "native_file_name": result["native_file_name"], + "object_type": result["object_type"], + "geode_object": result["geode_object"], + } + + # Add binary_light_viewable if it exists + if "binary_light_viewable" in result: + response["binary_light_viewable"] = result["binary_light_viewable"] + + return flask.make_response(response, 200) + + +# Load schema for AOI creation +with open(os.path.join(schemas, "create_aoi.json"), "r") as file: + create_aoi_json = json.load(file) + +@routes.route( + create_aoi_json["route"], + methods=create_aoi_json["methods"] +) +def create_aoi(): + """Endpoint to create an Area of Interest (AOI) as an EdgedCurve3D.""" + print(f"create_aoi : {flask.request=}", flush=True) + + utils_functions.validate_request(flask.request, create_aoi_json) + + # Extract data from request + name = flask.request.json["name"] + points = flask.request.json["points"] + z = flask.request.json["z"] + + # Create the edged curve + class_ = geode_functions.geode_object_class("EdgedCurve3D") + edged_curve = class_.create() + builder = geode_functions.create_builder("EdgedCurve3D", edged_curve) + builder.set_name(name) + + # Create vertices first + vertex_indices = [] + for point in points: + vertex_id = builder.create_point(opengeode.Point3D([point["x"], point["y"], z])) + vertex_indices.append(vertex_id) + + # Create edges between consecutive vertices and close the loop + num_vertices = len(vertex_indices) + for i in range(num_vertices): + edge_id = builder.create_edge() + next_i = (i + 1) % num_vertices # Wrap around to close the loop + builder.set_edge_vertex( + opengeode.EdgeVertex(edge_id, 0), vertex_indices[i] + ) + builder.set_edge_vertex( + opengeode.EdgeVertex(edge_id, 1), vertex_indices[next_i] + ) + + # Save and get info + result = save_all_viewables_and_return_info( + geode_object="EdgedCurve3D", + data=edged_curve, + input_file=None, + additional_files=[] + ) + + # Prepare response + response = { + "viewable_file_name": result["viewable_file_name"], + "id": result["id"], + "name": name, + "native_file_name": result["native_file_name"], + "object_type": result["object_type"], + "geode_object": result["geode_object"], + } + + # Add binary_light_viewable if it exists + if "binary_light_viewable" in result: + response["binary_light_viewable"] = result["binary_light_viewable"] + + return flask.make_response(response, 200) diff --git a/src/opengeodeweb_back/routes/create/schemas/create_aoi.json b/src/opengeodeweb_back/routes/create/schemas/create_aoi.json new file mode 100644 index 00000000..0dc7a3f4 --- /dev/null +++ b/src/opengeodeweb_back/routes/create/schemas/create_aoi.json @@ -0,0 +1,46 @@ +{ + "route": "/create_aoi", + "methods": [ + "POST" + ], + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the AOI" + }, + "points": { + "type": "array", + "items": { + "type": "object", + "properties": { + "x": { + "type": "number" + }, + "y": { + "type": "number" + } + }, + "required": [ + "x", + "y" + ], + "additionalProperties": false + }, + "minItems": 4, + "maxItems": 4 + }, + "z": { + "type": "number" + }, + "id": { + "type": "string" + } + }, + "required": [ + "name", + "points", + "z" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/src/opengeodeweb_back/routes/create/schemas/create_point.json b/src/opengeodeweb_back/routes/create/schemas/create_point.json new file mode 100644 index 00000000..3611b2ae --- /dev/null +++ b/src/opengeodeweb_back/routes/create/schemas/create_point.json @@ -0,0 +1,29 @@ +{ + "route": "/create_point", + "methods": [ + "POST" + ], + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 1 + }, + "x": { + "type": "number" + }, + "y": { + "type": "number" + }, + "z": { + "type": "number" + } + }, + "required": [ + "title", + "x", + "y", + "z" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/src/opengeodeweb_back/utils_functions.py b/src/opengeodeweb_back/utils_functions.py index 92633d03..a447ffee 100644 --- a/src/opengeodeweb_back/utils_functions.py +++ b/src/opengeodeweb_back/utils_functions.py @@ -163,7 +163,7 @@ def create_data_folder_from_id(data_id: str) -> str: def save_all_viewables_and_return_info( geode_object: str, data: Any, - input_file: str, + input_file: str | None = None, additional_files: list[str] | None = None, ) -> dict[str, Any]: if additional_files is None: diff --git a/tests/conftest.py b/tests/conftest.py index 04a50e03..25cc679f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,7 +9,7 @@ import pytest # Local application imports -from opengeodeweb_back.app import app +from src.opengeodeweb_back.app import app # from opengeodeweb_back import app_config from opengeodeweb_microservice.database.connection import init_database diff --git a/tests/test_create_routes.py b/tests/test_create_routes.py new file mode 100644 index 00000000..3fa01c4e --- /dev/null +++ b/tests/test_create_routes.py @@ -0,0 +1,208 @@ +# Standard library imports +import os +import uuid + +# Third party imports +import pytest + +# Local application imports +from src.opengeodeweb_back import test_utils + +@pytest.fixture +def point_data(): + return { + "title": "test_point", + "x": 1.0, + "y": 2.0, + "z": 3.0 + } + +@pytest.fixture +def aoi_data(): + return { + "name": "test_aoi", + "points": [ + {"x": 0.0, "y": 0.0}, + {"x": 1.0, "y": 0.0}, + {"x": 1.0, "y": 1.0}, + {"x": 0.0, "y": 1.0} + ], + "z": 0.0 + } + +def test_create_point(client, point_data): + """Test the creation of a point with valid data.""" + route = "/opengeodeweb_back/create/create_point" + + # Test with all required data + response = client.post(route, json=point_data) + assert response.status_code == 200 + + # Verify response data + response_data = response.json + assert "viewable_file_name" in response_data + assert "id" in response_data + assert "name" in response_data + assert "native_file_name" in response_data + assert "object_type" in response_data + assert "geode_object" in response_data + + assert response_data["name"] == point_data["title"] + assert response_data["object_type"] == "mesh" + assert response_data["geode_object"] == "PointSet3D" + + # Test with missing parameters - IMPORTANT: use .copy() to avoid mutation + test_utils.test_route_wrong_params(client, route, lambda: point_data.copy()) + +def test_create_aoi(client, aoi_data): + """Test the creation of an AOI with valid data.""" + route = "/opengeodeweb_back/create/create_aoi" + + # Test with all required data + response = client.post(route, json=aoi_data) + assert response.status_code == 200 + + # Verify response data + response_data = response.json + assert "viewable_file_name" in response_data + assert "id" in response_data + assert "name" in response_data + assert "native_file_name" in response_data + assert "object_type" in response_data + assert "geode_object" in response_data + + assert response_data["name"] == aoi_data["name"] + assert response_data["object_type"] == "mesh" + assert response_data["geode_object"] == "EdgedCurve3D" + + # Test with missing parameters - IMPORTANT: use .copy() to avoid mutation + test_utils.test_route_wrong_params(client, route, lambda: aoi_data.copy()) + +def test_create_point_with_invalid_data(client): + """Test the point creation endpoint with invalid data.""" + route = "/opengeodeweb_back/create/create_point" + + # Test with non-numeric coordinates + invalid_data = { + "title": "invalid_point", + "x": "not_a_number", + "y": 2.0, + "z": 3.0 + } + response = client.post(route, json=invalid_data) + assert response.status_code == 400 + + # Test with missing coordinates + invalid_data = { + "title": "invalid_point", + "y": 2.0, + "z": 3.0 + } + response = client.post(route, json=invalid_data) + assert response.status_code == 400 + +def test_create_aoi_with_invalid_data(client, aoi_data): + """Test the AOI creation endpoint with invalid data.""" + route = "/opengeodeweb_back/create/create_aoi" + + # Test with invalid points + invalid_data = { + **aoi_data, + "points": [ + {"x": "not_a_number", "y": 0.0}, + {"x": 1.0, "y": 0.0}, + {"x": 1.0, "y": 1.0}, + {"x": 0.0, "y": 1.0} + ] + } + response = client.post(route, json=invalid_data) + assert response.status_code == 400 + + # Test with too few points + invalid_data = { + **aoi_data, + "points": [ + {"x": 0.0, "y": 0.0}, + {"x": 1.0, "y": 0.0} + ] + } + response = client.post(route, json=invalid_data) + assert response.status_code == 400 + + # Test with invalid z value + invalid_data = { + **aoi_data, + "z": "not_a_number" + } + response = client.post(route, json=invalid_data) + assert response.status_code == 400 + +def test_create_point_file_generation(client, point_data): + """Test that the point creation generates the correct files.""" + route = "/opengeodeweb_back/create/create_point" + + # Make the request + response = client.post(route, json=point_data) + assert response.status_code == 200 + response_data = response.json + + # Get the data folder path for this specific ID + DATA_FOLDER_PATH = client.application.config["DATA_FOLDER_PATH"] + data_id = response_data["id"] + data_folder = os.path.join(DATA_FOLDER_PATH, data_id) + + # Check that the data folder exists + assert os.path.exists(data_folder) + assert os.path.isdir(data_folder) + + # Check native file exists + native_file_path = os.path.join(data_folder, response_data["native_file_name"]) + assert os.path.exists(native_file_path) + + # Check viewable file exists + viewable_file_path = os.path.join(data_folder, response_data["viewable_file_name"]) + assert os.path.exists(viewable_file_path) + + # Check light viewable file exists if present + if "binary_light_viewable" in response_data: + light_viewable_file_path = os.path.join(data_folder, "light_viewable.vtp") + assert os.path.exists(light_viewable_file_path) + + # Verify file extensions + assert response_data["native_file_name"].endswith(".og_pts3d") + assert response_data["viewable_file_name"].endswith(".vtp") + +def test_create_aoi_file_generation(client, aoi_data): + """Test that the AOI creation generates the correct files.""" + route = "/opengeodeweb_back/create/create_aoi" + + # Make the request + response = client.post(route, json=aoi_data) + assert response.status_code == 200 + response_data = response.json + + # Get the data folder path for this specific ID + DATA_FOLDER_PATH = client.application.config["DATA_FOLDER_PATH"] + data_id = response_data["id"] + data_folder = os.path.join(DATA_FOLDER_PATH, data_id) + + # Check that the data folder exists + assert os.path.exists(data_folder) + assert os.path.isdir(data_folder) + + # Check native file exists + native_file_path = os.path.join(data_folder, response_data["native_file_name"]) + assert os.path.exists(native_file_path) + + # Check viewable file exists + viewable_file_path = os.path.join(data_folder, response_data["viewable_file_name"]) + assert os.path.exists(viewable_file_path) + + # Check light viewable file exists if present + if "binary_light_viewable" in response_data: + light_viewable_file_path = os.path.join(data_folder, "light_viewable.vtp") + assert os.path.exists(light_viewable_file_path) + + # Verify file extensions + assert response_data["native_file_name"].endswith(".og_edc3d") + assert response_data["viewable_file_name"].endswith(".vtp") diff --git a/tests/test_routes.py b/tests/test_routes.py index fd1095e2..3d47d597 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -258,22 +258,6 @@ def test_polyhedron_attribute_names(client, test_id): assert type(polyhedron_attribute_name) is str -def test_create_point(client): - route = f"/opengeodeweb_back/create_point" - get_full_data = lambda: {"title": "test_point", "x": 1, "y": 2, "z": 3} - - # Normal test with all keys - response = client.post(route, json=get_full_data()) - assert response.status_code == 200 - viewable_file_name = response.json["viewable_file_name"] - assert type(viewable_file_name) is str - id = response.json.get("id") - assert type(id) is str - - # Test all params - test_utils.test_route_wrong_params(client, route, get_full_data) - - def test_database_uri_path(client): app = client.application with app.app_context(): From 7801d91168bc3f79cc3ea364508966d305f7802c Mon Sep 17 00:00:00 2001 From: SpliiT Date: Tue, 7 Oct 2025 14:01:49 +0200 Subject: [PATCH 2/8] feat(NewBlueprint): New blueprint_create From cb2c54920829e0016b0a7def14c97a5a6abcd57b Mon Sep 17 00:00:00 2001 From: SpliiT Date: Tue, 7 Oct 2025 14:02:11 +0200 Subject: [PATCH 3/8] feat(NewBlueprint): New blueprint_create --- src/opengeodeweb_back/routes/blueprint_routes.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/opengeodeweb_back/routes/blueprint_routes.py b/src/opengeodeweb_back/routes/blueprint_routes.py index 7682d904..b17da837 100644 --- a/src/opengeodeweb_back/routes/blueprint_routes.py +++ b/src/opengeodeweb_back/routes/blueprint_routes.py @@ -37,7 +37,6 @@ def teardown_request(exception): name=blueprint_models.routes.name, ) - schemas = os.path.join(os.path.dirname(__file__), "schemas") with open( From 65517a809c36eca6e332ce5a273223ff7eae94aa Mon Sep 17 00:00:00 2001 From: SpliiT <106495600+SpliiT@users.noreply.github.com> Date: Tue, 7 Oct 2025 12:25:19 +0000 Subject: [PATCH 4/8] Apply prepare changes --- requirements.txt | 22 ++++----- .../routes/blueprint_routes.py | 1 + .../routes/create/blueprint_create.py | 27 ++++------ src/opengeodeweb_back/utils_functions.py | 2 +- tests/test_create_routes.py | 49 +++++++------------ 5 files changed, 39 insertions(+), 62 deletions(-) diff --git a/requirements.txt b/requirements.txt index c4d40f78..d94d0c61 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,15 +2,15 @@ # This file is autogenerated by pip-compile with Python 3.12 # by the following command: # -# pip-compile --cert=None --client-cert=None --index-url=None --pip-args=None requirements.in +# pip-compile --output-file=./requirements.txt --pre ./requirements.in # -asgiref==3.10.0 +asgiref~=3.10 # via flask -blinker==1.9.0 +blinker~=1.9 # via flask -click==8.3.0 +click~=8.3 # via flask -flask[async]==3.1.2 +flask[async]~=3.1 # via # -r requirements.in # flask-cors @@ -25,13 +25,13 @@ geode-common==33.11.0 # geode-viewables geode-viewables==3.3.0 # via -r requirements.in -greenlet==3.2.4 +greenlet~=3.2 # via sqlalchemy -itsdangerous==2.2.0 +itsdangerous~=2.2 # via flask -jinja2==3.1.6 +jinja2~=3.1 # via flask -markupsafe==3.0.3 +markupsafe~=3.0 # via # flask # jinja2 @@ -59,9 +59,9 @@ opengeode-io==7.4.0 # -r requirements.in # geode-viewables # opengeode-geosciencesio -sqlalchemy==2.0.43 +sqlalchemy~=2.0 # via flask-sqlalchemy -typing-extensions==4.15.0 +typing-extensions~=4.15 # via sqlalchemy werkzeug==3.1.2 # via diff --git a/src/opengeodeweb_back/routes/blueprint_routes.py b/src/opengeodeweb_back/routes/blueprint_routes.py index b17da837..453ce892 100644 --- a/src/opengeodeweb_back/routes/blueprint_routes.py +++ b/src/opengeodeweb_back/routes/blueprint_routes.py @@ -257,6 +257,7 @@ def save_viewable_file(): 200, ) + with open(os.path.join(schemas, "texture_coordinates.json"), "r") as file: texture_coordinates_json = json.load(file) diff --git a/src/opengeodeweb_back/routes/create/blueprint_create.py b/src/opengeodeweb_back/routes/create/blueprint_create.py index ce88c3d7..b835da02 100644 --- a/src/opengeodeweb_back/routes/create/blueprint_create.py +++ b/src/opengeodeweb_back/routes/create/blueprint_create.py @@ -22,10 +22,8 @@ with open(os.path.join(schemas, "create_point.json"), "r") as file: create_point_json = json.load(file) -@routes.route( - create_point_json["route"], - methods=create_point_json["methods"] -) + +@routes.route(create_point_json["route"], methods=create_point_json["methods"]) def create_point(): """Endpoint to create a single point in 3D space.""" print(f"create_point : {flask.request=}", flush=True) @@ -47,10 +45,7 @@ def create_point(): # Save and get info result = save_all_viewables_and_return_info( - geode_object="PointSet3D", - data=point_set, - input_file=None, - additional_files=[] + geode_object="PointSet3D", data=point_set, input_file=None, additional_files=[] ) # Prepare response with the title @@ -62,7 +57,7 @@ def create_point(): "object_type": result["object_type"], "geode_object": result["geode_object"], } - + # Add binary_light_viewable if it exists if "binary_light_viewable" in result: response["binary_light_viewable"] = result["binary_light_viewable"] @@ -74,10 +69,8 @@ def create_point(): with open(os.path.join(schemas, "create_aoi.json"), "r") as file: create_aoi_json = json.load(file) -@routes.route( - create_aoi_json["route"], - methods=create_aoi_json["methods"] -) + +@routes.route(create_aoi_json["route"], methods=create_aoi_json["methods"]) def create_aoi(): """Endpoint to create an Area of Interest (AOI) as an EdgedCurve3D.""" print(f"create_aoi : {flask.request=}", flush=True) @@ -106,9 +99,7 @@ def create_aoi(): for i in range(num_vertices): edge_id = builder.create_edge() next_i = (i + 1) % num_vertices # Wrap around to close the loop - builder.set_edge_vertex( - opengeode.EdgeVertex(edge_id, 0), vertex_indices[i] - ) + builder.set_edge_vertex(opengeode.EdgeVertex(edge_id, 0), vertex_indices[i]) builder.set_edge_vertex( opengeode.EdgeVertex(edge_id, 1), vertex_indices[next_i] ) @@ -118,7 +109,7 @@ def create_aoi(): geode_object="EdgedCurve3D", data=edged_curve, input_file=None, - additional_files=[] + additional_files=[], ) # Prepare response @@ -130,7 +121,7 @@ def create_aoi(): "object_type": result["object_type"], "geode_object": result["geode_object"], } - + # Add binary_light_viewable if it exists if "binary_light_viewable" in result: response["binary_light_viewable"] = result["binary_light_viewable"] diff --git a/src/opengeodeweb_back/utils_functions.py b/src/opengeodeweb_back/utils_functions.py index a447ffee..40826c6c 100644 --- a/src/opengeodeweb_back/utils_functions.py +++ b/src/opengeodeweb_back/utils_functions.py @@ -163,7 +163,7 @@ def create_data_folder_from_id(data_id: str) -> str: def save_all_viewables_and_return_info( geode_object: str, data: Any, - input_file: str | None = None, + input_file: str | None = None, additional_files: list[str] | None = None, ) -> dict[str, Any]: if additional_files is None: diff --git a/tests/test_create_routes.py b/tests/test_create_routes.py index 3fa01c4e..ef960462 100644 --- a/tests/test_create_routes.py +++ b/tests/test_create_routes.py @@ -8,14 +8,11 @@ # Local application imports from src.opengeodeweb_back import test_utils + @pytest.fixture def point_data(): - return { - "title": "test_point", - "x": 1.0, - "y": 2.0, - "z": 3.0 - } + return {"title": "test_point", "x": 1.0, "y": 2.0, "z": 3.0} + @pytest.fixture def aoi_data(): @@ -25,11 +22,12 @@ def aoi_data(): {"x": 0.0, "y": 0.0}, {"x": 1.0, "y": 0.0}, {"x": 1.0, "y": 1.0}, - {"x": 0.0, "y": 1.0} + {"x": 0.0, "y": 1.0}, ], - "z": 0.0 + "z": 0.0, } + def test_create_point(client, point_data): """Test the creation of a point with valid data.""" route = "/opengeodeweb_back/create/create_point" @@ -54,6 +52,7 @@ def test_create_point(client, point_data): # Test with missing parameters - IMPORTANT: use .copy() to avoid mutation test_utils.test_route_wrong_params(client, route, lambda: point_data.copy()) + def test_create_aoi(client, aoi_data): """Test the creation of an AOI with valid data.""" route = "/opengeodeweb_back/create/create_aoi" @@ -78,29 +77,22 @@ def test_create_aoi(client, aoi_data): # Test with missing parameters - IMPORTANT: use .copy() to avoid mutation test_utils.test_route_wrong_params(client, route, lambda: aoi_data.copy()) + def test_create_point_with_invalid_data(client): """Test the point creation endpoint with invalid data.""" route = "/opengeodeweb_back/create/create_point" # Test with non-numeric coordinates - invalid_data = { - "title": "invalid_point", - "x": "not_a_number", - "y": 2.0, - "z": 3.0 - } + invalid_data = {"title": "invalid_point", "x": "not_a_number", "y": 2.0, "z": 3.0} response = client.post(route, json=invalid_data) assert response.status_code == 400 # Test with missing coordinates - invalid_data = { - "title": "invalid_point", - "y": 2.0, - "z": 3.0 - } + invalid_data = {"title": "invalid_point", "y": 2.0, "z": 3.0} response = client.post(route, json=invalid_data) assert response.status_code == 400 + def test_create_aoi_with_invalid_data(client, aoi_data): """Test the AOI creation endpoint with invalid data.""" route = "/opengeodeweb_back/create/create_aoi" @@ -112,31 +104,23 @@ def test_create_aoi_with_invalid_data(client, aoi_data): {"x": "not_a_number", "y": 0.0}, {"x": 1.0, "y": 0.0}, {"x": 1.0, "y": 1.0}, - {"x": 0.0, "y": 1.0} - ] + {"x": 0.0, "y": 1.0}, + ], } response = client.post(route, json=invalid_data) assert response.status_code == 400 # Test with too few points - invalid_data = { - **aoi_data, - "points": [ - {"x": 0.0, "y": 0.0}, - {"x": 1.0, "y": 0.0} - ] - } + invalid_data = {**aoi_data, "points": [{"x": 0.0, "y": 0.0}, {"x": 1.0, "y": 0.0}]} response = client.post(route, json=invalid_data) assert response.status_code == 400 # Test with invalid z value - invalid_data = { - **aoi_data, - "z": "not_a_number" - } + invalid_data = {**aoi_data, "z": "not_a_number"} response = client.post(route, json=invalid_data) assert response.status_code == 400 + def test_create_point_file_generation(client, point_data): """Test that the point creation generates the correct files.""" route = "/opengeodeweb_back/create/create_point" @@ -172,6 +156,7 @@ def test_create_point_file_generation(client, point_data): assert response_data["native_file_name"].endswith(".og_pts3d") assert response_data["viewable_file_name"].endswith(".vtp") + def test_create_aoi_file_generation(client, aoi_data): """Test that the AOI creation generates the correct files.""" route = "/opengeodeweb_back/create/create_aoi" From 1dece19fdb04d7d5be09639736617d75f1642a9b Mon Sep 17 00:00:00 2001 From: SpliiT Date: Wed, 8 Oct 2025 17:34:44 +0200 Subject: [PATCH 5/8] fix mypy & other comments --- .../routes/create/blueprint_create.py | 204 ++++++++++-------- .../routes/create/schemas/create_point.json | 4 +- tests/test_create_routes.py | 123 +++++------ 3 files changed, 169 insertions(+), 162 deletions(-) diff --git a/src/opengeodeweb_back/routes/create/blueprint_create.py b/src/opengeodeweb_back/routes/create/blueprint_create.py index ce88c3d7..0993194e 100644 --- a/src/opengeodeweb_back/routes/create/blueprint_create.py +++ b/src/opengeodeweb_back/routes/create/blueprint_create.py @@ -1,7 +1,7 @@ # Standard library imports import json import os -import uuid +from typing import Dict, Any, List, TypedDict, cast # Third party imports import flask @@ -15,124 +15,140 @@ from opengeodeweb_back.utils_functions import save_all_viewables_and_return_info routes = flask.Blueprint("create", __name__, url_prefix="/opengeodeweb_back/create") - schemas = os.path.join(os.path.dirname(__file__), "schemas") +# --- Type definitions for request validation --- +class Point(TypedDict): + x: float + y: float + +class CreatePointRequest(TypedDict): + name: str + x: float + y: float + z: float + +class CreateAOIRequest(TypedDict): + name: str + points: List[Point] + z: float + # Load schema for point creation with open(os.path.join(schemas, "create_point.json"), "r") as file: - create_point_json = json.load(file) + create_point_json = cast(Dict[str, Any], json.load(file)) @routes.route( create_point_json["route"], methods=create_point_json["methods"] ) -def create_point(): +def create_point() -> flask.Response: """Endpoint to create a single point in 3D space.""" print(f"create_point : {flask.request=}", flush=True) - utils_functions.validate_request(flask.request, create_point_json) - # Extract data from request - title = flask.request.json["title"] - x = flask.request.json["x"] - y = flask.request.json["y"] - z = flask.request.json["z"] - - # Create the point set - class_ = geode_functions.geode_object_class("PointSet3D") - point_set = class_.create() - builder = geode_functions.create_builder("PointSet3D", point_set) - builder.create_point(opengeode.Point3D([x, y, z])) - builder.set_name(title) - - # Save and get info - result = save_all_viewables_and_return_info( - geode_object="PointSet3D", - data=point_set, - input_file=None, - additional_files=[] - ) - - # Prepare response with the title - response = { - "viewable_file_name": result["viewable_file_name"], - "id": result["id"], - "name": title, - "native_file_name": result["native_file_name"], - "object_type": result["object_type"], - "geode_object": result["geode_object"], - } - - # Add binary_light_viewable if it exists - if "binary_light_viewable" in result: - response["binary_light_viewable"] = result["binary_light_viewable"] - - return flask.make_response(response, 200) + try: + # Extract and validate data from request + request_data = cast(CreatePointRequest, flask.request.json) + name: str = request_data["name"] + x: float = request_data["x"] + y: float = request_data["y"] + z: float = request_data["z"] + + # Create the point set + class_ = geode_functions.geode_object_class("PointSet3D") + point_set = class_.create() + builder = geode_functions.create_builder("PointSet3D", point_set) + builder.create_point(opengeode.Point3D([x, y, z])) # type: ignore + builder.set_name(name) + + # Save and get info + result = save_all_viewables_and_return_info( + geode_object="PointSet3D", + data=point_set, + ) + # Vérifier que binary_light_viewable est présent + if "binary_light_viewable" not in result: + raise ValueError("binary_light_viewable is missing in the result") + + # Prepare response + response: Dict[str, Any] = { + "viewable_file_name": result["viewable_file_name"], + "id": result["id"], + "name": name, + "native_file_name": result["native_file_name"], + "object_type": result["object_type"], + "geode_object": result["geode_object"], + "binary_light_viewable": result["binary_light_viewable"], + } + + return flask.make_response(response, 200) + + except Exception as e: + return flask.make_response({"error": str(e)}, 500) # Load schema for AOI creation with open(os.path.join(schemas, "create_aoi.json"), "r") as file: - create_aoi_json = json.load(file) + create_aoi_json = cast(Dict[str, Any], json.load(file)) @routes.route( create_aoi_json["route"], methods=create_aoi_json["methods"] ) -def create_aoi(): +def create_aoi() -> flask.Response: """Endpoint to create an Area of Interest (AOI) as an EdgedCurve3D.""" print(f"create_aoi : {flask.request=}", flush=True) - utils_functions.validate_request(flask.request, create_aoi_json) - # Extract data from request - name = flask.request.json["name"] - points = flask.request.json["points"] - z = flask.request.json["z"] - - # Create the edged curve - class_ = geode_functions.geode_object_class("EdgedCurve3D") - edged_curve = class_.create() - builder = geode_functions.create_builder("EdgedCurve3D", edged_curve) - builder.set_name(name) - - # Create vertices first - vertex_indices = [] - for point in points: - vertex_id = builder.create_point(opengeode.Point3D([point["x"], point["y"], z])) - vertex_indices.append(vertex_id) - - # Create edges between consecutive vertices and close the loop - num_vertices = len(vertex_indices) - for i in range(num_vertices): - edge_id = builder.create_edge() - next_i = (i + 1) % num_vertices # Wrap around to close the loop - builder.set_edge_vertex( - opengeode.EdgeVertex(edge_id, 0), vertex_indices[i] - ) - builder.set_edge_vertex( - opengeode.EdgeVertex(edge_id, 1), vertex_indices[next_i] + try: + # Extract and validate data from request + request_data = cast(CreateAOIRequest, flask.request.json) + name: str = request_data["name"] + points: List[Point] = request_data["points"] + z: float = request_data["z"] + + # Create the edged curve + class_ = geode_functions.geode_object_class("EdgedCurve3D") + edged_curve = class_.create() + builder = geode_functions.create_builder("EdgedCurve3D", edged_curve) + builder.set_name(name) + + # Create vertices first + vertex_indices: List[int] = [] + for point in points: + vertex_id = builder.create_point(opengeode.Point3D([point["x"], point["y"], z])) # type: ignore + vertex_indices.append(vertex_id) + + # Create edges between consecutive vertices and close the loop + num_vertices = len(vertex_indices) + for i in range(num_vertices): + next_i = (i + 1) % num_vertices + edge_id = builder.create_edge() + builder.set_edge_vertex(opengeode.EdgeVertex(edge_id, 0), vertex_indices[i]) # type: ignore + builder.set_edge_vertex(opengeode.EdgeVertex(edge_id, 1), vertex_indices[next_i]) # type: ignore + + # Save and get info + result = save_all_viewables_and_return_info( + geode_object="EdgedCurve3D", + data=edged_curve, ) - # Save and get info - result = save_all_viewables_and_return_info( - geode_object="EdgedCurve3D", - data=edged_curve, - input_file=None, - additional_files=[] - ) - - # Prepare response - response = { - "viewable_file_name": result["viewable_file_name"], - "id": result["id"], - "name": name, - "native_file_name": result["native_file_name"], - "object_type": result["object_type"], - "geode_object": result["geode_object"], - } - - # Add binary_light_viewable if it exists - if "binary_light_viewable" in result: - response["binary_light_viewable"] = result["binary_light_viewable"] - - return flask.make_response(response, 200) + # Vérifier que binary_light_viewable est présent + if "binary_light_viewable" not in result: + raise ValueError("binary_light_viewable is missing in the result") + + # Prepare response + response: Dict[str, Any] = { + "viewable_file_name": result["viewable_file_name"], + "id": result["id"], + "name": name, + "native_file_name": result["native_file_name"], + "object_type": result["object_type"], + "geode_object": result["geode_object"], + "binary_light_viewable": result["binary_light_viewable"], + } + + return flask.make_response(response, 200) + + except Exception as e: + return flask.make_response({"error": str(e)}, 500) diff --git a/src/opengeodeweb_back/routes/create/schemas/create_point.json b/src/opengeodeweb_back/routes/create/schemas/create_point.json index 3611b2ae..3f0da7dc 100644 --- a/src/opengeodeweb_back/routes/create/schemas/create_point.json +++ b/src/opengeodeweb_back/routes/create/schemas/create_point.json @@ -5,7 +5,7 @@ ], "type": "object", "properties": { - "title": { + "name": { "type": "string", "minLength": 1 }, @@ -20,7 +20,7 @@ } }, "required": [ - "title", + "name", "x", "y", "z" diff --git a/tests/test_create_routes.py b/tests/test_create_routes.py index 3fa01c4e..1c912398 100644 --- a/tests/test_create_routes.py +++ b/tests/test_create_routes.py @@ -1,6 +1,6 @@ # Standard library imports import os -import uuid +from typing import Dict, Any, TypedDict, cast # Third party imports import pytest @@ -8,17 +8,34 @@ # Local application imports from src.opengeodeweb_back import test_utils +# --- Type definitions for test data --- +class Point(TypedDict): + x: float + y: float + +class PointData(TypedDict): + name: str + x: float + y: float + z: float + +class AOIData(TypedDict): + name: str + points: list[Point] + z: float + +# --- Fixtures --- @pytest.fixture -def point_data(): +def point_data() -> PointData: return { - "title": "test_point", + "name": "test_point", "x": 1.0, "y": 2.0, "z": 3.0 } @pytest.fixture -def aoi_data(): +def aoi_data() -> AOIData: return { "name": "test_aoi", "points": [ @@ -30,83 +47,73 @@ def aoi_data(): "z": 0.0 } -def test_create_point(client, point_data): +# --- Tests --- +def test_create_point(client: Any, point_data: PointData) -> None: """Test the creation of a point with valid data.""" - route = "/opengeodeweb_back/create/create_point" - + route: str = "/opengeodeweb_back/create/create_point" # Test with all required data response = client.post(route, json=point_data) assert response.status_code == 200 - # Verify response data - response_data = response.json + response_data: Dict[str, Any] = response.json assert "viewable_file_name" in response_data assert "id" in response_data assert "name" in response_data assert "native_file_name" in response_data assert "object_type" in response_data assert "geode_object" in response_data - - assert response_data["name"] == point_data["title"] + assert response_data["name"] == point_data["name"] assert response_data["object_type"] == "mesh" assert response_data["geode_object"] == "PointSet3D" - # Test with missing parameters - IMPORTANT: use .copy() to avoid mutation - test_utils.test_route_wrong_params(client, route, lambda: point_data.copy()) + test_utils.test_route_wrong_params(client, route, lambda: point_data.copy()) # type: ignore -def test_create_aoi(client, aoi_data): +def test_create_aoi(client: Any, aoi_data: AOIData) -> None: """Test the creation of an AOI with valid data.""" - route = "/opengeodeweb_back/create/create_aoi" - + route: str = "/opengeodeweb_back/create/create_aoi" # Test with all required data response = client.post(route, json=aoi_data) assert response.status_code == 200 - # Verify response data - response_data = response.json + response_data: Dict[str, Any] = response.json assert "viewable_file_name" in response_data assert "id" in response_data assert "name" in response_data assert "native_file_name" in response_data assert "object_type" in response_data assert "geode_object" in response_data - assert response_data["name"] == aoi_data["name"] assert response_data["object_type"] == "mesh" assert response_data["geode_object"] == "EdgedCurve3D" - # Test with missing parameters - IMPORTANT: use .copy() to avoid mutation - test_utils.test_route_wrong_params(client, route, lambda: aoi_data.copy()) + test_utils.test_route_wrong_params(client, route, lambda: aoi_data.copy()) # type: ignore -def test_create_point_with_invalid_data(client): +def test_create_point_with_invalid_data(client: Any) -> None: """Test the point creation endpoint with invalid data.""" - route = "/opengeodeweb_back/create/create_point" - + route: str = "/opengeodeweb_back/create/create_point" # Test with non-numeric coordinates - invalid_data = { - "title": "invalid_point", + invalid_data: Dict[str, Any] = { + "name": "invalid_point", "x": "not_a_number", "y": 2.0, "z": 3.0 } response = client.post(route, json=invalid_data) assert response.status_code == 400 - # Test with missing coordinates invalid_data = { - "title": "invalid_point", + "name": "invalid_point", "y": 2.0, "z": 3.0 } response = client.post(route, json=invalid_data) assert response.status_code == 400 -def test_create_aoi_with_invalid_data(client, aoi_data): +def test_create_aoi_with_invalid_data(client: Any, aoi_data: AOIData) -> None: """Test the AOI creation endpoint with invalid data.""" - route = "/opengeodeweb_back/create/create_aoi" - + route: str = "/opengeodeweb_back/create/create_aoi" # Test with invalid points - invalid_data = { + invalid_data: Dict[str, Any] = { **aoi_data, "points": [ {"x": "not_a_number", "y": 0.0}, @@ -117,7 +124,6 @@ def test_create_aoi_with_invalid_data(client, aoi_data): } response = client.post(route, json=invalid_data) assert response.status_code == 400 - # Test with too few points invalid_data = { **aoi_data, @@ -128,7 +134,6 @@ def test_create_aoi_with_invalid_data(client, aoi_data): } response = client.post(route, json=invalid_data) assert response.status_code == 400 - # Test with invalid z value invalid_data = { **aoi_data, @@ -137,72 +142,58 @@ def test_create_aoi_with_invalid_data(client, aoi_data): response = client.post(route, json=invalid_data) assert response.status_code == 400 -def test_create_point_file_generation(client, point_data): +def test_create_point_file_generation(client: Any, point_data: PointData) -> None: """Test that the point creation generates the correct files.""" - route = "/opengeodeweb_back/create/create_point" - + route: str = "/opengeodeweb_back/create/create_point" # Make the request response = client.post(route, json=point_data) assert response.status_code == 200 - response_data = response.json - + response_data: Dict[str, Any] = response.json # Get the data folder path for this specific ID - DATA_FOLDER_PATH = client.application.config["DATA_FOLDER_PATH"] - data_id = response_data["id"] - data_folder = os.path.join(DATA_FOLDER_PATH, data_id) - + DATA_FOLDER_PATH: str = client.application.config["DATA_FOLDER_PATH"] + data_id: str = response_data["id"] + data_folder: str = os.path.join(DATA_FOLDER_PATH, data_id) # Check that the data folder exists assert os.path.exists(data_folder) assert os.path.isdir(data_folder) - # Check native file exists - native_file_path = os.path.join(data_folder, response_data["native_file_name"]) + native_file_path: str = os.path.join(data_folder, response_data["native_file_name"]) assert os.path.exists(native_file_path) - # Check viewable file exists - viewable_file_path = os.path.join(data_folder, response_data["viewable_file_name"]) + viewable_file_path: str = os.path.join(data_folder, response_data["viewable_file_name"]) assert os.path.exists(viewable_file_path) - # Check light viewable file exists if present if "binary_light_viewable" in response_data: - light_viewable_file_path = os.path.join(data_folder, "light_viewable.vtp") + light_viewable_file_path: str = os.path.join(data_folder, "light_viewable.vtp") assert os.path.exists(light_viewable_file_path) - # Verify file extensions assert response_data["native_file_name"].endswith(".og_pts3d") assert response_data["viewable_file_name"].endswith(".vtp") -def test_create_aoi_file_generation(client, aoi_data): +def test_create_aoi_file_generation(client: Any, aoi_data: AOIData) -> None: """Test that the AOI creation generates the correct files.""" - route = "/opengeodeweb_back/create/create_aoi" - + route: str = "/opengeodeweb_back/create/create_aoi" # Make the request response = client.post(route, json=aoi_data) assert response.status_code == 200 - response_data = response.json - + response_data: Dict[str, Any] = response.json # Get the data folder path for this specific ID - DATA_FOLDER_PATH = client.application.config["DATA_FOLDER_PATH"] - data_id = response_data["id"] - data_folder = os.path.join(DATA_FOLDER_PATH, data_id) - + DATA_FOLDER_PATH: str = client.application.config["DATA_FOLDER_PATH"] + data_id: str = response_data["id"] + data_folder: str = os.path.join(DATA_FOLDER_PATH, data_id) # Check that the data folder exists assert os.path.exists(data_folder) assert os.path.isdir(data_folder) - # Check native file exists - native_file_path = os.path.join(data_folder, response_data["native_file_name"]) + native_file_path: str = os.path.join(data_folder, response_data["native_file_name"]) assert os.path.exists(native_file_path) - # Check viewable file exists - viewable_file_path = os.path.join(data_folder, response_data["viewable_file_name"]) + viewable_file_path: str = os.path.join(data_folder, response_data["viewable_file_name"]) assert os.path.exists(viewable_file_path) - # Check light viewable file exists if present if "binary_light_viewable" in response_data: - light_viewable_file_path = os.path.join(data_folder, "light_viewable.vtp") + light_viewable_file_path: str = os.path.join(data_folder, "light_viewable.vtp") assert os.path.exists(light_viewable_file_path) - # Verify file extensions assert response_data["native_file_name"].endswith(".og_edc3d") assert response_data["viewable_file_name"].endswith(".vtp") From d2a53df9aaa588c0c82e7ab8e61fd6f755cad971 Mon Sep 17 00:00:00 2001 From: SpliiT Date: Thu, 9 Oct 2025 10:47:17 +0200 Subject: [PATCH 6/8] rm create_point from src/routes/schemas --- .../routes/schemas/create_point.json | 29 ------------------- 1 file changed, 29 deletions(-) delete mode 100644 src/opengeodeweb_back/routes/schemas/create_point.json diff --git a/src/opengeodeweb_back/routes/schemas/create_point.json b/src/opengeodeweb_back/routes/schemas/create_point.json deleted file mode 100644 index 9e204def..00000000 --- a/src/opengeodeweb_back/routes/schemas/create_point.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "route": "/create_point", - "methods": [ - "POST" - ], - "type": "object", - "properties": { - "title": { - "type": "string", - "minLength": 1 - }, - "x": { - "type": "number" - }, - "y": { - "type": "number" - }, - "z": { - "type": "number" - } - }, - "required": [ - "title", - "x", - "y", - "z" - ], - "additionalProperties": false -} \ No newline at end of file From 9a1cadea031e2e87989db7bb0c3f5edfaa40af96 Mon Sep 17 00:00:00 2001 From: SpliiT Date: Thu, 9 Oct 2025 15:12:02 +0200 Subject: [PATCH 7/8] feat(api): global error handling & direct response usage in create endpoints --- requirements.txt | 2 +- src/opengeodeweb_back/app.py | 22 +-- .../routes/create/blueprint_create.py | 160 +++++++----------- 3 files changed, 66 insertions(+), 118 deletions(-) diff --git a/requirements.txt b/requirements.txt index d94d0c61..9746355d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -36,7 +36,7 @@ markupsafe~=3.0 # flask # jinja2 # werkzeug -opengeode-core==15.27.4 +opengeode-core==15.29.0 # via # -r requirements.in # geode-common diff --git a/src/opengeodeweb_back/app.py b/src/opengeodeweb_back/app.py index 79a32f02..2be200ad 100644 --- a/src/opengeodeweb_back/app.py +++ b/src/opengeodeweb_back/app.py @@ -1,34 +1,28 @@ """Packages""" - import argparse import os import time from typing import Any - import flask import flask_cors # type: ignore from flask import Flask, Response from flask_cors import cross_origin from werkzeug.exceptions import HTTPException - from opengeodeweb_back import utils_functions, app_config from opengeodeweb_back.routes import blueprint_routes from opengeodeweb_back.routes.models import blueprint_models from opengeodeweb_back.routes.create import blueprint_create from opengeodeweb_microservice.database.connection import init_database - """ Global config """ app: Flask = flask.Flask(__name__) """ Config variables """ FLASK_DEBUG = True if os.environ.get("FLASK_DEBUG", default=None) == "True" else False - if FLASK_DEBUG == False: app.config.from_object(app_config.ProdConfig) else: app.config.from_object(app_config.DevConfig) - DEFAULT_HOST: str = app.config.get("DEFAULT_HOST") or "localhost" DEFAULT_PORT: int = int(app.config.get("DEFAULT_PORT") or 5000) DEFAULT_DATA_FOLDER_PATH: str = app.config.get("DEFAULT_DATA_FOLDER_PATH") or "./data" @@ -39,19 +33,16 @@ app.config.get("SECONDS_BETWEEN_SHUTDOWNS") or 60.0 ) - app.register_blueprint( blueprint_routes.routes, url_prefix="/opengeodeweb_back", name="opengeodeweb_back", ) - app.register_blueprint( blueprint_models.routes, url_prefix="/opengeodeweb_back/models", name="opengeodeweb_models", ) - app.register_blueprint( blueprint_create.routes, url_prefix="/opengeodeweb_back/create", @@ -63,11 +54,13 @@ utils_functions.kill_task, SECONDS_BETWEEN_SHUTDOWNS, app ) - @app.errorhandler(HTTPException) def errorhandler(e: HTTPException) -> tuple[dict[str, Any], int] | Response: return utils_functions.handle_exception(e) +@app.errorhandler(Exception) +def handle_generic_exception(e: Exception) -> Response: + return flask.make_response({"error": str(e)}, 500) @app.route( "/error", @@ -77,20 +70,17 @@ def return_error() -> Response: flask.abort(500, f"Test") return flask.make_response({}, 500) - @app.route("/", methods=["POST"]) @cross_origin() def root() -> Response: return flask.make_response({}, 200) - @app.route("/kill", methods=["POST"]) @cross_origin() def kill() -> None: print("Manual server kill, shutting down...", flush=True) os._exit(0) - def run_server() -> None: parser = argparse.ArgumentParser( prog="OpenGeodeWeb-Back", description="Backend server for OpenGeodeWeb" @@ -133,20 +123,16 @@ def run_server() -> None: help="Number of minutes before the server times out", ) args = parser.parse_args() - app.config.update(DATA_FOLDER_PATH=args.data_folder_path) app.config.update(UPLOAD_FOLDER=args.upload_folder_path) app.config.update(MINUTES_BEFORE_TIMEOUT=args.timeout) - flask_cors.CORS(app, origins=args.allowed_origins) - print( f"Host: {args.host}, Port: {args.port}, Debug: {args.debug}, " f"Data folder path: {args.data_folder_path}, Timeout: {args.timeout}, " f"Origins: {args.allowed_origins}", flush=True, ) - db_filename: str = app.config.get("DATABASE_FILENAME") or "database.db" db_path = os.path.join(args.data_folder_path, db_filename) os.makedirs(os.path.dirname(db_path), exist_ok=True) @@ -154,10 +140,8 @@ def run_server() -> None: app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False init_database(app, db_filename) print(f"Database initialized at: {db_path}", flush=True) - app.run(debug=args.debug, host=args.host, port=args.port, ssl_context=SSL) - # ''' Main ''' if __name__ == "__main__": run_server() diff --git a/src/opengeodeweb_back/routes/create/blueprint_create.py b/src/opengeodeweb_back/routes/create/blueprint_create.py index 0993194e..f5f3a8c1 100644 --- a/src/opengeodeweb_back/routes/create/blueprint_create.py +++ b/src/opengeodeweb_back/routes/create/blueprint_create.py @@ -1,13 +1,11 @@ # Standard library imports import json import os -from typing import Dict, Any, List, TypedDict, cast - +from typing import Any, TypedDict, cast # Third party imports import flask import opengeode import werkzeug - # Local application imports from opengeodeweb_back import geode_functions, utils_functions from opengeodeweb_microservice.database.data import Data @@ -30,12 +28,12 @@ class CreatePointRequest(TypedDict): class CreateAOIRequest(TypedDict): name: str - points: List[Point] + points: list[Point] z: float # Load schema for point creation with open(os.path.join(schemas, "create_point.json"), "r") as file: - create_point_json = cast(Dict[str, Any], json.load(file)) + create_point_json = cast(dict[str, Any], json.load(file)) @routes.route( create_point_json["route"], @@ -46,50 +44,33 @@ def create_point() -> flask.Response: print(f"create_point : {flask.request=}", flush=True) utils_functions.validate_request(flask.request, create_point_json) - try: - # Extract and validate data from request - request_data = cast(CreatePointRequest, flask.request.json) - name: str = request_data["name"] - x: float = request_data["x"] - y: float = request_data["y"] - z: float = request_data["z"] - - # Create the point set - class_ = geode_functions.geode_object_class("PointSet3D") - point_set = class_.create() - builder = geode_functions.create_builder("PointSet3D", point_set) - builder.create_point(opengeode.Point3D([x, y, z])) # type: ignore - builder.set_name(name) - - # Save and get info - result = save_all_viewables_and_return_info( - geode_object="PointSet3D", - data=point_set, - ) - - # Vérifier que binary_light_viewable est présent - if "binary_light_viewable" not in result: - raise ValueError("binary_light_viewable is missing in the result") - - # Prepare response - response: Dict[str, Any] = { - "viewable_file_name": result["viewable_file_name"], - "id": result["id"], - "name": name, - "native_file_name": result["native_file_name"], - "object_type": result["object_type"], - "geode_object": result["geode_object"], - "binary_light_viewable": result["binary_light_viewable"], - } - - return flask.make_response(response, 200) - - except Exception as e: - return flask.make_response({"error": str(e)}, 500) + # Extract and validate data from request + request_data = cast(CreatePointRequest, flask.request.json) + name: str = request_data["name"] + x: float = request_data["x"] + y: float = request_data["y"] + z: float = request_data["z"] + + # Create the point set + class_ = geode_functions.geode_object_class("PointSet3D") + point_set = class_.create() + builder = geode_functions.create_builder("PointSet3D", point_set) + builder.create_point(opengeode.Point3D([x, y, z])) + builder.set_name(name) + + # Save and get info + result = save_all_viewables_and_return_info( + geode_object="PointSet3D", + data=point_set, + ) + result["name"] = name + if "binary_light_viewable" not in result: + raise ValueError("binary_light_viewable is missing in the result") + return flask.make_response(result, 200) # Load schema for AOI creation with open(os.path.join(schemas, "create_aoi.json"), "r") as file: - create_aoi_json = cast(Dict[str, Any], json.load(file)) + create_aoi_json = cast(dict[str, Any], json.load(file)) @routes.route( create_aoi_json["route"], @@ -100,55 +81,38 @@ def create_aoi() -> flask.Response: print(f"create_aoi : {flask.request=}", flush=True) utils_functions.validate_request(flask.request, create_aoi_json) - try: - # Extract and validate data from request - request_data = cast(CreateAOIRequest, flask.request.json) - name: str = request_data["name"] - points: List[Point] = request_data["points"] - z: float = request_data["z"] - - # Create the edged curve - class_ = geode_functions.geode_object_class("EdgedCurve3D") - edged_curve = class_.create() - builder = geode_functions.create_builder("EdgedCurve3D", edged_curve) - builder.set_name(name) - - # Create vertices first - vertex_indices: List[int] = [] - for point in points: - vertex_id = builder.create_point(opengeode.Point3D([point["x"], point["y"], z])) # type: ignore - vertex_indices.append(vertex_id) - - # Create edges between consecutive vertices and close the loop - num_vertices = len(vertex_indices) - for i in range(num_vertices): - next_i = (i + 1) % num_vertices - edge_id = builder.create_edge() - builder.set_edge_vertex(opengeode.EdgeVertex(edge_id, 0), vertex_indices[i]) # type: ignore - builder.set_edge_vertex(opengeode.EdgeVertex(edge_id, 1), vertex_indices[next_i]) # type: ignore - - # Save and get info - result = save_all_viewables_and_return_info( - geode_object="EdgedCurve3D", - data=edged_curve, - ) - - # Vérifier que binary_light_viewable est présent - if "binary_light_viewable" not in result: - raise ValueError("binary_light_viewable is missing in the result") - - # Prepare response - response: Dict[str, Any] = { - "viewable_file_name": result["viewable_file_name"], - "id": result["id"], - "name": name, - "native_file_name": result["native_file_name"], - "object_type": result["object_type"], - "geode_object": result["geode_object"], - "binary_light_viewable": result["binary_light_viewable"], - } - - return flask.make_response(response, 200) - - except Exception as e: - return flask.make_response({"error": str(e)}, 500) + # Extract and validate data from request + request_data = cast(CreateAOIRequest, flask.request.json) + name: str = request_data["name"] + points: list[Point] = request_data["points"] + z: float = request_data["z"] + + # Create the edged curve + class_ = geode_functions.geode_object_class("EdgedCurve3D") + edged_curve = class_.create() + builder = geode_functions.create_builder("EdgedCurve3D", edged_curve) + builder.set_name(name) + + # Create vertices first + vertex_indices: list[int] = [] + for point in points: + vertex_id = builder.create_point(opengeode.Point3D([point["x"], point["y"], z])) + vertex_indices.append(vertex_id) + + # Create edges between consecutive vertices and close the loop + num_vertices = len(vertex_indices) + for i in range(num_vertices): + next_i = (i + 1) % num_vertices + edge_id = builder.create_edge() + builder.set_edge_vertex(opengeode.EdgeVertex(edge_id, 0), vertex_indices[i]) + builder.set_edge_vertex(opengeode.EdgeVertex(edge_id, 1), vertex_indices[next_i]) + + # Save and get info + result = save_all_viewables_and_return_info( + geode_object="EdgedCurve3D", + data=edged_curve, + ) + result["name"] = name + if "binary_light_viewable" not in result: + raise ValueError("binary_light_viewable is missing in the result") + return flask.make_response(result, 200) From 0a6b95f0741ff969fee3e1f0fe86f5af820403c5 Mon Sep 17 00:00:00 2001 From: SpliiT Date: Fri, 10 Oct 2025 17:35:05 +0200 Subject: [PATCH 8/8] change typage --- .../routes/create/blueprint_create.py | 60 ++++++++----------- 1 file changed, 26 insertions(+), 34 deletions(-) diff --git a/src/opengeodeweb_back/routes/create/blueprint_create.py b/src/opengeodeweb_back/routes/create/blueprint_create.py index f5f3a8c1..bcbac4ad 100644 --- a/src/opengeodeweb_back/routes/create/blueprint_create.py +++ b/src/opengeodeweb_back/routes/create/blueprint_create.py @@ -1,39 +1,31 @@ # Standard library imports import json import os -from typing import Any, TypedDict, cast +from typing import cast, Any # Third party imports import flask import opengeode -import werkzeug # Local application imports from opengeodeweb_back import geode_functions, utils_functions -from opengeodeweb_microservice.database.data import Data -from opengeodeweb_microservice.database.connection import get_session from opengeodeweb_back.utils_functions import save_all_viewables_and_return_info routes = flask.Blueprint("create", __name__, url_prefix="/opengeodeweb_back/create") schemas = os.path.join(os.path.dirname(__file__), "schemas") -# --- Type definitions for request validation --- -class Point(TypedDict): - x: float - y: float +# --- Type definitions for JSON and RPC --- +type JsonPrimitive = str | int | float | bool +type JsonValue = JsonPrimitive | dict[str, JsonValue] | list[JsonValue] +type RpcParams = dict[str, JsonValue] +type SchemaDict = dict[str, Any] # Changé pour éviter cast sur json.load -class CreatePointRequest(TypedDict): - name: str - x: float - y: float - z: float - -class CreateAOIRequest(TypedDict): - name: str - points: list[Point] - z: float +# --- Specialized type aliases for each RPC endpoint --- +type PointDict = dict[str, float] # {"x": float, "y": float} +type CreatePointParams = dict[str, str | float] # {"name": str, "x": float, "y": float, "z": float} +type CreateAOIParams = dict[str, str | float | list[PointDict]] # {"name": str, "points": list[PointDict], "z": float} # Load schema for point creation with open(os.path.join(schemas, "create_point.json"), "r") as file: - create_point_json = cast(dict[str, Any], json.load(file)) + create_point_json: SchemaDict = json.load(file) @routes.route( create_point_json["route"], @@ -45,23 +37,23 @@ def create_point() -> flask.Response: utils_functions.validate_request(flask.request, create_point_json) # Extract and validate data from request - request_data = cast(CreatePointRequest, flask.request.json) - name: str = request_data["name"] - x: float = request_data["x"] - y: float = request_data["y"] - z: float = request_data["z"] + params: CreatePointParams = flask.request.get_json() # type: ignore + name: str = params["name"] # type: ignore + x: float = params["x"] # type: ignore + y: float = params["y"] # type: ignore + z: float = params["z"] # type: ignore - # Create the point set + # Create the point class_ = geode_functions.geode_object_class("PointSet3D") - point_set = class_.create() - builder = geode_functions.create_builder("PointSet3D", point_set) - builder.create_point(opengeode.Point3D([x, y, z])) + pointset = class_.create() + builder = geode_functions.create_builder("PointSet3D", pointset) builder.set_name(name) + builder.create_point(opengeode.Point3D([x, y, z])) # Save and get info result = save_all_viewables_and_return_info( geode_object="PointSet3D", - data=point_set, + data=pointset, ) result["name"] = name if "binary_light_viewable" not in result: @@ -70,7 +62,7 @@ def create_point() -> flask.Response: # Load schema for AOI creation with open(os.path.join(schemas, "create_aoi.json"), "r") as file: - create_aoi_json = cast(dict[str, Any], json.load(file)) + create_aoi_json: SchemaDict = json.load(file) @routes.route( create_aoi_json["route"], @@ -82,10 +74,10 @@ def create_aoi() -> flask.Response: utils_functions.validate_request(flask.request, create_aoi_json) # Extract and validate data from request - request_data = cast(CreateAOIRequest, flask.request.json) - name: str = request_data["name"] - points: list[Point] = request_data["points"] - z: float = request_data["z"] + params: CreateAOIParams = flask.request.get_json() # type: ignore + name: str = params["name"] # type: ignore + points: list[PointDict] = params["points"] # type: ignore + z: float = params["z"] # type: ignore # Create the edged curve class_ = geode_functions.geode_object_class("EdgedCurve3D")