diff --git a/.gitignore b/.gitignore index 4dc48e0..5830f28 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,7 @@ report.xml .coverage coverage.xml .pytest_cache/ -.ruff_cache/ \ No newline at end of file +.ruff_cache/ + +# Docs +_build/ \ No newline at end of file diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..c208fa7 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,11 @@ +version: 2 + +python: + install: + - requirements: docs/requirements.txt + - requirements: requirements.txt + +sphinx: + builder: html + configuration: docs/conf.py + diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000..c7e9fa5 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,43 @@ +.. currentmodule:: scryfall +.. _API Reference: + +API Reference +=========================== + +Client +****** +.. automodule:: scryfall.client + :members: + :member-order: bysource + +HTTP +~~~~ +.. automodule:: scryfall.client.http + :members: + :member-order: bysource + +Card +---- +.. automodule:: scryfall.client.http.card + :members: + :member-order: bysource + +Set +---- +.. automodule:: scryfall.client.http.set + :members: + :member-order: bysource + +Models +****** +.. automodule:: scryfall.models + :members: + :member-order: bysource + +.. Card +.. ~~~~ +.. .. automodule:: scryfall.models.cards + +.. Set +.. ~~~~ +.. .. automodule:: scryfall.models.sets diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..0f7c722 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,38 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = "scryfall-py" +copyright = "2025, zevaryx" +author = "zevaryx" +release = "0.1.1" + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [ + "sphinx.ext.duration", + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.napoleon", + "enum_tools.autoenum", +] + +templates_path = ["_templates"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = "sphinx_rtd_theme" +html_static_path = ["_static"] + +# Custom config +autodoc_default_flags = ["members"] +master_doc = "index" +source_suffix = [".rst", ".md"] diff --git a/docs/guide.rst b/docs/guide.rst new file mode 100644 index 0000000..fbd2d8a --- /dev/null +++ b/docs/guide.rst @@ -0,0 +1,25 @@ +Usage +=============== + +Installation +************ + +To get started, install ``scryfall-py``:: + + pip install scryfall-py + + +Getting Started +*************** + +Hashing Files +~~~~~~~~~~~~~ + +.. code-block:: python + + from scryfall import Scryfall + client = Scryfall() + + card = await client.search_cards_named("Arcades, the Strategist") + +This will fetch the card Arcades, the Strategist from the Scryfall API diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..8446626 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,17 @@ +Welcome to the scryfall-py documentation! +===================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + guide + api + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..954237b --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/pyproject.toml b/pyproject.toml index 050304b..0ef8390 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ dev = [ "sphinx>=8.2.1", "sphinx-rtd-theme>=3.0.2", "pytest-asyncio>=0.25.3", + "enum-tools>=0.12.0", ] [build-system] diff --git a/scryfall/client/__init__.py b/scryfall/client/__init__.py index 39cb764..d21f925 100644 --- a/scryfall/client/__init__.py +++ b/scryfall/client/__init__.py @@ -10,8 +10,8 @@ from httpx import AsyncClient from scryfall.const import get_logger from scryfall.client.error import LibraryException, HTTPException, ScryfallError, Forbidden, NotFound -from scryfall.client.http.http_requests.card import CardRequests -from scryfall.client.http.http_requests.set import SetRequests +from scryfall.client.http.card import CardRequests +from scryfall.client.http.set import SetRequests from scryfall.client.route import Route diff --git a/scryfall/client/http/http_requests/card.py b/scryfall/client/http/card.py similarity index 100% rename from scryfall/client/http/http_requests/card.py rename to scryfall/client/http/card.py diff --git a/scryfall/client/http/http_requests/set.py b/scryfall/client/http/set.py similarity index 100% rename from scryfall/client/http/http_requests/set.py rename to scryfall/client/http/set.py diff --git a/scryfall/models/api.py b/scryfall/models/api.py index 5f5393b..8f7b00d 100644 --- a/scryfall/models/api.py +++ b/scryfall/models/api.py @@ -12,20 +12,44 @@ CLASS_LOOKUP = {"card": Card, "card_symbol": CardSymbol, "ruling": Ruling, "set" class APIError(BaseModel): + """Scryfall API error model.""" + status: int + """HTTP status code""" + code: str + """HTTP status code text (i.e. not_found)""" + details: str + """More details""" + type: str | None = None + """The error type""" + warnings: list[str] | None = None + """Any associated warnings that are not quite errors""" class APIList(BaseAPIModel): + """Scryfall API list model for paginated requests""" + object: Literal["list"] + """The object type, `list` in this case""" + data: list[Card | CardSymbol | Ruling | Set] + """The page of data, max 175 items unless it is an entire set of cards""" + has_more: bool + """If there are more pages""" + next_page: HttpUrl | None = None + """The URL to the next page""" + total_cards: int | None = None + """The total number of cards found by the original query""" + warnings: list[str] | None = None + """Warnings generated by the query that are not quite errors""" @model_validator(mode="before") @classmethod @@ -40,6 +64,7 @@ class APIList(BaseAPIModel): return data async def get_next_page(self) -> "APIList | None": + """Get the next page of the query""" if self.has_more and self.next_page is not None: params = dict(self.next_page.query_params()) params.pop("format", None) diff --git a/scryfall/models/base.py b/scryfall/models/base.py index d4b0b7b..269c00c 100644 --- a/scryfall/models/base.py +++ b/scryfall/models/base.py @@ -10,6 +10,7 @@ class BaseAPIModel(BaseModel): """Base API model for base API calls.""" _client: "Scryfall" + """Internal Scryfall client""" def __init__(self, **data): client: "Scryfall" = data["_client"] diff --git a/scryfall/models/bulk.py b/scryfall/models/bulk.py index 7fb648d..f3aa729 100644 --- a/scryfall/models/bulk.py +++ b/scryfall/models/bulk.py @@ -5,13 +5,34 @@ from pydantic import BaseModel, HttpUrl class BulkData(BaseModel): + """Bulk data model""" + id: UUID + """UUID of the bulk data""" + uri: HttpUrl + """URI of the bulk data endpoint""" + type: str + """The type of data in the download""" + name: str + """The name of the download""" + description: str + """A description of the download""" + download_uri: HttpUrl + """The download URL for the bulk data""" + updated_at: datetime + """When this bulk data was last updated""" + size: int + """The size of the data in bytes""" + content_type: str + """The content type""" + content_encoding: str + """The MIME encoding""" diff --git a/scryfall/models/cards.py b/scryfall/models/cards.py index 1a8575f..cc88fcd 100644 --- a/scryfall/models/cards.py +++ b/scryfall/models/cards.py @@ -13,44 +13,112 @@ if TYPE_CHECKING: class RelatedCard(BaseModel): + """A card that's related to the current card""" + id: UUID + """The UUID of the related card""" + object: Literal["related_card"] + """The object type, literally `related_card`""" + component: Literal["token", "meld_part", "meld_result", "combo_piece"] + """How the card is related""" + name: str + """The name of the related card""" + type_line: str + """The type line of the related card""" + uri: HttpUrl + """The URL of the related card""" class CardFace(BaseModel): + """A card face, for multi-faced cards""" + artist: str | None = None + """The face artist""" + artist_id: str | None = None + """The ID of the face artist""" + cmc: float | None = None + """Card Mana Cost""" + color_indicator: list[Color] | None = None + """The color indicator(s) of the face""" + colors: list[Color] | None = None + """The color(s) of the face""" + defense: str | None = None + """The toughness of the face, if it exists""" + flavor_text: str | None = None + """The face's flavor text""" + illustration_id: UUID | None = None + """The face's illustration UUID""" + image_uris: dict[Literal["small", "normal", "large", "png", "art_crop", "border_crop"], HttpUrl] | None = None + """URLs for the image face for different formats.""" + layout: str | None = None + """Layout of the face.""" + loyalty: str | None = None + """Face loyalty""" + mana_cost: str + """Face mana cost""" + name: str + """Name of the face""" + object: Literal["card_face"] + """Object type, literally `card_face`""" + oracle_id: UUID | None = None + """Oracle ID of the face""" + oracle_text: str | None = None + """Oracle text of the face""" + power: str | None = None + """The face's power""" + printed_name: str | None = None + """The printed name of the face""" + printed_text: str | None = None + """The printed text of the face""" + printed_type_line: str | None = None + """The printed type line of the face""" + toughness: str | None = None + """The toughness of the face""" + type_line: str | None = None + """The type line of the face""" + watermark: str | None = None + """The watermark of the face""" class Preview(BaseModel): + """Object for defining when the card was previewed.""" + previewed_at: date | None = None + + """When it was previewed""" + source_uri: HttpUrl | None = None + """The URL of where it was previewed""" + source: str | None = None + """The source of the preview""" @field_validator("source_uri", mode="before") @classmethod @@ -62,48 +130,104 @@ class Preview(BaseModel): class Card(BaseAPIModel): + """Main Card model.""" + # Core fields arena_id: int | None = None + """MTG Arena ID""" + id: UUID + """Scryfall card UUID""" + lang: str + """Card language""" + mtgo_id: int | None = None + """MTG Online ID""" + mtgo_foil_id: int | None = None + """MTG Online Foil ID""" + multiverse_ids: list[int] | None = None + """MTG Multiverse IDs""" + tcgplayer_id: int | None = None + """TCGPlayer ID""" + tcgplayer_etched_id: int | None = None + """TCGPlayer Etched ID""" + cardmarket_id: int | None = None + """Cardmarket ID""" + object: Literal["card"] + """Object type, literally `card`""" + layout: str + """Card layout""" + oracle_id: UUID | None = None + """Oracle UUID""" + prints_search_uri: HttpUrl + """Print search URL for other printings""" + rulings_uri: HttpUrl + """Rulings URL""" + scryfall_uri: HttpUrl + """Scryfall URL""" + uri: HttpUrl + """Card URL""" # Gameplay fields all_parts: list[RelatedCard] | None = None + """If this card is closely related to other cards, this property will be an array with Related Card objects.""" card_faces: list[CardFace] | None = None + """All faces of the card""" cmc: float + """Card mana value""" color_identity: list[Color] + """Card color identity""" color_indicator: list[Color] | None = None + """Card color indicator, if any""" colors: list[Color] | None = None + """Card colors, if any. Colors may be on the card's faces instead""" defense: str | None = None + """Toughness""" edhrec_rank: int | None = None + """EDHRec card rank/popularity""" game_changer: bool | None = None + """If this car is on the Commander Game Changer list""" hand_modifier: str | None = None + """Vanguard card hand modifier""" keywords: list[str] + """List of keywords, such as `Flying` or `Cumulative upkeep`""" legalities: dict[str, Literal["legal", "not_legal", "restricted", "banned"]] + """List of legalitites across different formats""" life_modifier: str | None = None + """The card's life modifier, if it is a Vanguard card. This value will contain a delta, such as `+2`.""" loyalty: str | None = None + """This loyalty if any. Note that some cards have loyalties that are not numeric, such as X.""" mana_cost: str | None = None + """The mana cost for this card. This value will be any empty string `""` if the cost is absent. Remember that per the game rules, a missing mana cost and a mana cost of `{0}` are different values. Multi-faced cards will report this value in card faces.""" name: str + """The name of this card. If this card has multiple faces, this field will contain both names separated by `␣//␣`.""" oracle_text: str | None = None + """The Oracle text for this card, if any.""" penny_rank: int | None = None + """This card's rank/popularity on Penny Dreadful. Not all cards are ranked.""" power: str | None = None + """This card's power, if any. Note that some cards have powers that are not numeric, such as `*`.""" produced_mana: list[Color] | None = None + """Colors of mana that this card could produce.""" reserved: bool + """True if this card is on the Reserved List.""" toughness: str | None = None + """This card's toughness, if any. Note that some cards have toughnesses that are not numeric, such as `*`.""" type_line: str + """The type line of this card.""" # Print fields artist: str | None = None diff --git a/uv.lock b/uv.lock index feed700..8dbac1b 100644 --- a/uv.lock +++ b/uv.lock @@ -297,6 +297,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/11/208f72084084d3f6a2ed5ebfdfc846692c3f7ad6dce65e400194924f7eed/domdf_python_tools-3.10.0-py3-none-any.whl", hash = "sha256:5e71c1be71bbcc1f881d690c8984b60e64298ec256903b3147f068bc33090c36", size = 126860 }, ] +[[package]] +name = "enum-tools" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/06/55dfd19df0386a2a90d325dba67b0b5ba0b879a4bdac9cd225c22f6736d8/enum_tools-0.12.0.tar.gz", hash = "sha256:13ceb9376a4c5f574a1e7c5f9c8eb7f3d3fbfbb361cc18a738df1a58dfefd460", size = 18931 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/fc/cc600677fe58519352ae5fe9367d05d0054faa47e8c57ef50a1bb9c77b0e/enum_tools-0.12.0-py3-none-any.whl", hash = "sha256:d69b019f193c7b850b17d9ce18440db7ed62381571409af80ccc08c5218b340a", size = 22356 }, +] + [[package]] name = "executing" version = "2.2.0" @@ -716,51 +729,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, ] -[[package]] -name = "scryfall" -version = "0.1.0" -source = { editable = "." } -dependencies = [ - { name = "httpx" }, - { name = "pydantic" }, -] - -[package.dev-dependencies] -dev = [ - { name = "black" }, - { name = "ipython" }, - { name = "pre-commit" }, - { name = "pytest" }, - { name = "pytest-asyncio" }, - { name = "pytest-cov" }, - { name = "rich" }, - { name = "ruff" }, - { name = "sphinx" }, - { name = "sphinx-rtd-theme" }, - { name = "sphinx-toolbox" }, -] - -[package.metadata] -requires-dist = [ - { name = "httpx", specifier = ">=0.28.1" }, - { name = "pydantic", specifier = ">=2.10.6" }, -] - -[package.metadata.requires-dev] -dev = [ - { name = "black", specifier = ">=25.1.0" }, - { name = "ipython", specifier = ">=9.0.0" }, - { name = "pre-commit", specifier = ">=4.0.1" }, - { name = "pytest", specifier = ">=8.3.3" }, - { name = "pytest-asyncio", specifier = ">=0.25.3" }, - { name = "pytest-cov", specifier = ">=6.0.0" }, - { name = "rich", specifier = ">=13.9.4" }, - { name = "ruff", specifier = ">=0.9.9" }, - { name = "sphinx", specifier = ">=8.2.1" }, - { name = "sphinx-rtd-theme", specifier = ">=3.0.2" }, - { name = "sphinx-toolbox", specifier = ">=3.9.0" }, -] - [[package]] name = "pygments" version = "2.19.1" @@ -898,6 +866,53 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/31/d8/de873d1c1b020d668d8ec9855d390764cb90cf8f6486c0983da52be8b7b7/ruff-0.9.9-py3-none-win_arm64.whl", hash = "sha256:3ac78f127517209fe6d96ab00f3ba97cafe38718b23b1db3e96d8b2d39e37ddf", size = 10435860 }, ] +[[package]] +name = "scryfall-py" +version = "0.1.1" +source = { editable = "." } +dependencies = [ + { name = "httpx" }, + { name = "pydantic" }, +] + +[package.dev-dependencies] +dev = [ + { name = "black" }, + { name = "enum-tools" }, + { name = "ipython" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "rich" }, + { name = "ruff" }, + { name = "sphinx" }, + { name = "sphinx-rtd-theme" }, + { name = "sphinx-toolbox" }, +] + +[package.metadata] +requires-dist = [ + { name = "httpx", specifier = ">=0.28.1" }, + { name = "pydantic", specifier = ">=2.10.6" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "black", specifier = ">=25.1.0" }, + { name = "enum-tools", specifier = ">=0.12.0" }, + { name = "ipython", specifier = ">=9.0.0" }, + { name = "pre-commit", specifier = ">=4.0.1" }, + { name = "pytest", specifier = ">=8.3.3" }, + { name = "pytest-asyncio", specifier = ">=0.25.3" }, + { name = "pytest-cov", specifier = ">=6.0.0" }, + { name = "rich", specifier = ">=13.9.4" }, + { name = "ruff", specifier = ">=0.9.9" }, + { name = "sphinx", specifier = ">=8.2.1" }, + { name = "sphinx-rtd-theme", specifier = ">=3.0.2" }, + { name = "sphinx-toolbox", specifier = ">=3.9.0" }, +] + [[package]] name = "six" version = "1.17.0"