From e6ff5186408a52ede5c86232f10ccb9698313436 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Mon, 8 Dec 2025 10:33:38 +0100 Subject: [PATCH 01/18] feat(extensions): add new blueprint and tests for extension imports --- .../routes/blueprint_routes.py | 77 ++++++++++++++++++ .../routes/schemas/__init__.py | 1 + .../routes/schemas/import_extension.json | 10 +++ .../routes/schemas/import_extension.py | 10 +++ tests/test_models_routes.py | 79 +++++++++++++++++++ 5 files changed, 177 insertions(+) create mode 100644 src/opengeodeweb_back/routes/schemas/import_extension.json create mode 100644 src/opengeodeweb_back/routes/schemas/import_extension.py diff --git a/src/opengeodeweb_back/routes/blueprint_routes.py b/src/opengeodeweb_back/routes/blueprint_routes.py index 654c1116..d5860f5d 100644 --- a/src/opengeodeweb_back/routes/blueprint_routes.py +++ b/src/opengeodeweb_back/routes/blueprint_routes.py @@ -502,3 +502,80 @@ def import_project() -> flask.Response: except KeyError: snapshot = {} return flask.make_response({"snapshot": snapshot}, 200) + + +@routes.route( + schemas_dict["import_extension"]["route"], + methods=schemas_dict["import_extension"]["methods"], +) +def import_extension() -> flask.Response: + """Import a .vext extension file and extract its contents.""" + utils_functions.validate_request(flask.request, schemas_dict["import_extension"]) + + if "file" not in flask.request.files: + flask.abort(400, "No .vext file provided under 'file'") + + vext_file = flask.request.files["file"] + assert vext_file.filename is not None + filename = werkzeug.utils.secure_filename(os.path.basename(vext_file.filename)) + + if not filename.lower().endswith(".vext"): + flask.abort(400, "Uploaded file must be a .vext") + + # Create extensions directory in the data folder + data_folder_path: str = flask.current_app.config.get("DATA_FOLDER_PATH", "") + extensions_folder = os.path.join(data_folder_path, "extensions") + os.makedirs(extensions_folder, exist_ok=True) + + # Extract extension name from filename (e.g., "vease-modeling-0.0.0.vext" -> "vease-modeling") + extension_name = ( + filename.rsplit("-", 1)[0] if "-" in filename else filename.replace(".vext", "") + ) + extension_path = os.path.join(extensions_folder, extension_name) + + # Remove existing extension if present + if os.path.exists(extension_path): + shutil.rmtree(extension_path) + + os.makedirs(extension_path, exist_ok=True) + + # Extract the .vext file + vext_file.stream.seek(0) + with zipfile.ZipFile(vext_file.stream) as zip_archive: + zip_archive.extractall(extension_path) + + # Find the extracted files + dist_path = os.path.join(extension_path, "dist") + if not os.path.exists(dist_path): + flask.abort(400, "Invalid .vext file: missing dist folder") + + # Look for the backend executable and frontend JS + backend_executable = None + frontend_file = None + + for file in os.listdir(dist_path): + file_path = os.path.join(dist_path, file) + if os.path.isfile(file_path): + if file.endswith(".es.js"): + frontend_file = file_path + elif not file.endswith(".js") and not file.endswith(".css"): + # Assume it's the backend executable + backend_executable = file_path + # Make it executable + os.chmod(backend_executable, 0o755) + + if not frontend_file: + flask.abort(400, "Invalid .vext file: missing frontend JavaScript") + + if not backend_executable: + flask.abort(400, "Invalid .vext file: missing backend executable") + + return flask.make_response( + { + "extension_name": extension_name, + "frontend_path": frontend_file, + "backend_path": backend_executable, + "extension_folder": extension_path, + }, + 200, + ) diff --git a/src/opengeodeweb_back/routes/schemas/__init__.py b/src/opengeodeweb_back/routes/schemas/__init__.py index 4ad76b82..4ca67755 100644 --- a/src/opengeodeweb_back/routes/schemas/__init__.py +++ b/src/opengeodeweb_back/routes/schemas/__init__.py @@ -15,3 +15,4 @@ from .cell_attribute_names import * from .allowed_objects import * from .allowed_files import * +from .import_extension import * diff --git a/src/opengeodeweb_back/routes/schemas/import_extension.json b/src/opengeodeweb_back/routes/schemas/import_extension.json new file mode 100644 index 00000000..4d80f553 --- /dev/null +++ b/src/opengeodeweb_back/routes/schemas/import_extension.json @@ -0,0 +1,10 @@ +{ + "route": "/import_extension", + "methods": [ + "POST" + ], + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": false +} diff --git a/src/opengeodeweb_back/routes/schemas/import_extension.py b/src/opengeodeweb_back/routes/schemas/import_extension.py new file mode 100644 index 00000000..256cb5fa --- /dev/null +++ b/src/opengeodeweb_back/routes/schemas/import_extension.py @@ -0,0 +1,10 @@ +from dataclasses_json import DataClassJsonMixin +from dataclasses import dataclass + + +@dataclass +class ImportExtension(DataClassJsonMixin): + def __post_init__(self) -> None: + print(self, flush=True) + + pass diff --git a/tests/test_models_routes.py b/tests/test_models_routes.py index 379e5b16..c187048d 100644 --- a/tests/test_models_routes.py +++ b/tests/test_models_routes.py @@ -185,3 +185,82 @@ def test_save_viewable_workflow_from_object(client: FlaskClient) -> None: assert isinstance(data_id, str) and len(data_id) > 0 assert response.get_json()["geode_object_type"] == "EdgedCurve3D" assert response.get_json()["viewable_file"].endswith(".vtp") + + +def test_import_extension_route(client: FlaskClient, tmp_path: Path) -> None: + """Test importing a .vext extension file.""" + route = "/opengeodeweb_back/import_extension" + original_data_folder = client.application.config["DATA_FOLDER_PATH"] + client.application.config["DATA_FOLDER_PATH"] = os.path.join( + str(tmp_path), "extension_test_data" + ) + vext_path = tmp_path / "test-extension-1.0.0.vext" + with zipfile.ZipFile(vext_path, "w", compression=zipfile.ZIP_DEFLATED) as zipf: + zipf.writestr( + "dist/test-extension-extension.es.js", + "export const metadata = { id: 'test-extension', name: 'Test Extension' };", + ) + zipf.writestr("dist/test-extension-back", "#!/bin/bash\necho 'mock backend'") + zipf.writestr("dist/test-extension.css", ".test { color: red; }") + with open(vext_path, "rb") as f: + response = client.post( + route, + data={"file": (f, "test-extension-1.0.0.vext")}, + content_type="multipart/form-data", + ) + assert response.status_code == 200 + json_data = response.get_json() + assert "extension_name" in json_data + assert "frontend_path" in json_data + assert "backend_path" in json_data + assert "extension_folder" in json_data + assert json_data["extension_name"] == "test-extension" + extensions_folder = os.path.join( + client.application.config["DATA_FOLDER_PATH"], "extensions" + ) + extension_path = os.path.join(extensions_folder, "test-extension") + assert os.path.exists(extension_path) + dist_path = os.path.join(extension_path, "dist") + assert os.path.exists(dist_path) + frontend_js = json_data["frontend_path"] + assert os.path.exists(frontend_js) + assert frontend_js.endswith("-extension.es.js") + backend_exec = json_data["backend_path"] + assert os.path.exists(backend_exec) + assert os.access(backend_exec, os.X_OK) + client.application.config["DATA_FOLDER_PATH"] = original_data_folder + + +def test_import_extension_invalid_file(client: FlaskClient, tmp_path: Path) -> None: + """Test importing an invalid .vext file (missing dist folder).""" + route = "/opengeodeweb_back/import_extension" + original_data_folder = client.application.config["DATA_FOLDER_PATH"] + client.application.config["DATA_FOLDER_PATH"] = os.path.join( + str(tmp_path), "extension_invalid_test" + ) + vext_path = tmp_path / "invalid-extension.vext" + with zipfile.ZipFile(vext_path, "w") as zipf: + zipf.writestr("README.md", "This is invalid") + with open(vext_path, "rb") as f: + response = client.post( + route, + data={"file": (f, "invalid-extension.vext")}, + content_type="multipart/form-data", + ) + assert response.status_code == 400 + client.application.config["DATA_FOLDER_PATH"] = original_data_folder + + +def test_import_extension_wrong_extension(client: FlaskClient, tmp_path: Path) -> None: + """Test uploading a file with wrong extension.""" + route = "/opengeodeweb_back/import_extension" + wrong_file = tmp_path / "not-an-extension.zip" + with open(wrong_file, "wb") as f: + f.write(b"test content") + with open(wrong_file, "rb") as f: + response = client.post( + route, + data={"file": (f, "not-an-extension.zip")}, + content_type="multipart/form-data", + ) + assert response.status_code == 400 From 7090cc1cbef620cbbb6e5f2a8d36430403a572ae Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Mon, 8 Dec 2025 11:59:44 +0100 Subject: [PATCH 02/18] frontend_file as read --- opengeodeweb_back_schemas.json | 11 +++++++++++ src/opengeodeweb_back/routes/blueprint_routes.py | 9 ++++++--- src/opengeodeweb_back/routes/schemas/__init__.py | 2 +- tests/test_models_routes.py | 13 ++++++++----- 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/opengeodeweb_back_schemas.json b/opengeodeweb_back_schemas.json index 939f3206..42494821 100644 --- a/opengeodeweb_back_schemas.json +++ b/opengeodeweb_back_schemas.json @@ -343,6 +343,17 @@ "required": [], "additionalProperties": false }, + "import_extension": { + "$id": "opengeodeweb_back/import_extension", + "route": "/import_extension", + "methods": [ + "POST" + ], + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": false + }, "geographic_coordinate_systems": { "$id": "opengeodeweb_back/geographic_coordinate_systems", "route": "/geographic_coordinate_systems", diff --git a/src/opengeodeweb_back/routes/blueprint_routes.py b/src/opengeodeweb_back/routes/blueprint_routes.py index d5860f5d..040fbd78 100644 --- a/src/opengeodeweb_back/routes/blueprint_routes.py +++ b/src/opengeodeweb_back/routes/blueprint_routes.py @@ -566,16 +566,19 @@ def import_extension() -> flask.Response: if not frontend_file: flask.abort(400, "Invalid .vext file: missing frontend JavaScript") - if not backend_executable: flask.abort(400, "Invalid .vext file: missing backend executable") + # Read the frontend JS content + assert frontend_file is not None + with open(frontend_file, "r", encoding="utf-8") as f: + frontend_content = f.read() + return flask.make_response( { "extension_name": extension_name, - "frontend_path": frontend_file, + "frontend_content": frontend_content, "backend_path": backend_executable, - "extension_folder": extension_path, }, 200, ) diff --git a/src/opengeodeweb_back/routes/schemas/__init__.py b/src/opengeodeweb_back/routes/schemas/__init__.py index 4ca67755..5d37ca18 100644 --- a/src/opengeodeweb_back/routes/schemas/__init__.py +++ b/src/opengeodeweb_back/routes/schemas/__init__.py @@ -9,10 +9,10 @@ from .kill import * from .inspect_file import * from .import_project import * +from .import_extension import * from .geographic_coordinate_systems import * from .geode_objects_and_output_extensions import * from .export_project import * from .cell_attribute_names import * from .allowed_objects import * from .allowed_files import * -from .import_extension import * diff --git a/tests/test_models_routes.py b/tests/test_models_routes.py index c187048d..1c9fd115 100644 --- a/tests/test_models_routes.py +++ b/tests/test_models_routes.py @@ -211,9 +211,8 @@ def test_import_extension_route(client: FlaskClient, tmp_path: Path) -> None: assert response.status_code == 200 json_data = response.get_json() assert "extension_name" in json_data - assert "frontend_path" in json_data + assert "frontend_content" in json_data assert "backend_path" in json_data - assert "extension_folder" in json_data assert json_data["extension_name"] == "test-extension" extensions_folder = os.path.join( client.application.config["DATA_FOLDER_PATH"], "extensions" @@ -222,9 +221,13 @@ def test_import_extension_route(client: FlaskClient, tmp_path: Path) -> None: assert os.path.exists(extension_path) dist_path = os.path.join(extension_path, "dist") assert os.path.exists(dist_path) - frontend_js = json_data["frontend_path"] - assert os.path.exists(frontend_js) - assert frontend_js.endswith("-extension.es.js") + + # Verify frontend content is returned + frontend_content = json_data["frontend_content"] + assert isinstance(frontend_content, str) + assert len(frontend_content) > 0 + assert "export const metadata" in frontend_content + backend_exec = json_data["backend_path"] assert os.path.exists(backend_exec) assert os.access(backend_exec, os.X_OK) From 56b7696904999452550fe5f1fa23e3fa38da18cf Mon Sep 17 00:00:00 2001 From: MaxNumerique <144453705+MaxNumerique@users.noreply.github.com> Date: Mon, 8 Dec 2025 12:51:11 +0000 Subject: [PATCH 03/18] Apply prepare changes --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8325a25b..08d64bdf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -60,4 +60,3 @@ werkzeug==3.1.2 # flask # flask-cors -opengeodeweb-microservice==1.*,>=1.0.10rc1 From 35ead1d0f63f345b961049a733ff580ff239ce5a Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Tue, 9 Dec 2025 14:10:23 +0100 Subject: [PATCH 04/18] EXTENSION_FOLDER_PATH in config --- src/opengeodeweb_back/app.py | 3 +++ src/opengeodeweb_back/app_config.py | 2 ++ src/opengeodeweb_back/routes/blueprint_routes.py | 3 +-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/opengeodeweb_back/app.py b/src/opengeodeweb_back/app.py index bf167398..9b608987 100644 --- a/src/opengeodeweb_back/app.py +++ b/src/opengeodeweb_back/app.py @@ -147,6 +147,9 @@ def run_server() -> None: ) args = parser.parse_args() app.config.update(DATA_FOLDER_PATH=args.data_folder_path) + app.config.update( + EXTENSIONS_FOLDER_PATH=os.path.join(args.data_folder_path, "extensions") + ) 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) diff --git a/src/opengeodeweb_back/app_config.py b/src/opengeodeweb_back/app_config.py index 095b2754..2b17c4dc 100644 --- a/src/opengeodeweb_back/app_config.py +++ b/src/opengeodeweb_back/app_config.py @@ -24,6 +24,7 @@ class ProdConfig(Config): MINUTES_BEFORE_TIMEOUT = "1" SECONDS_BETWEEN_SHUTDOWNS = "10" DATA_FOLDER_PATH = "/data" + EXTENSIONS_FOLDER_PATH = os.path.join(DATA_FOLDER_PATH, "extensions") class DevConfig(Config): @@ -33,3 +34,4 @@ class DevConfig(Config): SECONDS_BETWEEN_SHUTDOWNS = "10" BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DATA_FOLDER_PATH = os.path.join(BASE_DIR, "data") + EXTENSIONS_FOLDER_PATH = os.path.join(DATA_FOLDER_PATH, "extensions") diff --git a/src/opengeodeweb_back/routes/blueprint_routes.py b/src/opengeodeweb_back/routes/blueprint_routes.py index 040fbd78..e65fb7b9 100644 --- a/src/opengeodeweb_back/routes/blueprint_routes.py +++ b/src/opengeodeweb_back/routes/blueprint_routes.py @@ -523,8 +523,7 @@ def import_extension() -> flask.Response: flask.abort(400, "Uploaded file must be a .vext") # Create extensions directory in the data folder - data_folder_path: str = flask.current_app.config.get("DATA_FOLDER_PATH", "") - extensions_folder = os.path.join(data_folder_path, "extensions") + extensions_folder = flask.current_app.config["EXTENSIONS_FOLDER_PATH"] os.makedirs(extensions_folder, exist_ok=True) # Extract extension name from filename (e.g., "vease-modeling-0.0.0.vext" -> "vease-modeling") From 069cc6a19c043ab0eb769cdb896e44b03b879a59 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Tue, 9 Dec 2025 14:36:35 +0100 Subject: [PATCH 05/18] fix test with EXTENSION_FOLDER_PATH --- src/opengeodeweb_back/routes/blueprint_routes.py | 4 ---- tests/test_models_routes.py | 12 ++++++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/opengeodeweb_back/routes/blueprint_routes.py b/src/opengeodeweb_back/routes/blueprint_routes.py index e65fb7b9..6a17e308 100644 --- a/src/opengeodeweb_back/routes/blueprint_routes.py +++ b/src/opengeodeweb_back/routes/blueprint_routes.py @@ -526,7 +526,6 @@ def import_extension() -> flask.Response: extensions_folder = flask.current_app.config["EXTENSIONS_FOLDER_PATH"] os.makedirs(extensions_folder, exist_ok=True) - # Extract extension name from filename (e.g., "vease-modeling-0.0.0.vext" -> "vease-modeling") extension_name = ( filename.rsplit("-", 1)[0] if "-" in filename else filename.replace(".vext", "") ) @@ -558,9 +557,7 @@ def import_extension() -> flask.Response: if file.endswith(".es.js"): frontend_file = file_path elif not file.endswith(".js") and not file.endswith(".css"): - # Assume it's the backend executable backend_executable = file_path - # Make it executable os.chmod(backend_executable, 0o755) if not frontend_file: @@ -568,7 +565,6 @@ def import_extension() -> flask.Response: if not backend_executable: flask.abort(400, "Invalid .vext file: missing backend executable") - # Read the frontend JS content assert frontend_file is not None with open(frontend_file, "r", encoding="utf-8") as f: frontend_content = f.read() diff --git a/tests/test_models_routes.py b/tests/test_models_routes.py index 1c9fd115..b08ad3d7 100644 --- a/tests/test_models_routes.py +++ b/tests/test_models_routes.py @@ -191,8 +191,10 @@ def test_import_extension_route(client: FlaskClient, tmp_path: Path) -> None: """Test importing a .vext extension file.""" route = "/opengeodeweb_back/import_extension" original_data_folder = client.application.config["DATA_FOLDER_PATH"] - client.application.config["DATA_FOLDER_PATH"] = os.path.join( - str(tmp_path), "extension_test_data" + new_data_folder = os.path.join(str(tmp_path), "extension_test_data") + client.application.config["DATA_FOLDER_PATH"] = new_data_folder + client.application.config["EXTENSIONS_FOLDER_PATH"] = os.path.join( + new_data_folder, "extensions" ) vext_path = tmp_path / "test-extension-1.0.0.vext" with zipfile.ZipFile(vext_path, "w", compression=zipfile.ZIP_DEFLATED) as zipf: @@ -238,8 +240,10 @@ def test_import_extension_invalid_file(client: FlaskClient, tmp_path: Path) -> N """Test importing an invalid .vext file (missing dist folder).""" route = "/opengeodeweb_back/import_extension" original_data_folder = client.application.config["DATA_FOLDER_PATH"] - client.application.config["DATA_FOLDER_PATH"] = os.path.join( - str(tmp_path), "extension_invalid_test" + new_data_folder = os.path.join(str(tmp_path), "extension_invalid_test") + client.application.config["DATA_FOLDER_PATH"] = new_data_folder + client.application.config["EXTENSIONS_FOLDER_PATH"] = os.path.join( + new_data_folder, "extensions" ) vext_path = tmp_path / "invalid-extension.vext" with zipfile.ZipFile(vext_path, "w") as zipf: From 1c9bb6806cb4ea3ed0ab2336140cb4d057bc61fe Mon Sep 17 00:00:00 2001 From: JulienChampagnol Date: Wed, 7 Jan 2026 14:31:14 +0100 Subject: [PATCH 06/18] Feat(App): create_app, register_ogw_back_blueprints, run_server --- src/opengeodeweb_back/app.py | 184 ++++++++---------- .../routes/blueprint_routes.py | 13 +- 2 files changed, 90 insertions(+), 107 deletions(-) diff --git a/src/opengeodeweb_back/app.py b/src/opengeodeweb_back/app.py index 9b608987..1caaa7c9 100644 --- a/src/opengeodeweb_back/app.py +++ b/src/opengeodeweb_back/app.py @@ -14,108 +14,100 @@ from opengeodeweb_back.routes.create import blueprint_create from opengeodeweb_microservice.database import connection -""" 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" -ORIGINS: Any = app.config.get("ORIGINS") -TIMEOUT: int = int(app.config.get("MINUTES_BEFORE_TIMEOUT") or 30) -SSL: Any = app.config.get("SSL") -SECONDS_BETWEEN_SHUTDOWNS: float = float( - app.config.get("SECONDS_BETWEEN_SHUTDOWNS") or 60.0 -) - - -@app.before_request -def before_request() -> flask.Response | None: - if flask.request.method == "OPTIONS": - response = flask.make_response() - response.headers["Access-Control-Allow-Methods"] = "GET,POST,PUT,DELETE,OPTIONS" - return response - utils_functions.before_request(flask.current_app) - return None - - -@app.teardown_request -def teardown_request(exception: BaseException | None) -> None: - utils_functions.teardown_request(flask.current_app, exception) - - -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", - name="opengeodeweb_create", -) - -if FLASK_DEBUG == False: - utils_functions.set_interval( - utils_functions.kill_task, SECONDS_BETWEEN_SHUTDOWNS, app +def create_app(name: str) -> flask.Flask: + app = 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) + + if FLASK_DEBUG == False: + SECONDS_BETWEEN_SHUTDOWNS: float = float( + app.config.get("SECONDS_BETWEEN_SHUTDOWNS") or 60.0 + ) + utils_functions.set_interval( + utils_functions.kill_task, SECONDS_BETWEEN_SHUTDOWNS, app + ) + + @app.before_request + def before_request() -> flask.Response | None: + if flask.request.method == "OPTIONS": + response = flask.make_response() + response.headers["Access-Control-Allow-Methods"] = "GET,POST,PUT,DELETE,OPTIONS" + return response + utils_functions.before_request(flask.current_app) + return None + + @app.teardown_request + def teardown_request(exception: BaseException | None) -> None: + utils_functions.teardown_request(flask.current_app, exception) + + @app.errorhandler(HTTPException) + def errorhandler(exception: HTTPException) -> tuple[dict[str, Any], int] | Response: + return utils_functions.handle_exception(exception) + + + @app.errorhandler(Exception) + def handle_generic_exception(exception: Exception) -> Response: + print("\033[91mError:\033[0m \033[91m" + str(exception) + "\033[0m", flush=True) + return flask.make_response({"description": str(exception)}, 500) + + + @app.route( + "/error", + methods=["POST"], ) + def return_error() -> Response: + flask.abort(500, f"Test") + return flask.make_response({}, 500) -@app.errorhandler(HTTPException) -def errorhandler(exception: HTTPException) -> tuple[dict[str, Any], int] | Response: - return utils_functions.handle_exception(exception) + @app.route("/", methods=["POST"]) + @cross_origin() + def root() -> Response: + return flask.make_response({}, 200) -@app.errorhandler(Exception) -def handle_generic_exception(exception: Exception) -> Response: - print("\033[91mError:\033[0m \033[91m" + str(exception) + "\033[0m", flush=True) - return flask.make_response({"description": str(exception)}, 500) + @app.route("/kill", methods=["POST"]) + @cross_origin() + def kill() -> None: + print("Manual server kill, shutting down...", flush=True) + os._exit(0) + return app -@app.route( - "/error", - methods=["POST"], -) -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 register_ogw_back_blueprints(app: flask.Flask) -> None: + 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", + name="opengeodeweb_create", + ) -def run_server() -> None: +def run_server(app: Flask) -> None: parser = argparse.ArgumentParser( prog="OpenGeodeWeb-Back", description="Backend server for OpenGeodeWeb" ) - parser.add_argument("--host", type=str, default=DEFAULT_HOST, help="Host to run on") + parser.add_argument("--host", type=str, default=app.config.get("DEFAULT_HOST"), help="Host to run on") parser.add_argument( - "-p", "--port", type=int, default=DEFAULT_PORT, help="Port to listen on" + "-p", "--port", type=int, default=app.config.get("DEFAULT_PORT"), help="Port to listen on" ) parser.add_argument( "-d", "--debug", - default=FLASK_DEBUG, + default=app.config.get("FLASK_DEBUG"), help="Whether to run in debug mode", action="store_true", ) @@ -123,32 +115,32 @@ def run_server() -> None: "-dfp", "--data_folder_path", type=str, - default=DEFAULT_DATA_FOLDER_PATH, + default=app.config.get("DEFAULT_DATA_FOLDER_PATH"), help="Path to the folder where data is stored", ) parser.add_argument( "-ufp", "--upload_folder_path", type=str, - default=DEFAULT_DATA_FOLDER_PATH, + default=app.config.get("DEFAULT_DATA_FOLDER_PATH"), help="Path to the folder where uploads are stored", ) parser.add_argument( "-origins", "--allowed_origins", - default=ORIGINS, + default=app.config.get("ORIGINS"), help="Origins that are allowed to connect to the server", ) parser.add_argument( "-t", "--timeout", - default=TIMEOUT, + default=app.config.get("MINUTES_BEFORE_TIMEOUT"), 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( - EXTENSIONS_FOLDER_PATH=os.path.join(args.data_folder_path, "extensions") + EXTENSIONS_FOLDER_PATH=os.path.join(str(args.data_folder_path), "extensions") ) app.config.update(UPLOAD_FOLDER=args.upload_folder_path) app.config.update(MINUTES_BEFORE_TIMEOUT=args.timeout) @@ -161,16 +153,12 @@ def run_server() -> None: ) db_filename: str = app.config.get("DATABASE_FILENAME") or "project.db" - db_path = os.path.join(args.data_folder_path, db_filename) + db_path = os.path.join(str(args.data_folder_path), db_filename) os.makedirs(os.path.dirname(db_path), exist_ok=True) app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{db_path}" app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False connection.init_database(db_path) 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() + app.run(debug=args.debug, host=args.host, port=args.port, ssl_context=app.config.get("SSL")) + print("Server stopped", flush=True) diff --git a/src/opengeodeweb_back/routes/blueprint_routes.py b/src/opengeodeweb_back/routes/blueprint_routes.py index 25aa7a34..3c1eca0d 100644 --- a/src/opengeodeweb_back/routes/blueprint_routes.py +++ b/src/opengeodeweb_back/routes/blueprint_routes.py @@ -17,9 +17,9 @@ from opengeodeweb_microservice.database import connection # Local application imports -from .models import blueprint_models -from . import schemas from opengeodeweb_back import geode_functions, utils_functions +from opengeodeweb_back.routes import schemas +from opengeodeweb_back.routes.models import blueprint_models from opengeodeweb_back.geode_objects import geode_objects from opengeodeweb_back.geode_objects.types import geode_object_type from opengeodeweb_back.geode_objects.geode_mesh import GeodeMesh @@ -544,17 +544,12 @@ def import_extension() -> flask.Response: with zipfile.ZipFile(vext_file.stream) as zip_archive: zip_archive.extractall(extension_path) - # Find the extracted files - dist_path = os.path.join(extension_path, "dist") - if not os.path.exists(dist_path): - flask.abort(400, "Invalid .vext file: missing dist folder") - # Look for the backend executable and frontend JS backend_executable = None frontend_file = None - for file in os.listdir(dist_path): - file_path = os.path.join(dist_path, file) + for file in os.listdir(extension_path): + file_path = os.path.join(extension_path, file) if os.path.isfile(file_path): if file.endswith(".es.js"): frontend_file = file_path From 10431607819c8a6a5866bfed2685f560efcf08a4 Mon Sep 17 00:00:00 2001 From: JulienChampagnol <91873154+JulienChampagnol@users.noreply.github.com> Date: Wed, 7 Jan 2026 13:31:50 +0000 Subject: [PATCH 07/18] Apply prepare changes --- src/opengeodeweb_back/app.py | 37 ++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/src/opengeodeweb_back/app.py b/src/opengeodeweb_back/app.py index 1caaa7c9..58701ba0 100644 --- a/src/opengeodeweb_back/app.py +++ b/src/opengeodeweb_back/app.py @@ -14,16 +14,19 @@ from opengeodeweb_back.routes.create import blueprint_create from opengeodeweb_microservice.database import connection + def create_app(name: str) -> flask.Flask: app = flask.Flask(name) """ Config variables """ - FLASK_DEBUG = True if os.environ.get("FLASK_DEBUG", default=None) == "True" else False + 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) - + if FLASK_DEBUG == False: SECONDS_BETWEEN_SHUTDOWNS: float = float( app.config.get("SECONDS_BETWEEN_SHUTDOWNS") or 60.0 @@ -36,7 +39,9 @@ def create_app(name: str) -> flask.Flask: def before_request() -> flask.Response | None: if flask.request.method == "OPTIONS": response = flask.make_response() - response.headers["Access-Control-Allow-Methods"] = "GET,POST,PUT,DELETE,OPTIONS" + response.headers["Access-Control-Allow-Methods"] = ( + "GET,POST,PUT,DELETE,OPTIONS" + ) return response utils_functions.before_request(flask.current_app) return None @@ -49,13 +54,11 @@ def teardown_request(exception: BaseException | None) -> None: def errorhandler(exception: HTTPException) -> tuple[dict[str, Any], int] | Response: return utils_functions.handle_exception(exception) - @app.errorhandler(Exception) def handle_generic_exception(exception: Exception) -> Response: print("\033[91mError:\033[0m \033[91m" + str(exception) + "\033[0m", flush=True) return flask.make_response({"description": str(exception)}, 500) - @app.route( "/error", methods=["POST"], @@ -64,13 +67,11 @@ 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: @@ -79,6 +80,7 @@ def kill() -> None: return app + def register_ogw_back_blueprints(app: flask.Flask) -> None: app.register_blueprint( blueprint_routes.routes, @@ -96,13 +98,23 @@ def register_ogw_back_blueprints(app: flask.Flask) -> None: name="opengeodeweb_create", ) + def run_server(app: Flask) -> None: parser = argparse.ArgumentParser( prog="OpenGeodeWeb-Back", description="Backend server for OpenGeodeWeb" ) - parser.add_argument("--host", type=str, default=app.config.get("DEFAULT_HOST"), help="Host to run on") parser.add_argument( - "-p", "--port", type=int, default=app.config.get("DEFAULT_PORT"), help="Port to listen on" + "--host", + type=str, + default=app.config.get("DEFAULT_HOST"), + help="Host to run on", + ) + parser.add_argument( + "-p", + "--port", + type=int, + default=app.config.get("DEFAULT_PORT"), + help="Port to listen on", ) parser.add_argument( "-d", @@ -160,5 +172,10 @@ def run_server(app: Flask) -> None: connection.init_database(db_path) print(f"Database initialized at: {db_path}", flush=True) - app.run(debug=args.debug, host=args.host, port=args.port, ssl_context=app.config.get("SSL")) + app.run( + debug=args.debug, + host=args.host, + port=args.port, + ssl_context=app.config.get("SSL"), + ) print("Server stopped", flush=True) From 24d80c899f1eddc55e1bbdc7dd9fdfd5cecbd81b Mon Sep 17 00:00:00 2001 From: JulienChampagnol Date: Wed, 7 Jan 2026 15:08:14 +0100 Subject: [PATCH 08/18] wip unit tests [skip ci] --- tests/conftest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index f9815593..e64adda9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,12 +11,13 @@ import pytest # Local application imports -from opengeodeweb_back.app import app +from opengeodeweb_back.app import create_app from opengeodeweb_microservice.database.connection import init_database TEST_ID = "1" +app = create_app(__name__) @pytest.fixture(scope="session", autouse=True) def configure_test_environment() -> Generator[None, None, None]: From 1f433c882e655ace3341c26d46851e863d3f2242 Mon Sep 17 00:00:00 2001 From: JulienChampagnol <91873154+JulienChampagnol@users.noreply.github.com> Date: Wed, 7 Jan 2026 14:08:50 +0000 Subject: [PATCH 09/18] Apply prepare changes --- tests/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/conftest.py b/tests/conftest.py index e64adda9..feca0c98 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,6 +19,7 @@ app = create_app(__name__) + @pytest.fixture(scope="session", autouse=True) def configure_test_environment() -> Generator[None, None, None]: base_path = Path(__file__).parent From d068a0193a90feb4d39c983e1192192e2f8656f2 Mon Sep 17 00:00:00 2001 From: JulienChampagnol Date: Wed, 7 Jan 2026 15:18:45 +0100 Subject: [PATCH 10/18] test trigger tests --- .../routes/create/blueprint_create.py | 91 +----------- .../routes/create/schemas/__init__.py | 4 +- .../routes/create/schemas/create_aoi.json | 45 ------ .../routes/create/schemas/create_aoi.py | 25 ---- .../routes/create/schemas/create_point.json | 29 ---- .../routes/create/schemas/create_voi.json | 36 ----- .../routes/create/schemas/create_voi.py | 24 ---- .../routes/create/schemas/point.json | 22 +++ .../schemas/{create_point.py => point.py} | 2 +- tests/conftest.py | 5 +- tests/test_create_routes.py | 129 +----------------- 11 files changed, 34 insertions(+), 378 deletions(-) delete mode 100644 src/opengeodeweb_back/routes/create/schemas/create_aoi.json delete mode 100644 src/opengeodeweb_back/routes/create/schemas/create_aoi.py delete mode 100644 src/opengeodeweb_back/routes/create/schemas/create_point.json delete mode 100644 src/opengeodeweb_back/routes/create/schemas/create_voi.json delete mode 100644 src/opengeodeweb_back/routes/create/schemas/create_voi.py create mode 100644 src/opengeodeweb_back/routes/create/schemas/point.json rename src/opengeodeweb_back/routes/create/schemas/{create_point.py => point.py} (84%) diff --git a/src/opengeodeweb_back/routes/create/blueprint_create.py b/src/opengeodeweb_back/routes/create/blueprint_create.py index b788f261..38abbe5a 100644 --- a/src/opengeodeweb_back/routes/create/blueprint_create.py +++ b/src/opengeodeweb_back/routes/create/blueprint_create.py @@ -8,7 +8,7 @@ # Local application imports from opengeodeweb_back import geode_functions, utils_functions -from . import schemas +from opengeodeweb_back.routes.create.schemas import schemas from opengeodeweb_back.geode_objects.geode_point_set3d import GeodePointSet3D from opengeodeweb_back.geode_objects.geode_edged_curve3d import GeodeEdgedCurve3D @@ -17,15 +17,15 @@ @routes.route( - schemas_dict["create_point"]["route"], - methods=schemas_dict["create_point"]["methods"], + schemas_dict["point"]["route"], + methods=schemas_dict["point"]["methods"], ) -def create_point() -> flask.Response: +def point() -> flask.Response: """Endpoint to create a single point in 3D space.""" json_data = utils_functions.validate_request( flask.request, schemas_dict["create_point"] ) - params = schemas.CreatePoint.from_dict(json_data) + params = schemas.Point.from_dict(json_data) # Create the point pointset = GeodePointSet3D() @@ -38,84 +38,3 @@ def create_point() -> flask.Response: pointset ) return flask.make_response(result, 200) - - -@routes.route( - schemas_dict["create_aoi"]["route"], methods=schemas_dict["create_aoi"]["methods"] -) -def create_aoi() -> flask.Response: - """Endpoint to create an Area of Interest (AOI) as an EdgedCurve3D.""" - json_data = utils_functions.validate_request( - flask.request, schemas_dict["create_aoi"] - ) - params = schemas.CreateAoi.from_dict(json_data) - - # Create the edged curve - edged_curve = GeodeEdgedCurve3D() - builder = edged_curve.builder() - builder.set_name(params.name) - - # Create vertices first - for point in params.points: - builder.create_point(opengeode.Point3D([point.x, point.y, params.z])) - - # Create edges between consecutive vertices and close the loop - num_vertices = len(params.points) - for i in range(num_vertices): - next_i = (i + 1) % num_vertices - builder.create_edge_with_vertices(i, next_i) - - # Save and get info - result = utils_functions.generate_native_viewable_and_light_viewable_from_object( - edged_curve - ) - return flask.make_response(result, 200) - - -@routes.route( - schemas_dict["create_voi"]["route"], methods=schemas_dict["create_voi"]["methods"] -) -def create_voi() -> flask.Response: - """Endpoint to create a Volume of Interest (VOI) as an EdgedCurve3D (a bounding box/prism).""" - json_data = utils_functions.validate_request( - flask.request, schemas_dict["create_voi"] - ) - params = schemas.CreateVoi.from_dict(json_data) - - aoi_data = geode_functions.get_data_info(params.aoi_id) - if not aoi_data: - flask.abort(404, f"AOI with id {params.aoi_id} not found") - - aoi_object = geode_functions.load_geode_object(params.aoi_id) - if not isinstance(aoi_object, GeodeEdgedCurve3D): - flask.abort(400, f"AOI with id {params.aoi_id} is not a GeodeEdgedCurve3D") - - aoi_curve = aoi_object.edged_curve - nb_points = aoi_curve.nb_vertices() - - edged_curve = GeodeEdgedCurve3D() - builder = edged_curve.builder() - builder.set_name(params.name) - - for point_id in range(nb_points): - aoi_point = aoi_curve.point(point_id) - builder.create_point( - opengeode.Point3D([aoi_point.value(0), aoi_point.value(1), params.z_min]) - ) - - for point_id in range(nb_points): - aoi_point = aoi_curve.point(point_id) - builder.create_point( - opengeode.Point3D([aoi_point.value(0), aoi_point.value(1), params.z_max]) - ) - - for point_id in range(nb_points): - next_point = (point_id + 1) % nb_points - builder.create_edge_with_vertices(point_id, next_point) - builder.create_edge_with_vertices(point_id + nb_points, next_point + nb_points) - builder.create_edge_with_vertices(point_id, point_id + nb_points) - - result = utils_functions.generate_native_viewable_and_light_viewable_from_object( - edged_curve - ) - return flask.make_response(result, 200) diff --git a/src/opengeodeweb_back/routes/create/schemas/__init__.py b/src/opengeodeweb_back/routes/create/schemas/__init__.py index 3d3f9b73..8b137891 100644 --- a/src/opengeodeweb_back/routes/create/schemas/__init__.py +++ b/src/opengeodeweb_back/routes/create/schemas/__init__.py @@ -1,3 +1 @@ -from .create_voi import * -from .create_point import * -from .create_aoi import * + diff --git a/src/opengeodeweb_back/routes/create/schemas/create_aoi.json b/src/opengeodeweb_back/routes/create/schemas/create_aoi.json deleted file mode 100644 index c9321491..00000000 --- a/src/opengeodeweb_back/routes/create/schemas/create_aoi.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "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": 3 - }, - "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_aoi.py b/src/opengeodeweb_back/routes/create/schemas/create_aoi.py deleted file mode 100644 index 38f8f53a..00000000 --- a/src/opengeodeweb_back/routes/create/schemas/create_aoi.py +++ /dev/null @@ -1,25 +0,0 @@ -from dataclasses_json import DataClassJsonMixin -from dataclasses import dataclass -from typing import List, Optional - - -@dataclass -class Point(DataClassJsonMixin): - def __post_init__(self) -> None: - print(self, flush=True) - - x: float - y: float - - -@dataclass -class CreateAoi(DataClassJsonMixin): - def __post_init__(self) -> None: - print(self, flush=True) - - name: str - """Name of the AOI""" - - points: List[Point] - z: float - id: Optional[str] = None diff --git a/src/opengeodeweb_back/routes/create/schemas/create_point.json b/src/opengeodeweb_back/routes/create/schemas/create_point.json deleted file mode 100644 index 3f0da7dc..00000000 --- a/src/opengeodeweb_back/routes/create/schemas/create_point.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "route": "/create_point", - "methods": [ - "POST" - ], - "type": "object", - "properties": { - "name": { - "type": "string", - "minLength": 1 - }, - "x": { - "type": "number" - }, - "y": { - "type": "number" - }, - "z": { - "type": "number" - } - }, - "required": [ - "name", - "x", - "y", - "z" - ], - "additionalProperties": false -} \ No newline at end of file diff --git a/src/opengeodeweb_back/routes/create/schemas/create_voi.json b/src/opengeodeweb_back/routes/create/schemas/create_voi.json deleted file mode 100644 index 1b3d630d..00000000 --- a/src/opengeodeweb_back/routes/create/schemas/create_voi.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "route": "/create_voi", - "methods": [ - "POST" - ], - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Name of the VOI" - }, - "aoi_id": { - "type": "string", - "description": "ID of the corresponding AOI" - }, - "z_min": { - "type": "number", - "description": "Minimum Z coordinate" - }, - "z_max": { - "type": "number", - "description": "Maximum Z coordinate" - }, - "id": { - "type": "string", - "description": "Optional ID for updating existing VOI" - } - }, - "required": [ - "name", - "aoi_id", - "z_min", - "z_max" - ], - "additionalProperties": false -} \ No newline at end of file diff --git a/src/opengeodeweb_back/routes/create/schemas/create_voi.py b/src/opengeodeweb_back/routes/create/schemas/create_voi.py deleted file mode 100644 index 885d4dc7..00000000 --- a/src/opengeodeweb_back/routes/create/schemas/create_voi.py +++ /dev/null @@ -1,24 +0,0 @@ -from dataclasses_json import DataClassJsonMixin -from dataclasses import dataclass -from typing import Optional - - -@dataclass -class CreateVoi(DataClassJsonMixin): - def __post_init__(self) -> None: - print(self, flush=True) - - aoi_id: str - """ID of the corresponding AOI""" - - name: str - """Name of the VOI""" - - z_max: float - """Maximum Z coordinate""" - - z_min: float - """Minimum Z coordinate""" - - id: Optional[str] = None - """Optional ID for updating existing VOI""" diff --git a/src/opengeodeweb_back/routes/create/schemas/point.json b/src/opengeodeweb_back/routes/create/schemas/point.json new file mode 100644 index 00000000..07480ee3 --- /dev/null +++ b/src/opengeodeweb_back/routes/create/schemas/point.json @@ -0,0 +1,22 @@ +{ + "route": "/point", + "methods": ["POST"], + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "x": { + "type": "number" + }, + "y": { + "type": "number" + }, + "z": { + "type": "number" + } + }, + "required": ["name", "x", "y", "z"], + "additionalProperties": false +} diff --git a/src/opengeodeweb_back/routes/create/schemas/create_point.py b/src/opengeodeweb_back/routes/create/schemas/point.py similarity index 84% rename from src/opengeodeweb_back/routes/create/schemas/create_point.py rename to src/opengeodeweb_back/routes/create/schemas/point.py index e59577f6..d8b90216 100644 --- a/src/opengeodeweb_back/routes/create/schemas/create_point.py +++ b/src/opengeodeweb_back/routes/create/schemas/point.py @@ -3,7 +3,7 @@ @dataclass -class CreatePoint(DataClassJsonMixin): +class Point(DataClassJsonMixin): def __post_init__(self) -> None: print(self, flush=True) diff --git a/tests/conftest.py b/tests/conftest.py index feca0c98..4d57cbfd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,7 +11,7 @@ import pytest # Local application imports -from opengeodeweb_back.app import create_app +from opengeodeweb_back.app import create_app, register_ogw_back_blueprints from opengeodeweb_microservice.database.connection import init_database @@ -48,7 +48,8 @@ def configure_test_environment() -> Generator[None, None, None]: if tmp_data_path and os.path.exists(tmp_data_path): shutil.rmtree(tmp_data_path, ignore_errors=True) print(f"Cleaned up test data folder: {tmp_data_path}", flush=True) - + register_ogw_back_blueprints(app) + @pytest.fixture def client() -> Generator[FlaskClient, None, None]: diff --git a/tests/test_create_routes.py b/tests/test_create_routes.py index 61b1eb42..a90266a9 100644 --- a/tests/test_create_routes.py +++ b/tests/test_create_routes.py @@ -14,35 +14,9 @@ def point_data() -> test_utils.JsonData: return {"name": "test_point", "x": 1.0, "y": 2.0, "z": 3.0} -@pytest.fixture -def aoi_data() -> test_utils.JsonData: - 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, - } - - -@pytest.fixture -def voi_data() -> test_utils.JsonData: - """Fixture for Volume of Interest (VOI) test data.""" - return { - "name": "test_voi", - "aoi_id": str(uuid.uuid4()), - "z_min": -50.0, - "z_max": 100.0, - "id": str(uuid.uuid4()), - } - - def test_create_point(client: FlaskClient, point_data: test_utils.JsonData) -> None: """Test the creation of a point with valid data.""" - route: str = "/opengeodeweb_back/create/create_point" + route: str = "/opengeodeweb_back/create/point" # Test with all required data response = client.post(route, json=point_data) @@ -65,57 +39,9 @@ def test_create_point(client: FlaskClient, point_data: test_utils.JsonData) -> N test_utils.test_route_wrong_params(client, route, lambda: point_data.copy()) -def test_create_aoi(client: FlaskClient, aoi_data: test_utils.JsonData) -> None: - """Test the creation of an AOI with valid data.""" - 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.get_json() - assert "viewable_file" in response_data - assert "id" in response_data - assert "name" in response_data - assert "native_file" in response_data - assert "viewer_type" in response_data - assert "geode_object_type" in response_data - - assert response_data["name"] == aoi_data["name"] - assert response_data["viewer_type"] == "mesh" - assert response_data["geode_object_type"] == "EdgedCurve3D" - - # Test with missing parameters - test_utils.test_route_wrong_params(client, route, lambda: aoi_data.copy()) - - -def test_create_voi( - client: FlaskClient, aoi_data: test_utils.JsonData, voi_data: test_utils.JsonData -) -> None: - """Test the creation of a VOI with valid data (including optional id).""" - aoi_route = "/opengeodeweb_back/create/create_aoi" - aoi_response = client.post(aoi_route, json=aoi_data) - assert aoi_response.status_code == 200 - aoi_id = aoi_response.get_json()["id"] - - voi_data["aoi_id"] = aoi_id - - voi_route = "/opengeodeweb_back/create/create_voi" - response = client.post(voi_route, json=voi_data) - assert response.status_code == 200 - - response_data = response.get_json() - assert "id" in response_data - assert "name" in response_data - assert response_data["name"] == voi_data["name"] - assert response_data["viewer_type"] == "mesh" - assert response_data["geode_object_type"] == "EdgedCurve3D" - - def test_create_point_with_invalid_data(client: FlaskClient) -> None: """Test the point creation endpoint with invalid data.""" - route: str = "/opengeodeweb_back/create/create_point" + route: str = "/opengeodeweb_back/create/point" # Test with non-numeric coordinates invalid_data: test_utils.JsonData = { @@ -131,54 +57,3 @@ def test_create_point_with_invalid_data(client: FlaskClient) -> None: invalid_data = {"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: FlaskClient, aoi_data: test_utils.JsonData -) -> None: - """Test the AOI creation endpoint with invalid data.""" - route: str = "/opengeodeweb_back/create/create_aoi" - - invalid_data: test_utils.JsonData = { - **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 - - 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 - - invalid_data = {**aoi_data, "z": "not_a_number"} - response = client.post(route, json=invalid_data) - assert response.status_code == 400 - - -def test_create_voi_with_invalid_data( - client: FlaskClient, aoi_data: test_utils.JsonData, voi_data: test_utils.JsonData -) -> None: - """Test the VOI creation endpoint with invalid data.""" - aoi_route = "/opengeodeweb_back/create/create_aoi" - aoi_response = client.post(aoi_route, json=aoi_data) - assert aoi_response.status_code == 200 - aoi_id = aoi_response.get_json()["id"] - - route = "/opengeodeweb_back/create/create_aoi" - - invalid_data = {**voi_data, "aoi_id": aoi_id, "z_min": "not_a_number"} - response = client.post(route, json=invalid_data) - assert response.status_code == 400 - - invalid_data = {**voi_data, "aoi_id": aoi_id, "z_max": "not_a_number"} - response = client.post(route, json=invalid_data) - assert response.status_code == 400 - - invalid_data = {**voi_data, "aoi_id": 12345} - response = client.post(route, json=invalid_data) - assert response.status_code == 400 From f91d8e6ff378a2687d3393840060a49805992838 Mon Sep 17 00:00:00 2001 From: JulienChampagnol <91873154+JulienChampagnol@users.noreply.github.com> Date: Wed, 7 Jan 2026 14:19:14 +0000 Subject: [PATCH 11/18] Apply prepare changes --- opengeodeweb_back_schemas.json | 89 +------------------ .../routes/create/schemas/__init__.py | 2 +- tests/conftest.py | 2 +- 3 files changed, 5 insertions(+), 88 deletions(-) diff --git a/opengeodeweb_back_schemas.json b/opengeodeweb_back_schemas.json index 42494821..50408514 100644 --- a/opengeodeweb_back_schemas.json +++ b/opengeodeweb_back_schemas.json @@ -1,46 +1,9 @@ { "opengeodeweb_back": { "create": { - "create_voi": { - "$id": "opengeodeweb_back/create/create_voi", - "route": "/create_voi", - "methods": [ - "POST" - ], - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Name of the VOI" - }, - "aoi_id": { - "type": "string", - "description": "ID of the corresponding AOI" - }, - "z_min": { - "type": "number", - "description": "Minimum Z coordinate" - }, - "z_max": { - "type": "number", - "description": "Maximum Z coordinate" - }, - "id": { - "type": "string", - "description": "Optional ID for updating existing VOI" - } - }, - "required": [ - "name", - "aoi_id", - "z_min", - "z_max" - ], - "additionalProperties": false - }, - "create_point": { - "$id": "opengeodeweb_back/create/create_point", - "route": "/create_point", + "point": { + "$id": "opengeodeweb_back/create/point", + "route": "/point", "methods": [ "POST" ], @@ -67,52 +30,6 @@ "z" ], "additionalProperties": false - }, - "create_aoi": { - "$id": "opengeodeweb_back/create/create_aoi", - "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": 3 - }, - "z": { - "type": "number" - }, - "id": { - "type": "string" - } - }, - "required": [ - "name", - "points", - "z" - ], - "additionalProperties": false } }, "models": { diff --git a/src/opengeodeweb_back/routes/create/schemas/__init__.py b/src/opengeodeweb_back/routes/create/schemas/__init__.py index 8b137891..86ba70b7 100644 --- a/src/opengeodeweb_back/routes/create/schemas/__init__.py +++ b/src/opengeodeweb_back/routes/create/schemas/__init__.py @@ -1 +1 @@ - +from .point import * diff --git a/tests/conftest.py b/tests/conftest.py index 4d57cbfd..c08f6058 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -49,7 +49,7 @@ def configure_test_environment() -> Generator[None, None, None]: shutil.rmtree(tmp_data_path, ignore_errors=True) print(f"Cleaned up test data folder: {tmp_data_path}", flush=True) register_ogw_back_blueprints(app) - + @pytest.fixture def client() -> Generator[FlaskClient, None, None]: From c9ba3e7a666665e08c2e37db2f452593cfca6c70 Mon Sep 17 00:00:00 2001 From: JulienChampagnol Date: Wed, 7 Jan 2026 16:13:08 +0100 Subject: [PATCH 12/18] fix tests --- opengeodeweb_back_schemas.json | 89 +------------------ .../routes/create/blueprint_create.py | 4 +- .../routes/create/schemas/__init__.py | 2 +- tests/conftest.py | 4 +- tests/test_models_routes.py | 26 +++--- 5 files changed, 18 insertions(+), 107 deletions(-) diff --git a/opengeodeweb_back_schemas.json b/opengeodeweb_back_schemas.json index 42494821..50408514 100644 --- a/opengeodeweb_back_schemas.json +++ b/opengeodeweb_back_schemas.json @@ -1,46 +1,9 @@ { "opengeodeweb_back": { "create": { - "create_voi": { - "$id": "opengeodeweb_back/create/create_voi", - "route": "/create_voi", - "methods": [ - "POST" - ], - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Name of the VOI" - }, - "aoi_id": { - "type": "string", - "description": "ID of the corresponding AOI" - }, - "z_min": { - "type": "number", - "description": "Minimum Z coordinate" - }, - "z_max": { - "type": "number", - "description": "Maximum Z coordinate" - }, - "id": { - "type": "string", - "description": "Optional ID for updating existing VOI" - } - }, - "required": [ - "name", - "aoi_id", - "z_min", - "z_max" - ], - "additionalProperties": false - }, - "create_point": { - "$id": "opengeodeweb_back/create/create_point", - "route": "/create_point", + "point": { + "$id": "opengeodeweb_back/create/point", + "route": "/point", "methods": [ "POST" ], @@ -67,52 +30,6 @@ "z" ], "additionalProperties": false - }, - "create_aoi": { - "$id": "opengeodeweb_back/create/create_aoi", - "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": 3 - }, - "z": { - "type": "number" - }, - "id": { - "type": "string" - } - }, - "required": [ - "name", - "points", - "z" - ], - "additionalProperties": false } }, "models": { diff --git a/src/opengeodeweb_back/routes/create/blueprint_create.py b/src/opengeodeweb_back/routes/create/blueprint_create.py index 38abbe5a..38a703a0 100644 --- a/src/opengeodeweb_back/routes/create/blueprint_create.py +++ b/src/opengeodeweb_back/routes/create/blueprint_create.py @@ -8,7 +8,7 @@ # Local application imports from opengeodeweb_back import geode_functions, utils_functions -from opengeodeweb_back.routes.create.schemas import schemas +import opengeodeweb_back.routes.create.schemas as schemas from opengeodeweb_back.geode_objects.geode_point_set3d import GeodePointSet3D from opengeodeweb_back.geode_objects.geode_edged_curve3d import GeodeEdgedCurve3D @@ -23,7 +23,7 @@ def point() -> flask.Response: """Endpoint to create a single point in 3D space.""" json_data = utils_functions.validate_request( - flask.request, schemas_dict["create_point"] + flask.request, schemas_dict["point"] ) params = schemas.Point.from_dict(json_data) diff --git a/src/opengeodeweb_back/routes/create/schemas/__init__.py b/src/opengeodeweb_back/routes/create/schemas/__init__.py index 8b137891..86ba70b7 100644 --- a/src/opengeodeweb_back/routes/create/schemas/__init__.py +++ b/src/opengeodeweb_back/routes/create/schemas/__init__.py @@ -1 +1 @@ - +from .point import * diff --git a/tests/conftest.py b/tests/conftest.py index 4d57cbfd..2a7449d3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -41,14 +41,14 @@ def configure_test_environment() -> Generator[None, None, None]: init_database(db_path) os.environ["TEST_DB_PATH"] = str(db_path) - + register_ogw_back_blueprints(app) yield tmp_data_path = app.config.get("DATA_FOLDER_PATH") if tmp_data_path and os.path.exists(tmp_data_path): shutil.rmtree(tmp_data_path, ignore_errors=True) print(f"Cleaned up test data folder: {tmp_data_path}", flush=True) - register_ogw_back_blueprints(app) + @pytest.fixture diff --git a/tests/test_models_routes.py b/tests/test_models_routes.py index 6389b6b4..0b272202 100644 --- a/tests/test_models_routes.py +++ b/tests/test_models_routes.py @@ -206,24 +206,20 @@ def test_save_viewable_workflow_from_file(client: FlaskClient) -> None: def test_save_viewable_workflow_from_object(client: FlaskClient) -> None: - route = "/opengeodeweb_back/create/create_aoi" - aoi_data = { - "name": "workflow_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}, - ], + route = "/opengeodeweb_back/create/point" + point_data = { + "name": "workflow_point_3d", + "x": 0.0, + "y": 0.0, "z": 0.0, } - response = client.post(route, json=aoi_data) + response = client.post(route, json=point_data) assert response.status_code == 200 data_id = response.get_json()["id"] assert isinstance(data_id, str) and len(data_id) > 0 - assert response.get_json()["geode_object_type"] == "EdgedCurve3D" + assert response.get_json()["geode_object_type"] == "PointSet3D" assert response.get_json()["viewable_file"].endswith(".vtp") @@ -239,11 +235,11 @@ def test_import_extension_route(client: FlaskClient, tmp_path: Path) -> None: vext_path = tmp_path / "test-extension-1.0.0.vext" with zipfile.ZipFile(vext_path, "w", compression=zipfile.ZIP_DEFLATED) as zipf: zipf.writestr( - "dist/test-extension-extension.es.js", + "test-extension-extension.es.js", "export const metadata = { id: 'test-extension', name: 'Test Extension' };", ) - zipf.writestr("dist/test-extension-back", "#!/bin/bash\necho 'mock backend'") - zipf.writestr("dist/test-extension.css", ".test { color: red; }") + zipf.writestr("test-extension-back", "#!/bin/bash\necho 'mock backend'") + zipf.writestr("test-extension.css", ".test { color: red; }") with open(vext_path, "rb") as f: response = client.post( route, @@ -261,8 +257,6 @@ def test_import_extension_route(client: FlaskClient, tmp_path: Path) -> None: ) extension_path = os.path.join(extensions_folder, "test-extension") assert os.path.exists(extension_path) - dist_path = os.path.join(extension_path, "dist") - assert os.path.exists(dist_path) # Verify frontend content is returned frontend_content = json_data["frontend_content"] From dd71a91e6a5fa1ff59be37923600af4d16ffafdf Mon Sep 17 00:00:00 2001 From: JulienChampagnol <91873154+JulienChampagnol@users.noreply.github.com> Date: Wed, 7 Jan 2026 15:14:22 +0000 Subject: [PATCH 13/18] Apply prepare changes --- src/opengeodeweb_back/routes/create/blueprint_create.py | 4 +--- tests/conftest.py | 1 + 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/opengeodeweb_back/routes/create/blueprint_create.py b/src/opengeodeweb_back/routes/create/blueprint_create.py index 38a703a0..0eab0ca6 100644 --- a/src/opengeodeweb_back/routes/create/blueprint_create.py +++ b/src/opengeodeweb_back/routes/create/blueprint_create.py @@ -22,9 +22,7 @@ ) def point() -> flask.Response: """Endpoint to create a single point in 3D space.""" - json_data = utils_functions.validate_request( - flask.request, schemas_dict["point"] - ) + json_data = utils_functions.validate_request(flask.request, schemas_dict["point"]) params = schemas.Point.from_dict(json_data) # Create the point diff --git a/tests/conftest.py b/tests/conftest.py index 7dcb76f6..be69c56c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -49,6 +49,7 @@ def configure_test_environment() -> Generator[None, None, None]: shutil.rmtree(tmp_data_path, ignore_errors=True) print(f"Cleaned up test data folder: {tmp_data_path}", flush=True) + @pytest.fixture def client() -> Generator[FlaskClient, None, None]: app.config["REQUEST_COUNTER"] = 0 From 9b70f2db3b1573cef59ff5434e95f1c816347ac1 Mon Sep 17 00:00:00 2001 From: JulienChampagnol Date: Wed, 7 Jan 2026 16:32:47 +0100 Subject: [PATCH 14/18] feat(App): create_app, register_ogw_back_blueprints, run_server From 4105e00ee577e8520a6e6e692ece82e1fb704f9d Mon Sep 17 00:00:00 2001 From: JulienChampagnol Date: Thu, 8 Jan 2026 17:28:47 +0100 Subject: [PATCH 15/18] main run opengeodeweb_back --- src/opengeodeweb_back/app.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/opengeodeweb_back/app.py b/src/opengeodeweb_back/app.py index 58701ba0..e510aa16 100644 --- a/src/opengeodeweb_back/app.py +++ b/src/opengeodeweb_back/app.py @@ -178,4 +178,10 @@ def run_server(app: Flask) -> None: port=args.port, ssl_context=app.config.get("SSL"), ) + +# ''' Main ''' +if __name__ == "__main__": + app = create_app(__name__) + register_ogw_back_blueprints(app) + run_server(app) print("Server stopped", flush=True) From 82af674eeb4a3c3055d1bea2a29cc8bf063e2e5f Mon Sep 17 00:00:00 2001 From: JulienChampagnol <91873154+JulienChampagnol@users.noreply.github.com> Date: Thu, 8 Jan 2026 16:29:19 +0000 Subject: [PATCH 16/18] Apply prepare changes --- src/opengeodeweb_back/app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/opengeodeweb_back/app.py b/src/opengeodeweb_back/app.py index e510aa16..096fa739 100644 --- a/src/opengeodeweb_back/app.py +++ b/src/opengeodeweb_back/app.py @@ -179,6 +179,7 @@ def run_server(app: Flask) -> None: ssl_context=app.config.get("SSL"), ) + # ''' Main ''' if __name__ == "__main__": app = create_app(__name__) From 09929294771e422659c50450cfe07bcb7ea5862e Mon Sep 17 00:00:00 2001 From: JulienChampagnol Date: Thu, 8 Jan 2026 17:33:52 +0100 Subject: [PATCH 17/18] wrapper function --- pyproject.toml | 2 +- src/opengeodeweb_back/app.py | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c1add323..a721bab0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ classifiers = [ "Bug Tracker" = "https://github.com/Geode-solutions/OpenGeodeWeb-Back/issues" [project.scripts] -opengeodeweb-back = "opengeodeweb_back.app:run_server" +opengeodeweb-back = "opengeodeweb_back.app:run_opengeodeweb_back" [tool.setuptools.dynamic] dependencies = { file = ["requirements.txt"] } diff --git a/src/opengeodeweb_back/app.py b/src/opengeodeweb_back/app.py index e510aa16..710624e5 100644 --- a/src/opengeodeweb_back/app.py +++ b/src/opengeodeweb_back/app.py @@ -179,9 +179,14 @@ def run_server(app: Flask) -> None: ssl_context=app.config.get("SSL"), ) -# ''' Main ''' -if __name__ == "__main__": + +def run_opengeodeweb_back() -> None: app = create_app(__name__) register_ogw_back_blueprints(app) run_server(app) print("Server stopped", flush=True) + + +# ''' Main ''' +if __name__ == "__main__": + run_opengeodeweb_back() From ed2eca657e441d6ed21e0f03948d746f5e523eb1 Mon Sep 17 00:00:00 2001 From: JulienChampagnol <91873154+JulienChampagnol@users.noreply.github.com> Date: Fri, 9 Jan 2026 09:31:46 +0000 Subject: [PATCH 18/18] Apply prepare changes --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5697c0a5..08d64bdf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -60,4 +60,3 @@ werkzeug==3.1.2 # flask # flask-cors -opengeodeweb-microservice==1.*,>=1.0.12