Compare commits

..

5 Commits

Author SHA1 Message Date
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
26 changed files with 347 additions and 68 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 import create_engine
from sqlalchemy.orm import Session
from ...schema.library.base import Base from ...schema.library.base import Base
from ...db.config.config import get_engine_configuration from ...db.config.config import get_engine_configuration
import logging
logger = logging.getLogger(__name__)
def install(): def install():
logger.info("Installing")
engine_string, echo = get_engine_configuration() engine_string, echo = get_engine_configuration()
logger.debug(f"engine_string: {engine_string}")
engine = create_engine(engine_string, echo=echo=="true", future=True) # TODO engine = create_engine(engine_string, echo=echo=="true", future=True) # TODO
metadata = Base.metadata metadata = Base.metadata
metadata.create_all(engine) metadata.create_all(engine)
logger.info("Installed")
return engine return engine

View File

@@ -1,4 +1,4 @@
from .create import create from .create import create
from .read import read from .read import read, read_all
from .update import update 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() session.commit()
logger.debug("Committed Library") logger.debug("Committed Library")
session.refresh(library) session.refresh(library)
logger.debug("Rerfreshed Library") logger.debug("Refreshed Library")
return library

View File

@@ -6,3 +6,7 @@ from ....schema.library.base import Base
def read(session:Session, _id:int, obj:Base): def read(session:Session, _id:int, obj:Base):
stmt = select(obj).where(obj.id == _id) 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 from flask import Flask
from .routes.api import api
app = Flask(__name__) 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,7 +1,7 @@
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
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 app.schema.library import Library
from ..db.config.config import get_engine_configuration from ..db.config.config import get_engine_configuration
@@ -18,10 +18,24 @@ class LibraryController:
else: else:
self._engine = engine self._engine = engine
self._Session = sessionmaker(bind=self.engine) self._Session = sessionmaker(bind=self.engine)
self._session = self._Session()
self._library = None self._library = None
if library_id is not None: if library_id is not None:
self.read(library_id) 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 @property
def data(self): def data(self):
return self._library return self._library
@@ -36,25 +50,36 @@ class LibraryController:
@property @property
def session(self): 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
def create(self, library:Library): def create(self, library:Library):
with self.session as session: self._library = create(self.session, library)
self._library = create(session, library)
return self return self
def read(self, _id): def read(self, _id):
with self.session as session: self._library = read(self.session, _id, Library)
self._library = read(session, _id, Library)
return self return self
def read_all(self):
self._libraries = read_all(self.session, Library)
def update(self): def update(self):
with self.session as session: self.session.commit()
session.commit()
def delete(self): def delete(self):
with self.session as session: delete(self.session, self)
delete(session, self) del(self)
del(self)

View File

@@ -1,10 +1,12 @@
import os import os
from ...config import config
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def get_engine_configuration(): def get_engine_configuration():
echo = os.getenv("DEV_URIA_BIBLIOGAME_DEBUG", "false") 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}") logger.debug(f"engine_string, {engine_string}")
return (engine_string, echo) return (engine_string, echo)

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

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

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,28 @@
from flask import request
from sqlalchemy.exc import IntegrityError
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():
try:
data = request.json
except Exception as e:
logger.debug(f"{e}")
return { "status": "error", "error": "JSON Required" }, 415
with LibraryController() as controller:
try:
lib = Library(**data)
library = controller.create(lib)
except IntegrityError as e:
logger.debug(f"DB Error Creating {e}")
return { "status": "error", "error": f"{e.orig}" }, 400
else:
return { "status": "ok", "result": library.data.to_dict() }, 200

View File

@@ -0,0 +1,23 @@
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):
try: # TODO: function
controller = LibraryController(_id)
except NoResultFound as e:
logger.debug({e})
return { "status": "error", "error": "Library not found" }, 404
controller.delete()
return { "status": "ok" }, 201

View File

@@ -0,0 +1,27 @@
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):
try:
library = LibraryController(_id)
except NoResultFound as e:
logger.debug(f"No result found for Library wid id {_id}")
logger.debug(f"Error {e}")
logger.debug(f"Error {dir(e)}")
return { "status": "error", "result": "Library not found"}, 404
return { "status": "ok", "result": library.data.to_dict() }, 200

