Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
7888fdf
feat(database): Add persistent data management using sqlite & sqlalchemy
MaxNumerique Sep 1, 2025
cfcfdd4
default datetime changed for a non decprecated method
MaxNumerique Sep 1, 2025
2d24b77
Apply prepare changes
MaxNumerique Sep 1, 2025
73ff779
minor renames
MaxNumerique Sep 1, 2025
ee372e5
Merge branch 'feat/database' of https://github.com/Geode-solutions/Op…
MaxNumerique Sep 1, 2025
6163517
Apply prepare changes
MaxNumerique Sep 1, 2025
a53d000
test
MaxNumerique Sep 1, 2025
2974791
Apply prepare changes
MaxNumerique Sep 1, 2025
652149d
Using Flask-SQLAlchemy instead of SQLAlchemy for the database
MaxNumerique Sep 2, 2025
dad100a
Apply prepare changes
MaxNumerique Sep 2, 2025
1905756
clean this ?
MaxNumerique Sep 2, 2025
86560cb
Merge branch 'feat/database' of https://github.com/Geode-solutions/Op…
MaxNumerique Sep 2, 2025
1d4b231
Apply prepare changes
MaxNumerique Sep 2, 2025
62bf4bb
some comments
MaxNumerique Sep 2, 2025
bf9dfc0
delete commitlint
MaxNumerique Sep 3, 2025
ac4782f
Apply prepare changes
MaxNumerique Sep 3, 2025
6777263
tests added
MaxNumerique Sep 3, 2025
023ab17
Merge branch 'feat/database' of https://github.com/Geode-solutions/Op…
MaxNumerique Sep 3, 2025
2011ac5
Converted SQLALCHEMY_DATABASE_URI to use absolute paths
MaxNumerique Sep 3, 2025
a970a9c
Apply prepare changes
MaxNumerique Sep 3, 2025
0e9e361
abs path for sqlalchemy_database_uri
MaxNumerique Sep 3, 2025
db5a89a
Merge branch 'feat/database' of https://github.com/Geode-solutions/Op…
MaxNumerique Sep 3, 2025
a70ef53
abs path
MaxNumerique Sep 3, 2025
a985ba5
id and data infos set by database
MaxNumerique Sep 3, 2025
4772ac4
Apply prepare changes
MaxNumerique Sep 3, 2025
2e9266f
trigger
BotellaA Sep 3, 2025
6599029
again
BotellaA Sep 3, 2025
25adb82
trigger
BotellaA Sep 3, 2025
4088ff4
again
BotellaA Sep 4, 2025
b8e6ffc
new test session.commit to see if it is relevant
MaxNumerique Sep 4, 2025
5c90a33
Merge branch 'feat/database' of https://github.com/Geode-solutions/Op…
MaxNumerique Sep 4, 2025
cb60e9c
Apply prepare changes
MaxNumerique Sep 4, 2025
3ebe500
add type for sub-class of declarative_base()
MaxNumerique Sep 4, 2025
f2e07fc
Merge branch 'feat/database' of https://github.com/Geode-solutions/Op…
MaxNumerique Sep 4, 2025
83ee651
Apply prepare changes
MaxNumerique Sep 4, 2025
3bdf3b8
Type["Datas"]
MaxNumerique Sep 4, 2025
52e41bc
Merge branch 'feat/database' of https://github.com/Geode-solutions/Op…
MaxNumerique Sep 4, 2025
199727a
Apply prepare changes
MaxNumerique Sep 4, 2025
f597419
test type subclass
MaxNumerique Sep 4, 2025
8a778ad
Merge branch 'feat/database' of https://github.com/Geode-solutions/Op…
MaxNumerique Sep 4, 2025
e1490ca
Apply prepare changes
MaxNumerique Sep 4, 2025
76762fd
mypy Base(DeclarativeBase)
MaxNumerique Sep 4, 2025
a85d868
Apply prepare changes
MaxNumerique Sep 4, 2025
fd1967a
final mypy
MaxNumerique Sep 4, 2025
39764fc
Merge branch 'feat/database' of https://github.com/Geode-solutions/Op…
MaxNumerique Sep 4, 2025
410580f
Apply prepare changes
MaxNumerique Sep 4, 2025
4217ed4
input_file: str
MaxNumerique Sep 4, 2025
9db80dc
final
MaxNumerique Sep 4, 2025
5f4673c
input_file as str
MaxNumerique Sep 4, 2025
e3ac529
Apply prepare changes
MaxNumerique Sep 4, 2025
03ba365
Merge branch 'next' of https://github.com/Geode-solutions/OpenGeodeWe…
MaxNumerique Sep 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ __pycache__
uploads
node_modules
schemas.json
.mypy_cache
.mypy_cache
*.db
2 changes: 2 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
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 src.opengeodeweb_back.database import initialize_database


""" Global config """
Expand Down Expand Up @@ -57,5 +58,6 @@ def return_error():

