Initial API mappings for cards and sets

This commit is contained in:
zevaryx 2025-02-27 16:45:09 -07:00
commit 6bcf22458a
25 changed files with 1533 additions and 0 deletions

10
.gitignore vendored Normal file
View file

@ -0,0 +1,10 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv

1
.python-version Normal file
View file

@ -0,0 +1 @@
3.13

3
README.md Normal file
View file

@ -0,0 +1,3 @@
# Pyfall
An async Scryfall API wrapper written

0
pyfall/__init__.py Normal file
View file

180
pyfall/client/__init__.py Normal file
View file

@ -0,0 +1,180 @@
from typing import Any, Literal
import httpx
from pyfall.const import __version__
from pyfall.models import *
from pyfall.utils import UUID_CHECK
import asyncio
import logging
import time
from typing import Any
from httpx import AsyncClient
from pyfall.const import __version__, get_logger
from pyfall.client.error import *
from pyfall.client.http.http_requests.card import CardRequests
from pyfall.client.http.http_requests.set import SetRequests
from pyfall.client.route import Route
class GlobalLock:
def __init__(self) -> None:
self._lock = asyncio.Lock()
self.max_requests = 10
self._calls = self.max_requests
self._reset_time = 0
@property
def calls_remaining(self) -> int:
"""Returns the amount of calls remaining."""
return self.max_requests - self._calls
def reset_calls(self) -> None:
"""Resets the calls to the max amount."""
self._calls = self.max_requests
self._reset_time = time.perf_counter() + 1
def set_reset_time(self, delta: float) -> None:
"""
Sets the reset time to the current time + delta.
To be called if a 429 is received.
Args:
delta: The time to wait before resetting the calls.
"""
self._reset_time = time.perf_counter() + delta
self._calls = 0
async def wait(self) -> None:
"""Throttles calls to prevent hitting the global rate limit."""
async with self._lock:
if self._reset_time <= time.perf_counter():
self.reset_calls()
elif self._calls <= 0:
await asyncio.sleep(self._reset_time - time.perf_counter())
self.reset_calls()
self._calls -= 1
class Pyfall(CardRequests, SetRequests):
def __init__(self, logger: logging.Logger | None = None):
self.__headers = {
"Content-Type": "application/json",
"UserAgent": f"pyfall/{__version__}",
"Accept": "application/json"
}
self.__client: AsyncClient = None
self.global_lock: GlobalLock = GlobalLock()
self._max_attempts: int = 3
self.logger = logger
if self.logger is None:
self.logger = get_logger()
async def request(self, route: Route, params: dict | None = None, **kwargs: dict) -> dict[str, Any]:
"""Make a request to the Scryfall API.
Args:
route: Route to take
params: Query string parameters. Defaults to None.
Returns:
dict[str, Any]: Raw result
"""
if params is not None:
kwargs["params"] = params
if not self.__client:
self.__client = AsyncClient(headers=self.__headers)
for attempt in range(self._max_attempts):
if self.__client is None:
self.__client = AsyncClient()
await self.global_lock.wait()
response = await self.__client.request(route.method, route.url, **kwargs)
if response.status_code == 429:
self.logger.warning("Too many requests, waiting 5 seconds")
self.global_lock.set_reset_time(5)
continue
if response.status_code >= 500:
self.logger.warning(f"{route.resolved_endpoint} Received {response.status_code}... retrying in {1 + attempt * 2} seconds")
await asyncio.sleep(1 + attempt * 2)
continue
result = response.json()
if not 300 > response.status_code >= 200:
await self._raise_exception(response, route, result)
return result
async def _raise_exception(self, response, route, result) -> None:
self.logger.error(f"{route.method}::{route.url}: {response.status_code}")
if response.status_code == 403:
raise Forbidden(response, route)
if response.status_code == 404:
raise NotFound(response, route)
if response.status_code >= 500:
raise ScryfallError(response, route)
raise HTTPException(response, route)
async def close(self) -> None:
"""Close the session."""
if self.__client is not None and not self.__client.is_closed:
await self.__client.aclose()
# class Pyfall:
# """Scryfall API client."""
# def __init__(self):
# self._headers = {
# "Content-Type": "application/json",
# "UserAgent": f"pyfall/{__version__}",
# "Accept": "application/json"
# }
# self.__httpx: httpx.AsyncClient = None
# async def request(self, endpoint: str, method: str = "GET", **kwargs) -> dict[str, Any]:
# """Make a request to the API.
# Args:
# endpoint: Endpoint to request from
# Returns:
# Raw API result
# """
# if not self.__httpx:
# self.__httpx = httpx.AsyncClient(headers=self._headers, base_url="https://api.scryfall.com")
# args = {k: v for k, v in kwargs if v is not None}
# if method not in ("POST", "PATCH", "PUT"):
# response: httpx.Response = await self.__httpx.request(method=method, url=endpoint, params=args)
# else:
# response: httpx.Response = await self.__httpx.request(method=method, url=endpoint, data=args)
# if response.status_code >= 500:
# response.raise_for_status()
# data = response.json()
# return data
# async def get_card_by_id(self, id: str) -> Card:
# """Get a card by ID.
# Args:
# id: UUID of card
# """
# if not UUID_CHECK.match(id):
# raise ValueError("'id' must be a UUID")
# data = await self.request(f"cards/{id}")
# if data["object"] == "error":
# return APIError.model_validate(data)
# data["_client"] = self
# return Card(**data)

