Compare commits

...

8 Commits

Author SHA1 Message Date
969333998c [UPD] Exception handling 2026-02-15 20:53:32 +01:00
2a8a44e80b [WIP] Exception handling 2026-02-15 17:39:21 +01:00
7c3593d066 [UPD] Delete Library 2026-02-15 16:34:43 +01:00
c701a4ab9b [WIP] Library REST API 2026-02-15 11:32:33 +01:00
c468f51672 [UPD] Added api to create Library 2026-02-15 10:36:37 +01:00
bb8324ad1d [UPD] Added config file 2026-02-15 08:55:57 +01:00
d84d62c79f [FEA] Added Conroller for sanity 2026-02-15 08:47:46 +01:00
f50a9d0916 [FEA] Added Conroller for sanity 2026-02-15 08:38:55 +01:00
34 changed files with 477 additions and 73 deletions

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.12.3

View File

@@ -1,12 +1,17 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from ...schema.library.base import Base
from ...db.config.config import get_engine_configuration
import logging
logger = logging.getLogger(__name__)
def install():
logger.info("Installing")
engine_string, echo = get_engine_configuration()
logger.debug(f"engine_string: {engine_string}")
engine = create_engine(engine_string, echo=echo=="true", future=True) # TODO
metadata = Base.metadata
metadata.create_all(engine)
logger.info("Installed")
return engine

View File

@@ -1,4 +1,4 @@
from .create import create
from .read import read
from .read import read, read_all
from .update import update
from .delete import delete
from .delete import delete

View File

@@ -11,4 +11,5 @@ def create(session:Session, library:Base):
session.commit()
logger.debug("Committed Library")
session.refresh(library)
logger.debug("Rerfreshed Library")
logger.debug("Refreshed Library")
return library

View File

@@ -5,4 +5,8 @@ from ....schema.library.base import Base
def read(session:Session, _id:int, obj:Base):
stmt = select(obj).where(obj.id == _id)
return session.scalars(stmt).one()
return session.scalars(stmt).one()
def read_all(session:Session, obj:Base):
stmt = select(obj)
return session.scalars(stmt).fetchall() #TODO: Pagination

View File

@@ -1,3 +1,8 @@
__version__ = "0.2.0.dev"
from flask import Flask
app = Flask(__name__)
from .routes.api import api
app = Flask(__name__)
app.register_blueprint(api)

36
app/config/__init__.py Normal file
View File

@@ -0,0 +1,36 @@
import configparser
from .defaults import default_db_query
from .defaults import default_app_port, default_app_debug
import logging
logger = logging.getLogger(__name__)
config = configparser.ConfigParser()
config.read("config.ini")
logger.debug(f"config: {config.sections()}")
def save_config():
with open("config.ini", "w") as f:
config.write(f)
def check_config():
save = False
if not "DataBase" in config:
logger.debug("DataBase not found in Config")
config["DataBase"] = {
"query": default_db_query
}
save = True
if not "App" in config:
logger.debug("App not found in Config")
config["App"] = {
"port": default_app_port,
"debug": default_app_debug
}
save = True
if save: save_config()
check_config()

3
app/config/defaults.py Normal file
View File

@@ -0,0 +1,3 @@
default_db_query = "sqlite:///library.db"
default_app_port = 15012
default_app_debug = False

View File

@@ -1,10 +1,17 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.exc import IntegrityError
from app.api.cruds.base import create, read, update, delete
from app.api.cruds.base import create, read, update, delete, read_all
from app.schema.library import Library
from ..db.config.config import get_engine_configuration
from sqlalchemy.exc import NoResultFound
from .exceptions import LibraryCreationException
from .exceptions import LibraryReadException
from .exceptions import LibraryUpdateException
import logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
@@ -18,9 +25,23 @@ class LibraryController:
else:
self._engine = engine
self._Session = sessionmaker(bind=self.engine)
self._session = self._Session()
self._library = None
if library_id is not None:
self.read(library_id)
self._libraries = []
def __del__(self):
self.session.close()
def __enter__(self):
return self
def __exit__(self, *exc):
del(self)
return False
@property
def data(self):
@@ -36,25 +57,61 @@ class LibraryController:
@property
def session(self):
return self.Session()
return self._session
@property
def libraries(self):
if self._library and len(self._libraries) == 0:
self._libraries = [self._library]
return self._libraries
def set_library(self, _id):
libraries = filter(lambda x: x.get("id") == _id, self.libraries)
if len(libraries) == 1:
self._library = libraries[0]
return True
return False
#CRUDS
def create(self, library:Library):
with self.session as session:
self._library = create(session, library)
try:
self._library = create(self.session, library)
except IntegrityError as e:
raise LibraryCreationException(
"Cannot create library",
f"{e.orig}",
"library",
str(library)
)
return self
def read(self, _id):
with self.session as session:
self._library = read(session, _id, Library)
try:
self._library = read(self.session, _id, Library)
except NoResultFound as e:
raise LibraryReadException(
f"Cannot read Library with id {_id}",
f"{e}",
"library",
_id
)
return self
def read_all(self):
self._libraries = read_all(self.session, Library)
def update(self):
with self.session as session:
session.commit()
try:
self.session.commit()
except IntegrityError as e:
raise LibraryUpdateException(
f"Cannot update Library",
f"{e}",
"library",
None
)
def delete(self):
with self.session as session:
delete(session, self)
del(self)
delete(self.session, self.data)
del(self)

