update codebase

This commit is contained in:
2025-04-17 10:57:57 +02:00
parent e685c7b930
commit 759c01380f
37 changed files with 378 additions and 252 deletions

7
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,7 @@
{
"python.testing.pytestArgs": [
"tests"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
}

View File

@@ -7,8 +7,19 @@ authors = [
{ name = "WorldTeacher", email = "coding_contact@pm.me" } { name = "WorldTeacher", email = "coding_contact@pm.me" }
] ]
requires-python = ">=3.13" requires-python = ">=3.13"
dependencies = [] dependencies = [
"komconfig",
"typing-extensions>=4.12.2",
]
[build-system] [build-system]
requires = ["hatchling"] requires = ["hatchling"]
build-backend = "hatchling.build" build-backend = "hatchling.build"
[dependency-groups]
test = [
"pytest>=8.3.4",
]
[tool.uv.sources]
komconfig = { workspace = true }

View File

@@ -3,6 +3,14 @@ from komgapi.errors import KomgaError, LoginError, ResultErrror
from typing import Any, Union from typing import Any, Union
from limit import limit from limit import limit
import loguru
import sys
log = loguru.logger
log.remove()
log.add("logs/komga_api.log", rotation="1 week", retention="1 month")
log.add(sys.stdout, level="INFO")
class BaseAPI: class BaseAPI:
def __init__(self, username, password, url, timeout=20, api_version=1) -> None: def __init__(self, username, password, url, timeout=20, api_version=1) -> None:
@@ -39,15 +47,16 @@ class BaseAPI:
if params is None: if params is None:
params = {} params = {}
try: try:
# ic(url, params)
response = requests.get( response = requests.get(
url, url,
auth=(self._username, self._password), auth=(self._username, self._password),
params=params, params=params,
timeout=self.timeout, timeout=self.timeout,
) )
response.raise_for_status() if response.status_code != 200:
self.getRequest(url, params)
# print(response.content) # print(response.content)
log.debug(f"Response: {response.content}")
return response.json() return response.json()
except ConnectionError as e: except ConnectionError as e:
message = f"Connection Error: {e}" message = f"Connection Error: {e}"
@@ -55,22 +64,37 @@ class BaseAPI:
except requests.exceptions.Timeout as e: except requests.exceptions.Timeout as e:
raise KomgaError(f"Timeout Error: {e}") from e raise KomgaError(f"Timeout Error: {e}") from e
def postRequest(self, url, data: Union[dict, None] = None): def postRequest(self, url, data: Union[dict, None] = None, body: dict = None):
if data is None: if data is None:
data = {} data = {}
try: try:
response = requests.post( if body is not None:
url, response = requests.post(
auth=(self._username, self._password), url,
json=data, auth=(self._username, self._password),
timeout=self.timeout, json=body,
) params=data,
timeout=self.timeout,
)
else:
response = requests.post(
url,
auth=(self._username, self._password),
json=data,
timeout=self.timeout,
params=data,
)
response.raise_for_status() response.raise_for_status()
status_code = response.status_code status_code = response.status_code
if status_code != 202: if status_code == 202:
raise ResultErrror(f"Result Error: {response.json()}") log.debug(f"Response: {response}")
# raise ResultErrror(f"Result Error: {response}")
elif status_code == 200: elif status_code == 200:
log.debug(f"Response: {response}")
return response.json() return response.json()
else:
log.debug(f"Response: {response}")
raise ResultErrror(f"Result Error: {response.content}")
except ConnectionError as e: except ConnectionError as e:
message = f"Connection Error: {e}" message = f"Connection Error: {e}"
raise KomgaError(message) from e raise KomgaError(message) from e
@@ -81,6 +105,7 @@ class BaseAPI:
if data is None: if data is None:
data = {} data = {}
try: try:
print("patching data", data, url)
response = requests.patch( response = requests.patch(
url, url,
auth=(self._username, self._password), auth=(self._username, self._password),
@@ -88,6 +113,10 @@ class BaseAPI:
timeout=self.timeout, timeout=self.timeout,
) )
response.raise_for_status() response.raise_for_status()
log.debug(
f"Response: {response}, {response.status_code}, {response.content}"
)
print(response.status_code, response.content)
if response.status_code != 204: if response.status_code != 204:
raise ResultErrror(f"Result Error: {response.json()}") raise ResultErrror(f"Result Error: {response.json()}")
except ConnectionError as e: except ConnectionError as e:

View File

@@ -1,10 +1,5 @@
from .baseapi import BaseAPI from .baseapi import BaseAPI
import pathlib
import subprocess
import requests
import typing_extensions
from typing import List, Optional, Dict, Any, Union from typing import List, Optional, Dict, Any, Union
from komgapi.errors import KomgaError, LoginError, ResultErrror
from komgapi.schemas import * # Progress, Series from komgapi.schemas import * # Progress, Series

