diff --git a/app.py b/app.py deleted file mode 100644 index 3672fe02..00000000 --- a/app.py +++ /dev/null @@ -1,63 +0,0 @@ -"""Packages""" - -import os - -import flask -import flask_cors -from werkzeug.exceptions import HTTPException - -from src.opengeodeweb_back.routes import blueprint_routes -from src.opengeodeweb_back.routes.models import blueprint_models -from src.opengeodeweb_back.utils_functions import handle_exception -from src.opengeodeweb_back import app_config -from opengeodeweb_microservice.database.connection import init_database - - -""" Global config """ -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) - -DEFAULT_HOST = app.config.get("DEFAULT_HOST") -PORT = int(app.config.get("DEFAULT_PORT")) -ORIGINS = app.config.get("ORIGINS") -SSL = app.config.get("SSL") - -flask_cors.CORS(app, origins=ORIGINS) -app.register_blueprint( - blueprint_routes.routes, - url_prefix="/", - name="blueprint_routes", -) - -app.register_blueprint( - blueprint_models.routes, - url_prefix="/models", - name="blueprint_models", -) - - -@app.errorhandler(HTTPException) -def errorhandler(e): - return handle_exception(e) - - -@app.route( - "/error", - methods=["POST"], -) -def return_error(): - flask.abort(500, f"Test") - - -# ''' Main ''' -if __name__ == "__main__": - init_database(app) - print(f"Python is running in {FLASK_DEBUG} mode") - app.run(debug=FLASK_DEBUG, host=DEFAULT_HOST, port=PORT, ssl_context=SSL) diff --git a/pyproject.toml b/pyproject.toml index 1a5e40b9..929bb921 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,6 @@ requires = ["setuptools"] build-backend = "setuptools.build_meta" - [project] name = "OpenGeodeWeb-Back" version = "0.0.0" @@ -21,6 +20,9 @@ classifiers = [ "Homepage" = "https://github.com/Geode-solutions/OpenGeodeWeb-Back" "Bug Tracker" = "https://github.com/Geode-solutions/OpenGeodeWeb-Back/issues" +[project.scripts] +opengeodeweb-back = "opengeodeweb_back.app:run_server" + [tool.setuptools.dynamic] dependencies = { file = ["requirements.txt"] } diff --git a/requirements.in b/requirements.in index 9bfc7101..1a55b764 100644 --- a/requirements.in +++ b/requirements.in @@ -7,4 +7,5 @@ geode-common==33.11.0 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 diff --git a/requirements.txt b/requirements.txt index bb96cdb8..d94d0c61 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,32 +2,23 @@ # This file is autogenerated by pip-compile with Python 3.12 # by the following command: # -# pip-compile --output-file=./requirements.txt ./requirements-internal.in ./requirements.in +# pip-compile --output-file=./requirements.txt --pre ./requirements.in # -asgiref~=3.9 +asgiref~=3.10 # via flask blinker~=1.9 - # via - # flask - # opengeodeweb-microservice + # via flask click~=8.3 - # via - # flask - # opengeodeweb-microservice -fastjsonschema~=2.21 - # via opengeodeweb-microservice + # via flask flask[async]~=3.1 # 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 @@ -35,22 +26,15 @@ geode-common==33.11.0 geode-viewables==3.3.0 # via -r requirements.in greenlet~=3.2 - # via - # opengeodeweb-microservice - # sqlalchemy + # via sqlalchemy itsdangerous~=2.2 - # via - # flask - # opengeodeweb-microservice + # via flask jinja2~=3.1 - # via - # flask - # opengeodeweb-microservice + # via flask markupsafe~=3.0 # via # flask # jinja2 - # opengeodeweb-microservice # werkzeug opengeode-core==15.27.4 # via @@ -75,18 +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 + # via flask-sqlalchemy typing-extensions~=4.15 + # via sqlalchemy +werkzeug==3.1.2 # via - # opengeodeweb-microservice - # sqlalchemy -werkzeug~=3.1 - # via + # -r requirements.in # flask # flask-cors - # opengeodeweb-microservice diff --git a/src/opengeodeweb_back/app.py b/src/opengeodeweb_back/app.py new file mode 100644 index 00000000..48381159 --- /dev/null +++ b/src/opengeodeweb_back/app.py @@ -0,0 +1,156 @@ +"""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_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" +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.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", +) + +if FLASK_DEBUG == False: + utils_functions.set_interval( + 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.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 run_server() -> 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( + "-p", "--port", type=int, default=DEFAULT_PORT, help="Port to listen on" + ) + parser.add_argument( + "-d", + "--debug", + default=FLASK_DEBUG, + help="Whether to run in debug mode", + action="store_true", + ) + parser.add_argument( + "-dfp", + "--data_folder_path", + type=str, + default=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, + help="Path to the folder where uploads are stored", + ) + parser.add_argument( + "-origins", + "--allowed_origins", + default=ORIGINS, + help="Origins that are allowed to connect to the server", + ) + parser.add_argument( + "-t", + "--timeout", + default=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(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) + app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{db_path}" + 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/app_config.py b/src/opengeodeweb_back/app_config.py index 453a0eda..095b2754 100644 --- a/src/opengeodeweb_back/app_config.py +++ b/src/opengeodeweb_back/app_config.py @@ -4,9 +4,6 @@ # Third party imports # Local application imports -from opengeodeweb_microservice.database.connection import get_database - -DATABASE_FILENAME = "project.db" class Config(object): @@ -18,7 +15,7 @@ class Config(object): REQUEST_COUNTER = 0 LAST_REQUEST_TIME = time.time() LAST_PING_TIME = time.time() - SQLALCHEMY_TRACK_MODIFICATIONS = False + DATABASE_FILENAME = "project.db" class ProdConfig(Config): @@ -27,9 +24,6 @@ class ProdConfig(Config): MINUTES_BEFORE_TIMEOUT = "1" SECONDS_BETWEEN_SHUTDOWNS = "10" DATA_FOLDER_PATH = "/data" - SQLALCHEMY_DATABASE_URI = f"sqlite:///{os.path.abspath( - os.path.join(DATA_FOLDER_PATH, DATABASE_FILENAME) - )}" class DevConfig(Config): @@ -39,6 +33,3 @@ 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") - SQLALCHEMY_DATABASE_URI = f"sqlite:///{os.path.join( - BASE_DIR, DATA_FOLDER_PATH, DATABASE_FILENAME - )}" diff --git a/src/opengeodeweb_back/utils_functions.py b/src/opengeodeweb_back/utils_functions.py index ab3d05bf..92633d03 100644 --- a/src/opengeodeweb_back/utils_functions.py +++ b/src/opengeodeweb_back/utils_functions.py @@ -88,7 +88,6 @@ def validate_request(request: flask.Request, schema: dict[str, str]) -> None: if json_data is None: json_data = {} - try: validate = fastjsonschema.compile(schema) validate(json_data) @@ -259,10 +258,6 @@ def generate_native_viewable_and_light_viewable_from_file( data = geode_functions.load(geode_object, copied_full_path) - # Remplacer : - # database.session.delete(temp_data_entry) - # database.session.flush() - # Par : session = get_session() if session: session.delete(temp_data_entry) diff --git a/tests/conftest.py b/tests/conftest.py index e9b41626..04a50e03 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,37 +1,50 @@ # Standard library imports import time import shutil +import os +from pathlib import Path +from typing import Generator # Third party imports -import os import pytest # Local application imports -from app import app +from opengeodeweb_back.app import app + +# from opengeodeweb_back import app_config from opengeodeweb_microservice.database.connection import init_database TEST_ID = "1" @pytest.fixture(scope="session", autouse=True) -def copy_data(): +def configure_test_environment() -> Generator[None, None, None]: + base_path = Path(__file__).parent + test_data_path = base_path / "data" + shutil.rmtree("./data", ignore_errors=True) - shutil.copytree("./tests/data/", f"./data/{TEST_ID}/", dirs_exist_ok=True) + shutil.copytree(test_data_path, f"./data/{TEST_ID}/", dirs_exist_ok=True) + app.config["TESTING"] = True app.config["SERVER_NAME"] = "TEST" app.config["DATA_FOLDER_PATH"] = "./data/" app.config["UPLOAD_FOLDER"] = "./tests/data/" - BASE_DIR = os.path.abspath(os.path.dirname(__file__)) - db_path = os.path.join(BASE_DIR, "data", "project.db") + + db_path = os.path.join(base_path, "data", "project.db") app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{db_path}" print("Current working directory:", os.getcwd()) print("Directory contents:", os.listdir(".")) - init_database(app) - # print(list(app.blueprints.keys())) - # for rule in app.url_map.iter_rules(): - # print(f"Route: {rule.rule} -> {rule.endpoint}") + init_database(app, db_path) + os.environ["TEST_DB_PATH"] = str(db_path) + + 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) @pytest.fixture diff --git a/tests/test_geode_functions.py b/tests/test_geode_functions.py index 870a130a..b3c57b1c 100644 --- a/tests/test_geode_functions.py +++ b/tests/test_geode_functions.py @@ -5,7 +5,7 @@ # Third party imports # Local application imports -from src.opengeodeweb_back import geode_functions, geode_objects +from opengeodeweb_back import geode_functions, geode_objects data_folder = os.path.join(os.path.dirname(__file__), "data") diff --git a/tests/test_models_routes.py b/tests/test_models_routes.py index 687d6027..83fa4cbd 100644 --- a/tests/test_models_routes.py +++ b/tests/test_models_routes.py @@ -2,13 +2,13 @@ import shutil import flask -from src.opengeodeweb_back import geode_functions +from opengeodeweb_back import geode_functions from opengeodeweb_microservice.database.data import Data from opengeodeweb_microservice.database.connection import get_session def test_model_mesh_components(client, test_id): - route = f"/models/vtm_component_indices" + route = "/opengeodeweb_back/models/vtm_component_indices" with client.application.app_context(): data_path = geode_functions.data_file_path(test_id, "viewable.vtm") @@ -29,7 +29,7 @@ def test_model_mesh_components(client, test_id): def test_extract_brep_uuids(client, test_id): - route = "/models/mesh_components" + route = "/opengeodeweb_back/models/mesh_components" brep_filename = "cube.og_brep" with client.application.app_context(): diff --git a/tests/test_routes.py b/tests/test_routes.py index a46984b5..fd1095e2 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -8,11 +8,11 @@ # Local application imports from opengeodeweb_microservice.database.data import Data from opengeodeweb_microservice.database.connection import get_session -from src.opengeodeweb_back import geode_functions, test_utils +from opengeodeweb_back import geode_functions, test_utils def test_allowed_files(client): - route = f"/allowed_files" + route = f"/opengeodeweb_back/allowed_files" get_full_data = lambda: {"supported_feature": "None"} json = get_full_data() response = client.post(route, json=json) @@ -27,7 +27,7 @@ def test_allowed_files(client): def test_allowed_objects(client): - route = f"/allowed_objects" + route = f"/opengeodeweb_back/allowed_objects" def get_full_data(): return { @@ -49,14 +49,14 @@ def get_full_data(): def test_upload_file(client, filename="test.og_brep"): response = client.put( - f"/upload_file", + f"/opengeodeweb_back/upload_file", data={"file": FileStorage(open(f"./tests/data/{filename}", "rb"))}, ) assert response.status_code == 201 def test_missing_files(client): - route = f"/missing_files" + route = f"/opengeodeweb_back/missing_files" def get_full_data(): return { @@ -79,7 +79,7 @@ def get_full_data(): def test_geographic_coordinate_systems(client): - route = f"/geographic_coordinate_systems" + route = f"/opengeodeweb_back/geographic_coordinate_systems" get_full_data = lambda: {"input_geode_object": "BRep"} # Normal test with geode_object 'BRep' response = client.post(route, json=get_full_data()) @@ -94,7 +94,7 @@ def test_geographic_coordinate_systems(client): def test_inspect_file(client): - route = f"/inspect_file" + route = f"/opengeodeweb_back/inspect_file" def get_full_data(): return { @@ -115,7 +115,7 @@ def get_full_data(): def test_geode_objects_and_output_extensions(client): - route = "/geode_objects_and_output_extensions" + route = "/opengeodeweb_back/geode_objects_and_output_extensions" def get_full_data(): return { @@ -142,7 +142,7 @@ def get_full_data(): def test_save_viewable_file(client): test_upload_file(client, filename="corbi.og_brep") - route = f"/save_viewable_file" + route = f"/opengeodeweb_back/save_viewable_file" def get_full_data(): return { @@ -181,7 +181,9 @@ def test_texture_coordinates(client, test_id): os.makedirs(os.path.dirname(data_path), exist_ok=True) shutil.copy("./tests/data/hat.vtp", data_path) assert os.path.exists(data_path), f"File not found at {data_path}" - response = client.post("/texture_coordinates", json={"id": data.id}) + response = client.post( + "/opengeodeweb_back/texture_coordinates", json={"id": data.id} + ) assert response.status_code == 200 texture_coordinates = response.json["texture_coordinates"] assert type(texture_coordinates) is list @@ -190,7 +192,7 @@ def test_texture_coordinates(client, test_id): def test_vertex_attribute_names(client, test_id): - route = f"/vertex_attribute_names" + route = f"/opengeodeweb_back/vertex_attribute_names" with client.application.app_context(): data = Data.create(geode_object="PolygonalSurface3D", input_file="test.vtp") @@ -212,7 +214,7 @@ def test_vertex_attribute_names(client, test_id): def test_polygon_attribute_names(client, test_id): - route = f"/polygon_attribute_names" + route = f"/opengeodeweb_back/polygon_attribute_names" with client.application.app_context(): data = Data.create(geode_object="PolygonalSurface3D", input_file="test.vtp") @@ -234,7 +236,7 @@ def test_polygon_attribute_names(client, test_id): def test_polyhedron_attribute_names(client, test_id): - route = f"/polyhedron_attribute_names" + route = f"/opengeodeweb_back/polyhedron_attribute_names" with client.application.app_context(): data = Data.create(geode_object="PolyhedralSolid3D", input_file="test.vtu") @@ -257,7 +259,7 @@ def test_polyhedron_attribute_names(client, test_id): def test_create_point(client): - route = f"/create_point" + route = f"/opengeodeweb_back/create_point" get_full_data = lambda: {"title": "test_point", "x": 1, "y": 2, "z": 3} # Normal test with all keys diff --git a/tests/test_utils_functions.py b/tests/test_utils_functions.py index c7e8bce7..befcc14d 100644 --- a/tests/test_utils_functions.py +++ b/tests/test_utils_functions.py @@ -10,7 +10,7 @@ # Local application imports from opengeodeweb_microservice.database.data import Data from opengeodeweb_microservice.database.connection import get_session -from src.opengeodeweb_back import geode_functions, utils_functions +from opengeodeweb_back import geode_functions, utils_functions def test_increment_request_counter(app_context):