View File

@@ -0,0 +1,5 @@
from .base import LibraryExceptionBase
from .exc_01000X_data import LibraryDataExection
from .exc_01001X_create import LibraryCreationException
from .exc_01002X_read import LibraryReadException
from .exc_01003X_update import LibraryUpdateException

View File

@@ -0,0 +1,26 @@
class LibraryExceptionBase(Exception):
def __init__(self, name, error, object_name, data, *, status_code=400):
self.code = "000000"
self.name = name
self.error = error
self.object = object_name
self.data = data
self.status_code = status_code
def to_dict(self):
return {
"status": "error",
"name": self.name,
"code": self.code,
"error": self.error,
"object": self.object,
"data": self.data,
"status_code": self.status_code
}
def __str__(self):
return f"ERROR {self.code}: {self.error}"
def __repr__(self):
return f"{self.__class__}(code={self.code!r}, error={self.error!r}), " \
f"object={self.object!r}, data={self.data!r}, status_code={self._status_code})"

View File

@@ -0,0 +1,7 @@
from .base import LibraryExceptionBase
#010010
class LibraryDataExection(LibraryExceptionBase):
def __init__(self, name, error, object_name, data, *, status_code=400):
self.code = "010000"
super().__init__(name, error, object_name, data, status_code=status_code)

View File

@@ -0,0 +1,7 @@
from .base import LibraryExceptionBase
#010010
class LibraryCreationException(LibraryExceptionBase):
def __init__(self, name, error, object_name, data, *, status_code=400):
self.code = "010010"
super().__init__(name, error, object_name, data, status_code=status_code)

View File

@@ -0,0 +1,7 @@
from .base import LibraryExceptionBase
#010030
class LibraryReadException(LibraryExceptionBase):
def __init__(self, name, error, object_name, data, status_code=404):
super().__init__(name, error, object_name, data, status_code=status_code)
self.code = "010030"

View File

@@ -0,0 +1,7 @@
from .base import LibraryExceptionBase
#010020
class LibraryUpdateException(LibraryExceptionBase):
def __init__(self, name, error, object_name, data, status_code=404):
super().__init__(name, error, object_name, data, status_code=status_code)
self.code = "010020"

View File

@@ -0,0 +1 @@
from .update_item_key import update_item_key

View File

@@ -0,0 +1,21 @@
from ..exceptions import LibraryDataExection
from ...schema.library import Base
def update_item_key(obj:Base, key, value):
if key == "id":
raise LibraryDataExection(
"id is not updatable",
"The key ID is not Updatable",
obj.__class__,
{key: value}
)
try:
obj.__getattribute__(key)
except AttributeError:
raise LibraryDataExection(
f"{key} not in {obj.__class__}",
f"The key {key} is not in {obj.__class__}",
obj.__class__,
{key: value}
)
obj.__setattr__(key, value)

View File

@@ -1,10 +1,12 @@
import os
from ...config import config
import logging
logger = logging.getLogger(__name__)
def get_engine_configuration():
echo = os.getenv("DEV_URIA_BIBLIOGAME_DEBUG", "false")
engine_string = os.getenv("DEV_URIA_BIBLIOGAME_CONFIG_DB", "sqlite://")
engine_string = os.getenv("DEV_URIA_BIBLIOGAME_CONFIG_DB", config["DataBase"].get("query", "sqlite:///"))
logger.debug(f"engine_string, {engine_string}")
return (engine_string, echo)

40
app/routes/__init__.py Normal file
View File

