diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..d42765a --- /dev/null +++ b/.envrc @@ -0,0 +1,4 @@ +source .venv/bin/activate +uv sync +export PYTHONBREAKPOINT="pudb.set_trace" +export PYTHONSTARTUP="ipython_startup.py" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..025f5ca --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +test: + uv run pytest tests + +test-cov: + uv run coverage run -m pytest tests + +cov-report: + uv run coverage combine && uv run coverage report + +cov: test-cov cov-report + +typing: + uv run mypy --install-types --non-interactive src/pyshelf tests + +style: + uv run ruff . && uv run black --check --diff . + +fmt: + uv run black . && uv run ruff --fix . && make style + +lint: style typing + +compile: + cd src/frontend && sh compile.sh && cd ../.. + +install: + cd src/frontend && npm install diff --git a/pyproject.toml b/pyproject.toml index 788830d..d329f88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,14 +1,14 @@ [build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" +requires = ["setuptools>=64", "wheel"] +build-backend = "setuptools.build_meta" [project] name = "pyshelf" -dynamic = ["version"] -description = '' +version = "0.1.0" +description = "" readme = "README.md" requires-python = ">=3.12" -license = "GPL-3.0-or-later" +license = { text = "GPL-3.0-or-later" } keywords = [] authors = [ { name = "th3r00t", email = "tty0@th3r00t.dev" }, @@ -24,7 +24,12 @@ dependencies = [ "websockets", "loguru", "pypdf2", "bs4", "requests", "psycopg2-binary", "mobi-python", "lxml", "sqlalchemy", "sqlalchemy.orm", "fastapi[all]", "jinja2", "libsass", "nodejs-bin", "npm", "brotlipy", "debugpy", "pudb", - "ptipython", "chardet", "pre-commit" + "ptipython", "chardet", "pre-commit", "coverage[toml]>=6.5","pytest", + "black>=23.1.0", "mypy>=1.0.0", "ruff>=0.0.243" +] + +[project.optional-dependencies] +dev = [ ] [project.urls] @@ -32,51 +37,8 @@ Documentation = "https://github.com/th3r00t/pyshelf#readme" Issues = "https://github.com/th3r00t/pyshelf/issues" Source = "https://github.com/th3r00t/pyshelf" -[tool.hatch.version] -path = "src/__about__.py" - -[tool.hatch.envs.default] -dependencies = [ - "coverage[toml]>=6.5", - "pytest", -] -[tool.hatch.envs.default.scripts] -test = "pytest {args:tests}" -test-cov = "coverage run -m pytest {args:tests}" -cov-report = [ - "- coverage combine", - "coverage report", -] -cov = [ - "test-cov", - "cov-report", -] - -[[tool.hatch.envs.all.matrix]] -python = ["3.12"] - -[tool.hatch.envs.lint] -detached = true -dependencies = [ - "black>=23.1.0", - "mypy>=1.0.0", - "ruff>=0.0.243", -] -[tool.hatch.envs.lint.scripts] -typing = "mypy --install-types --non-interactive {args:src/pyshelf tests}" -style = [ - "ruff {args:.}", - "black --check --diff {args:.}", -] -fmt = [ - "black {args:.}", - "ruff --fix {args:.}", - "style", -] -all = [ - "style", - "typing", -] +[tool.setuptools.packages.find] +where = ["src"] [tool.black] target-version = ["py312"] @@ -87,46 +49,14 @@ skip-string-normalization = true target-version = "py312" line-length = 120 select = [ - "A", - "ARG", - "B", - "C", - "DTZ", - "E", - "EM", - "F", - "FBT", - "I", - "ICN", - "ISC", - "N", - "PLC", - "PLE", - "PLR", - "PLW", - "Q", - "RUF", - "S", - "T", - "TID", - "UP", - "W", - "YTT", + "A", "ARG", "B", "C", "DTZ", "E", "EM", "F", "FBT", "I", "ICN", "ISC", "N", + "PLC", "PLE", "PLR", "PLW", "Q", "RUF", "S", "T", "TID", "UP", "W", "YTT" ] ignore = [ - # Allow non-abstract empty methods in abstract base classes - "B027", - # Allow boolean positional values in function calls, like `dict.get(... True)` - "FBT003", - # Ignore checks for possible passwords - "S105", "S106", "S107", - # Ignore complexity - "C901", "PLR0911", "PLR0912", "PLR0913", "PLR0915", -] -unfixable = [ - # Don't touch unused imports - "F401", + "B027", "FBT003", "S105", "S106", "S107", "C901", "PLR0911", + "PLR0912", "PLR0913", "PLR0915" ] +unfixable = ["F401"] [tool.ruff.isort] known-first-party = ["pyshelf"] @@ -135,16 +65,13 @@ known-first-party = ["pyshelf"] ban-relative-imports = "all" [tool.ruff.per-file-ignores] -# Tests can use magic values, assertions, and relative imports "tests/**/*" = ["PLR2004", "S101", "TID252"] [tool.coverage.run] source_pkgs = ["pyshelf", "tests"] branch = true parallel = true -omit = [ - "src/pyshelf/__about__.py", -] +omit = ["src/pyshelf/__about__.py"] [tool.coverage.paths] pyshelf = ["src/pyshelf", "*/pyshelf/src/pyshelf"] @@ -156,13 +83,11 @@ exclude_lines = [ "if __name__ == .__main__.:", "if TYPE_CHECKING:", ] + [tool.isort] force_grid_wrap = 0 include_trailing_comma = true line_length = 88 multi_line_output = 3 use_parentheses = true -# NOTE: the known_third_party setting is managed by -# seed-isort-config and should not be modified directly. -# Any changes made to this setting will be overwritten. -known_third_party = ["backend", "bs4", "django", "interface", "mobi", "prompt_toolkit", "psycopg2-binary", "pyfiglet", "requests"] +known_third_party = ["backend", "bs4", "django", "interface", "mobi", "prompt_toolkit", "psycopg2-binary", "pyfiglet", "requests"] \ No newline at end of file diff --git a/src/backend/lib/models.py b/src/backend/lib/models.py index 6609632..f51be29 100644 --- a/src/backend/lib/models.py +++ b/src/backend/lib/models.py @@ -1,7 +1,8 @@ from typing import Optional from typing_extensions import Annotated -from sqlalchemy import func -from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column + +from sqlalchemy import func, ForeignKey +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship import datetime timestamp = Annotated[ @@ -13,6 +14,7 @@ timestamp = Annotated[ class Base(DeclarativeBase): """Base class for all models.""" +from sqlalchemy.orm import relationship class Book(Base): """Book model.""" @@ -34,6 +36,9 @@ class Book(Base): identifier: Mapped[Optional[str]] publisher: Mapped[Optional[str]] + # One book → many collection entries + collections = relationship("Collection", back_populates="book", cascade="all, delete-orphan") + class Collection(Base): """Collection model.""" @@ -42,3 +47,7 @@ class Collection(Base): id: Mapped[int] = mapped_column(primary_key=True) collection: Mapped[str] + book_id: Mapped[int] = mapped_column(ForeignKey("Book.id")) + + # Each collection entry points to one book + book = relationship("Book", back_populates="collections") \ No newline at end of file diff --git a/src/backend/lib/storage.py b/src/backend/lib/storage.py index 592da4f..c927c04 100644 --- a/src/backend/lib/storage.py +++ b/src/backend/lib/storage.py @@ -209,7 +209,8 @@ class Storage: _result = session.execute( select(Book) .join(Collection) - .where(Collection.id == collection) + # .where(Collection.id == collection) + .where(Collection.collection == collection) .offset(skip) .limit(limit) ).all() @@ -246,3 +247,16 @@ class Storage: _result = session.execute(select(Collection).join(Book)).all() session.close() return _result + + def get_collection(self, name): + """Get collection from database. + + Returns + ------- + _result : ScalarResult Object + """ + session = Session(self.engine) + _result = session.execute(select(Collection).where(Collection.collection == name).join(Book)).all() + breakpoint() + session.close() + return _result diff --git a/src/frontend/compile.sh b/src/frontend/compile.sh new file mode 100755 index 0000000..f3150ce --- /dev/null +++ b/src/frontend/compile.sh @@ -0,0 +1,2 @@ +#!/bin/sh +exec npx tsc static/script/pyshelf.ts diff --git a/src/frontend/lib/FastAPIServer.py b/src/frontend/lib/FastAPIServer.py index bbc9a1e..0ab49c8 100644 --- a/src/frontend/lib/FastAPIServer.py +++ b/src/frontend/lib/FastAPIServer.py @@ -8,7 +8,7 @@ import datetime from json import dumps from base64 import b64encode from fastapi import FastAPI, Request -from fastapi.responses import HTMLResponse, JSONResponse +from fastapi.responses import HTMLResponse, JSONResponse, FileResponse from fastapi.routing import APIRoute from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates @@ -85,7 +85,7 @@ def books_tojson(obj) -> dumps: def book_tojson(book) -> dumps: """Convert a book object to a json.""" return dumps({ - "book_id": book[0].book_id, + "book_id": book[0].id, "title": book[0].title, "author": book[0].author, "categories": book[0].categories, @@ -136,7 +136,7 @@ class FastAPIServer(): app.mount("/static", StaticFiles(directory="src/frontend/static"), name="static") - self.fe_config = uvicorn.Config(app, port=8080, + self.fe_config = uvicorn.Config(app, host="0.0.0.0", port=8080, log_level="info", reload=True) self.fe_server = uvicorn.Server(self.fe_config) self.JSInterface: JSInterface = JSInterface(self.config) @@ -147,7 +147,12 @@ class FastAPIServer(): _pyShelf_src = sass.compile( filename='src/frontend/static/styles/pyShelf.sass', source_map_filename='src/frontend/static/styles/pyShelf.sass', - output_style='compressed') + output_style='compressed', + include_paths=[ + 'node_modules', + 'src/frontend/static/styles' + ] + ) with open('src/frontend/static/styles/pyShelf.css', 'w') as _pyShelf: _pyShelf.write(_pyShelf_src[0]) @@ -161,12 +166,12 @@ class FastAPIServer(): route.operation_id = route.name @app.get("/", response_class=HTMLResponse) - async def index(request: Request, skip: int = 0, limit: int = 10): + async def index(request: Request, skip: int = 0, limit: int = 30): storage = Storage(Config(os.path.abspath(os.getcwd()))) - books = storage.get_books(collection=None, skip=skip, limit=limit) + books = storage.get_books(collection=None, skip=skip*limit, limit=limit) collections = storage.get_collections() """Home page responder.""" - context = {"request": request, "books": books, "collections": collections} + context = {"request": request, "books": books, "collections": collections, "page": skip, "limit": limit} return templates.TemplateResponse("index.html", context) @app.get("/api/books", response_class=JSONResponse) @@ -184,6 +189,16 @@ class FastAPIServer(): book = storage.get_book(book_id) """Home page responder.""" return JSONResponse(content=book_tojson(book)) + + @app.get("/api/get_book/{book_id}", response_class=FileResponse) + async def book(request: Request, book_id: int): + storage = Storage(Config(os.path.abspath(os.getcwd()))) + book = storage.get_book(book_id) + file_path = book[0].file_name + if not os.path.exists(file_path): + return JSONResponse(status_code=404, content={"error": "File not found"}) + """Book file responder.""" + return FileResponse(path=file_path, filename=os.path.basename(file_path), media_type="application/octet-stream") @app.get("/api/collections", response_class=JSONResponse) async def collections(request: Request): @@ -192,6 +207,17 @@ class FastAPIServer(): """Home page responder.""" return JSONResponse(content=collections_tojson(collections)) + @app.get("/api/collection/{collection}", response_class=JSONResponse) + async def collection(request: Request, collection: str, skip=0, limit=30): + storage = Storage(Config(os.path.abspath(os.getcwd()))) + # collection = storage.get_collection(collection_name) + collection = storage.get_books(collection) + """Collection file responder.""" + collections = storage.get_collections() + # books = JSONResponse(content=books_tojson(collection)) + context = {"request": request, "books": collection, "collections": collections, "page": skip, "limit": limit} + return templates.TemplateResponse("index.html", context) + # return JSONResponse(content=collections_tojson(collection)) async def run(self): """Front end server entrypoint.""" diff --git a/src/frontend/static/styles/pyShelf.sass b/src/frontend/static/styles/pyShelf.sass index eded395..644c7f0 100644 --- a/src/frontend/static/styles/pyShelf.sass +++ b/src/frontend/static/styles/pyShelf.sass @@ -24,72 +24,8 @@ $navbar-item-color: $white $navbar-item-hover-background-color: $ps-color-secondary $navbar-dropdown-background-color: $ps-color-primary-trans $navbar-dropdown-item-hover-color: $ps-color-background !important -$footer-background-color: $ps-color-primary-trans -$footer-padding: 0.5rem 0.5rem -//$navbar-dropdown-item-hover-background-color: $background !default -//$navbar-dropdown-item-active-color: $link !default -//$navbar-dropdown-item-active-background-color: $background !default -@import "../../node_modules/bulma/sass/utilities/_all.sass"; -@import "../../node_modules/bulma/sass/utilities/initial-variables.sass"; -@import "../../node_modules/bulma/sass/utilities/functions.sass"; -@import "../../node_modules/bulma/sass/utilities/derived-variables.sass"; -@import "../../node_modules/bulma/sass/utilities/mixins.sass"; -@import "../../node_modules/bulma/sass/utilities/controls.sass"; -@import "../../node_modules/bulma/sass/utilities/extends.sass"; -@import "../../node_modules/bulma/sass/base/_all.sass"; -@import "../../node_modules/bulma/sass/base/minireset.sass"; -@import "../../node_modules/bulma/sass/base/generic.sass"; -@import "../../node_modules/bulma/sass/base/animations.sass"; -@import "../../node_modules/bulma/sass/elements/_all.sass"; -@import "../../node_modules/bulma/sass/elements/box.sass"; -@import "../../node_modules/bulma/sass/elements/button.sass"; -@import "../../node_modules/bulma/sass/elements/container.sass"; -@import "../../node_modules/bulma/sass/elements/content.sass"; -@import "../../node_modules/bulma/sass/elements/icon.sass"; -@import "../../node_modules/bulma/sass/elements/image.sass"; -@import "../../node_modules/bulma/sass/elements/notification.sass"; -@import "../../node_modules/bulma/sass/elements/progress.sass"; -@import "../../node_modules/bulma/sass/elements/table.sass"; -@import "../../node_modules/bulma/sass/elements/tag.sass"; -@import "../../node_modules/bulma/sass/elements/title.sass"; -@import "../../node_modules/bulma/sass/elements/other.sass"; -@import "../../node_modules/bulma/sass/form/_all.sass"; -@import "../../node_modules/bulma/sass/form/shared.sass"; -@import "../../node_modules/bulma/sass/form/input-textarea.sass"; -@import "../../node_modules/bulma/sass/form/checkbox-radio.sass"; -@import "../../node_modules/bulma/sass/form/select.sass"; -@import "../../node_modules/bulma/sass/form/file.sass"; -@import "../../node_modules/bulma/sass/form/tools.sass"; -@import "../../node_modules/bulma/sass/components/_all.sass"; -@import "../../node_modules/bulma/sass/components/breadcrumb.sass"; -@import "../../node_modules/bulma/sass/components/card.sass"; -@import "../../node_modules/bulma/sass/components/dropdown.sass"; -@import "../../node_modules/bulma/sass/components/level.sass"; -@import "../../node_modules/bulma/sass/components/media.sass"; -@import "../../node_modules/bulma/sass/components/menu.sass"; -@import "../../node_modules/bulma/sass/components/message.sass"; -@import "../../node_modules/bulma/sass/components/modal.sass"; -@import "../../node_modules/bulma/sass/components/navbar.sass"; -@import "../../node_modules/bulma/sass/components/pagination.sass"; -@import "../../node_modules/bulma/sass/components/panel.sass"; -@import "../../node_modules/bulma/sass/components/tabs.sass"; -@import "../../node_modules/bulma/sass/grid/_all.sass"; -@import "../../node_modules/bulma/sass/grid/columns.sass"; -@import "../../node_modules/bulma/sass/grid/tiles.sass"; -@import "../../node_modules/bulma/sass/helpers/_all.sass"; -@import "../../node_modules/bulma/sass/helpers/color.sass"; -@import "../../node_modules/bulma/sass/helpers/flexbox.sass"; -@import "../../node_modules/bulma/sass/helpers/float.sass"; -@import "../../node_modules/bulma/sass/helpers/other.sass"; -@import "../../node_modules/bulma/sass/helpers/overflow.sass"; -@import "../../node_modules/bulma/sass/helpers/position.sass"; -@import "../../node_modules/bulma/sass/helpers/spacing.sass"; -@import "../../node_modules/bulma/sass/helpers/typography.sass"; -@import "../../node_modules/bulma/sass/helpers/visibility.sass"; -@import "../../node_modules/bulma/sass/layout/_all.sass"; -@import "../../node_modules/bulma/sass/layout/hero.sass"; -@import "../../node_modules/bulma/sass/layout/section.sass"; -@import "../../node_modules/bulma/sass/layout/footer.sass" +$footer-background-color: $ps-color-primary-trans !important +// $footer-padding: 0.5rem 0.5rem .center-all align-items: center; @@ -110,12 +46,60 @@ $footer-padding: 0.5rem 0.5rem #book-shelf display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)) // <== add this + gap: 1rem // space between rows and columns justify-content: space-evenly; - align-items: center; + // align-items: center; align-content: center; padding: 1rem; margin: 1rem; // background-color: $ps-color-background; + +.book-thumbnail + z-index: 1; + position: relative; + +.no-image-title + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: $white; + font-size: 1.2rem; + text-align: center; + background-color: rgba(0,0,0,0.7); + padding: 0.5rem; + border-radius: 5px; + +.no-image-author + position: absolute; + top: 60%; + left: 50%; + transform: translate(-50%, -50%); + color: $white; + font-size: 1rem; + text-align: center; + background-color: rgba(0,0,0,0.7); + padding: 0.5rem; + border-radius: 5px; + +.display-alt::after + content: attr(alt); + display: block !important; + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: rgba(0,0,0,0.7); + color: white; + font-size: 0.8rem; + padding: 0.2rem; + +.title, .subtitle + z-index: 2; + position: relative; + top: -155px; + .collection display: flex; flex-direction: column; @@ -123,3 +107,9 @@ $footer-padding: 0.5rem 0.5rem align-items: center; align-content: center; background-color: $ps-color-background; + +.footer + display: flex; + padding: 1rem !important; + +@import "../../node_modules/bulma/bulma.scss" diff --git a/src/frontend/templates/footer.html b/src/frontend/templates/footer.html index b1b43f9..f5dd893 100644 --- a/src/frontend/templates/footer.html +++ b/src/frontend/templates/footer.html @@ -1,18 +1,18 @@ - + diff --git a/src/frontend/templates/index.html b/src/frontend/templates/index.html index c02fac0..ee13c50 100644 --- a/src/frontend/templates/index.html +++ b/src/frontend/templates/index.html @@ -1,40 +1,53 @@ - + {% block javascript %} -{% endblock %} -{% include 'header.html' %} - {% include 'navigation.html' %} -
-
- {% for book in books %} - {% set cover = book[0].cover|b64decode %} - {% if cover != 'None' %} -
-
-
- {{ book[0].title }} -
-
-
- {% else %} -
-
-
- {{ book[0].title }} -
-
-

{{ book[0].title }}

-

{{ book[0].author }}

-

{{ book[0].description|summarize }}

-
- {% endif %} - {% endfor %} -
-
-{% include 'footer.html' %} + + {% endblock %} + {% include 'header.html' %} + {% include 'navigation.html' %} +
+

Total books: {{ books|length }}

+ +
+ {% for book in books %} + {% set cover = book[0].cover|b64decode %} + {% if cover != 'None' %} +
+
+
+ {{ book[0].title }} +
+
+
+ {% else %} +
+
+
+
{{ book[0].title }}
+ +
+
+ +
+ {% endif %} + {% endfor %} +
+ +
+ {% include 'footer.html' %} diff --git a/src/frontend/templates/navigation.html b/src/frontend/templates/navigation.html index a9f17ba..9060b31 100644 --- a/src/frontend/templates/navigation.html +++ b/src/frontend/templates/navigation.html @@ -47,11 +47,18 @@