View File

@@ -7,6 +7,14 @@ from typing import List, Optional, Dict, Any, Union
from komgapi.schemas import * # Progress, Series from komgapi.schemas import * # Progress, Series
import loguru
import sys
log = loguru.logger
log.remove()
log.add("logs/komgapi.log", rotation="1 week", retention="1 month")
log.add(sys.stdout)
class SeriesController(BaseAPI): class SeriesController(BaseAPI):
def __init__(self, username, password, url, timeout=20) -> None: def __init__(self, username, password, url, timeout=20) -> None:
@@ -14,56 +22,39 @@ class SeriesController(BaseAPI):
def getAllSeries( def getAllSeries(
self, self,
library_id: List[str] = None,
collection_id: List[str] = None,
status: List[str] = None,
publisher: List[str] = None,
lang: List[str] = None,
genre: List[str] = None,
tag: List[str] = None,
age_rating: List[str] = None,
release_year: List[str] = None,
deleted: bool = None,
complete: bool = None,
unpaged: bool = True, unpaged: bool = True,
sort: List[str] = None, sort: List[str] = None,
author: List[str] = None,
oneshot: bool = None,
size: int = None, size: int = None,
page: int = None, page: int = None,
body: dict[Any, Any] = {},
) -> list[Series]: ) -> list[Series]:
"""Get all series from the server. """
By default, this will return all series in the server. You can filter the results by using the parameters. Get all series from the server that match the query.
Args: Parameters
---- ----------
- library_id (List[str], optional): The library to be queried. If None, all available libraries will be used. Defaults to None. unpaged : bool, optional
- collection_id (List[str], optional): The collection to be queried. Defaults to None. limits the amount of results. If set to false, 20 entries will be returned, by default True
- status (List[str], optional): The status of the series. Can be: ENDED,ONGOING,ABANDONED,HIATUS. Defaults to None. sort : List[str], optional
- publisher (List[str], optional): Publisher(s) to be searched for. Defaults to None. sorting parameter, by default None
- lang (List[str], optional): Language to query for. Uses two-letter codec. Defaults to None. size : int, optional
- genre (List[str], optional): Genre(s) to query for. Defaults to None. How many entries should be returned. Set to None to get all entries, by default None
- tag (List[str], optional): Tag(s) to query. Defaults to None. page : int, optional
- age_rating (List[str], optional): A custom age-rating to search for. Needs to be configured manually. Defaults to None. If unpaged is False and size is set, this allows the selection of a subset of result, by default None
- release_year (List[str], optional): When the series were released. Defaults to None. body : dict, optional
- deleted (bool, optional): If the series is deleted. Defaults to None. The query that requests the data from the API, by default {}
- complete (bool, optional): Turn to true to only search series that are complete. Complete requires TotalBookCount to be set and equal to BookCount. Defaults to None.
- unpaged (bool, optional): Set to False if a single Page of results should be returned. By default, a page contains 20 entries. Defaults to True.
- sort (List[str], optional): Sorting of the returned data. Sort using asc|desc. Multiple sort criteria are supported. Defaults to None.
- author (List[str], optional): Author(s) to include in the query. Defaults to None.
- oneshot (bool, optional): If the series is categorized as oneshot. Defaults to None.
- size (int, optional): The size of the page. Defaults to None.
- page (int, optional): The page to be returned. Defaults to None.
Returns: Returns
------- -------
- list[Series]: a list of all series that match the query. Each series is represented as a Series object. list[Series]
The result of the query. Each series is represented as a Series object.
""" """
params = locals() params = locals()
body = params.pop("body")
params = self.setParams(params) params = self.setParams(params)
url = self.url + "series" url = self.url + "series/list"
data = self.getRequest(url, params) data = self.postRequest(url, data=params, body=body)
ret = [] ret = []
for series in data["content"]: for series in data["content"]:
ret.append(Series(**series)) ret.append(Series(**series))
@@ -250,6 +241,8 @@ class SeriesController(BaseAPI):
""" """
url = self.url + f"series/{series_id}/metadata" url = self.url + f"series/{series_id}/metadata"
data = self.patchRequest(url, changed_metadata) data = self.patchRequest(url, changed_metadata)
log.debug("Changed metadata: {}", data)
return data return data
def refreshMetadata(self, series_id: str) -> Optional[Dict[str, Any]]: def refreshMetadata(self, series_id: str) -> Optional[Dict[str, Any]]:

View File