@@ -0,0 +1,40 @@
from flask import json, make_response
from werkzeug.exceptions import HTTPException
from .api import api
from ..controller.exceptions import LibraryExceptionBase
@api.errorhandler(LibraryExceptionBase)
def handle_exception(e):
"""Return JSON instead of HTML for HTTP errors."""
# start with the correct headers and status code from the error
response = make_response()
# replace the body with JSON
response.data = json.dumps({
"status": "error",
"code": e.code,
"status_code": e.status_code,
"name": e.name,
"error": e.error,
"data": e.data
})
response.content_type = "application/json"
response.status_code = e.status_code
return response
@api.errorhandler(HTTPException)
def handle_exception(e):
"""Return JSON instead of HTML for HTTP errors."""
# start with the correct headers and status code from the error
response = e.get_response()
# replace the body with JSON
response.data = json.dumps({
"status": "error",
"code": f"000{e.code}",
"status_code": e.code,
"name": e.name,
"error": e.description,
})
response.content_type = "application/json"
return response

View File

@@ -0,0 +1,6 @@
from .blueprint import api
from .install import post_install
from .library import api_library
api.register_blueprint(api_library)

View File

@@ -0,0 +1,3 @@
from flask import Blueprint
api = Blueprint("api", __name__, url_prefix="/api")

24
app/routes/api/install.py Normal file
View File

@@ -0,0 +1,24 @@
from flask import request
from ...config import config, save_config
from ...api.actions.install import install
import logging
logger = logging.getLogger(__name__)
from .blueprint import api
@api.route("/install", methods=["POST"])
def post_install():
try:
body = request.json
except:
logger.debug("Installing with config.ini params")
else:
if body.get("query_string"):
config["DataBase"]["query"] = body.get("query_string")
save_config()
finally:
install()
return { "status": "ok" }, 200

View File

@@ -0,0 +1,5 @@
from .blueprint import api_library
from .create import create_library
from .read import read_libraries, read_library
from .update import update_library
from .delete import delete_library

View File

@@ -0,0 +1,3 @@
from flask import Blueprint
api_library = Blueprint("api_library", __name__, url_prefix="/library")

View File

@@ -0,0 +1,19 @@
from flask import request
from .blueprint import api_library
from ....controller import LibraryController
from ....schema.library.library import Library
import logging
logger = logging.getLogger(__name__)
@api_library.route("/", methods=["POST"])
def create_library():
data = request.json
with LibraryController() as controller:
lib = Library(**data)
library = controller.create(lib)
return { "status": "ok", "result": library.data.to_dict() }, 200

View File

@@ -0,0 +1,19 @@
from flask import request
from sqlalchemy.exc import NoResultFound
from .blueprint import api_library
from ....controller import LibraryController
from ....schema.library.library import Library
import logging
logger = logging.getLogger(__name__)
@api_library.route("/<_id>", methods=["DELETE"])
def delete_library(_id):
controller = LibraryController(_id)
controller.delete()
return { "status": "ok" }, 201

View File

@@ -0,0 +1,21 @@
from flask import request
from sqlalchemy.exc import NoResultFound
from .blueprint import api_library
from ....controller import LibraryController
from ....schema.library.library import Library
import logging
logger = logging.getLogger(__name__)
@api_library.route("/", methods=["GET"])
def read_libraries():
library = LibraryController()
library.read_all()
return { "status": "ok", "results": [lib.to_dict() for lib in library.libraries] }, 200
@api_library.route("/<_id>", methods=["GET"])
def read_library(_id):
library = LibraryController(_id)
return { "status": "ok", "result": library.data.to_dict() }, 200

View File

@@ -0,0 +1,27 @@
from flask import request
from sqlalchemy.exc import IntegrityError, NoResultFound
from .blueprint import api_library
from ....controller import LibraryController
from ....controller.functions import update_item_key
from ....schema.library.library import Library
import logging
logger = logging.getLogger(__name__)
@api_library.route("/<_id>", methods=["PATCH"])
def update_library(_id):
data:dict = request.json
controller = LibraryController(_id)
library = controller.data
for key, value in data.items():
update_item_key(library, key, value)
controller.update()
return { "status": "ok", "result": controller.data.to_dict() }, 200

View File