54
pyfall/client/error.py Normal file
View file

@ -0,0 +1,54 @@
from typing import TYPE_CHECKING
import httpx
if TYPE_CHECKING:
from pyfall.client.route import Route
class LibraryException(Exception):
"""Base Exception of pyfall."""
class HTTPException(LibraryException):
"""
An HTTP request resulted in an exception.
Attributes:
response httpx.Response: The response of the HTTP request
details str: The text of the exception, could be None
status int: The HTTP status code
route Route: The HTTP route that was used
"""
def __init__(self, response: httpx.Response, route: "Route") -> None:
self.response: httpx.Response = response
self.route: "Route" = route
data = response.json()
self.status: int = data.get("status")
self.code: str = data.get("code")
self.details: str = data.get("details")
self.type: str | None = data.get("type", None)
self.warnings: list[str] | None = data.get("warnings", None)
super().__init__(f"{self.status}|{self.code}: {f'({self.type})' if self.type else ''}{self.details}")
def __str__(self) -> str:
if not self.warnings:
return f"HTTPException: {self.status}|{self.code} || {self.details}"
return f"HTTPException: {self.status}|{self.code}: " + "\n".join(self.warnings)
def __repr__(self) -> str:
return str(self)
class ScryfallError(HTTPException):
"""An error occurred with Scryfall."""
class BadRequest(HTTPException):
"""A bad request was made."""
class Forbidden(HTTPException):
"""You do not have access to this."""
class NotFound(HTTPException):
"""This resource could not be found."""

View file

@ -0,0 +1,125 @@
import asyncio
import logging
import time
from typing import Any
from httpx import AsyncClient
from pyfall.const import __version__, get_logger
from pyfall.client.error import *
from pyfall.client.http.http_requests.card import CardRequests
from pyfall.client.http.http_requests.set import SetRequests
from pyfall.client.route import Route
class GlobalLock:
def __init__(self) -> None:
self._lock = asyncio.Lock()
self.max_requests = 10
self._calls = self.max_requests
self._reset_time = 0
@property
def calls_remaining(self) -> int:
"""Returns the amount of calls remaining."""
return self.max_requests - self._calls
def reset_calls(self) -> None:
"""Resets the calls to the max amount."""
self._calls = self.max_requests
self._reset_time = time.perf_counter() + 1
def set_reset_time(self, delta: float) -> None:
"""
Sets the reset time to the current time + delta.
To be called if a 429 is received.
Args:
delta: The time to wait before resetting the calls.
"""
self._reset_time = time.perf_counter() + delta
self._calls = 0
async def wait(self) -> None:
"""Throttles calls to prevent hitting the global rate limit."""
async with self._lock:
if self._reset_time <= time.perf_counter():
self.reset_calls()
elif self._calls <= 0:
await asyncio.sleep(self._reset_time - time.perf_counter())
self.reset_calls()
self._calls -= 1
class HTTPClient(CardRequests, SetRequests):
def __init__(self, logger: logging.Logger | None = None):
self.__headers = {
"Content-Type": "application/json",
"UserAgent": f"pyfall/{__version__}",
"Accept": "application/json"
}
self.__client: AsyncClient = None
self.global_lock: GlobalLock = GlobalLock()
self._max_attempts: int = 3
self.logger = logger
if self.logger is None:
self.logger = get_logger()
async def request(self, route: Route, params: dict | None = None, **kwargs: dict) -> dict[str, Any]:
"""Make a request to the Scryfall API.
Args:
route: Route to take
params: Query string parameters. Defaults to None.
Returns:
dict[str, Any]: Raw result
"""
if params is not None:
kwargs["params"] = params
if not self.__client:
self.__client = AsyncClient(headers=self.__headers)
for attempt in range(self._max_attempts):
if self.__client is None:
self.__client = AsyncClient()
await self.global_lock.wait()
response = await self.__client.request(route.method, route.url, **kwargs)
if response.status_code == 429:
self.logger.warning("Too many requests, waiting 5 seconds")
self.global_lock.set_reset_time(5)
continue
if response.status_code >= 500:
self.logger.warning(f"{route.resolved_endpoint} Received {response.status_code}... retrying in {1 + attempt * 2} seconds")
await asyncio.sleep(1 + attempt * 2)
continue
result = response.json()
if not 300 > response.status_code >= 200:
await self._raise_exception(response, route, result)
return result
async def _raise_exception(self, response, route, result) -> None:
self.logger.error(f"{route.method}::{route.url}: {response.status_code}")
if response.status_code == 403:
raise Forbidden(response, route)
if response.status_code == 404:
raise NotFound(response, route)
if response.status_code >= 500:
raise ScryfallError(response, route)
raise HTTPException(response, route)
async def close(self) -> None:
"""Close the session."""
if self.__client is not None and not self.__client.is_closed:
await self.__client.aclose()