@@ -1,4 +1,3 @@
"""Generic API for KOMGA""" """Generic API for KOMGA"""
import requests import requests
@@ -28,20 +27,28 @@ class KOMGAPI_REST:
self.timeout = timeout self.timeout = timeout
if not url.endswith("/"): if not url.endswith("/"):
url += "/" url += "/"
self.common_book_controller = CommonBookController(username, password, url, timeout) self.common_book_controller = CommonBookController(
username, password, url, timeout
)
self.series_controller = SeriesController(username, password, url, timeout) self.series_controller = SeriesController(username, password, url, timeout)
self.readlist_controller = ReadListController(username, password, url, timeout) self.readlist_controller = ReadListController(username, password, url, timeout)
self.page_hash_controller = None self.page_hash_controller = None
self.library_controller = LibraryController(username, password, url, timeout) self.library_controller = LibraryController(username, password, url, timeout)
self.series_collection_controller = SeriesCollectionController(username, password, url, timeout) self.series_collection_controller = SeriesCollectionController(
username, password, url, timeout
)
self.book_controler = BookController(username, password, url, timeout) self.book_controler = BookController(username, password, url, timeout)
self.announcement_controller = AnnouncementController(username, password, url, timeout) self.announcement_controller = AnnouncementController(
username, password, url, timeout
)
self.user_controller = UserController(username, password, url, timeout) self.user_controller = UserController(username, password, url, timeout)
self.transient_books_controller = None self.transient_books_controller = None
self.file_system_controller = None self.file_system_controller = None
self.claim_controller = ClaimController(username, password, url, timeout) self.claim_controller = ClaimController(username, password, url, timeout)
self.settings_controller = SettingsController(username, password, url, timeout) self.settings_controller = SettingsController(username, password, url, timeout)
self.referential_controller = ReferentialController(username, password, url, timeout) self.referential_controller = ReferentialController(
username, password, url, timeout
)
self.login_controller = None self.login_controller = None
self.historical_events_controller = None self.historical_events_controller = None
self.task_controller = None self.task_controller = None
@@ -51,7 +58,6 @@ class KOMGAPI_REST:
def notImplemented(self): def notImplemented(self):
raise NotImplementedError("Not implemented yet") raise NotImplementedError("Not implemented yet")
def from_env(self): def from_env(self):
"""Create a KOMGA API object from environment variables. """Create a KOMGA API object from environment variables.
@@ -73,18 +79,11 @@ class KOMGAPI_REST:
bool: True if the connection is successful, False otherwise. bool: True if the connection is successful, False otherwise.
""" """
try: try:
requests.get(self.url, auth=(self._username, self._password), timeout=self.timeout) requests.get(
self.url, auth=(self._username, self._password), timeout=self.timeout
)
return True return True
except Exception as e: except Exception as e:
return False return False
# Book controller # Book controller

View File

@@ -2,9 +2,10 @@ from __future__ import annotations
from typing import List from typing import List
from pydantic import BaseModel from dataclasses import dataclass
class AlternateTitle(BaseModel): @dataclass
class AlternateTitle:
label: str label: str
title: str title: str

View File

@@ -2,11 +2,12 @@ from __future__ import annotations
from typing import List, Dict from typing import List, Dict
from pydantic import BaseModel from dataclasses import dataclass
from komgapi.schemas.Author import Author from komgapi.schemas.Author import Author
class AnnouncementItem(BaseModel): @dataclass
class AnnouncementItem:
id: str id: str
url: str url: str
title: str title: str
@@ -16,7 +17,8 @@ class AnnouncementItem(BaseModel):
author: Author author: Author
class Announcement(BaseModel): @dataclass
class Announcement:
version: str version: str
title: str title: str
home_page_url: str home_page_url: str

View File

@@ -2,10 +2,11 @@ from __future__ import annotations
from typing import List from typing import List
from pydantic import BaseModel from dataclasses import dataclass
class UserAccess(BaseModel): @dataclass
class UserAccess:
userId: str userId: str
email: str email: str
apiKeyId: str apiKeyId: str
@@ -18,13 +19,15 @@ class UserAccess(BaseModel):
source: str source: str
@dataclass
class Sort(BaseModel): class Sort:
empty: bool empty: bool
sorted: bool sorted: bool
unsorted: bool unsorted: bool
class Pageable(BaseModel):
@dataclass
class Pageable:
offset: int offset: int
sort: Sort sort: Sort
pageNumber: int pageNumber: int
@@ -33,7 +36,8 @@ class Pageable(BaseModel):
unpaged: bool unpaged: bool
class Authentication(BaseModel): @dataclass
class Authentication:
totalElements: int totalElements: int
totalPages: int totalPages: int
size: int size: int
@@ -47,32 +51,25 @@ class Authentication(BaseModel):
empty: bool empty: bool
@dataclass
class Sort:
class Sort(BaseModel):
empty: bool empty: bool
sorted: bool sorted: bool
unsorted: bool unsorted: bool
class Sort1(BaseModel): @dataclass
empty: bool class Pageable:
sorted: bool
unsorted: bool
class Pageable(BaseModel):
offset: int offset: int
sort: Sort1 sort: Sort
pageNumber: int pageNumber: int
pageSize: int pageSize: int
paged: bool paged: bool
unpaged: bool unpaged: bool
class UserAuthActivity(BaseModel): @dataclass
class UserAuthActivity:
totalElements: int totalElements: int
totalPages: int totalPages: int
size: int size: int