# ''' Main '''
if __name__ == "__main__":
initialize_database(app)
print(f"Python is running in {FLASK_DEBUG} mode")
app.run(debug=FLASK_DEBUG, host=DEFAULT_HOST, port=PORT, ssl_context=SSL)
1 change: 1 addition & 0 deletions requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ fastjsonschema==2.16.2
Flask[async]==3.0.3
Flask-Cors==6.0.1
werkzeug==3.0.3
Flask-SQLAlchemy==3.1.1
9 changes: 9 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,17 @@ flask[async]==3.0.3
# -r requirements.in
# flask
# flask-cors
# flask-sqlalchemy
flask-cors==6.0.1
# via -r requirements.in
flask-sqlalchemy==3.1.1
# via -r requirements.in
geode-common==33.9.0
# via geode-viewables
geode-viewables==3.2.0
# via -r requirements.in
greenlet==3.2.4
# via sqlalchemy
itsdangerous==2.2.0
# via flask
jinja2==3.1.6
Expand Down Expand Up @@ -54,6 +59,10 @@ opengeode-io==7.3.2
# -r requirements.in
# geode-viewables
# opengeode-geosciencesio
sqlalchemy==2.0.43
# via flask-sqlalchemy
typing-extensions==4.15.0
# via sqlalchemy
werkzeug==3.0.3
# via
# -r requirements.in
Expand Down
13 changes: 11 additions & 2 deletions src/opengeodeweb_back/app_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

# Third party imports
# Local application imports
from .database import DATABASE_FILENAME


class Config(object):
Expand All @@ -15,19 +16,27 @@ class Config(object):
REQUEST_COUNTER = 0
LAST_REQUEST_TIME = time.time()
LAST_PING_TIME = time.time()
SQLALCHEMY_TRACK_MODIFICATIONS = False


class ProdConfig(Config):
SSL = None
ORIGINS = ""
MINUTES_BEFORE_TIMEOUT = "1"
SECONDS_BETWEEN_SHUTDOWNS = "10"
DATA_FOLDER_PATH = "/data/"
DATA_FOLDER_PATH = "/data"
SQLALCHEMY_DATABASE_URI = f"sqlite:///{os.path.abspath(
os.path.join(DATA_FOLDER_PATH, DATABASE_FILENAME)
)}"


class DevConfig(Config):
SSL = None
ORIGINS = "*"
MINUTES_BEFORE_TIMEOUT = "1"
SECONDS_BETWEEN_SHUTDOWNS = "10"
DATA_FOLDER_PATH = "./data/"
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
)}"
44 changes: 44 additions & 0 deletions src/opengeodeweb_back/data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from sqlalchemy import String, JSON
from sqlalchemy.orm import Mapped, mapped_column
from .database import database, Base
import uuid


class Data(Base):
__tablename__ = "datas"

id: Mapped[str] = mapped_column(
String, primary_key=True, default=lambda: str(uuid.uuid4()).replace("-", "")
)
name: Mapped[str] = mapped_column(String, nullable=False)
native_file_name: Mapped[str] = mapped_column(String, nullable=False)
viewable_file_name: Mapped[str] = mapped_column(String, nullable=False)
geode_object: Mapped[str] = mapped_column(String, nullable=False)

light_viewable: Mapped[str | None] = mapped_column(String, nullable=True)
input_file: Mapped[str | None] = mapped_column(String, nullable=True)
additional_files: Mapped[list[str] | None] = mapped_column(JSON, nullable=True)

@staticmethod
def create(
name: str,
geode_object: str,
input_file: str | None = None,
additional_files: list[str] | None = None,
) -> "Data":
input_file = input_file if input_file is not None else ""
additional_files = additional_files if additional_files is not None else []

data_entry = Data(
name=name,
geode_object=geode_object,
input_file=input_file,
additional_files=additional_files,
native_file_name="",
viewable_file_name="",
light_viewable=None,
)

database.session.add(data_entry)
database.session.flush()
return data_entry
19 changes: 19 additions & 0 deletions src/opengeodeweb_back/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm import DeclarativeBase

DATABASE_FILENAME = "project.db"


class Base(DeclarativeBase):
pass


database = SQLAlchemy(model_class=Base)


def initialize_database(app: Flask) -> SQLAlchemy:
database.init_app(app)
with app.app_context():
database.create_all()
return database
77 changes: 53 additions & 24 deletions src/opengeodeweb_back/utils_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import os
import threading
import time
import uuid
import zipfile
from collections.abc import Callable
from typing import Any
Expand All @@ -17,6 +16,8 @@

# Local application imports
from . import geode_functions
from .data import Data
from .database import database


def increment_request_counter(current_app: flask.Flask) -> None:
Expand Down Expand Up @@ -152,17 +153,29 @@ def handle_exception(exception: HTTPException) -> flask.Response:
return response


def create_unique_data_folder() -> tuple[str, str]:
def create_data_folder_from_id(data_id: str) -> str:
base_data_folder = flask.current_app.config["DATA_FOLDER_PATH"]
generated_id = str(uuid.uuid4()).replace("-", "")
data_path = os.path.join(base_data_folder, generated_id)
data_path = os.path.join(base_data_folder, data_id)
os.makedirs(data_path, exist_ok=True)
return generated_id, data_path
return data_path