@@ -10,8 +10,8 @@ from .base import Base
class Library(Base):
__tablename__ = "library"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(255))
notes: Mapped[str] = mapped_column(String(65656))
name: Mapped[str] = mapped_column(String(255), unique=True)
notes: Mapped[Optional[str]] = mapped_column(String(65656))
envs: Mapped[List["Env"]] = relationship(
back_populates="library", cascade="all, delete-orphan"
@@ -29,5 +29,12 @@ class Library(Base):
back_populates="library", cascade="all, delete-orphan"
)
def to_dict(self) -> dict:
return {
"id": self.id,
"name": self.name,
"notes": self.notes
}
def __repr__(self) -> str:
return f"Library(id={self.id!r}, name={self.name!r}, notes={self.notes!r})"

7
config.ini Normal file
View File

@@ -0,0 +1,7 @@
[DataBase]
query = sqlite:///test.db
[App]
port = 15012
debug = True

14
main.py
View File

@@ -0,0 +1,14 @@
from app import app
from app.config import config
import logging
level = config["App"].get("debug", False) and logging.DEBUG or logging.INFO
logging.basicConfig(level=level)
logger = logging.getLogger(__name__)
logger.info(f"Logging level set to {level}")
if __name__ == "__main__":
app.run(port=config["App"]["port"], debug=config["App"]["debug"])

BIN
test.db Normal file

Binary file not shown.

View File

@@ -1,21 +1,23 @@
import os
import unittest
from sqlalchemy.orm import sessionmaker
from sqlalchemy.exc import NoResultFound
from app.api.actions import install
from app.schema.library import Library, Tag, Book, BookTag, Path, Env
from app.controller import LibraryController
from app.controller.exceptions import LibraryReadException
import logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
class TestDB(unittest.TestCase):
class TestController(unittest.TestCase):
def setUp(self):
os.environ["DEV_URIA_BIBLIOGAME_CONFIG_DB"] = "sqlite:///"
os.environ["DEV_URIA_BIBLIOGAME_DEBUG"] = "true"
os.environ["DEV_URIA_BIBLIOGAME_DEBUG"] = "false"
self.engine = install()
self.library = LibraryController(engine = self.engine)
@@ -57,48 +59,47 @@ class TestDB(unittest.TestCase):
return super().setUp()
def test_install(self):
library = LibraryController(1, engine = self.engine)
library = LibraryController(1, engine=self.engine)
library_string = str(library.data)
self.assertEqual(library_string, str(self.library.data))
"""
def test_read_name(self):
library = read(self.session, 1)
logger.debug(f"Name: {library.name}")
self.assertEqual(library.name, self.library.name)
self.assertEqual(library.name, "Library Test")
library = LibraryController(1, engine=self.engine)
logger.debug(f"Name: {library.data.name}")
self.assertEqual(library.data.name, self.library.data.name)
self.assertEqual(library.data.name, "Library Test")
def test_read_notes(self):
library = read(self.session, 1)
logger.debug(f"Notes: {library.notes}")
self.assertEqual(library.notes, self.library.notes)
self.assertEqual(library.notes, "My duckling library test")
library = LibraryController(1, engine=self.engine)
logger.debug(f"Notes: {library.data.notes}")
self.assertEqual(library.data.notes, self.library.data.notes)
self.assertEqual(library.data.notes, "My duckling library test")
def test_read_path(self):
library = read(self.session, 1)
logger.debug(f"PATH: {library.paths[0].path}")
self.assertEqual(library.paths[0].path, self.library.paths[0].path)
self.assertEqual(library.paths[0].path, "/home/ivan/Documentos/ttprpg")
library = LibraryController(1, engine=self.engine)
logger.debug(f"PATH: {library.data.paths[0].path}")
self.assertEqual(library.data.paths[0].path, self.library.data.paths[0].path)
self.assertEqual(library.data.paths[0].path, "/home/ivan/Documentos/ttprpg")
def test_read_env(self):
library = read(self.session, 1)
logger.debug(f"ENV: {library.envs[0].key} - {library.envs[0].value}")
self.assertEqual(library.envs[0].key, self.library.envs[0].key)
self.assertEqual(library.envs[0].value, self.library.envs[0].value)
self.assertEqual(library.envs[0].key, "ENVIRONMENT_VARIABLE")
self.assertEqual(library.envs[0].value, "Clearly an environment variable")
library = LibraryController(1, engine=self.engine)
logger.debug(f"ENV: {library.data.envs[0].key} - {library.data.envs[0].value}")
self.assertEqual(library.data.envs[0].key, self.library.data.envs[0].key)
self.assertEqual(library.data.envs[0].value, self.library.data.envs[0].value)
self.assertEqual(library.data.envs[0].key, "ENVIRONMENT_VARIABLE")
self.assertEqual(library.data.envs[0].value, "Clearly an environment variable")
def test_read_book(self):
library = read(self.session, 1)
book = library.books[0]
library = LibraryController(1, engine=self.engine)
book = library.data.books[0]
logger.debug(f"BOOK: {book}")
self.assertEqual(book.name, self.library.books[0].name)
self.assertEqual(book.name, self.library.data.books[0].name)
self.assertEqual(book.name, "Test book")
def test_read_tags(self):
library = read(self.session, 1)
tags = library.tags
self.assertEqual(tags, self.library.tags)
self.assertEqual(tags, self.tags)
library = LibraryController(1, engine=self.engine)
tags = library.data.tags
self.assertEqual(str(tags), str(self.library.data.tags))
self.assertEqual(str(tags[0]), str(self.tags[0]))
self.assertEqual(tags[0].name, self.tags[0].name)
self.assertEqual(tags[0].name, "Foo")
@@ -107,9 +108,9 @@ class TestDB(unittest.TestCase):
self.assertEqual(tags[1].name, "Bar")
def test_read_book_tags(self):
library = read(self.session, 1)
book = library.books[0]
tags = library.tags
library = LibraryController(1, engine=self.engine)
book = library.data.books[0]
tags = library.data.tags
logger.debug(f"BOOK TAGS: {book.tags}")
self.assertEqual(str(book.tags[0].tag), str(self.tags[0]))
self.assertEqual(str(book.tags[0].tag), str(tags[0]))
@@ -118,26 +119,31 @@ class TestDB(unittest.TestCase):
self.assertNotEqual(book.tags[0].tag.name, "Bar")
def test_update_name(self):
library = read(self.session, 1)
library.name = "Another Library"
update(self.session, library)
library1 = read(self.session, 1)
self.assertEqual(library1.name, self.library.name)
self.assertNotEqual(library1.name, "Library Test")
self.assertEqual(library1.name, "Another Library")
library = LibraryController(1, engine=self.engine)
library.data.name = "Another Library"
library.update()
library1 = LibraryController(1, engine=self.engine)
self.assertEqual(library1.data.name, self.library.data.name)
self.assertNotEqual(library1.data.name, "Library Test")
self.assertEqual(library1.data.name, "Another Library")
def test_update_name(self):
library = read(self.session, 1)
library.books[0].name = "Another Book on the shelf"
update(self.session, library)
library = LibraryController(1, engine=self.engine)
library.data.books[0].name = "Another Book on the shelf"
library.update()
library1 = read(self.session, 1)
book = library1.books[0]
library1 = LibraryController(1, engine=self.engine)
book = library1.data.books[0]
self.assertEqual(book.name, self.library.books[0].name)
self.assertEqual(book.name, self.library.data.books[0].name)
self.assertNotEqual(book.name, "Test book")
self.assertEqual(book.name, "Another Book on the shelf")
"""
self.assertEqual(book.name, "Another Book on the shelf")
def test_delete_library(self):
library = LibraryController(1, engine=self.engine)
library.delete()
self.assertRaises(LibraryReadException, LibraryController, 1, engine=self.engine)
if __name__ == "__main__":
unittest.main()