View File

@@ -2,20 +2,24 @@ from __future__ import annotations
from typing import List from typing import List
from pydantic import BaseModel from dataclasses import dataclass
class Author(BaseModel): @dataclass
class Author:
name: str name: str
role: str role: str
class Sort(BaseModel): @dataclass
class Sort:
empty: bool empty: bool
sorted: bool sorted: bool
unsorted: bool unsorted: bool
class Pageable(BaseModel):
@dataclass
class Pageable:
offset: int offset: int
sort: Sort sort: Sort
pageNumber: int pageNumber: int
@@ -24,7 +28,8 @@ class Pageable(BaseModel):
unpaged: bool unpaged: bool
class AuthorResponse(BaseModel): @dataclass
class AuthorResponse:
totalElements: int totalElements: int
totalPages: int totalPages: int
size: int size: int

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
from typing import Any, List, Optional from typing import Any, List, Optional
from pydantic import BaseModel from dataclasses import dataclass
from .Author import Author from .Author import Author
from .BooksMetadata import BookMetadata from .BooksMetadata import BookMetadata
@@ -10,7 +10,8 @@ from .Link import Link
from .Media import Media from .Media import Media
class Book(BaseModel): @dataclass
class Book:
id: Optional[str] = None id: Optional[str] = None
seriesId: Optional[str] = None seriesId: Optional[str] = None
seriesTitle: Optional[str] = None seriesTitle: Optional[str] = None

View File

@@ -2,13 +2,14 @@ from __future__ import annotations
from typing import List, Optional from typing import List, Optional
from pydantic import BaseModel from dataclasses import dataclass
from .Author import Author from .Author import Author
from .Link import Link from .Link import Link
class BooksMetadata(BaseModel): @dataclass
class BooksMetadata:
authors: Optional[List[Author]] = None authors: Optional[List[Author]] = None
tags: Optional[List[str]] = None tags: Optional[List[str]] = None
releaseDate: Optional[str] = None releaseDate: Optional[str] = None
@@ -18,7 +19,8 @@ class BooksMetadata(BaseModel):
lastModified: Optional[str] = None lastModified: Optional[str] = None
class BookMetadata(BaseModel): @dataclass
class BookMetadata:
title: Optional[str] = None title: Optional[str] = None
titleLock: Optional[bool] = None titleLock: Optional[bool] = None
summary: Optional[str] = None summary: Optional[str] = None
@@ -29,13 +31,13 @@ class BookMetadata(BaseModel):
numberSortLock: Optional[bool] = None numberSortLock: Optional[bool] = None
releaseDate: Optional[str] = None releaseDate: Optional[str] = None
releaseDateLock: Optional[bool] = None releaseDateLock: Optional[bool] = None
authors: List[Author] authors: List[Author] = None
authorsLock: Optional[bool] = None authorsLock: Optional[bool] = None
tags: List tags: List = None
tagsLock: Optional[bool] = None tagsLock: Optional[bool] = None
isbn: Optional[str] = None isbn: Optional[str] = None
isbnLock: Optional[bool] = None isbnLock: Optional[bool] = None
links: List[Link] links: List[Link] = None
linksLock: Optional[bool] = None linksLock: Optional[bool] = None
created: Optional[str] = None created: Optional[str] = None
lastModified: Optional[str] = None lastModified: Optional[str] = None

View File

@@ -2,10 +2,11 @@ from __future__ import annotations
from typing import List from typing import List
from pydantic import BaseModel from dataclasses import dataclass
class Collection(BaseModel): @dataclass
class Collection:
id: str id: str
name: str name: str
ordered: bool ordered: bool

View File

@@ -1,11 +1,13 @@
from __future__ import annotations from __future__ import annotations
from typing import List, Optional from typing import List, Optional
from pydantic import BaseModel from dataclasses import dataclass
from .Sort import Sort from .Sort import Sort
from .Pageable import Pageable from .Pageable import Pageable
from .Book import Book from .Book import Book
class Duplicate(BaseModel):
@dataclass
class Duplicate:
totalElements: Optional[int] = None totalElements: Optional[int] = None
totalPages: Optional[int] = None totalPages: Optional[int] = None
size: Optional[int] = None size: Optional[int] = None

View File

