summaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorWolfgang Müller2024-11-29 16:02:17 +0100
committerWolfgang Müller2025-01-19 16:10:05 +0100
commit261ceaa057742fc70c52885021221d7a89c28af7 (patch)
tree6256a4968a98320c999f41c7c4eeed5a9ed03802
parent9dea5cbc01e2d447d77d5fb205acb1d17fe9c55f (diff)
downloadhircine-261ceaa057742fc70c52885021221d7a89c28af7.tar.gz
backend: Add basic statistics query endpoint
For now we simply collect totals for all scrapers, models, and comic associations. These should be sufficient to compile some basic but still interesting statistics.
Diffstat (limited to '')
-rw-r--r--src/hircine/api/query/__init__.py2
-rw-r--r--src/hircine/api/query/resolvers.py29
-rw-r--r--src/hircine/api/responses.py30
-rw-r--r--src/hircine/db/ops.py8
-rw-r--r--tests/api/test_statistics.py106
5 files changed, 174 insertions, 1 deletions
diff --git a/src/hircine/api/query/__init__.py b/src/hircine/api/query/__init__.py
index 2e07c71..7bc4b7d 100644
--- a/src/hircine/api/query/__init__.py
+++ b/src/hircine/api/query/__init__.py
@@ -24,6 +24,7 @@ from .resolvers import (
every,
scrape_comic,
single,
+ statistics,
)
@@ -52,3 +53,4 @@ class Query:
world: rp.WorldResponse = query(single(models.World))
worlds: FilterResult[World] = query(every(models.World))
scrape_comic: rp.ScrapeComicResponse = query(scrape_comic)
+ statistics: rp.Statistics = query(statistics)
diff --git a/src/hircine/api/query/resolvers.py b/src/hircine/api/query/resolvers.py
index 6609cc1..389a200 100644
--- a/src/hircine/api/query/resolvers.py
+++ b/src/hircine/api/query/resolvers.py
@@ -10,10 +10,13 @@ import hircine.plugins as plugins
from hircine.api.filters import Input as FilterInput
from hircine.api.inputs import Pagination
from hircine.api.responses import (
+ ComicTotals,
IDNotFoundError,
ScraperError,
ScraperNotAvailableError,
ScraperNotFoundError,
+ Statistics,
+ Totals,
)
from hircine.api.sort import Input as SortInput
from hircine.api.types import (
@@ -144,3 +147,29 @@ async def scrape_comic(id: int, scraper: str):
)
except ScrapeError as e:
return ScraperError(error=str(e))
+
+
+async def statistics():
+ async with db.session() as s:
+ total = Totals(
+ archives=await ops.count(s, models.Archive),
+ artists=await ops.count(s, models.Artist),
+ characters=await ops.count(s, models.Character),
+ circles=await ops.count(s, models.Circle),
+ comic=ComicTotals(
+ artists=await ops.count(s, models.ComicArtist),
+ characters=await ops.count(s, models.ComicCharacter),
+ circles=await ops.count(s, models.ComicCircle),
+ tags=await ops.count(s, models.ComicTag),
+ worlds=await ops.count(s, models.ComicWorld),
+ ),
+ comics=await ops.count(s, models.Comic),
+ images=await ops.count(s, models.Image),
+ namespaces=await ops.count(s, models.Namespace),
+ pages=await ops.count(s, models.Page),
+ scrapers=len(plugins.get_scrapers()),
+ tags=await ops.count(s, models.Tag),
+ worlds=await ops.count(s, models.World),
+ )
+
+ return Statistics(total=total)
diff --git a/src/hircine/api/responses.py b/src/hircine/api/responses.py
index 99d5113..883705b 100644
--- a/src/hircine/api/responses.py
+++ b/src/hircine/api/responses.py
@@ -147,6 +147,36 @@ class ScraperNotAvailableError(Error):
return f"Scraper {self.scraper} not available for comic ID {self.comic_id}"
+@strawberry.type
+class ComicTotals:
+ artists: int
+ characters: int
+ circles: int
+ tags: int
+ worlds: int
+
+
+@strawberry.type
+class Totals:
+ archives: int
+ artists: int
+ characters: int
+ circles: int
+ comics: int
+ comic: ComicTotals
+ images: int
+ namespaces: int
+ pages: int
+ scrapers: int
+ tags: int
+ worlds: int
+
+
+@strawberry.type
+class Statistics:
+ total: Totals
+
+
AddComicResponse = Annotated[
Union[
AddComicSuccess,
diff --git a/src/hircine/db/ops.py b/src/hircine/db/ops.py
index 91c830d..8cc5ddc 100644
--- a/src/hircine/db/ops.py
+++ b/src/hircine/db/ops.py
@@ -1,7 +1,7 @@
import random
from collections import defaultdict
-from sqlalchemy import delete, func, null, select, text, tuple_
+from sqlalchemy import delete, func, literal_column, null, select, text, tuple_
from sqlalchemy.orm import contains_eager, undefer
from sqlalchemy.orm.util import identity_key
from strawberry import UNSET
@@ -204,3 +204,9 @@ async def delete_all(session, model, ids):
result = await session.execute(delete(model).where(model.id.in_(ids)))
return result.rowcount
+
+
+async def count(session, model):
+ sql = select(func.count(literal_column("1"))).select_from(model)
+
+ return (await session.execute(sql)).scalar_one()
diff --git a/tests/api/test_statistics.py b/tests/api/test_statistics.py
new file mode 100644
index 0000000..98c8dc7
--- /dev/null
+++ b/tests/api/test_statistics.py
@@ -0,0 +1,106 @@
+import pytest
+from conftest import DB, Response
+
+import hircine.plugins
+from hircine.db.models import (
+ Artist,
+ Character,
+ Circle,
+ Page,
+ Tag,
+ World,
+)
+from hircine.scraper import Scraper
+
+totals_fragment = """
+ fragment Totals on Statistics {
+ total {
+ archives
+ artists
+ characters
+ circles
+ comics
+ namespaces
+ scrapers
+ tags
+ worlds
+ images
+ pages
+ comic {
+ artists
+ characters
+ circles
+ tags
+ worlds
+ }
+ }
+ }
+"""
+
+
+@pytest.fixture
+def query_statistics(execute):
+ query = """
+ query statistics {
+ statistics {
+ __typename
+ ... Totals
+ }
+ }
+ """
+
+ return execute(totals_fragment + query)
+
+
+@pytest.mark.anyio
+async def test_statistics_returns_totals(
+ gen_comic, gen_image, query_statistics, empty_plugins
+):
+ comic = next(gen_comic)
+ await DB.add(comic)
+ await DB.add(Artist(name="foo"))
+ await DB.add(Character(name="foo"))
+ await DB.add(Circle(name="foo"))
+ await DB.add(World(name="foo"))
+ await DB.add(Tag(name="foo"))
+
+ image = next(gen_image)
+ await DB.add(image)
+ await DB.add(
+ Page(id=100, index=100, path="100.png", image=image, archive=comic.archive)
+ )
+ await DB.add(
+ Page(id=101, index=101, path="101.png", image=image, archive=comic.archive)
+ )
+
+ namespaces = set()
+ for tag in comic.tags:
+ namespaces.add(tag.namespace.id)
+
+ class MockScraper(Scraper):
+ name = "Scraper"
+
+ def scrape(self):
+ yield None
+
+ hircine.plugins.register_scraper("mock", MockScraper)
+
+ response = Response(await query_statistics())
+
+ response.assert_is("Statistics")
+ assert response.total["comics"] == 1
+ assert response.total["archives"] == 1
+ assert response.total["artists"] == len(comic.artists) + 1
+ assert response.total["characters"] == len(comic.characters) + 1
+ assert response.total["circles"] == len(comic.circles) + 1
+ assert response.total["worlds"] == len(comic.worlds) + 1
+ assert response.total["tags"] == len(comic.tags) + 1
+ assert response.total["namespaces"] == len(namespaces)
+ assert response.total["images"] == len(comic.pages) + 1
+ assert response.total["pages"] == len(comic.pages) + 2
+ assert response.total["scrapers"] == 1
+ assert response.total["comic"]["artists"] == len(comic.artists)
+ assert response.total["comic"]["characters"] == len(comic.characters)
+ assert response.total["comic"]["circles"] == len(comic.circles)
+ assert response.total["comic"]["tags"] == len(comic.tags)
+ assert response.total["comic"]["worlds"] == len(comic.worlds)