View file

@ -0,0 +1,213 @@
from typing import Any, Literal
from pyfall.models.catalogs import Catalog
from pyfall.client.route import Route
from pyfall.models.cards import Card
from pyfall.models.api import APIList
from pyfall.models.rulings import Ruling
from pyfall.models.internal.protocols import CanRequest
class CardRequests(CanRequest):
async def get_card_by_id(self, id: str) -> Card:
"""Get a card by ID.
Args:
id: UUID of card
"""
result = await self.request(Route("GET", f"/cards/{id}"))
result["_client"] = self
return Card(**result)
async def get_card_by_tcgplayer_id(self, id: int) -> Card:
"""Get card by TCGPlayer ID
Args:
id: TCGPlayer ID
"""
result = await self.request(Route("GET", f"/cards/tcgplayer/{id}"))
result["_client"] = self
return Card(**result)
async def get_card_by_multiverse_id(self, id: int) -> Card:
"""Get card by Multiverse ID
Args:
id: Multiverse ID
"""
result = await self.request(Route("GET", f"/cards/multiverse/{id}"))
result["_client"] = self
return Card(**result)
async def get_card_by_mtgo_id(self, id: int) -> Card:
"""Get card by MTGO ID
Args:
id: MTGO ID
"""
result = await self.request(Route("GET", f"/cards/mtgo/{id}"))
result["_client"] = self
return Card(**result)
async def get_card_by_arena_id(self, id: int) -> Card:
"""Get card by MTG Arena ID
Args:
id: MTG Arena ID
"""
result = await self.request(Route("GET", f"/cards/arena/{id}"))
result["_client"] = self
return Card(**result)
async def get_card_by_cardmarket_id(self, id: int) -> Card:
"""Get card by Cardmarket ID
Args:
id: Cardmarket ID
"""
result = await self.request(Route("GET", f"/cards/cardmarket/{id}"))
result["_client"] = self
return Card(**result)
async def get_rulings_by_card_id(self, id: str) -> list[Ruling]:
"""Get card rulings by card ID.
Args:
id: UUID of card
"""
result = await self.request(Route("GET", f"/cards/{id}/rulings"))
return [Ruling(**x) for x in result.get("data")]
async def search_cards(
self,
q: str,
unique: Literal["cards", "art", "prints"] = "cards",
order: Literal["name", "set", "released", "rarity", "color", "usd", "tix", "eur", "cmc", "power", "toughness", "edhrec", "penny", "artist", "review"] = "name",
dir: Literal["auto", "asc", "desc"] = "auto",
include_extras: bool = False,
include_multilingual: bool = False,
include_variations: bool = False,
page: int = 1,
) -> APIList:
"""Search for a card using a fulltext string search.
Args:
q: A fulltext search query. Max length: 1000 Unicode characters
unique: The strategy for omitting cards. Default `cards`
order: The method to sort returned cards. Default `name`
dir: Direction to sort cards. Default `auto`
include_extras: If true, extra cards (tokens, planes, etc) will be included. Equivalent to adding `include:extras` to the fulltext search. Default `false`
include_multilingual: If true, cards in every language supported by Scryfall will be included. Default `false`
include_variations: If true, rare card variants will by included. Default `false`
page: Page number to return. Default `1`
"""
if len(q) > 1000:
raise ValueError("Query can only be max of 1000 Unicode characters")
params = dict(
q=q,
unique=unique,
order=order,
dir=dir,
include_extras=include_extras,
include_multilingual=include_multilingual,
include_variations=include_variations,
page=page
)
result = await self.request(
Route(
"GET",
"/cards/search",
),
params=params
)
result["_client"] = self
return APIList(**result)
async def search_cards_named(
self,
exact: str | None = None,
fuzzy: str | None = None,
set: str | None = None
) -> Card:
"""Search for a card using name search.
Args:
exact: Exact string to search for
fuzzy: Fuzzy string to search for
set: Set to search in
"""
if (not exact and not fuzzy) or (exact and fuzzy):
raise ValueError("Either exact or fuzzy needs provided")
params = dict(set=set)
if exact:
params["exact"] = exact
elif fuzzy:
params["fuzzy"] = fuzzy
result = await self.request(
Route(
"GET",
"/cards/named",
),
params=params,
)
result["_client"] = self
return Card(**result)
async def cards_autocomplete(
self,
q: str,
include_extras: bool = False
) -> Catalog:
"""Returns a Catalog containing up to 20 full English card names for autocomplete purposes.
Args:
q: The string to autocomplete
include_extras: If true, extra cards (tokens, planes, vanguards, etc) will be included. Default False
"""
params = dict(q=q, include_extras=include_extras)
result = await self.request(
Route(
"GET",
"/cards/autocomplete"
),
params=params,
)
return Catalog(**result)
async def get_random_card(
self,
q: str | None = None,
) -> Card:
params = dict(q=q)
result = await self.request(
Route(
"GET",
"/cards/random"
),
params=params,
)
result["_client"] = self
return Card(**result)
async def get_card_collection(self, identifiers: list[dict[str, Any]]):
raise NotImplementedError("This endpoint has not been implemented")
async def get_card_by_set_code_and_collector_number(self, code: str, number: str, lang: str | None = None) -> Card:
result = await self.request(
Route(
"GET",
f"/cards/{code}/{number}{f'/{lang}' if lang else ''}"
)
)
result["_client"] = self
return Card(**result)