@@ -2,14 +2,15 @@ from __future__ import annotations
from typing import List, Optional from typing import List, Optional
from pydantic import BaseModel from dataclasses import dataclass
from .Pageable import Pageable from .Pageable import Pageable
from .Series import Series from .Series import Series
from .Sort import Sort from .Sort import Sort
class LatestSeriesData(BaseModel): @dataclass
class LatestSeriesData:
totalElements: Optional[int] = None totalElements: Optional[int] = None
totalPages: Optional[int] = None totalPages: Optional[int] = None
size: Optional[int] = None size: Optional[int] = None

View File

@@ -2,13 +2,14 @@ from __future__ import annotations
from typing import List, Optional from typing import List, Optional
from pydantic import BaseModel from dataclasses import dataclass
class Library(BaseModel): @dataclass
id: Optional[str] = None class Library:
name: Optional[str] = None id: str
root: Optional[str] = None name: str
root: str
importComicInfoBook: Optional[bool] = None importComicInfoBook: Optional[bool] = None
importComicInfoSeries: Optional[bool] = None importComicInfoSeries: Optional[bool] = None
importComicInfoCollection: Optional[bool] = None importComicInfoCollection: Optional[bool] = None
@@ -35,8 +36,10 @@ class Library(BaseModel):
analyzeDimensions: Optional[bool] = None analyzeDimensions: Optional[bool] = None
oneshotsDirectory: Optional[str] = None oneshotsDirectory: Optional[str] = None
unavailable: Optional[bool] = None unavailable: Optional[bool] = None
hashKoreader: Optional[bool] = None
class CreateLibrary(BaseModel):
class CreateLibrary:
name: str name: str
root: str root: str
importComicInfoBook: bool importComicInfoBook: bool
@@ -64,4 +67,3 @@ class CreateLibrary(BaseModel):
hashPages: bool hashPages: bool
analyzeDimensions: bool analyzeDimensions: bool
oneshotsDirectory: str oneshotsDirectory: str

View File

@@ -2,9 +2,10 @@ from __future__ import annotations
from typing import List from typing import List
from pydantic import BaseModel from dataclasses import dataclass
class Link(BaseModel): @dataclass
class Link:
label: str label: str
url: str url: str

View File

@@ -1,10 +1,11 @@
from __future__ import annotations from __future__ import annotations
from typing import List, Optional from typing import List, Optional
from pydantic import BaseModel from dataclasses import dataclass
class Locations(BaseModel):
@dataclass
class Locations:
fragments: Optional[List[str]] = None fragments: Optional[List[str]] = None
progression: Optional[int] = None progression: Optional[int] = None
position: Optional[int] = None position: Optional[int] = None
totalProgression: Optional[int] = None totalProgression: Optional[int] = None

View File

@@ -2,10 +2,11 @@ from __future__ import annotations
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from pydantic import BaseModel from dataclasses import dataclass
class Link(BaseModel): @dataclass
class Link:
title: Optional[str] = None title: Optional[str] = None
rel: Optional[str] = None rel: Optional[str] = None
href: Optional[str] = None href: Optional[str] = None
@@ -15,30 +16,35 @@ class Link(BaseModel):
height: Optional[int] = None height: Optional[int] = None
class Series(BaseModel): @dataclass
class Series:
name: Optional[str] = None name: Optional[str] = None
position: Optional[int] = None position: Optional[int] = None
links: Optional[List[Link]] = None links: Optional[List[Link]] = None
class CollectionItem(BaseModel): @dataclass
class CollectionItem:
name: Optional[str] = None name: Optional[str] = None
position: Optional[int] = None position: Optional[int] = None
links: Optional[List[Link]] = None links: Optional[List[Link]] = None
class BelongsTo(BaseModel): @dataclass
class BelongsTo:
series: Optional[List[Series]] = None series: Optional[List[Series]] = None
collection: Optional[List[CollectionItem]] = None collection: Optional[List[CollectionItem]] = None
class Rendition(BaseModel): @dataclass
class Rendition:
additionalProp1: Dict[str, Any] additionalProp1: Dict[str, Any]
additionalProp2: Dict[str, Any] additionalProp2: Dict[str, Any]
additionalProp3: Dict[str, Any] additionalProp3: Dict[str, Any]
class Metadata(BaseModel): @dataclass
class Metadata:
title: Optional[str] = None title: Optional[str] = None
identifier: Optional[str] = None identifier: Optional[str] = None
type: Optional[str] = None type: Optional[str] = None
@@ -67,7 +73,8 @@ class Metadata(BaseModel):
rendition: Optional[Rendition] = None rendition: Optional[Rendition] = None
class Image(BaseModel): @dataclass
class Image:
title: Optional[str] = None title: Optional[str] = None
rel: Optional[str] = None rel: Optional[str] = None
href: Optional[str] = None href: Optional[str] = None
@@ -77,7 +84,8 @@ class Image(BaseModel):
height: Optional[int] = None height: Optional[int] = None
class ReadingOrderItem(BaseModel): @dataclass
class ReadingOrderItem:
title: Optional[str] = None title: Optional[str] = None
rel: Optional[str] = None rel: Optional[str] = None
href: Optional[str] = None href: Optional[str] = None
@@ -87,7 +95,8 @@ class ReadingOrderItem(BaseModel):
height: Optional[int] = None height: Optional[int] = None
class Resource(BaseModel): @dataclass
class Resource:
title: Optional[str] = None title: Optional[str] = None
rel: Optional[str] = None rel: Optional[str] = None
href: Optional[str] = None href: Optional[str] = None
@@ -97,7 +106,8 @@ class Resource(BaseModel):
height: Optional[int] = None height: Optional[int] = None
class TocItem(BaseModel): @dataclass
class TocItem:
title: Optional[str] = None title: Optional[str] = None
rel: Optional[str] = None rel: Optional[str] = None
href: Optional[str] = None href: Optional[str] = None
@@ -107,7 +117,8 @@ class TocItem(BaseModel):
height: Optional[int] = None height: Optional[int] = None
class Landmark(BaseModel): @dataclass
class Landmark:
title: Optional[str] = None title: Optional[str] = None
rel: Optional[str] = None rel: Optional[str] = None
href: Optional[str] = None href: Optional[str] = None
@@ -117,7 +128,8 @@ class Landmark(BaseModel):
height: Optional[int] = None height: Optional[int] = None
class PageListItem(BaseModel): @dataclass
class PageListItem:
title: Optional[str] = None title: Optional[str] = None
rel: Optional[str] = None rel: Optional[str] = None
href: Optional[str] = None href: Optional[str] = None
@@ -127,7 +139,8 @@ class PageListItem(BaseModel):
height: Optional[int] = None height: Optional[int] = None
class Manifest(BaseModel): @dataclass
class Manifest:
context: Optional[str] = None context: Optional[str] = None
metadata: Optional[Metadata] = None metadata: Optional[Metadata] = None
links: Optional[List[Link]] = None links: Optional[List[Link]] = None

