From 28a38f3f330fe0801d218b99b4b483f7c1118ce8 Mon Sep 17 00:00:00 2001 From: Brian Pepple Date: Sun, 28 Jan 2024 11:14:57 -0500 Subject: [PATCH] Migrate to Pydantic (#42) * Migrate project to pydantic. * Drop support for Python 3.9 --- .flake8 | 2 +- .github/workflows/testing.yml | 2 +- .pre-commit-config.yaml | 8 +- .readthedocs.yml | 2 +- docs/source/conf.py | 1 + docs/source/mokkari.rst | 94 +-------- docs/source/mokkari.schemas.rst | 93 +++++++++ mokkari/arc.py | 120 ------------ mokkari/character.py | 129 ------------- mokkari/creator.py | 129 ------------- mokkari/genre.py | 53 ------ mokkari/issue.py | 327 -------------------------------- mokkari/publisher.py | 126 ------------ mokkari/rating.py | 53 ------ mokkari/reprint.py | 55 ------ mokkari/schemas/__init__.py | 20 ++ mokkari/schemas/arc.py | 46 +++++ mokkari/schemas/character.py | 54 ++++++ mokkari/schemas/creator.py | 52 +++++ mokkari/schemas/generic.py | 22 +++ mokkari/schemas/issue.py | 161 ++++++++++++++++ mokkari/schemas/publisher.py | 48 +++++ mokkari/schemas/reprint.py | 22 +++ mokkari/schemas/series.py | 89 +++++++++ mokkari/schemas/team.py | 52 +++++ mokkari/schemas/variant.py | 28 +++ mokkari/series.py | 277 --------------------------- mokkari/session.py | 251 ++++++++++++++---------- mokkari/sqlite_cache.py | 5 +- mokkari/team.py | 125 ------------ mokkari/variant.py | 55 ------ poetry.lock | 312 +++++++++++++++++++----------- pyproject.toml | 14 +- tests/conftest.py | 1 + tests/test_arcs.py | 32 ++-- tests/test_cache.py | 1 + tests/test_characters.py | 40 ++-- tests/test_creator.py | 25 +-- tests/test_init.py | 1 + tests/test_issues.py | 55 +++--- tests/test_publishers.py | 28 ++- tests/test_role.py | 8 - tests/test_series.py | 42 ++-- tests/test_series_type.py | 12 +- tests/test_teams.py | 28 ++- tests/testing_mock.sqlite | Bin 544768 -> 1142784 bytes tox.ini | 2 +- 47 files changed, 1191 insertions(+), 1911 deletions(-) create mode 100644 docs/source/mokkari.schemas.rst delete mode 100644 mokkari/arc.py delete mode 100644 mokkari/character.py delete mode 100644 mokkari/creator.py delete mode 100644 mokkari/genre.py delete mode 100644 mokkari/issue.py delete mode 100644 mokkari/publisher.py delete mode 100644 mokkari/rating.py delete mode 100644 mokkari/reprint.py create mode 100644 mokkari/schemas/__init__.py create mode 100644 mokkari/schemas/arc.py create mode 100644 mokkari/schemas/character.py create mode 100644 mokkari/schemas/creator.py create mode 100644 mokkari/schemas/generic.py create mode 100644 mokkari/schemas/issue.py create mode 100644 mokkari/schemas/publisher.py create mode 100644 mokkari/schemas/reprint.py create mode 100644 mokkari/schemas/series.py create mode 100644 mokkari/schemas/team.py create mode 100644 mokkari/schemas/variant.py delete mode 100644 mokkari/series.py delete mode 100644 mokkari/team.py delete mode 100644 mokkari/variant.py diff --git a/.flake8 b/.flake8 index 54dde48..ae4540b 100644 --- a/.flake8 +++ b/.flake8 @@ -1,5 +1,5 @@ [flake8] -max-line-length = 95 +max-line-length = 100 max-complexity = 18 select = B,C,D,E,F,W,T4,B9 ignore = W503 diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index e8b5601..2497e81 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -14,7 +14,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11"] + python-version: ["3.10", "3.11", "3.12"] os: - ubuntu-latest - macos-latest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e044f66..baf8d56 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,16 +4,16 @@ repos: hooks: - id: seed-isort-config - repo: https://github.com/pycqa/isort - rev: 5.12.0 + rev: 5.13.2 hooks: - id: isort - repo: https://github.com/ambv/black - rev: 23.9.1 + rev: 24.1.1 hooks: - id: black language_version: python3.11 - repo: https://github.com/PyCQA/flake8 - rev: 6.1.0 + rev: 7.0.0 hooks: - id: flake8 additional_dependencies: @@ -22,4 +22,4 @@ repos: rev: v3.15.0 hooks: - id: pyupgrade - args: ["--py36-plus", "--py37-plus", "--py38-plus"] + args: ["--py36-plus", "--py37-plus", "--py38-plus", "--py39-plus"] diff --git a/.readthedocs.yml b/.readthedocs.yml index 8ae03ae..2c5c771 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -11,7 +11,7 @@ sphinx: # Optionally set the version of Python and requirements required to build your docs python: - version: "3.9" + version: "3.10" install: - method: pip path: . diff --git a/docs/source/conf.py b/docs/source/conf.py index 2200190..9019357 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,4 +1,5 @@ """Documentation configuration.""" + # Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full diff --git a/docs/source/mokkari.rst b/docs/source/mokkari.rst index b041c62..3c13fe3 100644 --- a/docs/source/mokkari.rst +++ b/docs/source/mokkari.rst @@ -1,32 +1,16 @@ mokkari package =============== -Submodules ----------- +Subpackages +----------- -mokkari.arc module ------------------- - -.. automodule:: mokkari.arc - :members: - :undoc-members: - :show-inheritance: +.. toctree:: + :maxdepth: 4 -mokkari.character module ------------------------- + mokkari.schemas -.. automodule:: mokkari.character - :members: - :undoc-members: - :show-inheritance: - -mokkari.creator module ----------------------- - -.. automodule:: mokkari.creator - :members: - :undoc-members: - :show-inheritance: +Submodules +---------- mokkari.exceptions module ------------------------- @@ -36,54 +20,6 @@ mokkari.exceptions module :undoc-members: :show-inheritance: -mokkari.genre module --------------------- - -.. automodule:: mokkari.genre - :members: - :undoc-members: - :show-inheritance: - -mokkari.issue module --------------------- - -.. automodule:: mokkari.issue - :members: - :undoc-members: - :show-inheritance: - -mokkari.publisher module ------------------------- - -.. automodule:: mokkari.publisher - :members: - :undoc-members: - :show-inheritance: - -mokkari.rating module ---------------------- - -.. automodule:: mokkari.rating - :members: - :undoc-members: - :show-inheritance: - -mokkari.reprint module ----------------------- - -.. automodule:: mokkari.reprint - :members: - :undoc-members: - :show-inheritance: - -mokkari.series module ---------------------- - -.. automodule:: mokkari.series - :members: - :undoc-members: - :show-inheritance: - mokkari.session module ---------------------- @@ -100,22 +36,6 @@ mokkari.sqlite\_cache module :undoc-members: :show-inheritance: -mokkari.team module -------------------- - -.. automodule:: mokkari.team - :members: - :undoc-members: - :show-inheritance: - -mokkari.variant module ----------------------- - -.. automodule:: mokkari.variant - :members: - :undoc-members: - :show-inheritance: - Module contents --------------- diff --git a/docs/source/mokkari.schemas.rst b/docs/source/mokkari.schemas.rst new file mode 100644 index 0000000..15d0f30 --- /dev/null +++ b/docs/source/mokkari.schemas.rst @@ -0,0 +1,93 @@ +mokkari.schemas package +======================= + +Submodules +---------- + +mokkari.schemas.arc module +-------------------------- + +.. automodule:: mokkari.schemas.arc + :members: + :undoc-members: + :show-inheritance: + +mokkari.schemas.character module +-------------------------------- + +.. automodule:: mokkari.schemas.character + :members: + :undoc-members: + :show-inheritance: + +mokkari.schemas.creator module +------------------------------ + +.. automodule:: mokkari.schemas.creator + :members: + :undoc-members: + :show-inheritance: + +mokkari.schemas.generic module +------------------------------ + +.. automodule:: mokkari.schemas.generic + :members: + :undoc-members: + :show-inheritance: + +mokkari.schemas.issue module +---------------------------- + +.. automodule:: mokkari.schemas.issue + :members: + :undoc-members: + :show-inheritance: + +mokkari.schemas.publisher module +-------------------------------- + +.. automodule:: mokkari.schemas.publisher + :members: + :undoc-members: + :show-inheritance: + +mokkari.schemas.reprint module +------------------------------ + +.. automodule:: mokkari.schemas.reprint + :members: + :undoc-members: + :show-inheritance: + +mokkari.schemas.series module +----------------------------- + +.. automodule:: mokkari.schemas.series + :members: + :undoc-members: + :show-inheritance: + +mokkari.schemas.team module +--------------------------- + +.. automodule:: mokkari.schemas.team + :members: + :undoc-members: + :show-inheritance: + +mokkari.schemas.variant module +------------------------------ + +.. automodule:: mokkari.schemas.variant + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: mokkari.schemas + :members: + :undoc-members: + :show-inheritance: diff --git a/mokkari/arc.py b/mokkari/arc.py deleted file mode 100644 index f8842e8..0000000 --- a/mokkari/arc.py +++ /dev/null @@ -1,120 +0,0 @@ -""" -Arc module. - -This module provides the following classes: - -- Arc -- ArcSchema -- ArcsList -""" -from marshmallow import EXCLUDE, Schema, ValidationError, fields, post_load - -from mokkari import exceptions - - -class Arc: - """ - The Arc object contains information for story arcs. - - Args: - **kwargs (Any): The keyword arguments is used for setting arc data from Metron. - - Attributes: - id (int): The Metron identification number for the story arc. - name (str): The name of the story arc. - desc (str): The description of the story arc. - image (url): The url for an image associated with the story arc. - cv_id (int): Comic Vine ID for the story arc. - resource_url (url): The url for the resource. - modified (datetime): The date/time the story arc was last changed. - """ - - def __init__(self, **kwargs): - """Initialize a new Arc.""" - for k, v in kwargs.items(): - setattr(self, k, v) - - -class ArcSchema(Schema): - """ - Schema for the Arc API. - - .. versionchanged:: 1.0.0 - - - Added ``modified`` field - - .. versionchanged:: 2.3.3 - - - Added ``resource_url`` field. - - .. versionadded:: 2.4.0 - - - Added ``cv_id`` field. - """ - - id = fields.Int() - name = fields.Str() - desc = fields.Str() - image = fields.Url(allow_none=True) - cv_id = fields.Int(allow_none=True) - resource_url = fields.URL() - modified = fields.DateTime() - - class Meta: - """Any unknown fields will be excluded.""" - - unknown = EXCLUDE - datetime = "%Y-%m-%dT%H:%M:%S%z" - - @post_load - def make_object(self, data, **kwargs): - """ - Make the arc object. - - Args: - data (Any): Data from Metron response. - **kwargs (Any): Any additional keyword arguments. - - Returns: - An :obj:`Arc` object - """ - return Arc(**data) - - -class ArcsList: - """ - The :obj:`ArcsList` object contains a list of story arcs. - - Attributes: - id (int): The Metron identification number for the story arc. - name (str): The name of the story arc. - modified (datetime): The date/time the story arc was last changed. - - Returns: - A list of story arcs. - """ - - def __init__(self, response): - """Initialize a new ArcsList.""" - self.arcs = [] - - schema = ArcSchema() - for arc_dict in response["results"]: - try: - result = schema.load(arc_dict) - except ValidationError as error: - raise exceptions.ApiError(error) from error - - self.arcs.append(result) - - def __iter__(self): - """Return an iterator object.""" - return iter(self.arcs) - - def __len__(self): - """Return the length of the object.""" - return len(self.arcs) - - def __getitem__(self, index: int): - """Return the object of a at index.""" - return self.arcs[index] diff --git a/mokkari/character.py b/mokkari/character.py deleted file mode 100644 index a852691..0000000 --- a/mokkari/character.py +++ /dev/null @@ -1,129 +0,0 @@ -""" -Character module. - -This module provides the following classes: - -- Character -- CharacterSchema -- CharactersList -""" -from marshmallow import EXCLUDE, Schema, ValidationError, fields, post_load - -from mokkari import creator, exceptions, team - - -class Character: - """ - The Character object contains information for characters. - - Args: - **kwargs (Any): The keyword arguments is used for setting character data from Metron. - - Attributes: - id (int): The Metron identification number for the character. - name (str): The name of the character. - alias (list[str]): List of aliases the character may have. - desc (str): The description of the character. - image (url): The url for an image associated with the character. - creators (list[Creator]): A list of creators for the character. - teams (list[Team]): A list of teams the character is a member of. - cv_id (int): Comic Vine ID for the Character. - resource_url (url): The url for the resource. - modified (datetime): The date/time the character was last changed. - """ - - def __init__(self, **kwargs): - """Initialize a new Character.""" - for k, v in kwargs.items(): - setattr(self, k, v) - - -class CharacterSchema(Schema): - """ - Schema for the Character API. - - .. versionchanged:: 1.0.0 - - - Added ``modified`` field - - .. versionchanged:: 2.0.2 - - Removed ``wikipedia`` field - - .. versionchanged:: 2.3.3 - - - Added ``resource_url`` field. - - .. versionadded:: 2.4.0 - - - Added ``cv_id`` field. - """ - - id = fields.Int() - name = fields.Str() - alias = fields.List(fields.Str) - desc = fields.Str() - image = fields.Url(allow_none=True) - creators = fields.Nested(creator.CreatorSchema, many=True) - teams = fields.Nested(team.TeamSchema, many=True) - cv_id = fields.Int(allow_none=True) - resource_url = fields.URL() - modified = fields.DateTime() - - class Meta: - """Any unknown fields will be excluded.""" - - unknown = EXCLUDE - datetime = "%Y-%m-%dT%H:%M:%S%z" - - @post_load - def make_object(self, data, **kwargs): - """ - Make the character object. - - Args: - data (Any): Data from Metron response. - **kwargs (Any): Any additional keyword arguments. - - Returns: - A :obj:`Character` object - """ - return Character(**data) - - -class CharactersList: - """ - The :obj:`CharactersList` object contains a list of characters. - - Attributes: - id (int): The Metron identification number for the character. - name (str): The name of the character. - modified (datetime): The date/time the character was last changed. - - Returns: - A list of characters. - """ - - def __init__(self, response): - """Initialize a new CharactersList.""" - self.characters = [] - - schema = CharacterSchema() - for character_dict in response["results"]: - try: - result = schema.load(character_dict) - except ValidationError as error: - raise exceptions.ApiError(error) from error - - self.characters.append(result) - - def __iter__(self): - """Return an iterator object.""" - return iter(self.characters) - - def __len__(self): - """Return the length of the object.""" - return len(self.characters) - - def __getitem__(self, index: int): - """Return the object of a at index.""" - return self.characters[index] diff --git a/mokkari/creator.py b/mokkari/creator.py deleted file mode 100644 index 5450e03..0000000 --- a/mokkari/creator.py +++ /dev/null @@ -1,129 +0,0 @@ -""" -Creator module. - -This module provides the following classes: - -- Creator -- CreatorSchema -- CreatorsList -""" -from marshmallow import EXCLUDE, Schema, ValidationError, fields, post_load - -from mokkari import exceptions - - -class Creator: - """ - The Creator object contains information for creators. - - Args: - **kwargs (Any): The keyword arguments is used for setting creator data from Metron. - - Attributes: - id (int): The Metron identification number for the creator. - name (str): The name of the creator. - birth (date): The date of birth for the creator. - death (date): The date of death for the creator. - desc (str): The description of the creator. - image (url): The url for an image associated with the creator. - cv_id (int): Comic Vine ID for the Creator. - resource_url (url): The url for the resource. - modified (datetime): The date/time the creator was last changed. - """ - - def __init__(self, **kwargs): - """Initialize a new Creator.""" - for k, v in kwargs.items(): - setattr(self, k, v) - - -class CreatorSchema(Schema): - """ - Schema for the Creator API. - - .. versionchanged:: 1.0.0 - - - Added ``modified`` field - - .. versionchanged:: 2.0.2 - - - Removed ``wikipedia`` field - - .. versionchanged:: 2.3.3 - - - Added ``resource_url`` field. - - .. versionadded:: 2.4.0 - - - Added ``cv_id`` field. - """ - - id = fields.Int() - name = fields.Str() - birth = fields.Date(allow_none=True) - death = fields.Date(allow_none=True) - desc = fields.Str() - image = fields.Url(allow_none=True) - cv_id = fields.Int(allow_none=True) - resource_url = fields.URL() - modified = fields.DateTime() - - class Meta: - """Any unknown fields will be excluded.""" - - unknown = EXCLUDE - dateformat = "%Y-%m-%d" - datetime = "%Y-%m-%dT%H:%M:%S%z" - - @post_load - def make_object(self, data, **kwargs): - """ - Make the Creator object. - - Args: - data (Any): Data from Metron response. - **kwargs (Any): Any additional keyword arguments. - - Returns: - A :obj:`Creator` object. - """ - return Creator(**data) - - -class CreatorsList: - """ - The :obj:`CreatorsList` object contains a list of creators. - - Attributes: - id (int): The Metron identification number for the creator. - name (str): The name of the creator. - modified (datetime): The date/time the creator was last changed. - - Returns: - A list of creators. - """ - - def __init__(self, response) -> None: - """Initialize a new CreatorsList.""" - self.creators = [] - - schema = CreatorSchema() - for creator_dict in response["results"]: - try: - result = schema.load(creator_dict) - except ValidationError as error: - raise exceptions.ApiError(error) from error - - self.creators.append(result) - - def __iter__(self): - """Return an iterator object.""" - return iter(self.creators) - - def __len__(self): - """Return the length of the object.""" - return len(self.creators) - - def __getitem__(self, index: int): - """Return the object of a at index.""" - return self.creators[index] diff --git a/mokkari/genre.py b/mokkari/genre.py deleted file mode 100644 index b3c8ea7..0000000 --- a/mokkari/genre.py +++ /dev/null @@ -1,53 +0,0 @@ -""" -Genre Module. - -This module provides the following classes: - -- Genre -- GenreSchema -""" -from marshmallow import EXCLUDE, Schema, fields, post_load - - -class Genre: - """ - The Genre object contains information about a Series or Issue genre. - - Args: - **kwargs (Any): The keyword arguments is used for setting genre data from Metron. - - Attributes: - id (int): The Metron identification number for the genre. - name (str): The name of the genre. - """ - - def __init__(self, **kwargs): - """Initialize a new Genre.""" - for k, v in kwargs.items(): - setattr(self, k, v) - - -class GenreSchema(Schema): - """Schema for the Genre.""" - - id = fields.Int() - name = fields.Str() - - class Meta: - """Any unknown fields will be excluded.""" - - unknown = EXCLUDE - - @post_load - def make_object(self, data, **kwargs): - """ - Make the Genre object. - - Args: - data (Any): Data from Metron response. - **kwargs (Any): Any additional keyword arguments. - - Returns: - A :obj:`Genre` object. - """ - return Genre(**data) diff --git a/mokkari/issue.py b/mokkari/issue.py deleted file mode 100644 index 4aabc27..0000000 --- a/mokkari/issue.py +++ /dev/null @@ -1,327 +0,0 @@ -""" -Issue module. - -This module provides the following classes: - -- Role -- RolesSchema -- RoleList -- Credit -- CreditsSchema -- Issue -- IssueSchema -- IssuesList -""" -from marshmallow import EXCLUDE, Schema, ValidationError, fields, post_load - -from mokkari import exceptions -from mokkari.arc import ArcSchema -from mokkari.character import CharacterSchema -from mokkari.publisher import PublisherSchema -from mokkari.rating import RatingSchema -from mokkari.reprint import ReprintSchema -from mokkari.series import SeriesSchema -from mokkari.team import TeamSchema -from mokkari.variant import VariantSchema - - -class Role: - """ - The Role object contains information for creators' role. - - Args: - **kwargs (Any): The keyword arguments is used for setting role data from Metron. - - Attributes: - id (int): The Metron identification number for the role. - name (str): The name of the role. - """ - - def __init__(self, **kwargs): - """Initialize a new Role.""" - for k, v in kwargs.items(): - setattr(self, k, v) - - -class RolesSchema(Schema): - """Schema for the Roles.""" - - id = fields.Int() - name = fields.Str() - - @post_load - def make_object(self, data, **kwargs): - """ - Make the Role object. - - Args: - data (Any): Data from Metron response. - **kwargs (Any): Any additional keyword arguments. - - Returns: - A :obj:`Role` object. - """ - return Role(**data) - - -class RoleList: - """ - The :obj:`RoleList` object contains a list of roles. - - Attributes: - id (int): The Metron identification number for the role. - name (str): The name of the role. - - Returns: - A list of roles. - """ - - def __init__(self, response): - """Initialize a new RoleList.""" - self.roles = [] - - schema = RolesSchema() - for role_dict in response["results"]: - try: - result = schema.load(role_dict) - except ValidationError as error: - raise exceptions.ApiError(error) from error - - self.roles.append(result) - - def __iter__(self): - """Return an iterator object.""" - return iter(self.roles) - - def __len__(self): - """Return the length of the object.""" - return len(self.roles) - - def __getitem__(self, index: int): - """Return the object of a at index.""" - return self.roles[index] - - -class Credit: - """ - The Credit object contains information for creators credits for an issue. - - Args: - **kwargs (Any): The keyword arguments is used for setting credits data from Metron. - - Attributes: - id (int): The Metron identification number for the credit. - creator (str): The name of the creator. - role (RoleList): A list of roles for the creator. - """ - - def __init__(self, **kwargs): - """Initialize a new Credit.""" - for k, v in kwargs.items(): - setattr(self, k, v) - - -class CreditsSchema(Schema): - """Schema for the Credits.""" - - id = fields.Int() - creator = fields.Str() - role = fields.Nested(RolesSchema, many=True) - - @post_load - def make_object(self, data, **kwargs): - """ - Make the Credit object. - - Args: - data (Any): Data from Metron response. - **kwargs (Any): Any additional keyword arguments. - - Returns: - A :obj:`Credit` object. - """ - return Credit(**data) - - -class Issue: - """ - The Issue object contains information for an issue. - - Args: - **kwargs (Any): The keyword arguments is used for setting creator data from Metron. - - Attributes: - id (int): The Metron identification number for the creator. - publisher (Publisher): The publisher information for the issue. - series (Series): The series information for the issue. - number (str): The issue number. - collection_title (str): The title of a Trade Paperback. - story_titles (list[str]): A list of stories contained in the issue. - cover_date (date): The cover date of the issue. - store_date (date, optional): The date the issue went for sale. - price (decimal): The price of the issue. - rating (Rating): The issue rating. - sku (str): Stock keeping unit for the issue. - upc (str): UPC barcode for the issue. - page_count (int): Number of pages for the issue. - desc (str): Summary description for the issue. - image (url): The url for a cover image associated with the issue. - cover_hash (str): A Perceptual hash string for the cover image. - arcs (list[:obj:`Arc`]): A list of story arcs. - credits (list[:obj:`Credit`]): A list of creator credits for the issue. - characters (list[:obj:`Character`]): A list of characters who appear in the issue. - teams (list[:obj:`Team`]): A list of teams who appear in the issue. - reprints (list[:obj:`Reprint`]): A list of reprinted issue contained in the issue. - issue_name (str): The name used to identified the issue. - variants (list[:obj:`Variant`]): A list of variant covers for the issue. - cv_id (int): Comic Vine ID for the issue. - resource_url (url): The url for the resource. - modified (datetime): The date/time the issue was last changed. - """ - - def __init__(self, **kwargs): - """Initialize a new Issue.""" - for k, v in kwargs.items(): - setattr(self, k, v) - - -class IssueSchema(Schema): - """ - Schema for the Issue API. - - .. versionchanged:: 0.1.6 - - - ``name`` field changed to ``story_titles`` - - ``__str__`` field change to ``issue_name`` - - .. versionchanged:: 0.2.0 - Added ``price`` and ``sku`` fields - - .. versionchanged:: 0.2.2 - Added ``upc`` field - - .. versionchanged:: 0.2.4 - - - Added ``page_count`` field - - Changed ``price`` field from a string to float value. - - .. versionchanged:: 1.0.0 - - - Changed ``price`` field to a decimal type. - - Added ``modified`` field - - .. versionchanged:: 2.1.0 - - - Add ``reprints`` field - - .. versionadded:: 2.2.2 - - - Add ``collection_title`` field - - .. versionchanged:: 2.3.0 - - - Removed ``volume`` field. The series object will have that information. - - .. versionchanged:: 2.3.2 - - - Added ``rating`` field. - - .. versionchanged:: 2.3.3 - - - Added ``resource_url`` field. - - .. versionadded:: 2.4.0 - - - Added ``cv_id`` field. - - .. versionadded:: 2.6.0 - - - Add ``cover_hash`` field. - """ - - id = fields.Int() - publisher = fields.Nested(PublisherSchema) - series = fields.Nested(SeriesSchema) - number = fields.Str() - collection_title = fields.Str(allow_none=True, data_key="title") - story_titles = fields.List(fields.Str(allow_none=True), data_key="name") - cover_date = fields.Date() - store_date = fields.Date(allow_none=True) - price = fields.Decimal(places=2, allow_none=True) - rating = fields.Nested(RatingSchema) - sku = fields.Str() - upc = fields.Str() - page_count = fields.Int(allow_none=True, data_key="page") - desc = fields.Str(allow_none=True) - image = fields.URL(allow_none=True) - cover_hash = fields.Str() - arcs = fields.Nested(ArcSchema, many=True) - credits = fields.Nested(CreditsSchema, many=True) - characters = fields.Nested(CharacterSchema, many=True) - teams = fields.Nested(TeamSchema, many=True) - reprints = fields.Nested(ReprintSchema, many=True) - issue_name = fields.Str(data_key="issue") - variants = fields.Nested(VariantSchema, many=True) - cv_id = fields.Int(allow_none=True) - resource_url = fields.URL() - modified = fields.DateTime() - - class Meta: - """Any unknown fields will be excluded.""" - - unknown = EXCLUDE - datetime = "%Y-%m-%dT%H:%M:%S%z" - - @post_load - def make_object(self, data, **kwargs): - """ - Make the issue object. - - Args: - data (Any): Data from Metron response. - **kwargs (Any): Any additional keyword arguments. - - Returns: - An :obj:`Issue` object. - """ - return Issue(**data) - - -class IssuesList: - """ - The :obj:`IssuesList` object contains a list of issues. - - Attributes: - id (int): The Metron identification number for the issue. - issue (str): The name of the issue. - cover_date (date): The cover date for the issue. - modified (datetime): The date/time the creator was last changed. - - Returns: - A list of issues. - """ - - def __init__(self, response): - """Initialize a new IssuesList.""" - self.issues = [] - - schema = IssueSchema() - for issue_dict in response["results"]: - try: - result = schema.load(issue_dict) - except ValidationError as error: - raise exceptions.ApiError(error) from error - - self.issues.append(result) - - def __iter__(self): - """Return an iterator object.""" - return iter(self.issues) - - def __len__(self): - """Return the length of the object.""" - return len(self.issues) - - def __getitem__(self, index: int): - """Return the object of a at index.""" - return self.issues[index] diff --git a/mokkari/publisher.py b/mokkari/publisher.py deleted file mode 100644 index e56dc96..0000000 --- a/mokkari/publisher.py +++ /dev/null @@ -1,126 +0,0 @@ -""" -Publisher module. - -This module provides the following classes: - -- Publisher -- PublisherSchema -- PublishersList -""" -from marshmallow import EXCLUDE, Schema, ValidationError, fields, post_load - -from mokkari import exceptions - - -class Publisher: - """ - The Publisher object contains information for publishers. - - Args: - **kwargs (Any): The keyword arguments is used for setting publisher data from Metron. - - Attributes: - id (int): The Metron identification number for the publisher. - name (str): The name of the publisher. - founded (int): The year the publisher was founded. - desc (str): A summary description about the publisher. - image (url): The url for an image associated with the publisher. - cv_id (int): Comic Vine ID for the publisher. - resource_url (url): The url for the resource. - modified (datetime): The date/time the publisher was last changed. - """ - - def __init__(self, **kwargs): - """Initialize a new Publisher.""" - for k, v in kwargs.items(): - setattr(self, k, v) - - -class PublisherSchema(Schema): - """ - Schema for the Publisher API. - - .. versionchanged:: 1.0.0 - - - Added ``modified`` field - - .. versionchanged:: 2.0.2 - - - Removed ``wikipedia`` field - - .. versionchanged:: 2.3.3 - - - Added ``resource_url`` field. - - .. versionadded:: 2.4.0 - - - Added ``cv_id`` field. - """ - - id = fields.Int() - name = fields.Str() - founded = fields.Int() - desc = fields.Str() - image = fields.Url(allow_none=True) - cv_id = fields.Int(allow_none=True) - resource_url = fields.URL() - modified = fields.DateTime() - - class Meta: - """Any unknown fields will be excluded.""" - - unknown = EXCLUDE - datetime = "%Y-%m-%dT%H:%M:%S%z" - - @post_load - def make_object(self, data, **kwargs): - """ - Make the Publisher object. - - Args: - data (Any): Data from Metron response. - **kwargs (Any): Any additional keyword arguments. - - Returns: - A :obj:`Publisher` object. - """ - return Publisher(**data) - - -class PublishersList: - """ - The :obj:`PublishersList` object contains a list of publishers. - - Attributes: - id (int): The Metron identification number for the publisher. - name (str): The name of the publisher. - modified (datetime): The date/time the publisher was last changed. - - Returns: - A list of publishers. - """ - - def __init__(self, response): - """Initialize a new PublishersList.""" - self.publishers = [] - - schema = PublisherSchema() - for pub_dict in response["results"]: - try: - result = schema.load(pub_dict) - except ValidationError as error: - raise exceptions.ApiError(error) from error - - self.publishers.append(result) - - def __iter__(self): - """Return an iterator object.""" - return iter(self.publishers) - - def __len__(self): - """Return the length of the object.""" - return len(self.publishers) - - def __getitem__(self, index: int): - """Return the object of a at index.""" - return self.publishers[index] diff --git a/mokkari/rating.py b/mokkari/rating.py deleted file mode 100644 index 2829b5f..0000000 --- a/mokkari/rating.py +++ /dev/null @@ -1,53 +0,0 @@ -""" -Rating module. - -This module provides the following classes: - -- Rating -- RatingSchema -""" -from marshmallow import EXCLUDE, Schema, fields, post_load - - -class Rating: - """ - The rating object contains information for issue's rating. - - Args: - **kwargs (Any): The keyword arguments is used for setting Rating data from Metron. - - Attributes: - id (int): The Metron identification number for the rating. - name (str): The name of the rating. - """ - - def __init__(self, **kwargs): - """Initialize a Rating.""" - for k, v in kwargs.items(): - setattr(self, k, v) - - -class RatingSchema(Schema): - """Schema for Ratings.""" - - id = fields.Int() - name = fields.Str() - - class Meta: - """Any unknown fields will be excluded.""" - - unknown = EXCLUDE - - @post_load - def make_object(self, data, **kwargs): - """ - Make the rating object. - - Args: - data (Any): Data from the Metron response - **kwargs (Any): Any additional keyword arguments. - - Returns: - A :obj: `Rating` object. - """ - return Rating(**data) diff --git a/mokkari/reprint.py b/mokkari/reprint.py deleted file mode 100644 index 8552e54..0000000 --- a/mokkari/reprint.py +++ /dev/null @@ -1,55 +0,0 @@ -""" -Reprint module. - -This module provides the following classes: - -- Reprint -- ReprintSchema -- ReprintsList -""" -from marshmallow import EXCLUDE, Schema, fields, post_load - - -class Reprint: - """ - The Reprint object contains information for a reprint issue. - - Args: - **kwargs (Any): The keyword arguments is used for setting reprint issue - data from Metron. - - Attributes: - id (int): The Metron identification number for the issue. - issue (str): The name of the issue being reprinted. - """ - - def __init__(self, **kwargs): - """Initialize a new Reprint.""" - for k, v in kwargs.items(): - setattr(self, k, v) - - -class ReprintSchema(Schema): - """Schema for the Reprint API.""" - - id = fields.Int() - issue = fields.Str() - - class Meta: - """Any unknown fields will be excluded.""" - - unknown = EXCLUDE - - @post_load - def make_object(self, data, **kwargs): - """ - Make the reprint object. - - Args: - data (Any): Data from Metron response. - **kwargs (Any): Any additional keyword arguments. - - Returns: - A :obj:`Reprint` object. - """ - return Reprint(**data) diff --git a/mokkari/schemas/__init__.py b/mokkari/schemas/__init__.py new file mode 100644 index 0000000..842f8ae --- /dev/null +++ b/mokkari/schemas/__init__.py @@ -0,0 +1,20 @@ +""" +This module provides the following classes. + +- BaseModel +""" + +__all__ = ["BaseModel"] + +from pydantic import BaseModel as PydanticModel + + +class BaseModel( + PydanticModel, + populate_by_name=True, + str_strip_whitespace=True, + validate_assignment=True, + revalidate_instances="always", + extra="ignore", +): + """Base model for mokkari resources.""" diff --git a/mokkari/schemas/arc.py b/mokkari/schemas/arc.py new file mode 100644 index 0000000..575d972 --- /dev/null +++ b/mokkari/schemas/arc.py @@ -0,0 +1,46 @@ +""" +Arc module. + +This module provides the following classes: + +- Arc +- BaseArc +""" + +from datetime import datetime + +from pydantic import HttpUrl + +from mokkari.schemas import BaseModel + + +class BaseArc(BaseModel): + """ + The :obj:`BaseArc` object contains a list of story arcs. + + Attributes: + id (int): The Metron identification number for the story arc. + name (str): The name of the story arc. + modified (datetime): The date/time the story arc was last changed. + """ + + id: int + name: str + modified: datetime + + +class Arc(BaseArc): + """ + The Arc object extends :obj:`BaseArc` providing all information for a story arc. + + Attributes: + desc (str): The description of the story arc. + image (HttpUrl): The url for an image associated with the story arc. + cv_id (int): Comic Vine ID for the story arc. + resource_url (HttpUrl): The url for the resource. + """ + + desc: str | None = None + image: HttpUrl | None = None + cv_id: int | None = None + resource_url: HttpUrl diff --git a/mokkari/schemas/character.py b/mokkari/schemas/character.py new file mode 100644 index 0000000..9b8ba44 --- /dev/null +++ b/mokkari/schemas/character.py @@ -0,0 +1,54 @@ +""" +Character module. + +This module provides the following classes: + +- BaseCharacter +- Character +""" + +from datetime import datetime + +from pydantic import HttpUrl + +from mokkari.schemas import BaseModel +from mokkari.schemas.creator import BaseCreator +from mokkari.schemas.team import BaseTeam + + +class BaseCharacter(BaseModel): + """ + The :obj:`BaseCharacter` object contains a list of characters. + + Attributes: + id (int): The Metron identification number for the character. + name (str): The name of the character. + modified (datetime): The date/time the team was last changed. + """ + + id: int + name: str + modified: datetime + + +class Character(BaseCharacter): + """ + The Character object extends :obj:`BaseCharacter` providing all information for a character. + + Attributes: + alias (list[str]): The alias of the character. + desc (str): The description of the character. + image (url): The url for an image associated with the character. + creators (list[:obj:`Generic`]): A list of creators for the character. + teams (list[:obj:`Generic`]): A list of teams for the character. + cv_id (int): Comic Vine ID for the character. + resource_url (url): The url for the resource. + """ + + alias: list[str] | None = None + desc: str | None = None + image: HttpUrl | None = None + creators: list[BaseCreator] = [] + teams: list[BaseTeam] = [] + cv_id: int | None = None + resource_url: HttpUrl diff --git a/mokkari/schemas/creator.py b/mokkari/schemas/creator.py new file mode 100644 index 0000000..c6aa807 --- /dev/null +++ b/mokkari/schemas/creator.py @@ -0,0 +1,52 @@ +""" +Creator module. + +This module provides the following classes: + +- BaseCreator +- Creator +""" + +from datetime import date, datetime + +from pydantic import HttpUrl, PastDate + +from mokkari.schemas import BaseModel + + +class BaseCreator(BaseModel): + """ + The :obj:`BaseCreator` object contains a list of creators. + + Attributes: + id (int): The Metron identification number for the creator. + name (str): The name of the creator. + modified (datetime): The date/time the team was last changed. + """ + + id: int + name: str + modified: datetime + + +class Creator(BaseCreator): + """ + The Creator object extends :obj:`BaseCreator` providing all information for a creator. + + Attributes: + birth (date): The date of birth for the creator. + death (date): The date of death for the creator. + desc (str): The description of the creator. + image (HttpUrl): The url for an image associated with the creator. + alias (list[str]): The alias of the creator. + cv_id (int): Comic Vine ID for the creator. + resource_url (HttpUrl): The url for the resource. + """ + + birth: PastDate | None = None + death: date | None = None + desc: str | None = None + image: HttpUrl | None = None + alias: list[str] | None = None + cv_id: int | None = None + resource_url: HttpUrl diff --git a/mokkari/schemas/generic.py b/mokkari/schemas/generic.py new file mode 100644 index 0000000..84bf533 --- /dev/null +++ b/mokkari/schemas/generic.py @@ -0,0 +1,22 @@ +""" +Generic module. + +This module provides the following classes: + +- GenericItem +""" + +from mokkari.schemas import BaseModel + + +class GenericItem(BaseModel): + """ + The :obj:`GenericItem` object contains basic information for various resources. + + Attributes: + id (int): The id of the item. + name (str): The name of the item. + """ + + id: int + name: str diff --git a/mokkari/schemas/issue.py b/mokkari/schemas/issue.py new file mode 100644 index 0000000..5e45d13 --- /dev/null +++ b/mokkari/schemas/issue.py @@ -0,0 +1,161 @@ +""" +Issue module. + +This module provides the following classes: + +- Credit +- BasicSeries +- IssueSeries +- CommonIssue +- BaseIssue +- Issue +""" + +from datetime import date, datetime +from decimal import Decimal + +from pydantic import Field, HttpUrl + +from mokkari.schemas import BaseModel +from mokkari.schemas.arc import BaseArc +from mokkari.schemas.character import BaseCharacter +from mokkari.schemas.generic import GenericItem +from mokkari.schemas.reprint import Reprint +from mokkari.schemas.team import BaseTeam +from mokkari.schemas.variant import Variant + + +class Credit(BaseModel): + """ + The :obj:`Credit` object contains information about an issue creator credits. + + Attributes: + id (int): The Metron identification number for the issue credit. + creator (str): The name of the creator for the issue credit. + role (list[GenericItem]): The role of the creator for the issue. + """ + + id: int + creator: str + role: list[GenericItem] = [] + + +class BasicSeries(BaseModel): + """ + The :obj:`BasicSeries` object contains basic series information for an issue. + + Attributes: + name (str): The name of the series. + volume (int): The volume of the series. + year_began (int): The year the series began. + """ + + name: str + volume: int + year_began: int + + +class IssueSeries(BaseModel): + """ + The :obj:`AssociatedSeries` object contains more detailed series information. + + Attributes: + id (int): The Metron identification number for series. + name (str): The name of the series. + sort_name (str): The sort name of the series. + volume (int): The volume of the series. + series_type (GenericItem): The type of series. + genres (list[Generic]): The genres of the series. + """ + + id: int + name: str + sort_name: str + volume: int + series_type: GenericItem + genres: list[GenericItem] = [] + + +class CommonIssue(BaseModel): + """ + The :obj:`CommonIssue` object contains common information for BaseIssue and Issue objects. + + Attributes: + id (int): The Metron identification number for the associated series. + number (str): The number of the issue. + cover_date (date): The cover date of the issue. + image (HttpUrl): The url of the cover image for the issue. + cover_hash (str): The hash of the cover image for the issue. + modified (datetime): The modified date of the issue. + """ + + id: int + number: str + cover_date: date + image: HttpUrl | None = None + cover_hash: str | None = None + modified: datetime + + +class BaseIssue(CommonIssue): + """ + The :obj:`BaseIssue` object extends the :obj:`CommonIssue` object. + + Attributes: + issue_name (str): The name of the issue. + series (BasicSeries): The series for the issue. + """ + + issue_name: str = Field(alias="issue") + series: BasicSeries + + +class Issue(CommonIssue): + """ + The :obj:`Issue` object extends the :obj:`CommonIssue` object with all the info for an issue. + + Attributes: + publisher (GenericPublisher): The publisher of the issue. + series (IssueSeries): The series for the issue. + collection_title (str): The collection title of the issue. Normally only used with TPB. + story_titles (list[str]): A list of stories contained in the issue. + cover_date (date): The cover date of the issue. + store_date (date): The store date of the issue. + price (Decimal): The price of the issue. + rating (GenericItem): The rating of the issue. + sku (str): The sku of the issue. + isbn (str): The isbn of the issue. + upc (str): The upc of the issue. + page_count (int): The number of pages of the issue. + desc (str): The description of the issue. + arcs (list[BaseArc]): A list of story arcs for the issue. + credits (list[Credit]): A list of creator credits for the issue. + characters (list[BaseCharacter]): A list of characters for the issue. + teams (list[BaseTeam]): A list of teams for the issue. + reprints (list[Reprint]): A list of issues printed, + variants (list[Variant]): A list of variant covers for the issue. + cv_id (int): The Comic Vine ID of the issue. + resource_url (HttpUrl): The URL of the issue. + """ + + publisher: GenericItem + series: IssueSeries + collection_title: str = Field(alias="title") + story_titles: list[str] = Field(alias="name") + cover_date: date + store_date: date | None = None + price: Decimal | None = None + rating: GenericItem + sku: str | None = None + isbn: str | None = None + upc: str | None = None + page_count: int | None = Field(alias="page", default=None) + desc: str | None = None + arcs: list[BaseArc] = [] + credits: list[Credit] = [] + characters: list[BaseCharacter] = [] + teams: list[BaseTeam] = [] + reprints: list[Reprint] = [] + variants: list[Variant] = [] + cv_id: int | None = None + resource_url: HttpUrl diff --git a/mokkari/schemas/publisher.py b/mokkari/schemas/publisher.py new file mode 100644 index 0000000..a96d748 --- /dev/null +++ b/mokkari/schemas/publisher.py @@ -0,0 +1,48 @@ +""" +Publisher module. + +This module provides the following classes: + +- BasePublisher +- Publisher +""" + +from datetime import datetime + +from pydantic import HttpUrl + +from mokkari.schemas import BaseModel + + +class BasePublisher(BaseModel): + """ + The :obj:`BasePublisher` object contains a list of publishers. + + Attributes: + id (int): The Metron identification number for the publisher. + name (str): The name of the publisher. + modified (datetime): The date/time the team was last changed. + """ + + id: int + name: str + modified: datetime + + +class Publisher(BasePublisher): + """ + The Publisher object extends :obj:`BasePublisher` providing all information for a publisher. + + Attributes: + founded (int): The year the publisher was founded. + desc (str): The description of the publisher. + image (HttpUrl): The url for an image associated with the publisher. + cv_id (int): Comic Vine ID for the publisher. + resource_url (HttpUrl): The url for the resource. + """ + + founded: int | None = None + desc: str | None = None + image: HttpUrl | None = None + cv_id: int | None = None + resource_url: HttpUrl diff --git a/mokkari/schemas/reprint.py b/mokkari/schemas/reprint.py new file mode 100644 index 0000000..52d394e --- /dev/null +++ b/mokkari/schemas/reprint.py @@ -0,0 +1,22 @@ +""" +Reprint module. + +This module provides the following classes: + +- Reprint +""" + +from mokkari.schemas import BaseModel + + +class Reprint(BaseModel): + """ + The :obj:`Reprint` object contains a list of reprinted issues. + + Attributes: + id (int): The Metron identification number for the team. + issue (str): The name of the issue. + """ + + id: int + issue: str diff --git a/mokkari/schemas/series.py b/mokkari/schemas/series.py new file mode 100644 index 0000000..be25454 --- /dev/null +++ b/mokkari/schemas/series.py @@ -0,0 +1,89 @@ +""" +Series module. + +This module provides the following classes: + +- AssociatedSeries +- CommonSeries +- BaseSeries +- Series +""" + +from datetime import datetime + +from pydantic import Field, HttpUrl + +from mokkari.schemas import BaseModel +from mokkari.schemas.generic import GenericItem + + +class AssociatedSeries(BaseModel): + """ + The :obj:`AssociatedSeries` object contains information about an associated series. + + Attributes: + id (int): The Metron identification number for the associated series. + name (str): The name of the associated series. + """ + + id: int + name: str = Field(alias="series") + + +class CommonSeries(BaseModel): + """ + The :obj:`CommonSeries` contains fields common to :obj:`BaseSeries` & :obj:`Series` objects. + + Attributes: + id (int): The Metron identification number for the series. + year_began (int): The year the series began. + issue_count (int): The number of issues. + modified (datetime): The date/time the series was last changed. + """ + + id: int + year_began: int + issue_count: int + modified: datetime + + +class BaseSeries(CommonSeries): + """ + The :obj:`BaseSeries` object contains extend the :obj:`CommonSeries`. + + Attributes: + display_name (str): The name of the series. + """ + + display_name: str = Field(alias="series") + + +class Series(CommonSeries): + """ + :obj:`Series` extends :obj:`CommonSeries` and contains all information about a series. + + Attributes: + name (str): The name of the series. + sort_name (str): The name used to determine the sort order for a series. + volume (int): The volume of the series. + series_type (GenericItem): The type of series. + publisher (GenericItem): The publisher of the series. + year_end (int): The year the series ended. + desc (str): The description of the series. + genres list(Generic): The genres of the series. + associated list(AssociatedSeries): The series associated with the series. + cv_id (int): The Comic Vine ID of the series. + resource_url (HttpUrl): The URL of the series + """ + + name: str + sort_name: str + volume: int + series_type: GenericItem + publisher: GenericItem + year_end: int | None = None + desc: str | None = None + genres: list[GenericItem] = [] + associated: list[AssociatedSeries] = [] + cv_id: int | None = None + resource_url: HttpUrl diff --git a/mokkari/schemas/team.py b/mokkari/schemas/team.py new file mode 100644 index 0000000..edcaa2d --- /dev/null +++ b/mokkari/schemas/team.py @@ -0,0 +1,52 @@ +""" +Team module. + +This module provides the following classes: + +- BaseTeam +- Team +""" + +from datetime import datetime + +from pydantic import HttpUrl + +from mokkari.schemas import BaseModel +from mokkari.schemas.generic import GenericItem + + +class BaseTeam(BaseModel): + """ + The :obj:`BaseTeam` object contains a list of teams. + + Attributes: + id (int): The Metron identification number for the team. + name (str): The name of the team. + modified (datetime): The date/time the team was last changed. + + Returns: + A list of teams. + """ + + id: int + name: str + modified: datetime + + +class Team(BaseTeam): + """ + The Team object extends the :obj:`BaseTeam` by containing all information for a team. + + Attributes: + desc (str): The description of the team. + image (url): The url for an image associated with the team. + creators (list[:obj:`Generic`]): A list of creators for the team. + cv_id (int): Comic Vine ID for the team. + resource_url (url): The url for the resource. + """ + + desc: str | None = None + image: HttpUrl | None = None + creators: list[GenericItem] = [] + cv_id: int | None = None + resource_url: HttpUrl diff --git a/mokkari/schemas/variant.py b/mokkari/schemas/variant.py new file mode 100644 index 0000000..56822b1 --- /dev/null +++ b/mokkari/schemas/variant.py @@ -0,0 +1,28 @@ +""" +Variant module. + +This module provides the following classes: + +- Variant +""" + +from pydantic import HttpUrl + +from mokkari.schemas import BaseModel + + +class Variant(BaseModel): + """ + The :obj:`Variant` object contains information about a variant cover.. + + Attributes: + name (str): The name of the variant cover. + sku (str): The sku of the variant cover. + upc (str): The upc of the variant cover. + image (HttpUrl): The url for the variant cover. + """ + + name: str | None = None + sku: str | None = None + upc: str | None = None + image: HttpUrl diff --git a/mokkari/series.py b/mokkari/series.py deleted file mode 100644 index a86b608..0000000 --- a/mokkari/series.py +++ /dev/null @@ -1,277 +0,0 @@ -""" -Series module. - -This module provides the following classes: - -- SeriesType -- SeriesTypeSchema -- Series -- SeriesSchema -- SeriesList -""" -from marshmallow import EXCLUDE, Schema, ValidationError, fields, post_load - -from mokkari import exceptions -from mokkari.genre import GenreSchema -from mokkari.publisher import PublisherSchema - - -class SeriesType: - """ - The SeriesType object contains information for type of series. - - Args: - **kwargs (Any): The keyword arguments is used for setting series type data from Metron. - - Attributes: - id (int): The Metron identification number for the series type. - name (str): The name of the series type. - """ - - def __init__(self, **kwargs): - """Initialize a new SeriesType.""" - for k, v in kwargs.items(): - setattr(self, k, v) - - -class SeriesTypeSchema(Schema): - """Schema for the Series Type.""" - - id = fields.Int() - name = fields.Str() - - @post_load - def make_object(self, data, **kwargs): - """ - Make the SeriesType object. - - Args: - data (Any): Data from Metron response. - **kwargs (Any): Any additional keyword arguments. - - Returns: - A :obj:`SeriesType` object. - """ - return SeriesType(**data) - - -class SeriesTypeList: - """ - The :obj:`SeriesTypeList` object contains a list of series types. - - Attributes: - id (int): The Metron identification number for the series type. - name (str): The name of the series type. - - Returns: - A list of series types. - """ - - def __init__(self, response): - """Initialize a new SeriesTypeList.""" - self.series = [] - - schema = SeriesTypeSchema() - for series_type_dict in response["results"]: - try: - result = schema.load(series_type_dict) - except ValidationError as error: - raise exceptions.ApiError(error) from error - - self.series.append(result) - - def __iter__(self): - """Return an iterator object.""" - return iter(self.series) - - def __len__(self): - """Return the length of the object.""" - return len(self.series) - - def __getitem__(self, index: int): - """Return the object of a at index.""" - return self.series[index] - - -class AssociatedSeries: - """ - The AssociateSeries objects contains any associated series to the primary series. - - Args: - **kwargs (Any): The keyword arguments is used for setting associated series data - from Metron. - - Attributes: - id (int): The Metron identification number for the associated series. - name (str): The name of the associated series. - """ - - def __init__(self, **kwargs): - """Initialize a new AssociatedSeries.""" - for k, v in kwargs.items(): - setattr(self, k, v) - - -class AssociatedSeriesSchema(Schema): - """Schema for Associated Series.""" - - id = fields.Int() - name = fields.Str(data_key="series") - - @post_load - def make_object(self, data, **kwargs): - """ - Make the AssociatedSeries object. - - Args: - data (Any): Data from Metron response. - **kwargs (Any): Any additional keyword arguments. - - Returns: - An :obj:`AssociatedSeries` object. - """ - return AssociatedSeries(**data) - - -class Series: - """ - The Series object contains information for comic series. - - Args: - **kwargs (Any): The keyword arguments is used for setting series data from Metron. - - Attributes: - id (int): The Metron identification number for the series. - name (str): The name of the series. - sort_name (str): The name used to sort a series. - volume (int): The volume number for a series. - publisher (Publisher): The publisher of the series. - year_began (int): The cover year the series began. - year_end (int, optional): The cover year in which the series ended. - desc (str): A summary description of the series. - issue_count (int): The number of issues the series contains. - display_name (str): The display name for the series. - genres (list[Genre]): A list of genres for the series. - associated (list[AssociatedSeries]): A list of of series associated with the - primary series. - cv_id (int): Comic Vine ID for the series. - resource_url (url): The url for the resource. - modified (datetime): The date/time the series was last changed. - """ - - def __init__(self, **kwargs): - """Initialize a new Series.""" - for k, v in kwargs.items(): - setattr(self, k, v) - - -class SeriesSchema(Schema): - """ - Schema for the Series API. - - .. versionchanged:: 1.0.0 - - - Added ``modified`` field - - .. versionchanged:: 1.0.5 - - - Added ``associated`` field - - .. versionchanged:: 2.0.0 - - - Changed ``publisher`` to a nested field. - - .. versionchanged:: 2.0.3 - - Changed ``series_type`` to a string field. - - .. versionchanged:: 2.0.4 - - Reverted ``series_type`` back to a nested field. - - .. versionchanged:: 2.1.1 - Added ``genres`` fields - - .. versionchanged:: 2.3.3 - - - Added ``resource_url`` field. - - .. versionadded:: 2.4.0 - - - Added ``cv_id`` field. - """ - - id = fields.Int() - name = fields.Str() - sort_name = fields.Str() - volume = fields.Int() - series_type = fields.Nested(SeriesTypeSchema) - publisher = fields.Nested(PublisherSchema) - year_began = fields.Int() - year_end = fields.Int(allow_none=True) - desc = fields.Str() - issue_count = fields.Int() - image = fields.Url(allow_none=True) - display_name = fields.Str(data_key="series") - genres = fields.Nested(GenreSchema, many=True) - associated = fields.Nested(AssociatedSeriesSchema, many=True) - cv_id = fields.Int(allow_none=True) - resource_url = fields.URL() - modified = fields.DateTime() - - class Meta: - """Any unknown fields will be excluded.""" - - unknown = EXCLUDE - datetime = "%Y-%m-%dT%H:%M:%S%z" - - @post_load - def make_object(self, data, **kwargs): - """ - Make the Series object. - - Args: - data (Any): Data from Metron response. - **kwargs (Any): Any additional keyword arguments. - - Returns: - A :obj:`Series` object. - """ - return Series(**data) - - -class SeriesList: - """ - The :obj:`SeriesList` object contains a list of series. - - Attributes: - id (int): The Metron identification number for the series. - series (str): The name of the series. - modified (datetime): The date/time the series was last changed. - - Returns: - A list of series. - """ - - def __init__(self, response): - """Initialize a new SeriesList.""" - self.series = [] - - schema = SeriesSchema() - for series_dict in response["results"]: - try: - result = schema.load(series_dict) - except ValidationError as error: - raise exceptions.ApiError(error) from error - - self.series.append(result) - - def __iter__(self): - """Return an iterator object.""" - return iter(self.series) - - def __len__(self): - """Return the length of the object.""" - return len(self.series) - - def __getitem__(self, index: int): - """Return the object of a at index.""" - return self.series[index] diff --git a/mokkari/session.py b/mokkari/session.py index e953211..d4fe0e1 100644 --- a/mokkari/session.py +++ b/mokkari/session.py @@ -5,28 +5,28 @@ - Session """ + import platform from collections import OrderedDict -from typing import Any, Dict, List, Optional, Union +from typing import Any, Optional, Union from urllib.parse import urlencode import requests -from marshmallow import ValidationError +from pydantic import TypeAdapter, ValidationError from ratelimit import limits, sleep_and_retry from requests.adapters import HTTPAdapter from urllib3 import Retry # Alias these modules to prevent namespace collision with methods. -from mokkari import __version__ -from mokkari import arc as arcs -from mokkari import character as characters -from mokkari import creator as creators -from mokkari import exceptions -from mokkari import issue as issues -from mokkari import publisher as publishers -from mokkari import series as ser -from mokkari import sqlite_cache -from mokkari import team as teams +from mokkari import __version__, exceptions, sqlite_cache +from mokkari.schemas.arc import Arc, BaseArc +from mokkari.schemas.character import BaseCharacter, Character +from mokkari.schemas.creator import BaseCreator, Creator +from mokkari.schemas.generic import GenericItem +from mokkari.schemas.issue import BaseIssue, Issue +from mokkari.schemas.publisher import BasePublisher, Publisher +from mokkari.schemas.series import BaseSeries, Series +from mokkari.schemas.team import BaseTeam, Team ONE_MINUTE = 60 @@ -61,9 +61,9 @@ def __init__( def _call( self, - endpoint: List[Union[str, int]], - params: Optional[Dict[str, Union[str, int]]] = None, - ) -> Dict[str, Any]: + endpoint: list[Union[str, int]], + params: Optional[dict[str, Union[str, int]]] = None, + ) -> dict[str, Any]: """ Make request for api endpoints. @@ -95,7 +95,7 @@ def _call( return data - def creator(self, _id: int) -> creators.Creator: + def creator(self, _id: int) -> Creator: """ Request data for a creator based on its ``_id``. @@ -108,16 +108,17 @@ def creator(self, _id: int) -> creators.Creator: Raises: ApiError: If there is a problem with the API request. """ + resp = self._call(["creator", _id]) + adaptor = TypeAdapter(Creator) try: - result = creators.CreatorSchema().load(self._call(["creator", _id])) + result = adaptor.validate_python(resp) except ValidationError as error: raise exceptions.ApiError(error) from error - return result def creators_list( - self, params: Optional[Dict[str, Union[str, int]]] = None - ) -> creators.CreatorsList: + self, params: Optional[dict[str, Union[str, int]]] = None + ) -> list[BaseCreator]: """ Request a list of creators. @@ -127,10 +128,15 @@ def creators_list( Returns: A :obj:`CreatorsList` object. """ - res = self._get_results(["creator"], params) - return creators.CreatorsList(res) + resp = self._get_results(["creator"], params) + adaptor = TypeAdapter(list[BaseCreator]) + try: + result = adaptor.validate_python(resp["results"]) + except ValidationError as error: + raise exceptions.ApiError(error) from error + return result - def character(self, _id: int) -> characters.Character: + def character(self, _id: int) -> Character: """ Request data for a character based on its ``_id``. @@ -143,16 +149,17 @@ def character(self, _id: int) -> characters.Character: Raises: ApiError: If there is a problem with the API request. """ + resp = self._call(["character", _id]) + adaptor = TypeAdapter(Character) try: - result = characters.CharacterSchema().load(self._call(["character", _id])) + result = adaptor.validate_python(resp) except ValidationError as error: raise exceptions.ApiError(error) from error - return result def characters_list( - self, params: Optional[Dict[str, Union[str, int]]] = None - ) -> characters.CharactersList: + self, params: Optional[dict[str, Union[str, int]]] = None + ) -> list[BaseCharacter]: """ Request a list of characters. @@ -162,10 +169,15 @@ def characters_list( Returns: A :class:`CharactersList` object. """ - res = self._get_results(["character"], params) - return characters.CharactersList(res) + resp = self._get_results(["character"], params) + adaptor = TypeAdapter(list[BaseCharacter]) + try: + result = adaptor.validate_python(resp["results"]) + except ValidationError as error: + raise exceptions.ApiError(error) from error + return result - def character_issues_list(self, _id: int) -> List[issues.Issue]: + def character_issues_list(self, _id: int) -> list[BaseIssue]: """ Request a list of issues that a character appears in. @@ -177,10 +189,15 @@ def character_issues_list(self, _id: int) -> List[issues.Issue]: Returns: A list of :class:`Issue` objects. """ - result = self._get_results(["character", _id, "issue_list"]) - return issues.IssuesList(result) + resp = self._get_results(["character", _id, "issue_list"]) + adaptor = TypeAdapter(list[BaseIssue]) + try: + result = adaptor.validate_python(resp["results"]) + except ValidationError as err: + raise exceptions.ApiError(err) from err + return result - def publisher(self, _id: int) -> publishers.Publisher: + def publisher(self, _id: int) -> Publisher: """ Request data for a publisher based on its ``_id``. @@ -193,16 +210,17 @@ def publisher(self, _id: int) -> publishers.Publisher: Raises: ApiError: If there is a problem with the API request. """ + resp = self._call(["publisher", _id]) + adaptor = TypeAdapter(Publisher) try: - result = publishers.PublisherSchema().load(self._call(["publisher", _id])) - except ValidationError as error: - raise exceptions.ApiError(error) from error - + result = adaptor.validate_python(resp) + except ValidationError as err: + raise exceptions.ApiError(err) from err return result def publishers_list( - self, params: Optional[Dict[str, Union[str, int]]] = None - ) -> publishers.PublishersList: + self, params: Optional[dict[str, Union[str, int]]] = None + ) -> list[BasePublisher]: """ Request a list of publishers. @@ -212,10 +230,15 @@ def publishers_list( Returns: A :class:`PublishersList` object. """ - res = self._get_results(["publisher"], params) - return publishers.PublishersList(res) + resp = self._get_results(["publisher"], params) + adapter = TypeAdapter(list[BasePublisher]) + try: + result = adapter.validate_python(resp["results"]) + except ValidationError as err: + raise exceptions.ApiError(err) from err + return result - def team(self, _id: int) -> teams.Team: + def team(self, _id: int) -> Team: """ Request data for a team based on its ``_id``. @@ -228,16 +251,15 @@ def team(self, _id: int) -> teams.Team: Raises: ApiError: If there is a problem with the API request. """ + resp = self._call(["team", _id]) + adaptor = TypeAdapter(Team) try: - result = teams.TeamSchema().load(self._call(["team", _id])) + result = adaptor.validate_python(resp) except ValidationError as error: raise exceptions.ApiError(error) from error - return result - def teams_list( - self, params: Optional[Dict[str, Union[str, int]]] = None - ) -> teams.TeamsList: + def teams_list(self, params: Optional[dict[str, Union[str, int]]] = None) -> list[BaseTeam]: """ Request a list of teams. @@ -247,10 +269,15 @@ def teams_list( Returns: A :class:`TeamsList` object. """ - res = self._get_results(["team"], params) - return teams.TeamsList(res) + resp = self._get_results(["team"], params) + adapter = TypeAdapter(list[BaseTeam]) + try: + result = adapter.validate_python(resp["results"]) + except ValidationError as err: + raise exceptions.ApiError(err) from err + return result - def team_issues_list(self, _id: int) -> List[issues.Issue]: + def team_issues_list(self, _id: int) -> list[BaseIssue]: """ Request a list of issues that a team appears in. @@ -262,10 +289,15 @@ def team_issues_list(self, _id: int) -> List[issues.Issue]: Returns: A list of :class:`Issue` objects. """ - result = self._get_results(["team", _id, "issue_list"]) - return issues.IssuesList(result) + resp = self._get_results(["team", _id, "issue_list"]) + adapter = TypeAdapter(list[BaseIssue]) + try: + result = adapter.validate_python(resp["results"]) + except ValidationError as err: + raise exceptions.ApiError(err) from err + return result - def arc(self, _id: int) -> arcs.Arc: + def arc(self, _id: int) -> Arc: """ Request data for a story arc based on its ``_id``. @@ -278,14 +310,15 @@ def arc(self, _id: int) -> arcs.Arc: Raises: ApiError: If there is a problem with the API request. """ + resp = self._call(["arc", _id]) + adaptor = TypeAdapter(Arc) try: - result = arcs.ArcSchema().load(self._call(["arc", _id])) - except ValidationError as error: - raise exceptions.ApiError(error) from error - + result = adaptor.validate_python(resp) + except ValidationError as err: + raise exceptions.ApiError(err) from err return result - def arcs_list(self, params: Optional[Dict[str, Union[str, int]]] = None) -> arcs.ArcsList: + def arcs_list(self, params: Optional[dict[str, Union[str, int]]] = None) -> list[BaseArc]: """ Request a list of story arcs. @@ -295,10 +328,15 @@ def arcs_list(self, params: Optional[Dict[str, Union[str, int]]] = None) -> arcs Returns: A :class:`ArcsList` object. """ - res = self._get_results(["arc"], params) - return arcs.ArcsList(res) + resp = self._get_results(["arc"], params) + adapter = TypeAdapter(list[BaseArc]) + try: + result = adapter.validate_python(resp["results"]) + except ValidationError as err: + raise exceptions.ApiError(err) from err + return result - def arc_issues_list(self, _id: int) -> List[issues.Issue]: + def arc_issues_list(self, _id: int) -> list[BaseIssue]: """ Request a list of issues for a story arc. @@ -308,10 +346,15 @@ def arc_issues_list(self, _id: int) -> List[issues.Issue]: Returns: A list of :class:`Issue` objects. """ - result = self._get_results(["arc", _id, "issue_list"]) - return issues.IssuesList(result) + resp = self._get_results(["arc", _id, "issue_list"]) + adaptor = TypeAdapter(list[BaseIssue]) + try: + result = adaptor.validate_python(resp["results"]) + except ValidationError as err: + raise exceptions.ApiError(err) from err + return result - def series(self, _id: int) -> ser.Series: + def series(self, _id: int) -> Series: """ Request data for a series based on its ``_id``. @@ -324,16 +367,15 @@ def series(self, _id: int) -> ser.Series: Raises: ApiError: If there is a problem with the API request. """ + resp = self._call(["series", _id]) + adaptor = TypeAdapter(Series) try: - result = ser.SeriesSchema().load(self._call(["series", _id])) - except ValidationError as error: - raise exceptions.ApiError(error) from error - + result = adaptor.validate_python(resp) + except ValidationError as err: + raise exceptions.ApiError(err) from err return result - def series_list( - self, params: Optional[Dict[str, Union[str, int]]] = None - ) -> ser.SeriesList: + def series_list(self, params: Optional[dict[str, Union[str, int]]] = None) -> list[BaseSeries]: """ Request a list of series. @@ -343,12 +385,17 @@ def series_list( Returns: A :class:`SeriesList` object. """ - res = self._get_results(["series"], params) - return ser.SeriesList(res) + resp = self._get_results(["series"], params) + adaptor = TypeAdapter(list[BaseSeries]) + try: + result = adaptor.validate_python(resp["results"]) + except ValidationError as err: + raise exceptions.ApiError(err) from err + return result def series_type_list( - self, params: Optional[Dict[str, Union[str, int]]] = None - ) -> ser.SeriesTypeList: + self, params: Optional[dict[str, Union[str, int]]] = None + ) -> list[GenericItem]: """ Request a list of series types. @@ -362,10 +409,15 @@ def series_type_list( Returns: A :class:`SeriesTypeList` object. """ - res = self._get_results(["series_type"], params) - return ser.SeriesTypeList(res) + resp = self._get_results(["series_type"], params) + adaptor = TypeAdapter(list[GenericItem]) + try: + result = adaptor.validate_python(resp["results"]) + except ValidationError as err: + raise exceptions.ApiError(err) from err + return result - def issue(self, _id: int) -> issues.Issue: + def issue(self, _id: int) -> Issue: """ Request data for an issue based on it's ``_id``. @@ -378,16 +430,15 @@ def issue(self, _id: int) -> issues.Issue: Raises: ApiError: If there is a problem with the API request. """ + resp = self._call(["issue", _id]) + adaptor = TypeAdapter(Issue) try: - result = issues.IssueSchema().load(self._call(["issue", _id])) + result = adaptor.validate_python(resp) except ValidationError as error: raise exceptions.ApiError(error) from error - return result - def issues_list( - self, params: Optional[Dict[str, Union[str, int]]] = None - ) -> issues.IssuesList: + def issues_list(self, params: Optional[dict[str, Union[str, int]]] = None) -> list[BaseIssue]: """ Request a list of issues. @@ -397,12 +448,15 @@ def issues_list( Returns: A :class:`IssuesList` object. """ - res = self._get_results(["issue"], params) - return issues.IssuesList(res) + resp = self._get_results(["issue"], params) + adaptor = TypeAdapter(list[BaseIssue]) + try: + result = adaptor.validate_python(resp["results"]) + except ValidationError as err: + raise exceptions.ApiError(err) from err + return result - def role_list( - self, params: Optional[Dict[str, Union[str, int]]] = None - ) -> issues.RoleList: + def role_list(self, params: Optional[dict[str, Union[str, int]]] = None) -> list[GenericItem]: """ Request a list of creator roles. @@ -413,14 +467,19 @@ def role_list( A :class:`RoleList` object. """ - res = self._get_results(["role"], params) - return issues.RoleList(res) + resp = self._get_results(["role"], params) + adaptor = TypeAdapter(list[GenericItem]) + try: + result = adaptor.validate_python(resp["results"]) + except ValidationError as err: + raise exceptions.ApiError(err) from err + return result def _get_results( self, - endpoint: List[Union[str, int]], - params: Optional[Dict[str, Union[str, int]]] = None, - ) -> Dict[str, Any]: + endpoint: list[Union[str, int]], + params: Optional[dict[str, Union[str, int]]] = None, + ) -> dict[str, Any]: if params is None: params = {} @@ -429,7 +488,7 @@ def _get_results( result = self._retrieve_all_results(result) return result - def _retrieve_all_results(self, data: Dict[str, Any]) -> Dict[str, Any]: + def _retrieve_all_results(self, data: dict[str, Any]) -> dict[str, Any]: has_next_page = True next_page = data["next"] @@ -456,9 +515,7 @@ def _retrieve_all_results(self, data: Dict[str, Any]) -> Dict[str, Any]: @sleep_and_retry @limits(calls=25, period=ONE_MINUTE) - def _request_data( - self, url: str, params: Optional[Dict[str, Union[str, int]]] = None - ) -> Any: + def _request_data(self, url: str, params: Optional[dict[str, Union[str, int]]] = None) -> Any: if params is None: params = {} diff --git a/mokkari/sqlite_cache.py b/mokkari/sqlite_cache.py index be18ab8..e444e0e 100644 --- a/mokkari/sqlite_cache.py +++ b/mokkari/sqlite_cache.py @@ -5,6 +5,7 @@ - SqliteCache """ + import json import sqlite3 from datetime import datetime, timedelta @@ -21,9 +22,7 @@ class SqliteCache: before they expire. """ - def __init__( - self, db_name: str = "mokkari_cache.db", expire: Optional[int] = None - ) -> None: + def __init__(self, db_name: str = "mokkari_cache.db", expire: Optional[int] = None) -> None: """Initialize a new SqliteCache.""" self.expire = expire self.con = sqlite3.connect(db_name) diff --git a/mokkari/team.py b/mokkari/team.py deleted file mode 100644 index 7b6ec58..0000000 --- a/mokkari/team.py +++ /dev/null @@ -1,125 +0,0 @@ -""" -Team module. - -This module provides the following classes: - -- Team -- TeamSchema -- TeamsList -""" -from marshmallow import EXCLUDE, Schema, ValidationError, fields, post_load - -from mokkari import creator, exceptions - - -class Team: - """ - The Team object contains information for teams. - - Args: - **kwargs (Any): The keyword arguments is used for setting team data from Metron. - - Attributes: - id (int): The Metron identification number for the team. - name (str): The name of the team. - desc (str): The description of the team. - image (url): The url for an image associated with the team. - creators (list[:obj:`Creator`]): A list of creators for the team. - cv_id (int): Comic Vine ID for the team. - resource_url (url): The url for the resource. - modified (datetime): The date/time the team was last changed. - """ - - def __init__(self, **kwargs): - """Initialize a new Team.""" - for k, v in kwargs.items(): - setattr(self, k, v) - - -class TeamSchema(Schema): - """ - Schema for the Team API. - - .. versionchanged:: 1.0.0 - - - Added ``modified`` field - - .. versionchanged:: 2.0.2 - - Removed ``wikipedia`` field - - .. versionchanged:: 2.3.3 - - - Added ``resource_url`` field. - - .. versionadded:: 2.4.0 - - - Added ``cv_id`` field. - """ - - id = fields.Int() - name = fields.Str() - desc = fields.Str() - image = fields.Url(allow_none=True) - creators = fields.Nested(creator.CreatorSchema, many=True) - cv_id = fields.Int(allow_none=True) - resource_url = fields.URL() - modified = fields.DateTime() - - class Meta: - """Any unknown fields will be excluded.""" - - unknown = EXCLUDE - datetime = "%Y-%m-%dT%H:%M:%S%z" - - @post_load - def make_object(self, data, **kwargs): - """ - Make the Team object. - - Args: - data (Any): Data from Metron response. - **kwargs (Any): Any additional keyword arguments. - - Returns: - A :obj:`Team` object - """ - return Team(**data) - - -class TeamsList: - """ - The :obj:`TeamsList` object contains a list of teams. - - Attributes: - id (int): The Metron identification number for the team. - name (str): The name of the team. - modified (datetime): The date/time the team was last changed. - - Returns: - A list of teams. - """ - - def __init__(self, response): - """Initialize a new TeamsList.""" - self.teams = [] - - schema = TeamSchema() - for team_dict in response["results"]: - try: - result = schema.load(team_dict) - except ValidationError as error: - raise exceptions.ApiError(error) from error - - self.teams.append(result) - - def __iter__(self): - """Return an iterator object.""" - return iter(self.teams) - - def __len__(self): - """Return the length of the object.""" - return len(self.teams) - - def __getitem__(self, index: int): - """Return the object of a at index.""" - return self.teams[index] diff --git a/mokkari/variant.py b/mokkari/variant.py deleted file mode 100644 index 4067852..0000000 --- a/mokkari/variant.py +++ /dev/null @@ -1,55 +0,0 @@ -""" -Variant module. - -This module provides the following classes: - -- Variant -- VariantSchema -""" -from marshmallow import EXCLUDE, Schema, fields, post_load - - -class Variant: - """ - The Variant object contains information for variant issues. - - Args: - **kwargs (Any): The keyword arguments is used for setting arc data from Metron. - - Attributes: - name (int): The name of the variant cover. - sku (str): The stock keeping unit for the variant cover. - image (url): The url for an image for the variant cover. - """ - - def __init__(self, **kwargs): - """Initialize a new Variant.""" - for k, v in kwargs.items(): - setattr(self, k, v) - - -class VariantSchema(Schema): - """Schema for the Variant API.""" - - name = fields.Str() - sku = fields.Str() - image = fields.Url() - - class Meta: - """Any unknown fields will be excluded.""" - - unknown = EXCLUDE - - @post_load - def make_object(self, data, **kwargs): - """ - Make the Variant object. - - Args: - data (Any): Data from Metron response. - **kwargs (Any): Any additional keyword arguments. - - Returns: - An :obj:`Variant` object - """ - return Variant(**data) diff --git a/poetry.lock b/poetry.lock index 9cbd5e8..67c7edd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -11,6 +11,17 @@ files = [ {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, ] +[[package]] +name = "annotated-types" +version = "0.6.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, + {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, +] + [[package]] name = "aspy-refactor-imports" version = "3.0.2" @@ -252,63 +263,63 @@ files = [ [[package]] name = "coverage" -version = "7.4.0" +version = "7.4.1" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36b0ea8ab20d6a7564e89cb6135920bc9188fb5f1f7152e94e8300b7b189441a"}, - {file = "coverage-7.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0676cd0ba581e514b7f726495ea75aba3eb20899d824636c6f59b0ed2f88c471"}, - {file = "coverage-7.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0ca5c71a5a1765a0f8f88022c52b6b8be740e512980362f7fdbb03725a0d6b9"}, - {file = "coverage-7.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7c97726520f784239f6c62506bc70e48d01ae71e9da128259d61ca5e9788516"}, - {file = "coverage-7.4.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:815ac2d0f3398a14286dc2cea223a6f338109f9ecf39a71160cd1628786bc6f5"}, - {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:80b5ee39b7f0131ebec7968baa9b2309eddb35b8403d1869e08f024efd883566"}, - {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5b2ccb7548a0b65974860a78c9ffe1173cfb5877460e5a229238d985565574ae"}, - {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:995ea5c48c4ebfd898eacb098164b3cc826ba273b3049e4a889658548e321b43"}, - {file = "coverage-7.4.0-cp310-cp310-win32.whl", hash = "sha256:79287fd95585ed36e83182794a57a46aeae0b64ca53929d1176db56aacc83451"}, - {file = "coverage-7.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:5b14b4f8760006bfdb6e08667af7bc2d8d9bfdb648351915315ea17645347137"}, - {file = "coverage-7.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:04387a4a6ecb330c1878907ce0dc04078ea72a869263e53c72a1ba5bbdf380ca"}, - {file = "coverage-7.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea81d8f9691bb53f4fb4db603203029643caffc82bf998ab5b59ca05560f4c06"}, - {file = "coverage-7.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74775198b702868ec2d058cb92720a3c5a9177296f75bd97317c787daf711505"}, - {file = "coverage-7.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76f03940f9973bfaee8cfba70ac991825611b9aac047e5c80d499a44079ec0bc"}, - {file = "coverage-7.4.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:485e9f897cf4856a65a57c7f6ea3dc0d4e6c076c87311d4bc003f82cfe199d25"}, - {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6ae8c9d301207e6856865867d762a4b6fd379c714fcc0607a84b92ee63feff70"}, - {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bf477c355274a72435ceb140dc42de0dc1e1e0bf6e97195be30487d8eaaf1a09"}, - {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:83c2dda2666fe32332f8e87481eed056c8b4d163fe18ecc690b02802d36a4d26"}, - {file = "coverage-7.4.0-cp311-cp311-win32.whl", hash = "sha256:697d1317e5290a313ef0d369650cfee1a114abb6021fa239ca12b4849ebbd614"}, - {file = "coverage-7.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:26776ff6c711d9d835557ee453082025d871e30b3fd6c27fcef14733f67f0590"}, - {file = "coverage-7.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:13eaf476ec3e883fe3e5fe3707caeb88268a06284484a3daf8250259ef1ba143"}, - {file = "coverage-7.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846f52f46e212affb5bcf131c952fb4075b55aae6b61adc9856222df89cbe3e2"}, - {file = "coverage-7.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26f66da8695719ccf90e794ed567a1549bb2644a706b41e9f6eae6816b398c4a"}, - {file = "coverage-7.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:164fdcc3246c69a6526a59b744b62e303039a81e42cfbbdc171c91a8cc2f9446"}, - {file = "coverage-7.4.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:316543f71025a6565677d84bc4df2114e9b6a615aa39fb165d697dba06a54af9"}, - {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bb1de682da0b824411e00a0d4da5a784ec6496b6850fdf8c865c1d68c0e318dd"}, - {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:0e8d06778e8fbffccfe96331a3946237f87b1e1d359d7fbe8b06b96c95a5407a"}, - {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a56de34db7b7ff77056a37aedded01b2b98b508227d2d0979d373a9b5d353daa"}, - {file = "coverage-7.4.0-cp312-cp312-win32.whl", hash = "sha256:51456e6fa099a8d9d91497202d9563a320513fcf59f33991b0661a4a6f2ad450"}, - {file = "coverage-7.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd3c1e4cb2ff0083758f09be0f77402e1bdf704adb7f89108007300a6da587d0"}, - {file = "coverage-7.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e9d1bf53c4c8de58d22e0e956a79a5b37f754ed1ffdbf1a260d9dcfa2d8a325e"}, - {file = "coverage-7.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:109f5985182b6b81fe33323ab4707011875198c41964f014579cf82cebf2bb85"}, - {file = "coverage-7.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc9d4bc55de8003663ec94c2f215d12d42ceea128da8f0f4036235a119c88ac"}, - {file = "coverage-7.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc6d65b21c219ec2072c1293c505cf36e4e913a3f936d80028993dd73c7906b1"}, - {file = "coverage-7.4.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a10a4920def78bbfff4eff8a05c51be03e42f1c3735be42d851f199144897ba"}, - {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b8e99f06160602bc64da35158bb76c73522a4010f0649be44a4e167ff8555952"}, - {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7d360587e64d006402b7116623cebf9d48893329ef035278969fa3bbf75b697e"}, - {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:29f3abe810930311c0b5d1a7140f6395369c3db1be68345638c33eec07535105"}, - {file = "coverage-7.4.0-cp38-cp38-win32.whl", hash = "sha256:5040148f4ec43644702e7b16ca864c5314ccb8ee0751ef617d49aa0e2d6bf4f2"}, - {file = "coverage-7.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:9864463c1c2f9cb3b5db2cf1ff475eed2f0b4285c2aaf4d357b69959941aa555"}, - {file = "coverage-7.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:936d38794044b26c99d3dd004d8af0035ac535b92090f7f2bb5aa9c8e2f5cd42"}, - {file = "coverage-7.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:799c8f873794a08cdf216aa5d0531c6a3747793b70c53f70e98259720a6fe2d7"}, - {file = "coverage-7.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7defbb9737274023e2d7af02cac77043c86ce88a907c58f42b580a97d5bcca9"}, - {file = "coverage-7.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a1526d265743fb49363974b7aa8d5899ff64ee07df47dd8d3e37dcc0818f09ed"}, - {file = "coverage-7.4.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf635a52fc1ea401baf88843ae8708591aa4adff875e5c23220de43b1ccf575c"}, - {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:756ded44f47f330666843b5781be126ab57bb57c22adbb07d83f6b519783b870"}, - {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0eb3c2f32dabe3a4aaf6441dde94f35687224dfd7eb2a7f47f3fd9428e421058"}, - {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bfd5db349d15c08311702611f3dccbef4b4e2ec148fcc636cf8739519b4a5c0f"}, - {file = "coverage-7.4.0-cp39-cp39-win32.whl", hash = "sha256:53d7d9158ee03956e0eadac38dfa1ec8068431ef8058fe6447043db1fb40d932"}, - {file = "coverage-7.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:cfd2a8b6b0d8e66e944d47cdec2f47c48fef2ba2f2dff5a9a75757f64172857e"}, - {file = "coverage-7.4.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:c530833afc4707fe48524a44844493f36d8727f04dcce91fb978c414a8556cc6"}, - {file = "coverage-7.4.0.tar.gz", hash = "sha256:707c0f58cb1712b8809ece32b68996ee1e609f71bd14615bd8f87a1293cb610e"}, + {file = "coverage-7.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7"}, + {file = "coverage-7.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3277f5fa7483c927fe3a7b017b39351610265308f5267ac6d4c2b64cc1d8d25"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dceb61d40cbfcf45f51e59933c784a50846dc03211054bd76b421a713dcdf19"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b"}, + {file = "coverage-7.4.1-cp310-cp310-win32.whl", hash = "sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016"}, + {file = "coverage-7.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018"}, + {file = "coverage-7.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295"}, + {file = "coverage-7.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18e961aa13b6d47f758cc5879383d27b5b3f3dcd9ce8cdbfdc2571fe86feb4dd"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfd1e1b9f0898817babf840b77ce9fe655ecbe8b1b327983df485b30df8cc011"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6"}, + {file = "coverage-7.4.1-cp311-cp311-win32.whl", hash = "sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5"}, + {file = "coverage-7.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968"}, + {file = "coverage-7.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581"}, + {file = "coverage-7.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0860a348bf7004c812c8368d1fc7f77fe8e4c095d661a579196a9533778e156"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc"}, + {file = "coverage-7.4.1-cp312-cp312-win32.whl", hash = "sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74"}, + {file = "coverage-7.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448"}, + {file = "coverage-7.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218"}, + {file = "coverage-7.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b094116f0b6155e36a304ff912f89bbb5067157aff5f94060ff20bbabdc8da06"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2f5968608b1fe2a1d00d01ad1017ee27efd99b3437e08b83ded9b7af3f6f766"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad"}, + {file = "coverage-7.4.1-cp38-cp38-win32.whl", hash = "sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042"}, + {file = "coverage-7.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d"}, + {file = "coverage-7.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54"}, + {file = "coverage-7.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb2c2688ed93b027eb0d26aa188ada34acb22dceea256d76390eea135083950"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12c923757de24e4e2110cf8832d83a886a4cf215c6e61ed506006872b43a6d1"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35"}, + {file = "coverage-7.4.1-cp39-cp39-win32.whl", hash = "sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c"}, + {file = "coverage-7.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a"}, + {file = "coverage-7.4.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166"}, + {file = "coverage-7.4.1.tar.gz", hash = "sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04"}, ] [package.dependencies] @@ -486,25 +497,6 @@ files = [ {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, ] -[[package]] -name = "importlib-metadata" -version = "7.0.1" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "importlib_metadata-7.0.1-py3-none-any.whl", hash = "sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e"}, - {file = "importlib_metadata-7.0.1.tar.gz", hash = "sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc"}, -] - -[package.dependencies] -zipp = ">=0.5" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] -perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] - [[package]] name = "iniconfig" version = "2.0.0" @@ -616,26 +608,6 @@ files = [ {file = "MarkupSafe-2.1.4.tar.gz", hash = "sha256:3aae9af4cac263007fd6309c64c6ab4506dd2b79382d9d19a1994f9240b8db4f"}, ] -[[package]] -name = "marshmallow" -version = "3.20.2" -description = "A lightweight library for converting complex datatypes to and from native Python datatypes." -optional = false -python-versions = ">=3.8" -files = [ - {file = "marshmallow-3.20.2-py3-none-any.whl", hash = "sha256:c21d4b98fee747c130e6bc8f45c4b3199ea66bc00c12ee1f639f0aeca034d5e9"}, - {file = "marshmallow-3.20.2.tar.gz", hash = "sha256:4c1daff273513dc5eb24b219a8035559dc573c8f322558ef85f5438ddd1236dd"}, -] - -[package.dependencies] -packaging = ">=17.0" - -[package.extras] -dev = ["pre-commit (>=2.4,<4.0)", "pytest", "pytz", "simplejson", "tox"] -docs = ["alabaster (==0.7.15)", "autodocsumm (==0.2.12)", "sphinx (==7.2.6)", "sphinx-issues (==3.0.1)", "sphinx-version-warning (==1.1.2)"] -lint = ["pre-commit (>=2.4,<4.0)"] -tests = ["pytest", "pytz", "simplejson"] - [[package]] name = "mccabe" version = "0.7.0" @@ -767,6 +739,142 @@ files = [ {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, ] +[[package]] +name = "pydantic" +version = "2.5.3" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic-2.5.3-py3-none-any.whl", hash = "sha256:d0caf5954bee831b6bfe7e338c32b9e30c85dfe080c843680783ac2b631673b4"}, + {file = "pydantic-2.5.3.tar.gz", hash = "sha256:b3ef57c62535b0941697cce638c08900d87fcb67e29cfa99e8a68f747f393f7a"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.14.6" +typing-extensions = ">=4.6.1" + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.14.6" +description = "" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic_core-2.14.6-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:72f9a942d739f09cd42fffe5dc759928217649f070056f03c70df14f5770acf9"}, + {file = "pydantic_core-2.14.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6a31d98c0d69776c2576dda4b77b8e0c69ad08e8b539c25c7d0ca0dc19a50d6c"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5aa90562bc079c6c290f0512b21768967f9968e4cfea84ea4ff5af5d917016e4"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:370ffecb5316ed23b667d99ce4debe53ea664b99cc37bfa2af47bc769056d534"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f85f3843bdb1fe80e8c206fe6eed7a1caeae897e496542cee499c374a85c6e08"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9862bf828112e19685b76ca499b379338fd4c5c269d897e218b2ae8fcb80139d"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:036137b5ad0cb0004c75b579445a1efccd072387a36c7f217bb8efd1afbe5245"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92879bce89f91f4b2416eba4429c7b5ca22c45ef4a499c39f0c5c69257522c7c"}, + {file = "pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0c08de15d50fa190d577e8591f0329a643eeaed696d7771760295998aca6bc66"}, + {file = "pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:36099c69f6b14fc2c49d7996cbf4f87ec4f0e66d1c74aa05228583225a07b590"}, + {file = "pydantic_core-2.14.6-cp310-none-win32.whl", hash = "sha256:7be719e4d2ae6c314f72844ba9d69e38dff342bc360379f7c8537c48e23034b7"}, + {file = "pydantic_core-2.14.6-cp310-none-win_amd64.whl", hash = "sha256:36fa402dcdc8ea7f1b0ddcf0df4254cc6b2e08f8cd80e7010d4c4ae6e86b2a87"}, + {file = "pydantic_core-2.14.6-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:dea7fcd62915fb150cdc373212141a30037e11b761fbced340e9db3379b892d4"}, + {file = "pydantic_core-2.14.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffff855100bc066ff2cd3aa4a60bc9534661816b110f0243e59503ec2df38421"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b027c86c66b8627eb90e57aee1f526df77dc6d8b354ec498be9a757d513b92b"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:00b1087dabcee0b0ffd104f9f53d7d3eaddfaa314cdd6726143af6bc713aa27e"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:75ec284328b60a4e91010c1acade0c30584f28a1f345bc8f72fe8b9e46ec6a96"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e1f4744eea1501404b20b0ac059ff7e3f96a97d3e3f48ce27a139e053bb370b"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2602177668f89b38b9f84b7b3435d0a72511ddef45dc14446811759b82235a1"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c8edaea3089bf908dd27da8f5d9e395c5b4dc092dbcce9b65e7156099b4b937"}, + {file = "pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:478e9e7b360dfec451daafe286998d4a1eeaecf6d69c427b834ae771cad4b622"}, + {file = "pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b6ca36c12a5120bad343eef193cc0122928c5c7466121da7c20f41160ba00ba2"}, + {file = "pydantic_core-2.14.6-cp311-none-win32.whl", hash = "sha256:2b8719037e570639e6b665a4050add43134d80b687288ba3ade18b22bbb29dd2"}, + {file = "pydantic_core-2.14.6-cp311-none-win_amd64.whl", hash = "sha256:78ee52ecc088c61cce32b2d30a826f929e1708f7b9247dc3b921aec367dc1b23"}, + {file = "pydantic_core-2.14.6-cp311-none-win_arm64.whl", hash = "sha256:a19b794f8fe6569472ff77602437ec4430f9b2b9ec7a1105cfd2232f9ba355e6"}, + {file = "pydantic_core-2.14.6-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:667aa2eac9cd0700af1ddb38b7b1ef246d8cf94c85637cbb03d7757ca4c3fdec"}, + {file = "pydantic_core-2.14.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cdee837710ef6b56ebd20245b83799fce40b265b3b406e51e8ccc5b85b9099b7"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c5bcf3414367e29f83fd66f7de64509a8fd2368b1edf4351e862910727d3e51"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a92ae76f75d1915806b77cf459811e772d8f71fd1e4339c99750f0e7f6324f"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a983cca5ed1dd9a35e9e42ebf9f278d344603bfcb174ff99a5815f953925140a"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cb92f9061657287eded380d7dc455bbf115430b3aa4741bdc662d02977e7d0af"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ace1e220b078c8e48e82c081e35002038657e4b37d403ce940fa679e57113b"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef633add81832f4b56d3b4c9408b43d530dfca29e68fb1b797dcb861a2c734cd"}, + {file = "pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7e90d6cc4aad2cc1f5e16ed56e46cebf4877c62403a311af20459c15da76fd91"}, + {file = "pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e8a5ac97ea521d7bde7621d86c30e86b798cdecd985723c4ed737a2aa9e77d0c"}, + {file = "pydantic_core-2.14.6-cp312-none-win32.whl", hash = "sha256:f27207e8ca3e5e021e2402ba942e5b4c629718e665c81b8b306f3c8b1ddbb786"}, + {file = "pydantic_core-2.14.6-cp312-none-win_amd64.whl", hash = "sha256:b3e5fe4538001bb82e2295b8d2a39356a84694c97cb73a566dc36328b9f83b40"}, + {file = "pydantic_core-2.14.6-cp312-none-win_arm64.whl", hash = "sha256:64634ccf9d671c6be242a664a33c4acf12882670b09b3f163cd00a24cffbd74e"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:24368e31be2c88bd69340fbfe741b405302993242ccb476c5c3ff48aeee1afe0"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:e33b0834f1cf779aa839975f9d8755a7c2420510c0fa1e9fa0497de77cd35d2c"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6af4b3f52cc65f8a0bc8b1cd9676f8c21ef3e9132f21fed250f6958bd7223bed"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15687d7d7f40333bd8266f3814c591c2e2cd263fa2116e314f60d82086e353a"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:095b707bb287bfd534044166ab767bec70a9bba3175dcdc3371782175c14e43c"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94fc0e6621e07d1e91c44e016cc0b189b48db053061cc22d6298a611de8071bb"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ce830e480f6774608dedfd4a90c42aac4a7af0a711f1b52f807130c2e434c06"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a306cdd2ad3a7d795d8e617a58c3a2ed0f76c8496fb7621b6cd514eb1532cae8"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:2f5fa187bde8524b1e37ba894db13aadd64faa884657473b03a019f625cee9a8"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:438027a975cc213a47c5d70672e0d29776082155cfae540c4e225716586be75e"}, + {file = "pydantic_core-2.14.6-cp37-none-win32.whl", hash = "sha256:f96ae96a060a8072ceff4cfde89d261837b4294a4f28b84a28765470d502ccc6"}, + {file = "pydantic_core-2.14.6-cp37-none-win_amd64.whl", hash = "sha256:e646c0e282e960345314f42f2cea5e0b5f56938c093541ea6dbf11aec2862391"}, + {file = "pydantic_core-2.14.6-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:db453f2da3f59a348f514cfbfeb042393b68720787bbef2b4c6068ea362c8149"}, + {file = "pydantic_core-2.14.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3860c62057acd95cc84044e758e47b18dcd8871a328ebc8ccdefd18b0d26a21b"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36026d8f99c58d7044413e1b819a67ca0e0b8ebe0f25e775e6c3d1fabb3c38fb"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8ed1af8692bd8d2a29d702f1a2e6065416d76897d726e45a1775b1444f5928a7"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:314ccc4264ce7d854941231cf71b592e30d8d368a71e50197c905874feacc8a8"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:982487f8931067a32e72d40ab6b47b1628a9c5d344be7f1a4e668fb462d2da42"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dbe357bc4ddda078f79d2a36fc1dd0494a7f2fad83a0a684465b6f24b46fe80"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2f6ffc6701a0eb28648c845f4945a194dc7ab3c651f535b81793251e1185ac3d"}, + {file = "pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7f5025db12fc6de7bc1104d826d5aee1d172f9ba6ca936bf6474c2148ac336c1"}, + {file = "pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dab03ed811ed1c71d700ed08bde8431cf429bbe59e423394f0f4055f1ca0ea60"}, + {file = "pydantic_core-2.14.6-cp38-none-win32.whl", hash = "sha256:dfcbebdb3c4b6f739a91769aea5ed615023f3c88cb70df812849aef634c25fbe"}, + {file = "pydantic_core-2.14.6-cp38-none-win_amd64.whl", hash = "sha256:99b14dbea2fdb563d8b5a57c9badfcd72083f6006caf8e126b491519c7d64ca8"}, + {file = "pydantic_core-2.14.6-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:4ce8299b481bcb68e5c82002b96e411796b844d72b3e92a3fbedfe8e19813eab"}, + {file = "pydantic_core-2.14.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b9a9d92f10772d2a181b5ca339dee066ab7d1c9a34ae2421b2a52556e719756f"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd9e98b408384989ea4ab60206b8e100d8687da18b5c813c11e92fd8212a98e0"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4f86f1f318e56f5cbb282fe61eb84767aee743ebe32c7c0834690ebea50c0a6b"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86ce5fcfc3accf3a07a729779d0b86c5d0309a4764c897d86c11089be61da160"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dcf1978be02153c6a31692d4fbcc2a3f1db9da36039ead23173bc256ee3b91b"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eedf97be7bc3dbc8addcef4142f4b4164066df0c6f36397ae4aaed3eb187d8ab"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d5f916acf8afbcab6bacbb376ba7dc61f845367901ecd5e328fc4d4aef2fcab0"}, + {file = "pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8a14c192c1d724c3acbfb3f10a958c55a2638391319ce8078cb36c02283959b9"}, + {file = "pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0348b1dc6b76041516e8a854ff95b21c55f5a411c3297d2ca52f5528e49d8411"}, + {file = "pydantic_core-2.14.6-cp39-none-win32.whl", hash = "sha256:de2a0645a923ba57c5527497daf8ec5df69c6eadf869e9cd46e86349146e5975"}, + {file = "pydantic_core-2.14.6-cp39-none-win_amd64.whl", hash = "sha256:aca48506a9c20f68ee61c87f2008f81f8ee99f8d7f0104bff3c47e2d148f89d9"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d5c28525c19f5bb1e09511669bb57353d22b94cf8b65f3a8d141c389a55dec95"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:78d0768ee59baa3de0f4adac9e3748b4b1fffc52143caebddfd5ea2961595277"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b93785eadaef932e4fe9c6e12ba67beb1b3f1e5495631419c784ab87e975670"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a874f21f87c485310944b2b2734cd6d318765bcbb7515eead33af9641816506e"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89f4477d915ea43b4ceea6756f63f0288941b6443a2b28c69004fe07fde0d0d"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:172de779e2a153d36ee690dbc49c6db568d7b33b18dc56b69a7514aecbcf380d"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:dfcebb950aa7e667ec226a442722134539e77c575f6cfaa423f24371bb8d2e94"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:55a23dcd98c858c0db44fc5c04fc7ed81c4b4d33c653a7c45ddaebf6563a2f66"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:4241204e4b36ab5ae466ecec5c4c16527a054c69f99bba20f6f75232a6a534e2"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e574de99d735b3fc8364cba9912c2bec2da78775eba95cbb225ef7dda6acea24"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1302a54f87b5cd8528e4d6d1bf2133b6aa7c6122ff8e9dc5220fbc1e07bffebd"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8e81e4b55930e5ffab4a68db1af431629cf2e4066dbdbfef65348b8ab804ea8"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c99462ffc538717b3e60151dfaf91125f637e801f5ab008f81c402f1dff0cd0f"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e4cf2d5829f6963a5483ec01578ee76d329eb5caf330ecd05b3edd697e7d768a"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:cf10b7d58ae4a1f07fccbf4a0a956d705356fea05fb4c70608bb6fa81d103cda"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:399ac0891c284fa8eb998bcfa323f2234858f5d2efca3950ae58c8f88830f145"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c6a5c79b28003543db3ba67d1df336f253a87d3112dac3a51b94f7d48e4c0e1"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:599c87d79cab2a6a2a9df4aefe0455e61e7d2aeede2f8577c1b7c0aec643ee8e"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43e166ad47ba900f2542a80d83f9fc65fe99eb63ceec4debec160ae729824052"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a0b5db001b98e1c649dd55afa928e75aa4087e587b9524a4992316fa23c9fba"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:747265448cb57a9f37572a488a57d873fd96bf51e5bb7edb52cfb37124516da4"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7ebe3416785f65c28f4f9441e916bfc8a54179c8dea73c23023f7086fa601c5d"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:86c963186ca5e50d5c8287b1d1c9d3f8f024cbe343d048c5bd282aec2d8641f2"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e0641b506486f0b4cd1500a2a65740243e8670a2549bb02bc4556a83af84ae03"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71d72ca5eaaa8d38c8df16b7deb1a2da4f650c41b58bb142f3fb75d5ad4a611f"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27e524624eace5c59af499cd97dc18bb201dc6a7a2da24bfc66ef151c69a5f2a"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3dde6cac75e0b0902778978d3b1646ca9f438654395a362cb21d9ad34b24acf"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:00646784f6cd993b1e1c0e7b0fdcbccc375d539db95555477771c27555e3c556"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:23598acb8ccaa3d1d875ef3b35cb6376535095e9405d91a3d57a8c7db5d29341"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7f41533d7e3cf9520065f610b41ac1c76bc2161415955fbcead4981b22c7611e"}, + {file = "pydantic_core-2.14.6.tar.gz", hash = "sha256:1fd0c1d395372843fba13a51c28e3bb9d59bd7aebfeb17358ffaaa1e4dbbe948"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + [[package]] name = "pydocstyle" version = "6.3.0" @@ -1094,7 +1202,6 @@ babel = ">=2.9" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} docutils = ">=0.18.1,<0.21" imagesize = ">=1.3" -importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} Jinja2 = ">=3.0" packaging = ">=21.0" Pygments = ">=2.14" @@ -1350,22 +1457,7 @@ platformdirs = ">=3.9.1,<5" docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] -[[package]] -name = "zipp" -version = "3.17.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.8" -files = [ - {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, - {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] - [metadata] lock-version = "2.0" -python-versions = "^3.9" -content-hash = "37ffd05a8c29201fb78aa030f16030e15cc0b07f9a7c1d105240a7df7652a53c" +python-versions = "^3.10" +content-hash = "7e1e331b3701d973569ce02d1e29cdf1294d80c88d53e0c94bf981baf9b09e88" diff --git a/pyproject.toml b/pyproject.toml index c05c921..b4ee993 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,9 +18,9 @@ classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "Natural Language :: English", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Topic :: Internet", "Operating System :: MacOS :: MacOS X", "Operating System :: POSIX", @@ -31,10 +31,10 @@ classifiers = [ keywords=["comics", "comic", "metadata"] [tool.poetry.dependencies] -python = "^3.9" -marshmallow = "^3.13.0" +python = "^3.10" requests = "^2.26.0" ratelimit = "^2.2.1" +pydantic = "^2.5.3" [tool.poetry.group.dev.dependencies] pytest = "^7.4.2" @@ -64,16 +64,16 @@ requires = ["poetry-core>=1.1.0"] build-backend = "poetry.core.masonry.api" [tool.black] -line-length = 95 -target-version = ['py39'] +line-length = 100 +target-version = ['py310'] [tool.isort] profile = "black" multi_line_output = 3 -line_length = 95 +line_length = 100 default_section = "THIRDPARTY" known_first_party = [] -known_third_party = ["marshmallow", "pytest", "ratelimit", "requests", "requests_mock", "urllib3"] +known_third_party = ["pydantic", "pytest", "ratelimit", "requests", "requests_mock", "urllib3"] [tool.poetry.urls] "Homepage" = "https://github.com/Metron-Project/mokkari" diff --git a/tests/conftest.py b/tests/conftest.py index 9227cdb..a03e60a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,7 @@ This module contains pytest fixtures. """ + import os import pytest diff --git a/tests/test_arcs.py b/tests/test_arcs.py index 8591a34..c6491ee 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -3,46 +3,48 @@ This module contains tests for Arc objects. """ + import json from datetime import date, datetime, timedelta, timezone import pytest import requests_mock -from mokkari import arc, exceptions +from mokkari import exceptions from mokkari.session import Session def test_known_arc(talker: Session) -> None: """Test for known arcs.""" - heroes: arc.ArcSchema = talker.arc(1) - assert heroes.name == "Heroes In Crisis" + witching = talker.arc(2) + assert witching.name == "The Witching Hour" assert ( - heroes.image - == "https://static.metron.cloud/media/arc/2018/11/12/heroes-in-crisis.jpeg" + witching.image.__str__() + == "https://static.metron.cloud/media/arc/2018/11/13/witching-hour.jpg" ) - assert heroes.modified == datetime( + assert witching.modified == datetime( 2019, 6, 23, 15, 13, 19, - 456634, + 507207, tzinfo=timezone(timedelta(days=-1, seconds=72000), "-0400"), ) - assert heroes.resource_url == "https://metron.cloud/arc/heroes-crisis/" + assert witching.resource_url.__str__() == "https://metron.cloud/arc/witching-hour/" -def test_arcslist(talker: Session) -> None: +def test_arcs_list(talker: Session) -> None: """Test for ArcsList.""" arcs = talker.arcs_list() arc_iter = iter(arcs) assert next(arc_iter).name == "'Til Death Do Us..." + assert next(arc_iter).name == "(She) Drunk History" assert next(arc_iter).name == "1+2 = Fantastic Three" - assert next(arc_iter).name == "1883" - assert len(arcs) == 873 - assert arcs[2].name == "1883" + assert next(arc_iter).name == "1602" + assert len(arcs) == 1418 + assert arcs[3].name == "1602" def test_arc_issue_list(talker: Session) -> None: @@ -66,12 +68,6 @@ def test_bad_arc(talker: Session) -> None: talker.arc(-8) -def test_bad_response_data() -> None: - """Test for bad arc response.""" - with pytest.raises(exceptions.ApiError): - arc.ArcsList({"results": {"name": 1}}) - - def test_bad_arc_validate(talker: Session) -> None: """Test data with invalid data.""" # Change the 'name' field to an int, when it should be a string. diff --git a/tests/test_cache.py b/tests/test_cache.py index 3df4164..07ccd00 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -3,6 +3,7 @@ This module contains tests for SqliteCache objects. """ + import pytest import requests_mock diff --git a/tests/test_characters.py b/tests/test_characters.py index 8af16db..909dd1c 100644 --- a/tests/test_characters.py +++ b/tests/test_characters.py @@ -3,22 +3,36 @@ This module contains tests for Character objects. """ + import json from datetime import date, datetime, timedelta, timezone import pytest import requests_mock -from mokkari import character, exceptions +from mokkari import exceptions +from mokkari.schemas.character import Character from mokkari.session import Session +def test_no_alias(talker: Session) -> None: + """Test for no alias attribute.""" + character = talker.character(23843) + assert isinstance(character, Character) + assert character.name == "4-D Man" + assert character.alias is None + assert character.desc == "An alien from the 4th Dimension." + assert character.creators == [] + assert character.teams == [] + assert character.cv_id == 137999 + + def test_known_character(talker: Session) -> None: """Test for a known character.""" - black_bolt: character.CharacterSchema = talker.character(1) + black_bolt = talker.character(1) assert black_bolt.name == "Black Bolt" assert ( - black_bolt.image + black_bolt.image.__str__() == "https://static.metron.cloud/media/character/2018/11/11/black-bolt.jpg" ) assert len(black_bolt.creators) == 2 @@ -33,24 +47,24 @@ def test_known_character(talker: Session) -> None: 90281, tzinfo=timezone(timedelta(days=-1, seconds=72000), "-0400"), ) - assert black_bolt.resource_url == "https://metron.cloud/character/black-bolt/" + assert black_bolt.resource_url.__str__() == "https://metron.cloud/character/black-bolt/" -def test_characterlist(talker: Session) -> None: +def test_character_list(talker: Session) -> None: """Test the CharactersList.""" - character = talker.characters_list({"name": "man"}) - character_iter = iter(character) + chars = talker.characters_list({"name": "man"}) + character_iter = iter(chars) assert next(character_iter).name == "'Mazing Man" assert next(character_iter).name == "3-D Man (Chandler)" assert next(character_iter).name == "3-D Man (Garrett)" - assert len(character) == 576 - assert character[2].name == "3-D Man (Garrett)" + assert len(chars) == 865 + assert chars[2].name == "3-D Man (Garrett)" def test_character_issue_list(talker: Session) -> None: """Test for getting an issue list for an arc.""" issues = talker.character_issues_list(1) - assert len(issues) == 344 + assert len(issues) == 400 assert issues[0].id == 258 assert issues[0].issue_name == "Fantastic Four (1961) #45" assert issues[0].cover_date == date(1965, 12, 1) @@ -67,12 +81,6 @@ def test_bad_character(talker: Session) -> None: talker.character(-1) -def test_bad_response_data() -> None: - """Test for a bad character response.""" - with pytest.raises(exceptions.ApiError): - character.CharactersList({"results": {"name": 1}}) - - def test_bad_character_validate(talker: Session) -> None: """Test data with invalid data.""" # Change the 'name' field to an int, when it should be a string. diff --git a/tests/test_creator.py b/tests/test_creator.py index 10d3e69..096c82e 100644 --- a/tests/test_creator.py +++ b/tests/test_creator.py @@ -3,24 +3,25 @@ This module contains tests for Creator objects. """ + import json from datetime import date, datetime, timedelta, timezone import pytest import requests_mock -from mokkari import creator, exceptions +from mokkari import exceptions from mokkari.session import Session def test_known_creator(talker: Session) -> None: """Test for a known creator.""" - jack: creator.CreatorSchema = talker.creator(3) + jack = talker.creator(3) assert jack.name == "Jack Kirby" assert jack.birth == date(1917, 8, 28) assert jack.death == date(1994, 2, 6) assert ( - jack.image + jack.image.__str__() == "https://static.metron.cloud/media/creator/2018/11/11/432124-Jack_Kirby01.jpg" ) assert jack.modified == datetime( @@ -33,23 +34,23 @@ def test_known_creator(talker: Session) -> None: 311024, tzinfo=timezone(timedelta(days=-1, seconds=72000), "-0400"), ) - assert jack.resource_url == "https://metron.cloud/creator/jack-kirby/" + assert jack.resource_url.__str__() == "https://metron.cloud/creator/jack-kirby/" -def test_comiclist(talker: Session) -> None: +def test_comic_list(talker: Session) -> None: """Test the CreatorsList.""" creators = talker.creators_list({"name": "man"}) creator_iter = iter(creators) assert next(creator_iter).name == "A. J. Lieberman" + assert next(creator_iter).name == "Abel Laxamana" assert next(creator_iter).name == "Adam Freeman" assert next(creator_iter).name == "Adam Schlagman" - assert next(creator_iter).name == "Al Sulman" - assert len(creators) == 213 - assert creators[3].name == "Al Sulman" + assert len(creators) == 338 + assert creators[3].name == "Adam Schlagman" def test_bad_creator(talker: Session) -> None: - """Test for a non-existant creator.""" + """Test for a non-existent creator.""" with requests_mock.Mocker() as r: r.get( "https://metron.cloud/api/creator/-1/", @@ -59,12 +60,6 @@ def test_bad_creator(talker: Session) -> None: talker.creator(-1) -def test_bad_response_data() -> None: - """Test for a bad creator response.""" - with pytest.raises(exceptions.ApiError): - creator.CreatorsList({"results": {"name": 1}}) - - def test_bad_creator_validate(talker: Session) -> None: """Test data with invalid data.""" # Change the 'name' field to an int, when it should be a string. diff --git a/tests/test_init.py b/tests/test_init.py index 084b995..f4a48e5 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -3,6 +3,7 @@ This module contains tests for project init. """ + import pytest from mokkari import api, exceptions, session diff --git a/tests/test_issues.py b/tests/test_issues.py index 659784b..c2821e7 100644 --- a/tests/test_issues.py +++ b/tests/test_issues.py @@ -3,6 +3,7 @@ This module contains tests for Issue objects. """ + import json from datetime import date, datetime, timedelta, timezone from decimal import Decimal @@ -10,27 +11,27 @@ import pytest import requests_mock -from mokkari import exceptions, issue +from mokkari import exceptions from mokkari.session import Session def test_issue_with_rating(talker: Session) -> None: """Test issue with a rating.""" - ff: issue.IssueSchema = talker.issue(51658) + ff = talker.issue(51658) assert ff.series.name == "Fantastic Four" assert ff.series.volume == 7 - assert ff.rating.id == 4 - assert ff.rating.name == "Teen Plus" + assert ff.rating.id == 3 + assert ff.rating.name == "Teen" assert ff.cover_date == date(2022, 11, 1) assert ff.store_date == date(2022, 9, 21) assert ff.series.genres[0].id == 10 assert ff.series.genres[0].name == "Super-Hero" - assert ff.resource_url == "https://metron.cloud/issue/fantastic-four-2018-47/" + assert ff.resource_url.__str__() == "https://metron.cloud/issue/fantastic-four-2018-47/" def test_known_issue(talker: Session) -> None: """Test for a known issue.""" - death: issue.IssueSchema = talker.issue(1) + death = talker.issue(1) assert death.publisher.name == "Marvel" assert death.series.name == "Death of the Inhumans" assert death.series.volume == 1 @@ -39,18 +40,20 @@ def test_known_issue(talker: Session) -> None: assert death.store_date == date(2018, 7, 4) assert death.price == Decimal("4.99") assert not death.sku - assert death.image == "https://static.metron.cloud/media/issue/2018/11/11/6497376-01.jpg" + assert ( + death.image.__str__() == "https://static.metron.cloud/media/issue/2018/11/11/6497376-01.jpg" + ) assert len(death.characters) > 0 assert len(death.teams) > 0 assert len(death.credits) > 0 assert death.modified == datetime( - 2022, - 10, - 21, - 9, - 12, + 2023, + 5, 31, - 376522, + 9, + 0, + 46, + 300882, tzinfo=timezone(timedelta(days=-1, seconds=72000), "-0400"), ) assert death.teams[0].name == "Inhumans" @@ -65,12 +68,14 @@ def test_known_issue(talker: Session) -> None: 975156, tzinfo=timezone(timedelta(days=-1, seconds=72000), "-0400"), ) - assert death.resource_url == "https://metron.cloud/issue/death-of-the-inhumans-2018-1/" + assert ( + death.resource_url.__str__() == "https://metron.cloud/issue/death-of-the-inhumans-2018-1/" + ) def test_issue_with_price_and_sku(talker: Session) -> None: """Test issue with price & sku values.""" - die_16: issue.IssueSchema = talker.issue(36860) + die_16 = talker.issue(36860) assert die_16.price == Decimal("3.99") assert die_16.sku == "JUN210207" assert die_16.cover_date == date(2021, 8, 1) @@ -79,7 +84,7 @@ def test_issue_with_price_and_sku(talker: Session) -> None: def test_issue_without_store_date(talker: Session) -> None: """Test issue that does not have a store date.""" - spidey: issue.IssueSchema = talker.issue(31047) + spidey = talker.issue(31047) assert spidey.publisher.name == "Marvel" assert spidey.series.name == "The Spectacular Spider-Man" assert spidey.series.volume == 1 @@ -137,7 +142,7 @@ def test_issue_with_upc_sku_price(talker: Session) -> None: def test_issue_without_upc_sku_price(talker: Session) -> None: """Test issue without upc, sku, and price values.""" - bullets = talker.issue(3980) + bullets = talker.issue(89134) assert bullets.price is None assert bullets.sku == "" assert bullets.upc == "" @@ -150,7 +155,7 @@ def test_issue_with_reprints(talker: Session) -> None: assert wf.number == "228" assert wf.cover_date == date(1975, 3, 1) assert wf.price == Decimal(".6") - assert len(wf.reprints) == 3 + assert len(wf.reprints) == 4 assert wf.reprints[0].id == 35086 assert wf.reprints[0].issue == "Action Comics (1938) #193" assert wf.reprints[1].id == 3645 @@ -166,8 +171,8 @@ def test_issue_with_variants(talker: Session) -> None: assert paprika.series.name == "Mirka Andolfo's Sweet Paprika" assert paprika.series.sort_name == "Mirka Andolfo's Sweet Paprika" assert paprika.series.volume == 1 - assert paprika.series.series_type.name == "Maxi-Series" - assert paprika.series.series_type.id == 4 + assert paprika.series.series_type.name == "Limited Series" + assert paprika.series.series_type.id == 11 assert len(paprika.series.genres) == 1 assert paprika.number == "2" assert paprika.cover_date == date(2021, 9, 1) @@ -180,13 +185,13 @@ def test_issue_with_variants(talker: Session) -> None: assert paprika.variants[0].name == "Cover B Sejic" assert paprika.variants[0].sku == "JUN210257" assert ( - paprika.variants[0].image + paprika.variants[0].image.__str__() == "https://static.metron.cloud/media/variants/2021/08/26/sweet-paprika-2b.jpg" ) assert paprika.variants[1].name == "Cover C March" assert paprika.variants[1].sku == "JUN210258" assert ( - paprika.variants[1].image + paprika.variants[1].image.__str__() == "https://static.metron.cloud/media/variants/2021/08/26/sweet-paprika-2c.jpg" ) @@ -236,12 +241,6 @@ def test_bad_issue(talker: Session) -> None: talker.issue(-1) -def test_bad_response_data() -> None: - """Test for bad issue response.""" - with pytest.raises(exceptions.ApiError): - issue.IssuesList({"results": {"volume": "1"}}) - - def test_multi_page_results(talker: Session) -> None: """Test for multi page results.""" issues = talker.issues_list({"series_name": "action comics", "series_year_began": 1938}) diff --git a/tests/test_publishers.py b/tests/test_publishers.py index 4e853eb..36579e0 100644 --- a/tests/test_publishers.py +++ b/tests/test_publishers.py @@ -3,21 +3,25 @@ This module contains tests for Publisher objects. """ + import json from datetime import datetime, timedelta, timezone import pytest import requests_mock -from mokkari import exceptions, publisher +from mokkari import exceptions from mokkari.session import Session def test_known_publishers(talker: Session) -> None: """Test for a known publisher.""" - marvel: publisher.PublisherSchema = talker.publisher(1) + marvel = talker.publisher(1) assert marvel.name == "Marvel" - assert marvel.image == "https://static.metron.cloud/media/publisher/2018/11/11/marvel.jpg" + assert ( + marvel.image.__str__() + == "https://static.metron.cloud/media/publisher/2018/11/11/marvel.jpg" + ) assert marvel.founded == 1939 assert marvel.modified == datetime( 2019, @@ -29,22 +33,22 @@ def test_known_publishers(talker: Session) -> None: 591390, tzinfo=timezone(timedelta(days=-1, seconds=72000), "-0400"), ) - assert marvel.resource_url == "https://metron.cloud/publisher/marvel/" + assert marvel.resource_url.__str__() == "https://metron.cloud/publisher/marvel/" -def test_publisherlist(talker: Session) -> None: +def test_publisher_list(talker: Session) -> None: """Test the PublishersList.""" publishers = talker.publishers_list() publisher_iter = iter(publishers) assert next(publisher_iter).name == "12-Gauge Comics" + assert next(publisher_iter).name == "AAA Pop Comics" assert next(publisher_iter).name == "AWA Studios" - assert next(publisher_iter).name == "Abrams Books" - assert len(publishers) == 48 - assert publishers[2].name == "Abrams Books" + assert len(publishers) == 93 + assert publishers[2].name == "AWA Studios" def test_bad_publisher(talker: Session) -> None: - """Test for a non-existant publisher.""" + """Test for a non-existent publisher.""" with requests_mock.Mocker() as r: r.get( "https://metron.cloud/api/publisher/-1/", @@ -54,12 +58,6 @@ def test_bad_publisher(talker: Session) -> None: talker.publisher(-1) -def test_bad_response_data() -> None: - """Test for a bad publisher response.""" - with pytest.raises(exceptions.ApiError): - publisher.PublishersList({"results": {"name": 1}}) - - def test_bad_publisher_validate(talker: Session) -> None: """Test data with invalid data.""" # Change the 'name' field to an int, when it should be a string. diff --git a/tests/test_role.py b/tests/test_role.py index ec05d6f..f1f0d85 100644 --- a/tests/test_role.py +++ b/tests/test_role.py @@ -3,9 +3,7 @@ This module contains tests for Role objects. """ -import pytest -from mokkari import exceptions, issue from mokkari.session import Session @@ -18,9 +16,3 @@ def test_role_list(talker: Session) -> None: assert next(role_iter).name == "Assistant Editor" assert len(roles) == 11 assert roles[1].name == "Consulting Editor" - - -def test_bad_response_data() -> None: - """Test for a bad role response.""" - with pytest.raises(exceptions.ApiError): - issue.RoleList({"results": {"name": 1}}) diff --git a/tests/test_series.py b/tests/test_series.py index 637ed18..8dae721 100644 --- a/tests/test_series.py +++ b/tests/test_series.py @@ -3,6 +3,7 @@ This module contains tests for Series objects. """ + import json from datetime import datetime, timedelta, timezone @@ -10,34 +11,32 @@ import requests_mock from mokkari import exceptions -from mokkari import series as ser from mokkari.session import Session def test_known_series(talker: Session) -> None: """Test for a known series.""" - death: ser.SeriesSchema = talker.series(1) + death = talker.series(1) assert death.name == "Death of the Inhumans" assert death.sort_name == "Death of the Inhumans" assert death.volume == 1 assert death.year_began == 2018 assert death.year_end == 2018 assert death.issue_count == 5 - assert death.image == "https://static.metron.cloud/media/issue/2018/11/11/6497376-01.jpg" - assert death.series_type.name == "Mini-Series" + assert death.series_type.name == "Limited Series" assert death.publisher.id == 1 assert death.publisher.name == "Marvel" assert death.modified == datetime( - 2019, - 7, - 5, - 14, - 32, - 52, - 239629, + 2023, + 10, + 23, + 16, + 58, + 50, + 526656, tzinfo=timezone(timedelta(days=-1, seconds=72000), "-0400"), ) - assert death.resource_url == "https://metron.cloud/series/death-of-the-inhumans-2018/" + assert death.resource_url.__str__() == "https://metron.cloud/series/death-of-the-inhumans-2018/" def test_series_without_year_end(talker: Session) -> None: @@ -51,24 +50,25 @@ def test_series_without_year_end(talker: Session) -> None: assert abs_carnage.issue_count == 5 assert abs_carnage.publisher.id == 1 assert abs_carnage.publisher.name == "Marvel" - assert abs_carnage.series_type.name == "Mini-Series" + assert abs_carnage.series_type.name == "Limited Series" -def test_serieslist(talker: Session) -> None: +def test_series_list(talker: Session) -> None: """Test the SeriesList.""" series = talker.series_list({"name": "batman"}) series_iter = iter(series) assert next(series_iter).id == 2547 + assert next(series_iter).id == 5959 assert next(series_iter).id == 2481 assert next(series_iter).id == 763 assert next(series_iter).id == 93 - assert len(series) == 162 - assert series[3].id == 93 - assert series[3].display_name == "Batman (2016)" + assert len(series) == 219 + assert series[4].id == 93 + assert series[4].display_name == "Batman (2016)" def test_bad_series(talker: Session) -> None: - """Test for a non-existant series.""" + """Test for a non-existent series.""" with requests_mock.Mocker() as r: r.get( "https://metron.cloud/api/series/-1/", @@ -78,12 +78,6 @@ def test_bad_series(talker: Session) -> None: talker.series(-1) -def test_bad_response_data() -> None: - """Test for a bad series response.""" - with pytest.raises(exceptions.ApiError): - ser.SeriesList({"results": {"name": 1}}) - - def test_bad_series_validate(talker: Session) -> None: """Test data with invalid data.""" # Change the 'name' field to an int, when it should be a string. diff --git a/tests/test_series_type.py b/tests/test_series_type.py index 1753d12..7885987 100644 --- a/tests/test_series_type.py +++ b/tests/test_series_type.py @@ -4,10 +4,6 @@ This module contains tests for SeriesType objects. """ -import pytest - -from mokkari import exceptions -from mokkari.series import SeriesTypeList from mokkari.session import Session @@ -18,10 +14,4 @@ def test_series_type_list(talker: Session) -> None: assert next(st_iter).name == "Annual Series" assert next(st_iter).name == "Cancelled Series" assert series_types[3].name == "Hard Cover" - assert len(series_types) == 9 - - -def test_bad_response_data() -> None: - """Test for a bad series type response.""" - with pytest.raises(exceptions.ApiError): - SeriesTypeList({"results": {"name": 1}}) + assert len(series_types) == 8 diff --git a/tests/test_teams.py b/tests/test_teams.py index a63c716..dab5d77 100644 --- a/tests/test_teams.py +++ b/tests/test_teams.py @@ -3,21 +3,24 @@ This module contains tests for Team objects. """ + import json from datetime import date, datetime, timedelta, timezone import pytest import requests_mock -from mokkari import exceptions, team +from mokkari import exceptions from mokkari.session import Session def test_known_team(talker: Session) -> None: """Test for a known team.""" - inhumans: team.TeamSchema = talker.team(1) + inhumans = talker.team(1) assert inhumans.name == "Inhumans" - assert inhumans.image == "https://static.metron.cloud/media/team/2018/11/11/Inhumans.jpg" + assert ( + inhumans.image.__str__() == "https://static.metron.cloud/media/team/2018/11/11/Inhumans.jpg" + ) assert len(inhumans.creators) == 2 assert inhumans.modified == datetime( 2019, @@ -29,31 +32,32 @@ def test_known_team(talker: Session) -> None: 975156, tzinfo=timezone(timedelta(days=-1, seconds=72000), "-0400"), ) - assert inhumans.resource_url == "https://metron.cloud/team/inhumans/" + assert inhumans.resource_url.__str__() == "https://metron.cloud/team/inhumans/" -def test_teamlist(talker: Session) -> None: +def test_team_list(talker: Session) -> None: """Test the TeamsList.""" teams = talker.teams_list() team_iter = iter(teams) + assert next(team_iter).name == "501st Legion" assert next(team_iter).name == "A-Force" assert next(team_iter).name == "A-Next" assert next(team_iter).name == "A.I.M." - assert len(teams) == 591 - assert teams[2].name == "A.I.M." + assert len(teams) == 1331 + assert teams[3].name == "A.I.M." def test_team_issue_list(talker: Session) -> None: """Test for getting an issue list for an arc.""" issues = talker.team_issues_list(1) - assert len(issues) == 515 + assert len(issues) == 609 assert issues[0].id == 258 assert issues[0].issue_name == "Fantastic Four (1961) #45" assert issues[0].cover_date == date(1965, 12, 1) def test_bad_team(talker: Session) -> None: - """Test for a non-existant team.""" + """Test for a non-existent team.""" with requests_mock.Mocker() as r: r.get( "https://metron.cloud/api/team/-1/", @@ -63,12 +67,6 @@ def test_bad_team(talker: Session) -> None: talker.team(-1) -def test_bad_response_data() -> None: - """Test for a bad team response.""" - with pytest.raises(exceptions.ApiError): - team.TeamsList({"results": {"name": 1}}) - - def test_bad_team_validate(talker: Session) -> None: """Test data with invalid data.""" # Change the 'name' field to an int, when it should be a string. diff --git a/tests/testing_mock.sqlite b/tests/testing_mock.sqlite index 78c9e0b1d83731666d92481ee7fb466fdee8a160..1dc87fa9c26623874ccf3301d6450493c4438e3b 100644 GIT binary patch literal 1142784 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