View file

@ -0,0 +1,50 @@
from typing import Any, Literal
from pyfall.models.catalogs import Catalog
from pyfall.client.route import Route
from pyfall.models.cards import Card
from pyfall.models.api import APIList
from pyfall.models.rulings import Ruling
from pyfall.models.sets import Set
from pyfall.models.internal.protocols import CanRequest
class SetRequests(CanRequest):
async def get_all_sets(self) -> APIList:
"""Get all MTG sets."""
result = await self.request(Route("GET", "/sets"))
result["_client"] = self
return APIList(**result)
async def get_set_by_id(self, id: str) -> Set:
"""Get MTG set by ID.
Args:
id: UUID of set
"""
result = await self.request(Route("GET", f"/sets/{id}"))
result["_client"] = self
return Set(**result)
async def get_set_by_code(self, code: str) -> Set:
"""Get MTG set by set code.
Args:
code: Set code
"""
result = await self.request(Route("GET", f"/sets/{code}"))
result["_client"] = self
return Set(**result)
async def get_set_by_tcgplayer_id(self, id: str) -> Set:
"""Get MTG set by TCGPlayer ID.
Args:
id: TCGPlayer ID of set
"""
result = await self.request(Route("GET", f"/sets/tcgplayer/{id}"))
result["_client"] = self
return Set(**result)

44
pyfall/client/route.py Normal file
View file

@ -0,0 +1,44 @@
from typing import Any, ClassVar
from urllib.parse import quote as _uriquote
PAYLOAD_TYPE = dict[str, int | str | bool | list | None]
class Route:
BASE: ClassVar[str] = "https://api.scryfall.com"
path: str
params: dict[str, str | int | bool]
def __init__(self, method: str, path: str, **params: Any) -> None:
self.path: str = path
self.method: str = method
self.params = params
def __repr__(self) -> str:
return f"<Route {self.endpoint}>"
def __str__(self) -> str:
return self.endpoint
@property
def resolved_path(self) -> str:
"""The endpoint for this route, with all parameters resolved"""
return self.path.format_map({k: _uriquote(v) if isinstance(v, str) else v for k, v in self.params.items()})
@property
def endpoint(self) -> str:
"""The endpoint for this route"""
return f"{self.method} {self.path}"
@property
def resolved_endpoint(self) -> str:
"""The endpoint for this route, with all major parameters resolved"""
path = self.path
for key, value in self.major_params.items():
path = path.replace(f"{{{key}}}", str(value))
return f"{self.method} {path}"
@property
def url(self) -> str:
"""The full url for this route"""
return f"{self.BASE}{self.resolved_path}"

11
pyfall/const.py Normal file
View file

@ -0,0 +1,11 @@
import logging
__version__ = "0.1.0"
logger_name = "pyfall"
_logger = logging.getLogger(logger_name)
def get_logger() -> logging.Logger:
global _logger
return _logger

View file

@ -0,0 +1,9 @@
from .api import APIError, APIList
from .bulk import BulkData
from .cards import Card
from .catalogs import Catalog
from .rulings import Ruling
from .sets import Set
from .symbols import CardSymbol
__all__ = ["APIError", "APIList", "BulkData", "Card", "Catalog", "Ruling", "Set", "CardSymbol"]

47
pyfall/models/api.py Normal file
View file

@ -0,0 +1,47 @@
from typing import Any, Literal, Self
from pydantic import BaseModel, HttpUrl, ValidationError, model_validator
from pyfall.models.base import BaseAPIModel
from pyfall.models.cards import Card
from pyfall.models.rulings import Ruling
from pyfall.models.sets import Set
from pyfall.models.symbols import CardSymbol
CLASS_LOOKUP = {
"card": Card,
"card_symbol": CardSymbol,
"ruling": Ruling,
"set": Set
}
class APIError(BaseModel):
status: int
code: str
details: str
type: str | None = None
warnings: list[str] | None = None
class APIList(BaseAPIModel):
object: Literal["list"]
data: list[Card | CardSymbol | Ruling | Set]
has_more: bool
next_page: HttpUrl | None = None
total_cards: int | None = None
warnings: list[str] | None = None
@model_validator(mode="before")
@classmethod
def validate_data(cls, data: Any) -> Any:
if data.get("object") == "list":
for item in data.get("data"):
item["_client"] = data["_client"]
item = CLASS_LOOKUP.get(item.get("object"))(**item)
return data
async def get_next_page(self) -> Self | None:
if self.has_more:
params = dict(self.next_page.query_params())
params.pop("format", None)
return await self._client.cards_search(**params)
return None

15
pyfall/models/base.py Normal file
View file