View File

@@ -0,0 +1,50 @@
from flask import request
from sqlalchemy.exc import IntegrityError, NoResultFound
from .blueprint import api_library
from ....controller import LibraryController
from ....schema.library.library import Library
import logging
logger = logging.getLogger(__name__)
def update_library_item(library:Library, key, value):
if key == "id":
raise AttributeError("id is not updatable")
try:
library.__getattribute__(key)
except AttributeError:
raise AttributeError(f"{key} not in library")
library.__setattr__(key, value)
@api_library.route("/<_id>", methods=["PATCH"])
def update_library(_id):
try:
data:dict = request.json
logger.debug(f"data: {data}")
except Exception as e:
logger.debug(f"{e}")
return { "status": "error", "error": "JSON Required" }, 415
try: # TODO: function
controller = LibraryController(_id)
except NoResultFound as e:
logger.debug({e})
return { "status": "error", "error": "Library not found" }, 404
library = controller.data
for key, value in data.items():
try:
update_library_item(library, key, value)
except AttributeError as e:
logger.debug(f"Error updating {e}")
return { "status": "error", "error": e.name }
try:
controller.update()
except IntegrityError as e:
logger.debug(f"DB Error Creating {e}")
return { "status": "error", "error": f"{e.orig}" }, 400
else:
return { "status": "ok", "result": controller.data.to_dict() }, 200

View File

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

14
main.py
View File

@@ -0,0 +1,14 @@
__version__ = "0.2.0.dev"
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