View File

@@ -2,10 +2,11 @@ from __future__ import annotations
from typing import List from typing import List
from pydantic import BaseModel from dataclasses import dataclass
class Media(BaseModel): @dataclass
class Media:
status: str status: str
mediaType: str mediaType: str
pagesCount: int pagesCount: int

View File

@@ -1,41 +1,45 @@
from __future__ import annotations from __future__ import annotations
from typing import List, Optional from typing import List, Optional, Union
from dataclasses import dataclass
from pydantic import BaseModel
from .AlternateTitle import AlternateTitle from .AlternateTitle import AlternateTitle
from .Link import Link from .Link import Link
from .status import Status
class Metadata(BaseModel): @dataclass
status: Optional[str] | None = None class Metadata:
statusLock: bool # set status to be either ENDED, ONGOING, HIATUS, ABANDONED
# status has to be one of the status listed in enum status
status: Optional[Status] | None = None
statusLock: bool = None
title: Optional[str] | None = None title: Optional[str] | None = None
titleLock: bool titleLock: bool = None
titleSort: Optional[str] | None = None titleSort: Optional[str] | None = None
titleSortLock: bool titleSortLock: bool = None
summary: Optional[str] | None = None summary: Optional[str] | None = None
summaryLock: bool summaryLock: bool = None
readingDirection: Optional[str] | None = None readingDirection: Optional[str] | None = None
readingDirectionLock: bool readingDirectionLock: bool = None
publisher: Optional[str] | None = None publisher: Optional[str] | None = None
publisherLock: bool publisherLock: bool = None
ageRating: Optional[int] | None = None ageRating: Optional[int] | None = None
ageRatingLock: bool ageRatingLock: bool = None
language: Optional[str] | None = None language: Optional[str] | None = None
languageLock: bool languageLock: bool = None
genres: List[str] | None genres: List[str] | None = None
genresLock: bool genresLock: bool = None
tags: List[str] | None = None tags: List[str] | None = None
tagsLock: bool tagsLock: bool = None
totalBookCount: Optional[int] | None = None totalBookCount: Optional[int] | None = None
totalBookCountLock: bool totalBookCountLock: bool = None
sharingLabels: List[str | None] = None sharingLabels: List[str | None] = None
sharingLabelsLock: bool sharingLabelsLock: bool = None
links: List[Link] | None = None links: List[Link] | None = None
linksLock: bool linksLock: bool = None
alternateTitles: List[AlternateTitle] | None = None alternateTitles: List[AlternateTitle] | None = None
alternateTitlesLock: bool alternateTitlesLock: bool = None
created: Optional[str] | None = None created: Optional[str] | None = None
lastModified: Optional[str] | None = None lastModified: Optional[str] | None = None