@ -0,0 +1,15 @@
from typing import TYPE_CHECKING
from pydantic import BaseModel
if TYPE_CHECKING:
from pyfall.client import Pyfall
class BaseAPIModel(BaseModel):
"""Base API model for base API calls."""
_client: "Pyfall"
def __init__(self, **data):
client = data.get("_client")
super().__init__(**data)
self._client = client

16
pyfall/models/bulk.py Normal file
View file

@ -0,0 +1,16 @@
from datetime import datetime
from pydantic import BaseModel, HttpUrl
from pydantic.types import UUID
class BulkData(BaseModel):
id: UUID
uri: HttpUrl
type: str
name: str
description: str
download_uri: HttpUrl
updated_at: datetime
size: int
content_type: str
content_encoding: str

161
pyfall/models/cards.py Normal file
View file

@ -0,0 +1,161 @@
from datetime import date
from typing import Any, Literal, TYPE_CHECKING
from pydantic import BaseModel, HttpUrl, field_validator
from pydantic.types import UUID
# from pyfall.models.api import APIList
from pyfall.models.base import BaseAPIModel
from pyfall.models.enums import Color
if TYPE_CHECKING:
from pyfall.client import Pyfall
from pyfall.models.rulings import Ruling
from pyfall.models.sets import Set
class RelatedCard(BaseModel):
id: UUID
object: Literal["related_card"]
component: Literal["token", "meld_part", "meld_result", "combo_piece"]
name: str
type_line: str
uri: HttpUrl
class CardFace(BaseModel):
artist: str | None = None
artist_id: str | None = None
cmc: float | None = None
color_indicator: list[Color] | None = None
colors: list[Color] | None = None
defense: str | None = None
flavor_text: str | None = None
illustration_id: UUID | None = None
image_uris: dict[Literal["small", "normal", "large", "png", "art_crop", "border_crop"], HttpUrl] | None = None
layout: str | None = None
loyalty: str | None = None
mana_cost: str
name: str
object: Literal["card_face"]
oracle_id: UUID | None = None
oracle_text: str | None = None
power: str | None = None
printed_name: str | None = None
printed_text: str | None = None
printed_type_line: str | None = None
toughness: str | None = None
type_line: str | None = None
watermark: str | None = None
class Preview(BaseModel):
previewed_at: date | None = None
source_uri: HttpUrl | None = None
source: str | None = None
@field_validator("source_uri", mode="before")
@classmethod
def validate_source_uri(cls, value: Any) -> Any:
if isinstance(value, str):
if len(value) > 0:
return HttpUrl(value)
return None
class Card(BaseAPIModel):
# Core fields
arena_id: int | None = None
id: UUID
lang: str
mtgo_id: int | None = None
mtgo_foil_id: int | None = None
multiverse_ids: list[int] | None = None
tcgplayer_id: int | None = None
tcgplayer_etched_id: int | None = None
cardmarket_id: int | None = None
object: Literal["card"]
layout: str
oracle_id: UUID | None = None
prints_search_uri: HttpUrl
rulings_uri: HttpUrl
scryfall_uri: HttpUrl
uri: HttpUrl
# Gameplay fields
all_parts: list[RelatedCard] | None = None
card_faces: list[CardFace] | None = None
cmc: float
color_identity: list[Color]
color_indicator: list[Color] | None = None
colors: list[Color] | None = None
defense: str | None = None
edhrec_rank: int | None = None
game_changer: bool | None = None
hand_modifier: str | None = None
keywords: list[str]
legalities: dict[str, Literal["legal", "not_legal", "restricted", "banned"]]
life_modifier: str | None = None
loyalty: str | None = None
mana_cost: str | None = None
name: str
oracle_text: str | None = None
penny_rank: int | None = None
power: str | None = None
produced_mana: list[Color] | None = None
reserved: bool
toughness: str | None = None
type_line: str
# Print fields
artist: str | None = None
artist_ids: list[UUID] | None = None
attraction_lights: list[int] | None = None
booster: bool
border_color: Literal["black", "white", "borderless", "yellow", "silver", "gold"]
card_back_id: UUID | None = None
collector_number: str
content_warning: bool | None = None
digital: bool
finishes: list[Literal["foil", "nonfoil", "etched"]]
flavor_name: str | None = None
flavor_text: str | None = None
frame_effects: list[str] | None = None
frame: str
full_art: bool
games: list[Literal["paper", "arena", "mtgo"]]
highres_image: bool
illustration_id: UUID | None = None
image_status: Literal["missing", "placeholder", "lowres", "highres_scan"]
image_uris: dict[Literal["small", "normal", "large", "png", "art_crop", "border_crop"], HttpUrl] | None = None
oversized: bool
prices: dict[str, str | None]
printed_name: str | None = None
printed_text: str | None = None
printed_type_line: str | None = None
promo: bool
promo_types: list[str] | None = None
purchase_uris: dict[str, HttpUrl] | None = None
rarity: Literal["common", "uncommon", "rare", "special", "mythic", "bonus"]
related_uris: dict[str, HttpUrl]
released_at: date
reprint: bool
scryfall_set_uri: HttpUrl
set_name: str
set_search_uri: HttpUrl
set_type: str
set_uri: HttpUrl
set: str
set_id: UUID
story_spotlight: bool
textless: bool
variation: bool
variation_of: UUID | None = None
security_stamp: Literal["oval", "triangle", "acorn", "circle", "arena", "heart"] | None = None
watermark: str | None = None
preview: Preview | None = None
async def get_set(self) -> "Set":
"""Get set card is a part of."""
return await self._client.get_set_by_id(self.set_id)
async def get_rulings(self) -> "Ruling":
"""Get rulings for card."""
return await self._client.get_rulings_by_card_id(self.id)

