hardpycover 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hardpycover-0.1.0/PKG-INFO +54 -0
- hardpycover-0.1.0/README.md +38 -0
- hardpycover-0.1.0/hardcoverpy/__init__.py +8 -0
- hardpycover-0.1.0/hardcoverpy/classes/__init__.py +15 -0
- hardpycover-0.1.0/hardcoverpy/classes/author.py +34 -0
- hardpycover-0.1.0/hardcoverpy/classes/book_categories.py +6 -0
- hardpycover-0.1.0/hardcoverpy/classes/book_characters.py +15 -0
- hardpycover-0.1.0/hardcoverpy/classes/books.py +35 -0
- hardpycover-0.1.0/hardcoverpy/classes/characters.py +21 -0
- hardpycover-0.1.0/hardcoverpy/classes/contributors.py +23 -0
- hardpycover-0.1.0/hardcoverpy/classes/editions.py +33 -0
- hardpycover-0.1.0/hardcoverpy/classes/image.py +16 -0
- hardpycover-0.1.0/hardcoverpy/classes/language.py +8 -0
- hardpycover-0.1.0/hardcoverpy/classes/publishers.py +14 -0
- hardpycover-0.1.0/hardcoverpy/classes/search.py +29 -0
- hardpycover-0.1.0/hardcoverpy/classes/user.py +27 -0
- hardpycover-0.1.0/hardcoverpy/classes/user_stats.py +56 -0
- hardpycover-0.1.0/hardcoverpy/classes/userbooks.py +42 -0
- hardpycover-0.1.0/hardcoverpy/classes/utils.py +38 -0
- hardpycover-0.1.0/hardcoverpy/core/__init__.py +4 -0
- hardpycover-0.1.0/hardcoverpy/core/base.py +87 -0
- hardpycover-0.1.0/hardcoverpy/core/exceptions.py +20 -0
- hardpycover-0.1.0/hardcoverpy/core/http.py +265 -0
- hardpycover-0.1.0/hardcoverpy/core/query.py +171 -0
- hardpycover-0.1.0/hardcoverpy/filter.py +28 -0
- hardpycover-0.1.0/hardcoverpy/main.py +309 -0
- hardpycover-0.1.0/hardcoverpy/stats.py +122 -0
- hardpycover-0.1.0/hardcoverpy/utils.py +8 -0
- hardpycover-0.1.0/pyproject.toml +39 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hardpycover
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Hardcover API wrapper for Python
|
|
5
|
+
Keywords: hardcover,api,wrapper,graphql
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Dist: pydantic>=2.11.4
|
|
8
|
+
Requires-Dist: python-dotenv>=1.1.0
|
|
9
|
+
Requires-Dist: urllib3>=2.4.0
|
|
10
|
+
Requires-Dist: sgqlc ; extra == 'with-sgql'
|
|
11
|
+
Maintainer: Jay Cruz
|
|
12
|
+
Maintainer-email: Jay Cruz <jccruz009@yahoo.com>
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Provides-Extra: with-sgql
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
## hardcoverpy
|
|
18
|
+
|
|
19
|
+
Simplified API Wrapper for Hardcover, written in Python.
|
|
20
|
+
|
|
21
|
+
### Installation
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
pip install hardcoverpy
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
uv add hardcoverpy
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This is an API wrapper for the Hardcover API made usable in Python.
|
|
32
|
+
|
|
33
|
+
### Checklists
|
|
34
|
+
*To be checked off before updating visibility*
|
|
35
|
+
|
|
36
|
+
**Task list**
|
|
37
|
+
1. Create more comprehensive README.md file
|
|
38
|
+
2. Rewrite the querying script and pattern it after the `sgqlc` operations file.
|
|
39
|
+
|
|
40
|
+
**Important**
|
|
41
|
+
- [x] Licensing file
|
|
42
|
+
- [ ] Installation & about guide
|
|
43
|
+
- [x] Sufficient `pyproject.toml` file
|
|
44
|
+
|
|
45
|
+
### Development
|
|
46
|
+
|
|
47
|
+
_Ruff_
|
|
48
|
+
```zsh
|
|
49
|
+
ruff check
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
##### References
|
|
53
|
+
- This project will primarily be built off of the `praw` and `prawcore` repository
|
|
54
|
+
- The same ownership rights exist for this as anything on the site. You own your data. This means you can't use the API to access and use someone elses data.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
## hardcoverpy
|
|
2
|
+
|
|
3
|
+
Simplified API Wrapper for Hardcover, written in Python.
|
|
4
|
+
|
|
5
|
+
### Installation
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
pip install hardcoverpy
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
uv add hardcoverpy
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This is an API wrapper for the Hardcover API made usable in Python.
|
|
16
|
+
|
|
17
|
+
### Checklists
|
|
18
|
+
*To be checked off before updating visibility*
|
|
19
|
+
|
|
20
|
+
**Task list**
|
|
21
|
+
1. Create more comprehensive README.md file
|
|
22
|
+
2. Rewrite the querying script and pattern it after the `sgqlc` operations file.
|
|
23
|
+
|
|
24
|
+
**Important**
|
|
25
|
+
- [x] Licensing file
|
|
26
|
+
- [ ] Installation & about guide
|
|
27
|
+
- [x] Sufficient `pyproject.toml` file
|
|
28
|
+
|
|
29
|
+
### Development
|
|
30
|
+
|
|
31
|
+
_Ruff_
|
|
32
|
+
```zsh
|
|
33
|
+
ruff check
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
##### References
|
|
37
|
+
- This project will primarily be built off of the `praw` and `prawcore` repository
|
|
38
|
+
- The same ownership rights exist for this as anything on the site. You own your data. This means you can't use the API to access and use someone elses data.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .author import Author
|
|
2
|
+
from .userbooks import UserBook
|
|
3
|
+
from .user import User
|
|
4
|
+
from .search import Search
|
|
5
|
+
from .user_stats import UserBooksAggregate
|
|
6
|
+
from .publishers import Publisher
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"Author",
|
|
10
|
+
"User",
|
|
11
|
+
"UserBook",
|
|
12
|
+
"UserBooksAggregate",
|
|
13
|
+
"Publisher",
|
|
14
|
+
"Search"
|
|
15
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from typing import Optional, Dict, List
|
|
2
|
+
from datetime import date
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from .image import CachedImage
|
|
6
|
+
from .utils import Identifiers, Link
|
|
7
|
+
|
|
8
|
+
class Author(BaseModel):
|
|
9
|
+
alternate_names: Optional[List[str]] = None
|
|
10
|
+
alias_id: Optional[int] = None
|
|
11
|
+
bio: Optional[str] = None
|
|
12
|
+
books_count: Optional[int] = None
|
|
13
|
+
born_date: Optional[date] = None
|
|
14
|
+
born_year: Optional[int] = None
|
|
15
|
+
cached_image: Optional[CachedImage] = None
|
|
16
|
+
canonical_id: Optional[int] = None
|
|
17
|
+
death_date: Optional[date] = None
|
|
18
|
+
death_year: Optional[int] = None
|
|
19
|
+
gender_id: Optional[int] = None
|
|
20
|
+
id: Optional[int] = None
|
|
21
|
+
identifiers: Optional[Identifiers] = None
|
|
22
|
+
image_id: Optional[int] = None
|
|
23
|
+
is_bipoc: Optional[bool] = None
|
|
24
|
+
is_lgbtq: Optional[bool] = None
|
|
25
|
+
links: Optional[List[Link]] = None
|
|
26
|
+
location: Optional[str] = None
|
|
27
|
+
locked: Optional[bool] = None
|
|
28
|
+
name: Optional[str] = None
|
|
29
|
+
name_personal: Optional[str] = None
|
|
30
|
+
slug: Optional[str] = None
|
|
31
|
+
state: Optional[str] = None
|
|
32
|
+
title: Optional[str] = None
|
|
33
|
+
user_id: Optional[int] = None
|
|
34
|
+
users_count: Optional[int] = None
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
|
|
4
|
+
from .characters import Character
|
|
5
|
+
from .books import Book
|
|
6
|
+
|
|
7
|
+
class BookCharacter(BaseModel):
|
|
8
|
+
id: Optional[int] = None
|
|
9
|
+
book: Optional[Book] = None
|
|
10
|
+
book_id: Optional[int] = None
|
|
11
|
+
character: Optional[Character] = None
|
|
12
|
+
character_id: Optional[int] = None
|
|
13
|
+
only_mentioned: Optional[bool] = None
|
|
14
|
+
position: Optional[int] = None
|
|
15
|
+
spoiler: Optional[bool] = None
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from typing import Optional, Literal, List
|
|
2
|
+
from datetime import datetime, date
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
class RatingsDistribution(BaseModel):
|
|
6
|
+
count: int
|
|
7
|
+
rating: int
|
|
8
|
+
|
|
9
|
+
class Book(BaseModel):
|
|
10
|
+
id: Optional[int] = None
|
|
11
|
+
image_id: Optional[int] = None
|
|
12
|
+
description: Optional[str] = None
|
|
13
|
+
default_physical_edition_id: Optional[int] = None
|
|
14
|
+
default_ebook_edition_id: Optional[int] = None
|
|
15
|
+
default_cover_edition_id: Optional[int] = None
|
|
16
|
+
default_audio_edition_id: Optional[int] = None
|
|
17
|
+
created_by_user_id: Optional[int] = None
|
|
18
|
+
book_category_id: Optional[int] = None
|
|
19
|
+
users_read_count: Optional[int] = None
|
|
20
|
+
users_count: Optional[int] = None
|
|
21
|
+
user_added: Optional[bool] = None
|
|
22
|
+
updated_at: Optional[datetime] = None
|
|
23
|
+
title: Optional[str] = None
|
|
24
|
+
subtitle: Optional[str] = None
|
|
25
|
+
state: Optional[Literal['error', 'pending', 'normalized', 'duplicate']] = None # This is a Literal but we need to confirm w/c values
|
|
26
|
+
slug: Optional[str] = None
|
|
27
|
+
ratings_distribution: Optional[List[RatingsDistribution]] = None
|
|
28
|
+
ratings_count: Optional[int] = None
|
|
29
|
+
rating: Optional[int | float] = None
|
|
30
|
+
prompts_count: Optional[int] = None
|
|
31
|
+
release_date: Optional[date] = None
|
|
32
|
+
release_year: Optional[int] = None
|
|
33
|
+
reviews_count: Optional[int] = None
|
|
34
|
+
users_count: Optional[int] = None
|
|
35
|
+
users_read_count: Optional[int] = None
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from typing import Optional, Literal
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
class Character(BaseModel):
|
|
6
|
+
id: Optional[int] = None
|
|
7
|
+
name: Optional[str] = None
|
|
8
|
+
biography: Optional[str] = None
|
|
9
|
+
books_count: Optional[int] = None
|
|
10
|
+
canonical_books_count: Optional[int] = None
|
|
11
|
+
created_at: Optional[datetime] = None
|
|
12
|
+
updated_at: Optional[datetime] = None
|
|
13
|
+
has_disability: Optional[bool] = None
|
|
14
|
+
is_lgbtq: Optional[bool] = None
|
|
15
|
+
is_poc: Optional[bool] = None
|
|
16
|
+
image_id: Optional[str] = None
|
|
17
|
+
state: Optional[str] = None
|
|
18
|
+
slug: Optional[str] = None
|
|
19
|
+
object_type: Optional[Literal['Character']] = None
|
|
20
|
+
gender_id: Optional[int] = None
|
|
21
|
+
user_id: Optional[str] = None
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from typing import Optional, Literal
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from pydantic import BaseModel, ConfigDict
|
|
4
|
+
|
|
5
|
+
from .author import Author
|
|
6
|
+
|
|
7
|
+
class Contributor(BaseModel):
|
|
8
|
+
author_id: Optional[int] = None
|
|
9
|
+
contributable_type: Optional[str] = None # Could be a literal
|
|
10
|
+
contributable_id: Optional[int] = None
|
|
11
|
+
contribution: Optional[str] = None # Could be literal
|
|
12
|
+
created_at: Optional[datetime] = None
|
|
13
|
+
id: int
|
|
14
|
+
updated_at: Optional[datetime] = None
|
|
15
|
+
|
|
16
|
+
class CachedContributor(BaseModel):
|
|
17
|
+
author: Optional[Author] = None
|
|
18
|
+
contribution: Optional[str] = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# NOTE: list of contribution literals
|
|
22
|
+
# book, afterword, design, ...
|
|
23
|
+
# null, Contributor, Translator, translator
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from datetime import datetime, date
|
|
2
|
+
from typing import Optional, Literal
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from .language import Language
|
|
6
|
+
from .publishers import Publisher
|
|
7
|
+
|
|
8
|
+
class Edition(BaseModel):
|
|
9
|
+
title: Optional[str] = None
|
|
10
|
+
subtitle: Optional[str] = None
|
|
11
|
+
state: Optional[str] = None # normalized, duplicate, ???
|
|
12
|
+
score: Optional[int] = None
|
|
13
|
+
edition: Optional[str] = None
|
|
14
|
+
edition_format: Optional[str] = None
|
|
15
|
+
edition_information: Optional[str] = None
|
|
16
|
+
language_id: Optional[int] = None
|
|
17
|
+
language: Optional[Language] = None
|
|
18
|
+
country_id: Optional[int] = None
|
|
19
|
+
image_id: Optional[int] = None
|
|
20
|
+
publisher_id: Optional[int] = None
|
|
21
|
+
publisher: Optional[Publisher] = None
|
|
22
|
+
pages: Optional[int] = None
|
|
23
|
+
isbn_10: Optional[str] = None
|
|
24
|
+
isbn_13: Optional[str] = None
|
|
25
|
+
lists_count: Optional[int] = None
|
|
26
|
+
object_type: Optional[Literal['Edition']] = None
|
|
27
|
+
release_year: Optional[int] = None
|
|
28
|
+
release_date: Optional[date] = None
|
|
29
|
+
users_count: Optional[int] = None
|
|
30
|
+
users_read_count: Optional[int] = None
|
|
31
|
+
created_at: Optional[datetime] = None
|
|
32
|
+
updated_at: Optional[datetime] = None
|
|
33
|
+
# normalized_at: Optional[datetime] = None
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from typing import Optional, List
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
|
|
4
|
+
class Image(BaseModel):
|
|
5
|
+
color: Optional[str] = None
|
|
6
|
+
colors: Optional[List[str]] = None
|
|
7
|
+
height: Optional[int] = None
|
|
8
|
+
imageable_id: Optional[int] = None
|
|
9
|
+
id: Optional[int] = None
|
|
10
|
+
imageable_type: Optional[str] = None
|
|
11
|
+
ratio: Optional[int] = None
|
|
12
|
+
url: Optional[str] = None
|
|
13
|
+
width: Optional[int] = None
|
|
14
|
+
|
|
15
|
+
class CachedImage(Image):
|
|
16
|
+
pass
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
class Publisher(BaseModel):
|
|
6
|
+
id: Optional[int] = None,
|
|
7
|
+
created_at: Optional[datetime] = None
|
|
8
|
+
updated_at: Optional[datetime] = None
|
|
9
|
+
slug: Optional[str] = None
|
|
10
|
+
name: Optional[str] = None
|
|
11
|
+
editions_count: Optional[int] = None
|
|
12
|
+
|
|
13
|
+
class ParentPublisher(Publisher):
|
|
14
|
+
parent_publisher: Optional[Publisher] = None
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from .books import Book
|
|
2
|
+
from .utils import TextMatchInfo
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
# Fixed values:
|
|
7
|
+
# page = 1; per_page = 25
|
|
8
|
+
|
|
9
|
+
# NOTE: exempted fields: highlight and highlights
|
|
10
|
+
class SearchHits(BaseModel):
|
|
11
|
+
document: Optional[Book] = None
|
|
12
|
+
text_match: Optional[int] = None
|
|
13
|
+
text_match_info: Optional[TextMatchInfo] = None
|
|
14
|
+
|
|
15
|
+
class SearchBookResults(BaseModel):
|
|
16
|
+
facet_counts: Optional[list] = None
|
|
17
|
+
found: Optional[int] = None
|
|
18
|
+
hits: Optional[list[SearchHits]] = None
|
|
19
|
+
|
|
20
|
+
# NOTE: current search focuses on book
|
|
21
|
+
class Search(BaseModel):
|
|
22
|
+
error: Optional[str] = None
|
|
23
|
+
ids: Optional[list[int]] = None
|
|
24
|
+
page: Optional[int] = None
|
|
25
|
+
per_page: Optional[int] = None
|
|
26
|
+
query_type: Optional[str] = None # GraphQL returns this as "Book" by default
|
|
27
|
+
results: Optional[SearchBookResults] = None
|
|
28
|
+
|
|
29
|
+
# NOTE: for query_type; "Author" is also an appropriate field
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import Optional, Literal, List
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
from .utils import CachedImage
|
|
5
|
+
|
|
6
|
+
# NOTE: expand the User class accordingly
|
|
7
|
+
class User(BaseModel):
|
|
8
|
+
id: Optional[int] = None
|
|
9
|
+
bio: Optional[str] = None
|
|
10
|
+
cached_image: Optional[CachedImage] = None
|
|
11
|
+
birthdate: Optional[str] = None
|
|
12
|
+
books_count: Optional[int] = None
|
|
13
|
+
created_at: Optional[datetime] = None
|
|
14
|
+
email: Optional[str] = None
|
|
15
|
+
followers_count: Optional[int] = None
|
|
16
|
+
followed_users_count: Optional[int] = None
|
|
17
|
+
image_id: Optional[int] = None
|
|
18
|
+
last_activity_at: Optional[datetime] = None
|
|
19
|
+
last_sign_in_at: Optional[datetime] = None
|
|
20
|
+
librarian_roles: Optional[List[str]] = None # literal but which values? (appender, editor, librarian)
|
|
21
|
+
object_type: Optional[Literal['User']] = None
|
|
22
|
+
username: Optional[str] = None
|
|
23
|
+
updated_at: Optional[datetime] = None
|
|
24
|
+
|
|
25
|
+
class UserStatus(BaseModel):
|
|
26
|
+
id: Optional[int] = None
|
|
27
|
+
status: Optional[Literal["created", "activated", "banned"]] = None
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from datetime import datetime, date
|
|
2
|
+
from typing import Optional, Annotated
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
class SumAvgStdFields(BaseModel):
|
|
6
|
+
"""
|
|
7
|
+
List of available fields for use in the `avg` table of the API.
|
|
8
|
+
"""
|
|
9
|
+
book_id: Optional[int | float] = None
|
|
10
|
+
edition_id: Optional[int | float] = None
|
|
11
|
+
id: Optional[int | float] = None
|
|
12
|
+
likes_count: Optional[int | float] = None
|
|
13
|
+
original_book_id: Optional[int | float] = None
|
|
14
|
+
original_edition_id: Optional[int | float] = None
|
|
15
|
+
rating: Optional[int | float] = None
|
|
16
|
+
read_count: Optional[int | float] = None
|
|
17
|
+
referrer_user_id: Optional[int | float] = None
|
|
18
|
+
review_length: Optional[int | float] = None
|
|
19
|
+
user_id: Optional[int | float] = None
|
|
20
|
+
|
|
21
|
+
class MaxMinFields(BaseModel):
|
|
22
|
+
"""
|
|
23
|
+
List of available fields for use in the `max/min` table of the API.
|
|
24
|
+
"""
|
|
25
|
+
book_id: Optional[int | float] = None
|
|
26
|
+
created_at: Optional[datetime] = None
|
|
27
|
+
date_added: Optional[date] = None
|
|
28
|
+
edition_id: Optional[int] = None
|
|
29
|
+
first_read_date: Optional[date] = None
|
|
30
|
+
first_started_reading_date: Optional[date] = None
|
|
31
|
+
id: Optional[int] = None
|
|
32
|
+
last_read_date: Optional[date] = None
|
|
33
|
+
likes_count: Optional[int] = None
|
|
34
|
+
merged_at: Optional[datetime] = None
|
|
35
|
+
owned_copies: Optional[int] = None
|
|
36
|
+
rating: Optional[int] = None
|
|
37
|
+
read_count: Optional[int] = None
|
|
38
|
+
review: Optional[Annotated[str, "Review text with HTML tags"]] = None
|
|
39
|
+
review_length: Optional[int] = None
|
|
40
|
+
review_raw: Optional[Annotated[str, "Review text without HTML tags"]] = None
|
|
41
|
+
updated_at: Optional[datetime] = None
|
|
42
|
+
user_id: Optional[int] = None
|
|
43
|
+
|
|
44
|
+
class AggregateStats(BaseModel):
|
|
45
|
+
avg: Optional[SumAvgStdFields] = None
|
|
46
|
+
count: Optional[int] = None
|
|
47
|
+
max: Optional[MaxMinFields] = None
|
|
48
|
+
min: Optional[MaxMinFields] = None
|
|
49
|
+
stddev: Optional[SumAvgStdFields] = None
|
|
50
|
+
sum: Optional[SumAvgStdFields] = None
|
|
51
|
+
|
|
52
|
+
# basic: max, min, stddev
|
|
53
|
+
|
|
54
|
+
class UserBooksAggregate(BaseModel):
|
|
55
|
+
aggregate: Optional[AggregateStats] = None
|
|
56
|
+
# NOTE: nodes support currently on hold
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from datetime import datetime, date
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from .books import Book
|
|
6
|
+
from .editions import Edition
|
|
7
|
+
|
|
8
|
+
class UserBook(BaseModel):
|
|
9
|
+
book_id: Optional[int] = None # matches book id; separate from user_books_id (id)
|
|
10
|
+
book: Optional[Book] = None
|
|
11
|
+
created_at: Optional[datetime] = None
|
|
12
|
+
date_added: Optional[date] = None
|
|
13
|
+
edition_id: Optional[int] = None
|
|
14
|
+
edition: Optional[Edition] = None
|
|
15
|
+
first_read_date: Optional[date] = None
|
|
16
|
+
first_started_reading_date: Optional[date] = None
|
|
17
|
+
id: Optional[int] = None
|
|
18
|
+
has_review: Optional[bool] = None
|
|
19
|
+
imported: Optional[bool] = None #
|
|
20
|
+
last_read_date: Optional[date] = None
|
|
21
|
+
likes_count: Optional[int] = None
|
|
22
|
+
merged_at: Optional[datetime] = None
|
|
23
|
+
media_url: Optional[str] = None
|
|
24
|
+
object_type: Optional['UserBook'] = None
|
|
25
|
+
original_book_id: Optional[int] = None
|
|
26
|
+
original_edition_id: Optional[int] = None
|
|
27
|
+
owned: Optional[bool] = None
|
|
28
|
+
owned_copies: Optional[int] = None
|
|
29
|
+
privacy_setting_id: Optional[int] = None
|
|
30
|
+
read_count: Optional[int] = None
|
|
31
|
+
reading_format_id: Optional[int] = None
|
|
32
|
+
recommended_for: Optional[str] = None
|
|
33
|
+
recommended_by: Optional[str] = None
|
|
34
|
+
referrer_user_id: Optional[int] = None
|
|
35
|
+
review: Optional[str] = None
|
|
36
|
+
review_has_spoilers: Optional[bool] = None
|
|
37
|
+
rating: Optional[int | float] = None
|
|
38
|
+
starred: Optional[bool] = None
|
|
39
|
+
sponsored_review: Optional[bool] = None
|
|
40
|
+
url: Optional[str] = None
|
|
41
|
+
updated_at: Optional[datetime] = None
|
|
42
|
+
user_id: Optional[int] = None
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from typing import Optional, List
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
|
|
4
|
+
class Image(BaseModel):
|
|
5
|
+
id: int
|
|
6
|
+
url: str
|
|
7
|
+
color: str
|
|
8
|
+
width: int
|
|
9
|
+
height: int
|
|
10
|
+
color_name: str
|
|
11
|
+
|
|
12
|
+
class CachedImage(Image):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
class LinkType(BaseModel):
|
|
16
|
+
key: str
|
|
17
|
+
|
|
18
|
+
class Link(BaseModel):
|
|
19
|
+
url: str
|
|
20
|
+
type: LinkType
|
|
21
|
+
title: str
|
|
22
|
+
|
|
23
|
+
class Identifiers(BaseModel):
|
|
24
|
+
goodreads: Optional[List[str]] = None
|
|
25
|
+
openlibrary: Optional[List[str]] = None
|
|
26
|
+
|
|
27
|
+
class MatchedTokens(BaseModel):
|
|
28
|
+
matched_tokens: Optional[List[str]] = None
|
|
29
|
+
snippet: Optional[str] = None
|
|
30
|
+
|
|
31
|
+
class TextMatchInfo(BaseModel):
|
|
32
|
+
best_field_score: Optional[str] = None # String representation of an int value
|
|
33
|
+
best_field_weight: Optional[int] = None
|
|
34
|
+
fields_matched: Optional[int] = None
|
|
35
|
+
num_tokens_dropped: Optional[int] = None
|
|
36
|
+
score: Optional[str] = None
|
|
37
|
+
tokens_matched: Optional[int] = None
|
|
38
|
+
typo_prefix_score: Optional[int] = None
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import urllib
|
|
3
|
+
from urllib.request import urlopen
|
|
4
|
+
from urllib.error import HTTPError
|
|
5
|
+
from typing import Dict, Optional
|
|
6
|
+
from .exceptions import GraphQLError
|
|
7
|
+
|
|
8
|
+
__all__ = ('GraphQLClient')
|
|
9
|
+
|
|
10
|
+
class GraphQLClient:
|
|
11
|
+
"""
|
|
12
|
+
Instantiate a standard GraphQL client.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
endpoint (str): The API endpoint to be used for the request
|
|
16
|
+
headers (dict): Headers to be passed alongside request
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
endpoint: str,
|
|
22
|
+
headers: Optional[Dict[str, str]] = None
|
|
23
|
+
):
|
|
24
|
+
self.endpoint = endpoint
|
|
25
|
+
self.headers = headers or {}
|
|
26
|
+
|
|
27
|
+
if 'Content-Type' not in self.headers:
|
|
28
|
+
self.headers['Content-Type'] = 'application/json'
|
|
29
|
+
|
|
30
|
+
def execute(self, query: str):
|
|
31
|
+
payload = {
|
|
32
|
+
'query': query
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
data = json.dumps(payload).encode('utf-8')
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
request = urllib.request.Request(
|
|
39
|
+
self.endpoint,
|
|
40
|
+
data=data,
|
|
41
|
+
headers=self.headers,
|
|
42
|
+
method='POST'
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
with urlopen(request) as response:
|
|
46
|
+
response_data = json.loads(response.read().decode('utf-8'))
|
|
47
|
+
if 'errors' in response_data:
|
|
48
|
+
raise GraphQLError(
|
|
49
|
+
errors=response_data['errors'],
|
|
50
|
+
data=response_data.get('data')
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
return response_data.get('data', {}) # default
|
|
54
|
+
|
|
55
|
+
except HTTPError as e:
|
|
56
|
+
# Return custom exception based on HTTP error code
|
|
57
|
+
code = e.code
|
|
58
|
+
# body = e.read().decode('utf-8') if e.fp else 'No error details'
|
|
59
|
+
if code == 401:
|
|
60
|
+
print(f"HTTP {code}: Bearer token invalid/expired.")
|
|
61
|
+
return False
|
|
62
|
+
if code == 403:
|
|
63
|
+
print(f"HTTP {code}: User does not have access to requested resource.")
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
# raise HTTPError(
|
|
67
|
+
# e.url, e.code, f"HTTP {e.code}: {error_body}", e.headers, e.fp
|
|
68
|
+
# )
|
|
69
|
+
|
|
70
|
+
# TODO: implement cleaner exception handling
|
|
71
|
+
|
|
72
|
+
def set_auth_token(self, token: str, token_type: str = 'Bearer'):
|
|
73
|
+
"""
|
|
74
|
+
Set authentication token.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
token: The authentication token
|
|
78
|
+
token_type: The token type (e.g, 'Bearer', 'Token')
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
if 'Bearer' in token:
|
|
82
|
+
self.headers['Authorization'] = f'{token}'
|
|
83
|
+
elif 'Bearer' not in token and token_type == 'Bearer':
|
|
84
|
+
token = token.replace(' ', '')
|
|
85
|
+
self.headers['Authorization'] = f'Bearer {token}'
|
|
86
|
+
|
|
87
|
+
### build test cases somewhere?
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'''
|
|
2
|
+
To be deprecated as exceptions will be bundled within each request.
|
|
3
|
+
'''
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
class GraphQLError(Exception):
|
|
8
|
+
"""Custom exception for GraphQL errors"""
|
|
9
|
+
def __init__(self, errors: list, data: Any = None):
|
|
10
|
+
self.errors = errors
|
|
11
|
+
self.data = data
|
|
12
|
+
super().__init__(f"GraphQL errors: {errors}")
|
|
13
|
+
|
|
14
|
+
class InvalidTokenError(Exception):
|
|
15
|
+
def __init__(self):
|
|
16
|
+
super().__init__("Bearer token invalid/expired.")
|
|
17
|
+
|
|
18
|
+
class RestrictedAccessError(Exception):
|
|
19
|
+
def __init__(self):
|
|
20
|
+
super().__init__("User does not have access to requested resource.")
|