View File

@@ -2,10 +2,11 @@ from __future__ import annotations
from typing import List, Optional from typing import List, Optional
from pydantic import BaseModel from dataclasses import dataclass
class Page(BaseModel): @dataclass
class Page:
number: Optional[int] = None number: Optional[int] = None
fileName: Optional[str] = None fileName: Optional[str] = None
mediaType: Optional[str] = None mediaType: Optional[str] = None

View File

@@ -2,12 +2,13 @@ from __future__ import annotations
from typing import List from typing import List
from pydantic import BaseModel from dataclasses import dataclass
from .Sort import Sort from .Sort import Sort
class Pageable(BaseModel): @dataclass
class Pageable:
offset: int offset: int
sort: Sort sort: Sort
pageNumber: int pageNumber: int

View File

@@ -2,14 +2,13 @@ from __future__ import annotations
from typing import List, Optional from typing import List, Optional
from pydantic import BaseModel from dataclasses import dataclass
from .Text import Text from .Text import Text
from .Locations import Locations from .Locations import Locations
@dataclass
class Position:
class Position(BaseModel):
href: Optional[str] = None href: Optional[str] = None
type: Optional[str] = None type: Optional[str] = None
title: Optional[str] = None title: Optional[str] = None
@@ -17,6 +16,7 @@ class Position(BaseModel):
text: Optional[Text] = None text: Optional[Text] = None
class Position(BaseModel): @dataclass
class Position:
total: Optional[int] = None total: Optional[int] = None
positions: Optional[List[Position]] = None positions: Optional[List[Position]] = None

View File

@@ -2,17 +2,19 @@ from __future__ import annotations
from typing import List, Optional from typing import List, Optional
from pydantic import BaseModel from dataclasses import dataclass
from .Text import Text from .Text import Text
from .Locations import Locations from .Locations import Locations
class Device(BaseModel): @dataclass
class Device:
id: Optional[str] = None id: Optional[str] = None
name: Optional[str] = None name: Optional[str] = None
class Locator(BaseModel): @dataclass
class Locator:
href: Optional[str] = None href: Optional[str] = None
type: Optional[str] = None type: Optional[str] = None
title: Optional[str] = None title: Optional[str] = None
@@ -20,7 +22,8 @@ class Locator(BaseModel):
text: Optional[Text] = None text: Optional[Text] = None
class Progress(BaseModel): @dataclass
class Progress:
modified: Optional[str] = None modified: Optional[str] = None
device: Optional[Device] = None device: Optional[Device] = None
locator: Optional[Locator] = None locator: Optional[Locator] = None

View File

@@ -2,10 +2,11 @@ from __future__ import annotations
from typing import List from typing import List
from pydantic import BaseModel from dataclasses import dataclass
class Readlist(BaseModel): @dataclass
class Readlist:
name: str name: str
summary: str summary: str
ordered: bool ordered: bool

View File

@@ -1,28 +1,38 @@
from __future__ import annotations from __future__ import annotations
from typing import Optional
from typing import List, Optional from dataclasses import dataclass
from pydantic import BaseModel
from .AlternateTitle import AlternateTitle
from .BooksMetadata import BooksMetadata from .BooksMetadata import BooksMetadata
from .Link import Link
from .Metadata import Metadata from .Metadata import Metadata
class Series(BaseModel): @dataclass
id: Optional[str] = None class Series:
libraryId: Optional[str] = None id: str = None
name: Optional[str] = None libraryId: str = None
url: Optional[str] = None name: str = None
created: Optional[str] = None url: str = None
lastModified: Optional[str] = None created: str = None
fileLastModified: Optional[str] = None lastModified: str = None
booksCount: Optional[int] = None fileLastModified: str = None
booksReadCount: Optional[int] = None booksCount: int = 0
booksUnreadCount: Optional[int] = None booksReadCount: int = None
booksInProgressCount: Optional[int] = None booksUnreadCount: int = None
metadata: Optional[Metadata] = None booksInProgressCount: int = None
booksMetadata: Optional[BooksMetadata] = None metadata: Metadata = None
deleted: Optional[bool] = None booksMetadata: BooksMetadata = None
oneshot: Optional[bool] = None deleted: bool = None
oneshot: bool = None
def __post_init__(self):
if self.metadata is None:
self.metadata = Metadata()
if self.booksMetadata is None:
self.booksMetadata = BooksMetadata()
if self.deleted is None:
self.deleted = False
if self.oneshot is None:
self.oneshot = False
if self.metadata:
self.metadata = Metadata(**self.metadata)
if self.booksMetadata:
self.booksMetadata = BooksMetadata(**self.booksMetadata)

View File