View file

@ -0,0 +1,9 @@
from typing import Literal
from pydantic import BaseModel, HttpUrl
class Catalog(BaseModel):
object: Literal["catalog"]
uri: HttpUrl | None = None
total_values: int
data: list[str]

9
pyfall/models/enums.py Normal file
View file

@ -0,0 +1,9 @@
from enum import Enum
class Color(Enum):
White = "W"
Blue = "U"
Black = "B"
Red = "R"
Green = "G"
Colorless = "C"

View file

@ -0,0 +1,16 @@
import typing
from typing import Protocol, Any, TypeVar
from pyfall.client.route import Route
T_co = TypeVar("T", covariant=True)
@typing.runtime_checkable
class CanRequest(Protocol[T_co]):
async def request(
self,
route: Route,
params: dict | None = None,
**kwargs: dict,
) -> dict[str, Any]:
raise NotImplementedError("Derived classes need to implement this.")

12
pyfall/models/rulings.py Normal file
View file

@ -0,0 +1,12 @@
from datetime import date
from typing import Literal
from pydantic import BaseModel
from pydantic.types import UUID
class Ruling(BaseModel):
object: Literal["ruling"]
oracle_id: UUID
source: Literal["wotc", "scryfall"]
published_at: date
comment: str

39
pyfall/models/sets.py Normal file
View file

@ -0,0 +1,39 @@
from datetime import date
from typing import Literal, TYPE_CHECKING
from pydantic import HttpUrl
from pydantic.types import UUID
from pyfall.models.base import BaseAPIModel
if TYPE_CHECKING:
from pyfall.client import Pyfall
from pyfall.models.api import APIList
class Set(BaseAPIModel):
object: Literal["set"]
id: UUID
code: str
mtgo_code: str | None = None
arena_code: str | None = None
tcgplayer_id: int | None = None
name: str
set_type: str
released_at: date | None = None
block_code: str | None = None
block: str | None = None
parent_set_code: str | None = None
card_count: int
printed_size: int | None = None
digital: bool
foil_only: bool
nonfoil_only: bool
scryfall_uri: HttpUrl
uri: HttpUrl
icon_svg_uri: HttpUrl
search_uri: HttpUrl
async def get_cards(self) -> "APIList":
"""Get a list of cards from the set."""
params = dict(self.search_uri.query_params())
return await self._client.search_cards(**params)

21
pyfall/models/symbols.py Normal file
View file

@ -0,0 +1,21 @@
from typing import Literal
from pydantic import BaseModel, HttpUrl
from pyfall.models.enums import Color
class CardSymbol(BaseModel):
object: Literal["card_symbol"]
symbol: str
loose_variant: str | None = None
english: str
transposable: bool
represents_mana: bool
mana_value: float | None = None
appears_in_mana_costs: bool
funny: bool
colors: list[Color]
hybrid: bool
phyrexian: bool
gatherer_alternatives: str | None = None
svg_uri: HttpUrl | None = None

3
pyfall/utils.py Normal file
View file

@ -0,0 +1,3 @@
import re
UUID_CHECK = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", flags=re.I)

18
pyproject.toml Normal file
View file

@ -0,0 +1,18 @@
[project]
name = "pyfall"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"httpx>=0.28.1",
"pydantic>=2.10.6",
]
[dependency-groups]
dev = [
"black>=25.1.0",
"ipython>=8.32.0",
"rich>=13.9.4",
"ruff>=0.9.7",
]

467
uv.lock generated Normal file
View file

