Skip to content

Commit 97adade

Browse files
authored
Merge pull request #177 from Geode-solutions/feat/database
feat(database): Add persistent data management using sqlite & sqlalchemy
2 parents 07a969d + 03ba365 commit 97adade

File tree

11 files changed

+230
-49
lines changed

11 files changed

+230
-49
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ __pycache__
1010
uploads
1111
node_modules
1212
schemas.json
13-
.mypy_cache
13+
.mypy_cache
14+
*.db

app.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from src.opengeodeweb_back.routes.models import blueprint_models
1111
from src.opengeodeweb_back.utils_functions import handle_exception
1212
from src.opengeodeweb_back import app_config
13+
from src.opengeodeweb_back.database import initialize_database
1314

1415

1516
""" Global config """
@@ -57,5 +58,6 @@ def return_error():
5758

5859
# ''' Main '''
5960
if __name__ == "__main__":
61+
initialize_database(app)
6062
print(f"Python is running in {FLASK_DEBUG} mode")
6163
app.run(debug=FLASK_DEBUG, host=DEFAULT_HOST, port=PORT, ssl_context=SSL)

requirements.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ fastjsonschema==2.16.2
88
Flask[async]==3.0.3
99
Flask-Cors==6.0.1
1010
werkzeug==3.0.3
11+
Flask-SQLAlchemy==3.1.1

requirements.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,17 @@ flask[async]==3.0.3
1717
# -r requirements.in
1818
# flask
1919
# flask-cors
20+
# flask-sqlalchemy
2021
flask-cors==6.0.1
2122
# via -r requirements.in
23+
flask-sqlalchemy==3.1.1
24+
# via -r requirements.in
2225
geode-common==33.9.0
2326
# via geode-viewables
2427
geode-viewables==3.2.0
2528
# via -r requirements.in
29+
greenlet==3.2.4
30+
# via sqlalchemy
2631
itsdangerous==2.2.0
2732
# via flask
2833
jinja2==3.1.6
@@ -54,6 +59,10 @@ opengeode-io==7.3.2
5459
# -r requirements.in
5560
# geode-viewables
5661
# opengeode-geosciencesio
62+
sqlalchemy==2.0.43
63+
# via flask-sqlalchemy
64+
typing-extensions==4.15.0
65+
# via sqlalchemy
5766
werkzeug==3.0.3
5867
# via
5968
# -r requirements.in

src/opengeodeweb_back/app_config.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
# Third party imports
66
# Local application imports
7+
from .database import DATABASE_FILENAME
78

89

910
class Config(object):
@@ -15,19 +16,27 @@ class Config(object):
1516
REQUEST_COUNTER = 0
1617
LAST_REQUEST_TIME = time.time()
1718
LAST_PING_TIME = time.time()
19+
SQLALCHEMY_TRACK_MODIFICATIONS = False
1820

1921

2022
class ProdConfig(Config):
2123
SSL = None
2224
ORIGINS = ""
2325
MINUTES_BEFORE_TIMEOUT = "1"
2426
SECONDS_BETWEEN_SHUTDOWNS = "10"
25-
DATA_FOLDER_PATH = "/data/"
27+
DATA_FOLDER_PATH = "/data"
28+
SQLALCHEMY_DATABASE_URI = f"sqlite:///{os.path.abspath(
29+
os.path.join(DATA_FOLDER_PATH, DATABASE_FILENAME)
30+
)}"
2631

2732

2833
class DevConfig(Config):
2934
SSL = None
3035
ORIGINS = "*"
3136
MINUTES_BEFORE_TIMEOUT = "1"
3237
SECONDS_BETWEEN_SHUTDOWNS = "10"
33-
DATA_FOLDER_PATH = "./data/"
38+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
39+
DATA_FOLDER_PATH = os.path.join(BASE_DIR, "data")
40+
SQLALCHEMY_DATABASE_URI = f"sqlite:///{os.path.join(
41+
BASE_DIR, DATA_FOLDER_PATH, DATABASE_FILENAME
42+
)}"