@@ -2,10 +2,11 @@ from __future__ import annotations
from typing import List from typing import List
from pydantic import BaseModel from dataclasses import dataclass
class Sort(BaseModel): @dataclass
class Sort:
empty: bool empty: bool
sorted: bool sorted: bool
unsorted: bool unsorted: bool

View File

@@ -1,8 +1,10 @@
from __future__ import annotations from __future__ import annotations
from typing import List, Optional from typing import List, Optional
from pydantic import BaseModel from dataclasses import dataclass
class Text(BaseModel):
@dataclass
class Text:
after: Optional[str] = None after: Optional[str] = None
before: Optional[str] = None before: Optional[str] = None
highlight: Optional[str] = None highlight: Optional[str] = None

View File

@@ -2,10 +2,11 @@ from __future__ import annotations
from typing import List from typing import List
from pydantic import BaseModel from dataclasses import dataclass
class Thumbnail(BaseModel): @dataclass
class Thumbnail:
id: str id: str
seriesId: str seriesId: str
type: str type: str
@@ -16,7 +17,8 @@ class Thumbnail(BaseModel):
height: int height: int
class ReadlistThumbnail(BaseModel): @dataclass
class ReadlistThumbnail:
id: str id: str
readlistId: str readlistId: str
type: str type: str

View File

@@ -2,25 +2,24 @@ from __future__ import annotations
from typing import List, Dict from typing import List, Dict
from pydantic import BaseModel from dataclasses import dataclass
class User(BaseModel):
"""Object representing a User
Args:
BaseModel (Model): Pydantic BaseModel
"""
id:str
email:str
roles:List[str]
sharedAllLibraries:bool
sharedLibrariesIds:List[str]
labelsAllow:List[str]
labelsExclude:List[str]
ageRestriction:Dict[str, int|str] | None
class CreateUser(BaseModel): @dataclass
email:str class User:
password:str """Object representing a User"""
roles:List[str]
id: str
email: str
roles: List[str]
sharedAllLibraries: bool
sharedLibrariesIds: List[str]
labelsAllow: List[str]
labelsExclude: List[str]
ageRestriction: Dict[str, int | str] | None
class CreateUser:
email: str
password: str
roles: List[str]

View File

@@ -25,6 +25,6 @@ from .User import User, CreateUser
from .Authentication import Authentication, UserAuthActivity from .Authentication import Authentication, UserAuthActivity
from .apikey import APIKey from .apikey import APIKey
from .settings import Settings from .settings import Settings
from .status import Status
from .violation import Violation from .violation import Violation

View File

@@ -1,9 +1,10 @@
from __future__ import annotations from __future__ import annotations
from pydantic import BaseModel from dataclasses import dataclass
class APIKey(BaseModel): @dataclass
class APIKey:
id: str id: str
userId: str userId: str
key: str key: str

View File

@@ -1,27 +1,31 @@
from __future__ import annotations from __future__ import annotations
from pydantic import BaseModel from dataclasses import dataclass
class ServerPort(BaseModel): @dataclass
class ServerPort:
configurationSource: int configurationSource: int
databaseSource: int databaseSource: int
effectiveValue: int effectiveValue: int
class ServerContextPath(BaseModel): @dataclass
class ServerContextPath:
configurationSource: str configurationSource: str
databaseSource: str databaseSource: str
effectiveValue: str effectiveValue: str
class KepubifyPath(BaseModel): @dataclass
class KepubifyPath:
configurationSource: str configurationSource: str
databaseSource: str databaseSource: str
effectiveValue: str effectiveValue: str
class Settings(BaseModel): @dataclass
class Settings:
deleteEmptyCollections: bool deleteEmptyCollections: bool
deleteEmptyReadLists: bool deleteEmptyReadLists: bool
rememberMeDurationDays: int rememberMeDurationDays: int

View File

@@ -0,0 +1,15 @@
# enum
from enum import Enum
class Status(str, Enum):
ENDED = "ENDED"
ONGOING = "ONGOING"
HIATUS = "HIATUS"
ABANDONED = "ABANDONED"
if __name__ == "__main__":
options = [e.name for e in Status]
print(options)
print(Status("ENDED"))

View File

@@ -1,11 +1,10 @@
from __future__ import annotations from __future__ import annotations
from pydantic import BaseModel from dataclasses import dataclass
class Violation(BaseModel): @dataclass
class Violation:
fieldName: str fieldName: str
message: str message: str

19
tests/test_api.py Normal file
View File

@@ -0,0 +1,19 @@
from komgapi import komgapi
from komconfig import KomConfig
config = KomConfig()
client = komgapi(
url=config.komga.url,
username=config.komga.user,
password=config.komga.password,
)
def test_series_controller(client: komgapi):
assert client.series_controller.getAllSeries() is not None
if __name__ == "__main__":
test_series_controller(client)
print("All tests passed!")