@@ -15,7 +15,7 @@ class TestDB(unittest.TestCase):
def setUp(self): def setUp(self):
os.environ["DEV_URIA_BIBLIOGAME_CONFIG_DB"] = "sqlite:///" 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.engine = install()
self.library = LibraryController(engine = self.engine) self.library = LibraryController(engine = self.engine)
@@ -57,48 +57,47 @@ class TestDB(unittest.TestCase):
return super().setUp() return super().setUp()
def test_install(self): def test_install(self):
library = LibraryController(1, engine = self.engine) library = LibraryController(1, engine=self.engine)
library_string = str(library.data) library_string = str(library.data)
self.assertEqual(library_string, str(self.library.data)) self.assertEqual(library_string, str(self.library.data))
"""
def test_read_name(self): def test_read_name(self):
library = read(self.session, 1) library = LibraryController(1, engine=self.engine)
logger.debug(f"Name: {library.name}") logger.debug(f"Name: {library.data.name}")
self.assertEqual(library.name, self.library.name) self.assertEqual(library.data.name, self.library.data.name)
self.assertEqual(library.name, "Library Test") self.assertEqual(library.data.name, "Library Test")
def test_read_notes(self): def test_read_notes(self):
library = read(self.session, 1) library = LibraryController(1, engine=self.engine)
logger.debug(f"Notes: {library.notes}") logger.debug(f"Notes: {library.data.notes}")
self.assertEqual(library.notes, self.library.notes) self.assertEqual(library.data.notes, self.library.data.notes)
self.assertEqual(library.notes, "My duckling library test") self.assertEqual(library.data.notes, "My duckling library test")
def test_read_path(self): def test_read_path(self):
library = read(self.session, 1) library = LibraryController(1, engine=self.engine)
logger.debug(f"PATH: {library.paths[0].path}") logger.debug(f"PATH: {library.data.paths[0].path}")
self.assertEqual(library.paths[0].path, self.library.paths[0].path) self.assertEqual(library.data.paths[0].path, self.library.data.paths[0].path)
self.assertEqual(library.paths[0].path, "/home/ivan/Documentos/ttprpg") self.assertEqual(library.data.paths[0].path, "/home/ivan/Documentos/ttprpg")
def test_read_env(self): def test_read_env(self):
library = read(self.session, 1) library = LibraryController(1, engine=self.engine)
logger.debug(f"ENV: {library.envs[0].key} - {library.envs[0].value}") logger.debug(f"ENV: {library.data.envs[0].key} - {library.data.envs[0].value}")
self.assertEqual(library.envs[0].key, self.library.envs[0].key) self.assertEqual(library.data.envs[0].key, self.library.data.envs[0].key)
self.assertEqual(library.envs[0].value, self.library.envs[0].value) self.assertEqual(library.data.envs[0].value, self.library.data.envs[0].value)
self.assertEqual(library.envs[0].key, "ENVIRONMENT_VARIABLE") self.assertEqual(library.data.envs[0].key, "ENVIRONMENT_VARIABLE")
self.assertEqual(library.envs[0].value, "Clearly an environment variable") self.assertEqual(library.data.envs[0].value, "Clearly an environment variable")
def test_read_book(self): def test_read_book(self):
library = read(self.session, 1) library = LibraryController(1, engine=self.engine)
book = library.books[0] book = library.data.books[0]
logger.debug(f"BOOK: {book}") 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") self.assertEqual(book.name, "Test book")
def test_read_tags(self): def test_read_tags(self):
library = read(self.session, 1) library = LibraryController(1, engine=self.engine)
tags = library.tags tags = library.data.tags
self.assertEqual(tags, self.library.tags) self.assertEqual(str(tags), str(self.library.data.tags))
self.assertEqual(tags, self.tags)
self.assertEqual(str(tags[0]), str(self.tags[0])) self.assertEqual(str(tags[0]), str(self.tags[0]))
self.assertEqual(tags[0].name, self.tags[0].name) self.assertEqual(tags[0].name, self.tags[0].name)
self.assertEqual(tags[0].name, "Foo") self.assertEqual(tags[0].name, "Foo")
@@ -107,9 +106,9 @@ class TestDB(unittest.TestCase):
self.assertEqual(tags[1].name, "Bar") self.assertEqual(tags[1].name, "Bar")
def test_read_book_tags(self): def test_read_book_tags(self):
library = read(self.session, 1) library = LibraryController(1, engine=self.engine)
book = library.books[0] book = library.data.books[0]
tags = library.tags tags = library.data.tags
logger.debug(f"BOOK TAGS: {book.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(self.tags[0]))
self.assertEqual(str(book.tags[0].tag), str(tags[0])) self.assertEqual(str(book.tags[0].tag), str(tags[0]))
@@ -118,26 +117,26 @@ class TestDB(unittest.TestCase):
self.assertNotEqual(book.tags[0].tag.name, "Bar") self.assertNotEqual(book.tags[0].tag.name, "Bar")
def test_update_name(self): def test_update_name(self):
library = read(self.session, 1) library = LibraryController(1, engine=self.engine)
library.name = "Another Library" library.data.name = "Another Library"
update(self.session, library) library.update()
library1 = read(self.session, 1) library1 = LibraryController(1, engine=self.engine)
self.assertEqual(library1.name, self.library.name) self.assertEqual(library1.data.name, self.library.data.name)
self.assertNotEqual(library1.name, "Library Test") self.assertNotEqual(library1.data.name, "Library Test")
self.assertEqual(library1.name, "Another Library") self.assertEqual(library1.data.name, "Another Library")
def test_update_name(self): def test_update_name(self):
library = read(self.session, 1) library = LibraryController(1, engine=self.engine)
library.books[0].name = "Another Book on the shelf" library.data.books[0].name = "Another Book on the shelf"
update(self.session, library) library.update()
library1 = read(self.session, 1) library1 = LibraryController(1, engine=self.engine)
book = library1.books[0] 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.assertNotEqual(book.name, "Test book")
self.assertEqual(book.name, "Another Book on the shelf") self.assertEqual(book.name, "Another Book on the shelf")
"""
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View File

@@ -15,7 +15,7 @@ class TestDB(unittest.TestCase):
def setUp(self): def setUp(self):
os.environ["DEV_URIA_BIBLIOGAME_CONFIG_DB"] = "sqlite:///" 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.engine = install()
self.tags = [ self.tags = [
Tag( Tag(