View File

@@ -2,11 +2,13 @@ import os
import unittest
from sqlalchemy.orm import sessionmaker
from sqlalchemy.exc import NoResultFound
from app.api.actions import install
from app.api.cruds.base import create, read, update
from app.api.cruds.base import create, read, update, delete
from app.schema.library import Library, Path, Env, Book, Tag, BookTag
import logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
@@ -15,7 +17,7 @@ class TestDB(unittest.TestCase):
def setUp(self):
os.environ["DEV_URIA_BIBLIOGAME_CONFIG_DB"] = "sqlite:///"
os.environ["DEV_URIA_BIBLIOGAME_DEBUG"] = "true"
os.environ["DEV_URIA_BIBLIOGAME_DEBUG"] = "false"
self.engine = install()
self.tags = [
Tag(
@@ -135,7 +137,13 @@ class TestDB(unittest.TestCase):
self.assertEqual(book.name, self.library.books[0].name)
self.assertNotEqual(book.name, "Test book")
self.assertEqual(book.name, "Another Book on the shelf")
self.assertEqual(book.name, "Another Book on the shelf")
def test_delete_library(self):
library = read(self.session, 1, Library)
delete(self.session, library)
self.assertRaises(NoResultFound, read, self.session, 1, Library)
if __name__ == "__main__":