@ -0,0 +1,467 @@
version = 1
revision = 1
requires-python = ">=3.13"
[[package]]
name = "annotated-types"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
]
[[package]]
name = "anyio"
version = "4.8.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 },
]
[[package]]
name = "asttokens"
version = "3.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4a/e7/82da0a03e7ba5141f05cce0d302e6eed121ae055e0456ca228bf693984bc/asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", size = 61978 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918 },
]
[[package]]
name = "black"
version = "25.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "mypy-extensions" },
{ name = "packaging" },
{ name = "pathspec" },
{ name = "platformdirs" },
]
sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673 },
{ url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190 },
{ url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926 },
{ url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613 },
{ url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646 },
]
[[package]]
name = "certifi"
version = "2025.1.31"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
]
[[package]]
name = "click"
version = "8.1.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
]
[[package]]
name = "decorator"
version = "5.1.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/66/0c/8d907af351aa16b42caae42f9d6aa37b900c67308052d10fdce809f8d952/decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330", size = 35016 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d5/50/83c593b07763e1161326b3b8c6686f0f4b0f24d5526546bee538c89837d6/decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186", size = 9073 },
]
[[package]]
name = "executing"
version = "2.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/91/50/a9d80c47ff289c611ff12e63f7c5d13942c65d68125160cefd768c73e6e4/executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755", size = 978693 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702 },
]
[[package]]
name = "h11"
version = "0.14.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 },
]
[[package]]
name = "httpcore"
version = "1.0.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 },
]
[[package]]
name = "httpx"
version = "0.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 },
]
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
]
[[package]]
name = "ipython"
version = "8.32.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "decorator" },
{ name = "jedi" },
{ name = "matplotlib-inline" },
{ name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
{ name = "prompt-toolkit" },
{ name = "pygments" },
{ name = "stack-data" },
{ name = "traitlets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/36/80/4d2a072e0db7d250f134bc11676517299264ebe16d62a8619d49a78ced73/ipython-8.32.0.tar.gz", hash = "sha256:be2c91895b0b9ea7ba49d33b23e2040c352b33eb6a519cca7ce6e0c743444251", size = 5507441 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e7/e1/f4474a7ecdb7745a820f6f6039dc43c66add40f1bcc66485607d93571af6/ipython-8.32.0-py3-none-any.whl", hash = "sha256:cae85b0c61eff1fc48b0a8002de5958b6528fa9c8defb1894da63f42613708aa", size = 825524 },
]
[[package]]
name = "jedi"
version = "0.19.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "parso" },
]
sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278 },
]
[[package]]
name = "markdown-it-py"
version = "3.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mdurl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 },
]
[[package]]
name = "matplotlib-inline"
version = "0.1.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "traitlets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899 },
]
[[package]]
name = "mdurl"
version = "0.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
]
[[package]]
name = "mypy-extensions"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 },
]
[[package]]
name = "packaging"
version = "24.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
]
[[package]]
name = "parso"
version = "0.8.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size = 400609 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650 },
]
[[package]]
name = "pathspec"
version = "0.12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 },
]
[[package]]
name = "pexpect"
version = "4.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "ptyprocess" },
]
sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 },
]
[[package]]
name = "platformdirs"
version = "4.3.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 },
]
[[package]]
name = "prompt-toolkit"
version = "3.0.50"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "wcwidth" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a1/e1/bd15cb8ffdcfeeb2bdc215de3c3cffca11408d829e4b8416dcfe71ba8854/prompt_toolkit-3.0.50.tar.gz", hash = "sha256:544748f3860a2623ca5cd6d2795e7a14f3d0e1c3c9728359013f79877fc89bab", size = 429087 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/ea/d836f008d33151c7a1f62caf3d8dd782e4d15f6a43897f64480c2b8de2ad/prompt_toolkit-3.0.50-py3-none-any.whl", hash = "sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198", size = 387816 },
]
[[package]]
name = "ptyprocess"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993 },
]
[[package]]
name = "pure-eval"
version = "0.2.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842 },
]
[[package]]
name = "pydantic"
version = "2.10.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 },
]
[[package]]
name = "pydantic-core"
version = "2.27.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 },
{ url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 },
{ url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 },
{ url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 },
{ url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 },
{ url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 },
{ url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 },
{ url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 },
{ url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 },
{ url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 },
{ url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 },
{ url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 },
{ url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 },
{ 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 = "pyfall"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "httpx" },
{ name = "pydantic" },
]
[package.dev-dependencies]
dev = [
{ name = "black" },
{ name = "ipython" },
{ name = "rich" },
{ name = "ruff" },
]
[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 = ">=8.32.0" },
{ name = "rich", specifier = ">=13.9.4" },
{ name = "ruff", specifier = ">=0.9.7" },
]
[[package]]
name = "pygments"
version = "2.19.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
]
[[package]]
name = "rich"
version = "13.9.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown-it-py" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 },
]
[[package]]
name = "ruff"
version = "0.9.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/39/8b/a86c300359861b186f18359adf4437ac8e4c52e42daa9eedc731ef9d5b53/ruff-0.9.7.tar.gz", hash = "sha256:643757633417907510157b206e490c3aa11cab0c087c912f60e07fbafa87a4c6", size = 3669813 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b1/f3/3a1d22973291226df4b4e2ff70196b926b6f910c488479adb0eeb42a0d7f/ruff-0.9.7-py3-none-linux_armv6l.whl", hash = "sha256:99d50def47305fe6f233eb8dabfd60047578ca87c9dcb235c9723ab1175180f4", size = 11774588 },
{ url = "https://files.pythonhosted.org/packages/8e/c9/b881f4157b9b884f2994fd08ee92ae3663fb24e34b0372ac3af999aa7fc6/ruff-0.9.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d59105ae9c44152c3d40a9c40d6331a7acd1cdf5ef404fbe31178a77b174ea66", size = 11746848 },
{ url = "https://files.pythonhosted.org/packages/14/89/2f546c133f73886ed50a3d449e6bf4af27d92d2f960a43a93d89353f0945/ruff-0.9.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f313b5800483770bd540cddac7c90fc46f895f427b7820f18fe1822697f1fec9", size = 11177525 },
{ url = "https://files.pythonhosted.org/packages/d7/93/6b98f2c12bf28ab9def59c50c9c49508519c5b5cfecca6de871cf01237f6/ruff-0.9.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:042ae32b41343888f59c0a4148f103208bf6b21c90118d51dc93a68366f4e903", size = 11996580 },
{ url = "https://files.pythonhosted.org/packages/8e/3f/b3fcaf4f6d875e679ac2b71a72f6691a8128ea3cb7be07cbb249f477c061/ruff-0.9.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87862589373b33cc484b10831004e5e5ec47dc10d2b41ba770e837d4f429d721", size = 11525674 },
{ url = "https://files.pythonhosted.org/packages/f0/48/33fbf18defb74d624535d5d22adcb09a64c9bbabfa755bc666189a6b2210/ruff-0.9.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a17e1e01bee0926d351a1ee9bc15c445beae888f90069a6192a07a84af544b6b", size = 12739151 },
{ url = "https://files.pythonhosted.org/packages/63/b5/7e161080c5e19fa69495cbab7c00975ef8a90f3679caa6164921d7f52f4a/ruff-0.9.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7c1f880ac5b2cbebd58b8ebde57069a374865c73f3bf41f05fe7a179c1c8ef22", size = 13416128 },
{ url = "https://files.pythonhosted.org/packages/4e/c8/b5e7d61fb1c1b26f271ac301ff6d9de5e4d9a9a63f67d732fa8f200f0c88/ruff-0.9.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e63fc20143c291cab2841dbb8260e96bafbe1ba13fd3d60d28be2c71e312da49", size = 12870858 },
{ url = "https://files.pythonhosted.org/packages/da/cb/2a1a8e4e291a54d28259f8fc6a674cd5b8833e93852c7ef5de436d6ed729/ruff-0.9.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91ff963baed3e9a6a4eba2a02f4ca8eaa6eba1cc0521aec0987da8d62f53cbef", size = 14786046 },
{ url = "https://files.pythonhosted.org/packages/ca/6c/c8f8a313be1943f333f376d79724260da5701426c0905762e3ddb389e3f4/ruff-0.9.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88362e3227c82f63eaebf0b2eff5b88990280fb1ecf7105523883ba8c3aaf6fb", size = 12550834 },
{ url = "https://files.pythonhosted.org/packages/9d/ad/f70cf5e8e7c52a25e166bdc84c082163c9c6f82a073f654c321b4dff9660/ruff-0.9.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0372c5a90349f00212270421fe91874b866fd3626eb3b397ede06cd385f6f7e0", size = 11961307 },
{ url = "https://files.pythonhosted.org/packages/52/d5/4f303ea94a5f4f454daf4d02671b1fbfe2a318b5fcd009f957466f936c50/ruff-0.9.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d76b8ab60e99e6424cd9d3d923274a1324aefce04f8ea537136b8398bbae0a62", size = 11612039 },
{ url = "https://files.pythonhosted.org/packages/eb/c8/bd12a23a75603c704ce86723be0648ba3d4ecc2af07eecd2e9fa112f7e19/ruff-0.9.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0c439bdfc8983e1336577f00e09a4e7a78944fe01e4ea7fe616d00c3ec69a3d0", size = 12168177 },
{ url = "https://files.pythonhosted.org/packages/cc/57/d648d4f73400fef047d62d464d1a14591f2e6b3d4a15e93e23a53c20705d/ruff-0.9.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:115d1f15e8fdd445a7b4dc9a30abae22de3f6bcabeb503964904471691ef7606", size = 12610122 },
{ url = "https://files.pythonhosted.org/packages/49/79/acbc1edd03ac0e2a04ae2593555dbc9990b34090a9729a0c4c0cf20fb595/ruff-0.9.7-py3-none-win32.whl", hash = "sha256:e9ece95b7de5923cbf38893f066ed2872be2f2f477ba94f826c8defdd6ec6b7d", size = 9988751 },
{ url = "https://files.pythonhosted.org/packages/6d/95/67153a838c6b6ba7a2401241fd8a00cd8c627a8e4a0491b8d853dedeffe0/ruff-0.9.7-py3-none-win_amd64.whl", hash = "sha256:3770fe52b9d691a15f0b87ada29c45324b2ace8f01200fb0c14845e499eb0c2c", size = 11002987 },
{ url = "https://files.pythonhosted.org/packages/63/6a/aca01554949f3a401991dc32fe22837baeaccb8a0d868256cbb26a029778/ruff-0.9.7-py3-none-win_arm64.whl", hash = "sha256:b075a700b2533feb7a01130ff656a4ec0d5f340bb540ad98759b8401c32c2037", size = 10177763 },
]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
]
[[package]]
name = "stack-data"
version = "0.6.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asttokens" },
{ name = "executing" },
{ name = "pure-eval" },
]
sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521 },
]
[[package]]
name = "traitlets"
version = "5.14.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359 },
]
[[package]]
name = "typing-extensions"
version = "4.12.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
]
[[package]]
name = "wcwidth"
version = "0.2.13"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 },
]