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.
Files changed (29) hide show
  1. hardpycover-0.1.0/PKG-INFO +54 -0
  2. hardpycover-0.1.0/README.md +38 -0
  3. hardpycover-0.1.0/hardcoverpy/__init__.py +8 -0
  4. hardpycover-0.1.0/hardcoverpy/classes/__init__.py +15 -0
  5. hardpycover-0.1.0/hardcoverpy/classes/author.py +34 -0
  6. hardpycover-0.1.0/hardcoverpy/classes/book_categories.py +6 -0
  7. hardpycover-0.1.0/hardcoverpy/classes/book_characters.py +15 -0
  8. hardpycover-0.1.0/hardcoverpy/classes/books.py +35 -0
  9. hardpycover-0.1.0/hardcoverpy/classes/characters.py +21 -0
  10. hardpycover-0.1.0/hardcoverpy/classes/contributors.py +23 -0
  11. hardpycover-0.1.0/hardcoverpy/classes/editions.py +33 -0
  12. hardpycover-0.1.0/hardcoverpy/classes/image.py +16 -0
  13. hardpycover-0.1.0/hardcoverpy/classes/language.py +8 -0
  14. hardpycover-0.1.0/hardcoverpy/classes/publishers.py +14 -0
  15. hardpycover-0.1.0/hardcoverpy/classes/search.py +29 -0
  16. hardpycover-0.1.0/hardcoverpy/classes/user.py +27 -0
  17. hardpycover-0.1.0/hardcoverpy/classes/user_stats.py +56 -0
  18. hardpycover-0.1.0/hardcoverpy/classes/userbooks.py +42 -0
  19. hardpycover-0.1.0/hardcoverpy/classes/utils.py +38 -0
  20. hardpycover-0.1.0/hardcoverpy/core/__init__.py +4 -0
  21. hardpycover-0.1.0/hardcoverpy/core/base.py +87 -0
  22. hardpycover-0.1.0/hardcoverpy/core/exceptions.py +20 -0
  23. hardpycover-0.1.0/hardcoverpy/core/http.py +265 -0
  24. hardpycover-0.1.0/hardcoverpy/core/query.py +171 -0
  25. hardpycover-0.1.0/hardcoverpy/filter.py +28 -0
  26. hardpycover-0.1.0/hardcoverpy/main.py +309 -0
  27. hardpycover-0.1.0/hardcoverpy/stats.py +122 -0
  28. hardpycover-0.1.0/hardcoverpy/utils.py +8 -0
  29. 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,8 @@
1
+ from .main import Hardcover
2
+
3
+ __version__ = "1.0.0"
4
+ __author__ = "Jay Cruz"
5
+
6
+ __all__ = [
7
+ "Hardcover"
8
+ ]
@@ -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,6 @@
1
+ from typing import Optional
2
+ from pydantic import BaseModel
3
+
4
+ class BookCategory(BaseModel):
5
+ id: Optional[int] = None
6
+ name: Optional[str] = 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,8 @@
1
+ from typing import Optional
2
+ from pydantic import BaseModel
3
+
4
+ class Language(BaseModel):
5
+ code2: Optional[str] = None
6
+ code3: Optional[str] = None
7
+ id: Optional[int] = None
8
+ language: Optional[str] = None
@@ -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,4 @@
1
+ from .base import GraphQLClient
2
+ from .http import HardcoverEndpoint
3
+
4
+ __all__ = ("GraphQLClient", "HardcoverEndpoint")
@@ -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.")