src/opengeodeweb_back/data.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from sqlalchemy import String, JSON
2+
from sqlalchemy.orm import Mapped, mapped_column
3+
from .database import database, Base
4+
import uuid
5+
6+
7+
class Data(Base):
8+
__tablename__ = "datas"
9+
10+
id: Mapped[str] = mapped_column(
11+
String, primary_key=True, default=lambda: str(uuid.uuid4()).replace("-", "")
12+
)
13+
name: Mapped[str] = mapped_column(String, nullable=False)
14+
native_file_name: Mapped[str] = mapped_column(String, nullable=False)
15+
viewable_file_name: Mapped[str] = mapped_column(String, nullable=False)
16+
geode_object: Mapped[str] = mapped_column(String, nullable=False)
17+
18+
light_viewable: Mapped[str | None] = mapped_column(String, nullable=True)
19+
input_file: Mapped[str | None] = mapped_column(String, nullable=True)
20+
additional_files: Mapped[list[str] | None] = mapped_column(JSON, nullable=True)
21+
22+
@staticmethod
23+
def create(
24+
name: str,
25+
geode_object: str,
26+
input_file: str | None = None,
27+
additional_files: list[str] | None = None,
28+
) -> "Data":
29+
input_file = input_file if input_file is not None else ""
30+
additional_files = additional_files if additional_files is not None else []
31+
32+
data_entry = Data(
33+
name=name,
34+
geode_object=geode_object,
35+
input_file=input_file,
36+
additional_files=additional_files,
37+
native_file_name="",
38+
viewable_file_name="",
39+
light_viewable=None,
40+
)
41+
42+
database.session.add(data_entry)
43+
database.session.flush()
44+
return data_entry

src/opengeodeweb_back/database.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from flask import Flask
2+
from flask_sqlalchemy import SQLAlchemy
3+
from sqlalchemy.orm import DeclarativeBase
4+
5+
DATABASE_FILENAME = "project.db"
6+
7+
8+
class Base(DeclarativeBase):
9+
pass
10+
11+
12+
database = SQLAlchemy(model_class=Base)
13+
14+
15+
def initialize_database(app: Flask) -> SQLAlchemy:
16+
database.init_app(app)
17+
with app.app_context():
18+
database.create_all()
19+
return database

src/opengeodeweb_back/utils_functions.py

Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import os
33
import threading
44
import time
5-
import uuid
65
import zipfile
76
from collections.abc import Callable
87
from typing import Any
@@ -17,6 +16,8 @@
1716

1817
# Local application imports
1918
from . import geode_functions
19+
from .data import Data
20+
from .database import database
2021

2122

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

154155

155-
def create_unique_data_folder() -> tuple[str, str]:
156+
def create_data_folder_from_id(data_id: str) -> str:
156157
base_data_folder = flask.current_app.config["DATA_FOLDER_PATH"]
157-
generated_id = str(uuid.uuid4()).replace("-", "")
158-
data_path = os.path.join(base_data_folder, generated_id)
158+
data_path = os.path.join(base_data_folder, data_id)
159159
os.makedirs(data_path, exist_ok=True)
160-
return generated_id, data_path
160+
return data_path
161161

162162

