Initial API mappings for cards and sets
This commit is contained in:
commit
6bcf22458a
25 changed files with 1533 additions and 0 deletions
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# Python-generated files
|
||||||
|
__pycache__/
|
||||||
|
*.py[oc]
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
wheels/
|
||||||
|
*.egg-info
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv
|
1
.python-version
Normal file
1
.python-version
Normal file
|
@ -0,0 +1 @@
|
||||||
|
3.13
|
3
README.md
Normal file
3
README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# Pyfall
|
||||||
|
|
||||||
|
An async Scryfall API wrapper written
|
0
pyfall/__init__.py
Normal file
0
pyfall/__init__.py
Normal file
180
pyfall/client/__init__.py
Normal file
180
pyfall/client/__init__.py
Normal 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
54
pyfall/client/error.py
Normal 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."""
|
125
pyfall/client/http/http_client.py
Normal file
125
pyfall/client/http/http_client.py
Normal 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()
|
213
pyfall/client/http/http_requests/card.py
Normal file
213
pyfall/client/http/http_requests/card.py
Normal 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)
|
50
pyfall/client/http/http_requests/set.py
Normal file
50
pyfall/client/http/http_requests/set.py
Normal 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
44
pyfall/client/route.py
Normal 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
11
pyfall/const.py
Normal 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
|
9
pyfall/models/__init__.py
Normal file
9
pyfall/models/__init__.py
Normal 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
47
pyfall/models/api.py
Normal 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
15
pyfall/models/base.py
Normal 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
16
pyfall/models/bulk.py
Normal 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
161
pyfall/models/cards.py
Normal 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)
|
||||||
|
|
9
pyfall/models/catalogs.py
Normal file
9
pyfall/models/catalogs.py
Normal 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
9
pyfall/models/enums.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
class Color(Enum):
|
||||||
|
White = "W"
|
||||||
|
Blue = "U"
|
||||||
|
Black = "B"
|
||||||
|
Red = "R"
|
||||||
|
Green = "G"
|
||||||
|
Colorless = "C"
|
16
pyfall/models/internal/protocols.py
Normal file
16
pyfall/models/internal/protocols.py
Normal 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
12
pyfall/models/rulings.py
Normal 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
39
pyfall/models/sets.py
Normal 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
21
pyfall/models/symbols.py
Normal 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
3
pyfall/utils.py
Normal 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
18
pyproject.toml
Normal 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
467
uv.lock
generated
Normal 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 },
|
||||||
|
]
|
Loading…
Add table
Reference in a new issue