def save_all_viewables_and_return_info(
geode_object, data, generated_id, data_path, additional_files=None
):
geode_object: str,
data: Any,
input_file: str,
additional_files: list[str] | None = None,
) -> dict[str, Any]:
if additional_files is None:
additional_files = []

data_entry = Data.create(
name=data.name(),
geode_object=geode_object,
input_file=input_file,
additional_files=additional_files,
)
data_path = create_data_folder_from_id(data_entry.id)
saved_native_file_path = geode_functions.save(
geode_object,
data,
Expand All @@ -177,36 +190,50 @@ def save_all_viewables_and_return_info(
)
with open(saved_light_viewable_file_path, "rb") as f:
binary_light_viewable = f.read()
data_entry.native_file_name = os.path.basename(saved_native_file_path[0])
data_entry.viewable_file_name = os.path.basename(saved_viewable_file_path)
data_entry.light_viewable = os.path.basename(saved_light_viewable_file_path)

database.session.commit()

return {
"name": data.name(),
"native_file_name": os.path.basename(saved_native_file_path[0]),
"viewable_file_name": os.path.basename(saved_viewable_file_path),
"id": generated_id,
"name": data_entry.name,
"native_file_name": data_entry.native_file_name,
"viewable_file_name": data_entry.viewable_file_name,
"id": data_entry.id,
"object_type": geode_functions.get_object_type(geode_object),
"binary_light_viewable": binary_light_viewable.decode("utf-8"),
"geode_object": geode_object,
"input_files": additional_files or [],
"geode_object": data_entry.geode_object,
"input_files": data_entry.input_file,
"additional_files": data_entry.additional_files,
}


def generate_native_viewable_and_light_viewable_from_object(geode_object, data):
generated_id, data_path = create_unique_data_folder()
return save_all_viewables_and_return_info(
geode_object, data, generated_id, data_path
)
def generate_native_viewable_and_light_viewable_from_object(
geode_object: str, data: Any
) -> dict[str, Any]:
return save_all_viewables_and_return_info(geode_object, data, input_file="")


def generate_native_viewable_and_light_viewable_from_file(geode_object, input_filename):
generated_id, data_path = create_unique_data_folder()
def generate_native_viewable_and_light_viewable_from_file(
geode_object: str, input_filename: str
) -> dict[str, Any]:
temp_data_entry = Data.create(
name="temp",
geode_object=geode_object,
input_file=input_filename,
additional_files=[],
)

data_path = create_data_folder_from_id(temp_data_entry.id)

full_input_filename = geode_functions.upload_file_path(input_filename)
copied_full_path = os.path.join(
data_path, werkzeug.utils.secure_filename(input_filename)
)
shutil.copy2(full_input_filename, copied_full_path)

additional_files_copied = []
additional_files_copied: list[str] = []
additional = geode_functions.additional_files(geode_object, full_input_filename)
for additional_file in additional.mandatory_files + additional.optional_files:
if additional_file.is_missing:
Expand All @@ -221,12 +248,14 @@ def generate_native_viewable_and_light_viewable_from_file(geode_object, input_fi
shutil.copy2(source_path, dest_path)
additional_files_copied.append(additional_file.filename)

data = geode_functions.load_data(geode_object, generated_id, input_filename)
data = geode_functions.load_data(geode_object, temp_data_entry.id, input_filename)

database.session.delete(temp_data_entry)
database.session.flush()

return save_all_viewables_and_return_info(
geode_object,
data,
generated_id,
data_path,
input_file=input_filename,
additional_files=additional_files_copied,
)
18 changes: 14 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
import shutil

# Third party imports
import os
import pytest

# Local application imports
from app import app
from src.opengeodeweb_back.database import initialize_database

TEST_ID = "1"

Expand All @@ -15,14 +17,22 @@
def copy_data():
shutil.rmtree("./data", ignore_errors=True)
shutil.copytree("./tests/data/", f"./data/{TEST_ID}/", dirs_exist_ok=True)


@pytest.fixture
def client():
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")
app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{db_path}"

print("Current working directory:", os.getcwd())
print("Directory contents:", os.listdir("."))

initialize_database(app)


@pytest.fixture
def client():
app.config["REQUEST_COUNTER"] = 0
app.config["LAST_REQUEST_TIME"] = time.time()
client = app.test_client()
Expand Down
12 changes: 12 additions & 0 deletions tests/test_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,3 +321,15 @@ def test_create_point(client):

# 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():
base_dir = os.path.abspath(os.path.dirname(__file__))
expected_db_path = os.path.join(base_dir, "data", "project.db")
expected_uri = f"sqlite:///{expected_db_path}"

assert app.config["SQLALCHEMY_DATABASE_URI"] == expected_uri

assert os.path.exists(expected_db_path)
Loading
Loading