163163
def save_all_viewables_and_return_info(
164-
geode_object, data, generated_id, data_path, additional_files=None
165-
):
164+
geode_object: str,
165+
data: Any,
166+
input_file: str,
167+
additional_files: list[str] | None = None,
168+
) -> dict[str, Any]:
169+
if additional_files is None:
170+
additional_files = []
171+
172+
data_entry = Data.create(
173+
name=data.name(),
174+
geode_object=geode_object,
175+
input_file=input_file,
176+
additional_files=additional_files,
177+
)
178+
data_path = create_data_folder_from_id(data_entry.id)
166179
saved_native_file_path = geode_functions.save(
167180
geode_object,
168181
data,
@@ -177,36 +190,50 @@ def save_all_viewables_and_return_info(
177190
)
178191
with open(saved_light_viewable_file_path, "rb") as f:
179192
binary_light_viewable = f.read()
193+
data_entry.native_file_name = os.path.basename(saved_native_file_path[0])
194+
data_entry.viewable_file_name = os.path.basename(saved_viewable_file_path)
195+
data_entry.light_viewable = os.path.basename(saved_light_viewable_file_path)
196+
197+
database.session.commit()
180198

181199
return {
182-
"name": data.name(),
183-
"native_file_name": os.path.basename(saved_native_file_path[0]),
184-
"viewable_file_name": os.path.basename(saved_viewable_file_path),
185-
"id": generated_id,
200+
"name": data_entry.name,
201+
"native_file_name": data_entry.native_file_name,
202+
"viewable_file_name": data_entry.viewable_file_name,
203+
"id": data_entry.id,
186204
"object_type": geode_functions.get_object_type(geode_object),
187205
"binary_light_viewable": binary_light_viewable.decode("utf-8"),
188-
"geode_object": geode_object,
189-
"input_files": additional_files or [],
206+
"geode_object": data_entry.geode_object,
207+
"input_files": data_entry.input_file,
208+
"additional_files": data_entry.additional_files,
190209
}
191210

192211

193-
def generate_native_viewable_and_light_viewable_from_object(geode_object, data):
194-
generated_id, data_path = create_unique_data_folder()
195-
return save_all_viewables_and_return_info(
196-
geode_object, data, generated_id, data_path
197-
)
212+
def generate_native_viewable_and_light_viewable_from_object(
213+
geode_object: str, data: Any
214+
) -> dict[str, Any]:
215+
return save_all_viewables_and_return_info(geode_object, data, input_file="")
198216

199217

200-
def generate_native_viewable_and_light_viewable_from_file(geode_object, input_filename):
201-
generated_id, data_path = create_unique_data_folder()
218+
def generate_native_viewable_and_light_viewable_from_file(
219+
geode_object: str, input_filename: str
220+
) -> dict[str, Any]:
221+
temp_data_entry = Data.create(
222+
name="temp",
223+
geode_object=geode_object,
224+
input_file=input_filename,
225+
additional_files=[],
226+
)
227+
228+
data_path = create_data_folder_from_id(temp_data_entry.id)
202229

203230
full_input_filename = geode_functions.upload_file_path(input_filename)
204231
copied_full_path = os.path.join(
205232
data_path, werkzeug.utils.secure_filename(input_filename)
206233
)
207234
shutil.copy2(full_input_filename, copied_full_path)
208235

209-
additional_files_copied = []
236+
additional_files_copied: list[str] = []
210237
additional = geode_functions.additional_files(geode_object, full_input_filename)
211238
for additional_file in additional.mandatory_files + additional.optional_files:
212239
if additional_file.is_missing:
@@ -221,12 +248,14 @@ def generate_native_viewable_and_light_viewable_from_file(geode_object, input_fi
221248
shutil.copy2(source_path, dest_path)
222249
additional_files_copied.append(additional_file.filename)
223250

224-
data = geode_functions.load_data(geode_object, generated_id, input_filename)
251+
data = geode_functions.load_data(geode_object, temp_data_entry.id, input_filename)
252+
253+
database.session.delete(temp_data_entry)
254+
database.session.flush()
225255

226256
return save_all_viewables_and_return_info(
227257
geode_object,
228258
data,
229-
generated_id,
230-
data_path,
259+
input_file=input_filename,
231260
additional_files=additional_files_copied,
232261
)

tests/conftest.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
import shutil
44

55
# Third party imports
6+
import os
67
import pytest
78

89
# Local application imports
910
from app import app
11+
from src.opengeodeweb_back.database import initialize_database
1012

1113
TEST_ID = "1"
1214

@@ -15,14 +17,22 @@
1517
def copy_data():
1618
shutil.rmtree("./data", ignore_errors=True)
1719
shutil.copytree("./tests/data/", f"./data/{TEST_ID}/", dirs_exist_ok=True)
18-
19-
20-
@pytest.fixture
21-
def client():
2220
app.config["TESTING"] = True
2321
app.config["SERVER_NAME"] = "TEST"
2422
app.config["DATA_FOLDER_PATH"] = "./data/"
2523
app.config["UPLOAD_FOLDER"] = "./tests/data/"
24+
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
25+
db_path = os.path.join(BASE_DIR, "data", "project.db")
26+
app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{db_path}"
27+
28+
print("Current working directory:", os.getcwd())
29+
print("Directory contents:", os.listdir("."))
30+
31+
initialize_database(app)
32+
33+
34+
@pytest.fixture
35+
def client():
2636
app.config["REQUEST_COUNTER"] = 0
2737
app.config["LAST_REQUEST_TIME"] = time.time()
2838
client = app.test_client()

tests/test_routes.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,3 +321,15 @@ def test_create_point(client):
321321

322322
# Test all params
323323
test_utils.test_route_wrong_params(client, route, get_full_data)
324+
325+
326+
def test_database_uri_path(client):
327+
app = client.application
328+
with app.app_context():
329+
base_dir = os.path.abspath(os.path.dirname(__file__))
330+
expected_db_path = os.path.join(base_dir, "data", "project.db")
331+
expected_uri = f"sqlite:///{expected_db_path}"
332+
333+
assert app.config["SQLALCHEMY_DATABASE_URI"] == expected_uri
334+
335+
assert os.path.exists(expected_db_path)

0 commit comments

Comments
 (0)