diff --git a/pyproject.toml b/pyproject.toml index 775d50f..6035214 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,7 @@ description = "Monorepo for Satellite Tasking API (STAPI) Specification Python p readme = "README.md" requires-python = ">=3.10" dependencies = [ + "pystapi-client", "pystapi-validator", "stapi-pydantic", "stapi-fastapi" @@ -30,9 +31,10 @@ docs = [ default-groups = ["dev", "docs"] [tool.uv.workspace] -members = ["pystapi-validator", "stapi-pydantic", "stapi-fastapi"] +members = ["pystapi-validator", "stapi-pydantic", "pystapi-client", "stapi-fastapi"] [tool.uv.sources] +pystapi-client.workspace = true pystapi-validator.workspace = true stapi-pydantic.workspace = true stapi-fastapi.workspace = true @@ -59,6 +61,7 @@ max-complexity = 8 # default 10 [tool.mypy] strict = true files = [ + "pystapi-client/src/pystapi_client/**/*.py", "pystapi-validator/src/pystapi_validator/**/*.py", "stapi-pydantic/src/stapi_pydantic/**/*.py", "stapi-fastapi/src/stapi_fastapi/**/*.py" diff --git a/pystapi-client/README.md b/pystapi-client/README.md new file mode 100644 index 0000000..e69de29 diff --git a/pystapi-client/pyproject.toml b/pystapi-client/pyproject.toml new file mode 100644 index 0000000..b9177e7 --- /dev/null +++ b/pystapi-client/pyproject.toml @@ -0,0 +1,28 @@ +[project] +name = "pystapi-client" +version = "0.0.1" +description = "Python library for searching Satellite Tasking API (STAPI) APIs." +readme = "README.md" +authors = [ + { name = "Kaveh Karimi-Asli", email = "ka7eh@pm.me" }, + { name = "Philip Weiss", email = "philip.weiss@orbitalsidekick.com" } +] +maintainers = [{ name = "Pete Gadomski", email = "pete.gadomski@gmail.com" }] +keywords = ["stapi"] +license = { text = "MIT" } +requires-python = ">=3.10" +dependencies = [ + "httpx>=0.28.1", + "stapi-pydantic", + "python-dateutil>=2.8.2", +] + +[project.scripts] +stapi-client = "pystapi_client.cli:cli" + +[tool.uv.sources] +stapi-pydantic = { workspace = true } + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/pystapi-client/src/pystapi_client/__init__.py b/pystapi-client/src/pystapi_client/__init__.py new file mode 100644 index 0000000..e1fd054 --- /dev/null +++ b/pystapi-client/src/pystapi_client/__init__.py @@ -0,0 +1,7 @@ +__all__ = [ + "Client", + "ConformanceClasses", +] + +from pystapi_client.client import Client +from pystapi_client.conformance import ConformanceClasses diff --git a/pystapi-client/src/pystapi_client/client.py b/pystapi-client/src/pystapi_client/client.py new file mode 100644 index 0000000..49139c0 --- /dev/null +++ b/pystapi-client/src/pystapi_client/client.py @@ -0,0 +1,338 @@ +import re +import urllib +import urllib.parse +import warnings +from collections.abc import Callable, Iterable, Iterator +from typing import ( + Any, +) + +from httpx import URL, Request +from httpx._types import TimeoutTypes +from pydantic import AnyUrl +from stapi_pydantic import Link, Order, OrderCollection, Product, ProductsCollection + +from pystapi_client.conformance import ConformanceClasses +from pystapi_client.exceptions import APIError +from pystapi_client.stapi_api_io import StapiIO +from pystapi_client.warnings import NoConformsTo + +DEFAULT_LINKS = [ + { + "endpoint": "/conformance", + "rel": "conformance", + "method": "GET", + }, + { + "endpoint": "/products", + "rel": "products", + "method": "GET", + }, +] + + +class Client: + """A Client for interacting with the root of a STAPI + + Instances of the ``Client`` class provide a convenient way of interacting + with STAPI APIs that conform to the [STAPI API spec](https://github.com/stapi-spec/stapi-spec). + """ + + stapi_io: StapiIO + conforms_to: list[str] + links: list[Link] + + def __init__(self, stapi_io: StapiIO) -> None: + self.stapi_io = stapi_io + + def __repr__(self) -> str: + return f"" + + @classmethod + def open( + cls, + url: str, + headers: dict[str, str] | None = None, + parameters: dict[str, Any] | None = None, + request_modifier: Callable[[Request], Request] | None = None, + timeout: TimeoutTypes | None = None, + ) -> "Client": + """Opens a STAPI API client + + Args: + url : The URL of a STAPI API. + headers : A dictionary of additional headers to use in all requests + made to any part of this STAPI API. + parameters: Optional dictionary of query string parameters to + include in all requests. + request_modifier: A callable that either modifies a `Request` instance or + returns a new one. This can be useful for injecting Authentication + headers and/or signing fully-formed requests (e.g. signing requests + using AWS SigV4). + + The callable should expect a single argument, which will be an instance + of :class:`requests.Request`. + + If the callable returns a `requests.Request`, that will be used. + Alternately, the callable may simply modify the provided request object + and return `None`. + timeout: Optional float or (float, float) tuple following the semantics + defined by `Requests + `__. + + Return: + client : A :class:`Client` instance for this STAPI API + """ + stapi_io = StapiIO( + root_url=AnyUrl(url), + headers=headers, + parameters=parameters, + request_modifier=request_modifier, + timeout=timeout, + ) + client = Client(stapi_io=stapi_io) + + client.read_links() + client.read_conformance() + + if not client.has_conforms_to(): + warnings.warn(NoConformsTo()) + + return client + + def get_single_link( + self, + rel: str | None = None, + media_type: str | Iterable[str] | None = None, + ) -> Link | None: + """Get a single :class:`~stapi_pydantic.Link` instance associated with this object. + + Args: + rel : If set, filter links such that only those + matching this relationship are returned. + media_type: If set, filter the links such that only + those matching media_type are returned. media_type can + be a single value or a list of values. + + Returns: + :class:`~stapi_pydantic.Link` | None: First link that matches ``rel`` + and/or ``media_type``, or else the first link associated with + this object. + """ + if rel is None and media_type is None: + return next(iter(self.links), None) + if media_type and isinstance(media_type, str): + media_type = [media_type] + return next( + ( + link + for link in self.links + if (rel is None or link.rel == rel) and (media_type is None or (link.type or "") in media_type) + ), + None, + ) + + def read_links(self) -> None: + """Read the API links from the root of the STAPI API + + The links are stored in `Client._links`.""" + links = self.stapi_io.read_json("/").get("links", []) + if links: + self.links = [Link(**link) for link in links] + else: + warnings.warn("No links found in the root of the STAPI API") + self.links = [ + Link( + href=urllib.parse.urljoin(str(self.stapi_io.root_url), link["endpoint"]), + rel=link["rel"], + method=link["method"], + ) + for link in DEFAULT_LINKS + ] + + def read_conformance(self) -> None: + conformance: list[str] = [] + for endpoint in ["/conformance", "/"]: + try: + conformance = self.stapi_io.read_json(endpoint).get("conformsTo", []) + break + except APIError: + continue + + if conformance: + self.set_conforms_to(conformance) + + def has_conforms_to(self) -> bool: + """Whether server contains list of ``"conformsTo"`` URIs""" + return bool(self.conforms_to) + + def get_conforms_to(self) -> list[str]: + """List of ``"conformsTo"`` URIs + + Return: + list[str]: List of URIs that the server conforms to + """ + return self.conforms_to.copy() + + def set_conforms_to(self, conformance_uris: list[str]) -> None: + """Set list of ``"conformsTo"`` URIs + + Args: + conformance_uris : URIs indicating what the server conforms to + """ + self.conforms_to = conformance_uris + + def clear_conforms_to(self) -> None: + """Clear list of ``"conformsTo"`` urls + + Removes the entire list, so :py:meth:`has_conforms_to` will + return False after using this method. + """ + self.conforms_to = [] + + def add_conforms_to(self, name: str) -> None: + """Add ``"conformsTo"`` by name. + + Args: + name : name of :py:class:`ConformanceClasses` keys to add. + """ + conformance_class = ConformanceClasses.get_by_name(name) + + if not self.has_conformance(conformance_class): + self.set_conforms_to([*self.get_conforms_to(), conformance_class.valid_uri]) + + def remove_conforms_to(self, name: str) -> None: + """Remove ``"conformsTo"`` by name. + + Args: + name : name of :py:class:`ConformanceClasses` keys to remove. + """ + conformance_class = ConformanceClasses.get_by_name(name) + + self.set_conforms_to([uri for uri in self.get_conforms_to() if not re.match(conformance_class.pattern, uri)]) + + def has_conformance(self, conformance_class: ConformanceClasses | str) -> bool: + """Checks whether the API conforms to the given standard. + + This method only checks + against the ``"conformsTo"`` property from the API landing page and does not + make any additional calls to a ``/conformance`` endpoint even if the API + provides such an endpoint. + + Args: + name : name of :py:class:`ConformanceClasses` keys to check + conformance against. + + Return: + bool: Indicates if the API conforms to the given spec or URI. + """ + if isinstance(conformance_class, str): + conformance_class = ConformanceClasses.get_by_name(conformance_class) + + return any(re.match(conformance_class.pattern, uri) for uri in self.get_conforms_to()) + + def _supports_opportunities(self) -> bool: + return self.has_conformance(ConformanceClasses.OPPORTUNITIES) + + def _supports_async_opportunities(self) -> bool: + return self.has_conformance(ConformanceClasses.ASYNC_OPPORTUNITIES) + + def get_products(self, limit: int | None = None) -> Iterator[ProductsCollection]: + """Get all products from this STAPI API + + Returns: + ProductsCollection: A collection of STAPI Products + """ + products_endpoint = self._get_products_href() + + if limit is None: + parameters = {} + else: + parameters = {"limit": limit} + + products_collection_iterator = self.stapi_io.get_pages( + products_endpoint, parameters=parameters, lookup_key="products" + ) + for products_collection in products_collection_iterator: + yield ProductsCollection.model_validate(products_collection) + + def get_product(self, product_id: str) -> Product: + """Get a single product from this STAPI API + + Args: + product_id: The Product ID to get + + Returns: + Product: A STAPI Product + + Raises: + ValueError if product_id does not exist. + """ + + product_endpoint = self._get_products_href(product_id) + product_json = self.stapi_io.read_json(product_endpoint) + + if product_json is None: + raise ValueError(f"Product {product_id} not found") + + return Product.model_validate(product_json) + + def _get_products_href(self, product_id: str | None = None) -> str: + product_link = self.get_single_link("products") + if product_link is None: + raise ValueError("No products link found") + product_url = URL(str(product_link.href)) + if product_id is not None: + product_url = product_url.copy_with(path=f"{product_url.path}/{product_id}") + return str(product_url) + + def get_orders(self, limit: int | None = None) -> Iterator[OrderCollection]: # type: ignore[type-arg] + # TODO Update return type after the pydantic model generic type is fixed + """Get orders from this STAPI API + + Returns: + OrderCollection: A collection of STAPI Orders + """ + orders_endpoint = self._get_orders_href() + + if limit is None: + parameters = {} + else: + parameters = {"limit": limit} + + orders_collection_iterator = self.stapi_io.get_pages( + orders_endpoint, parameters=parameters, lookup_key="features" + ) + for orders_collection in orders_collection_iterator: + yield OrderCollection.model_validate(orders_collection) + + def get_order(self, order_id: str) -> Order: # type: ignore[type-arg] + # TODO Update return type after the pydantic model generic type is fixed + """Get a single order from this STAPI API + + Args: + order_id: The Order ID to get + + Returns: + Order: A STAPI Order + + Raises: + ValueError if order_id does not exist. + """ + + order_endpoint = self._get_orders_href(order_id) + order_json = self.stapi_io.read_json(order_endpoint) + + if order_json is None: + raise ValueError(f"Order {order_id} not found") + + return Order.model_validate(order_json) + + def _get_orders_href(self, order_id: str | None = None) -> str: + order_link = self.get_single_link("orders") + if order_link is None: + raise ValueError("No orders link found") + order_url = URL(str(order_link.href)) + if order_id is not None: + order_url = order_url.copy_with(path=f"{order_url.path}/{order_id}") + return str(order_url) diff --git a/pystapi-client/src/pystapi_client/conformance.py b/pystapi-client/src/pystapi_client/conformance.py new file mode 100644 index 0000000..f936173 --- /dev/null +++ b/pystapi-client/src/pystapi_client/conformance.py @@ -0,0 +1,32 @@ +import re +from enum import Enum + + +class ConformanceClasses(Enum): + """Enumeration class for Conformance Classes""" + + # defined conformance classes regexes + CORE = "/core" + OPPORTUNITIES = "/opportunities" + ASYNC_OPPORTUNITIES = "/async-opportunities" + + @classmethod + def get_by_name(cls, name: str) -> "ConformanceClasses": + for member in cls: + if member.name == name.upper(): + return member + raise ValueError(f"Invalid conformance class '{name}'. Options are: {list(cls)}") + + def __str__(self) -> str: + return f"{self.name}" + + def __repr__(self) -> str: + return str(self) + + @property + def valid_uri(self) -> str: + return f"https://stapi.example.com/v*{self.value}" + + @property + def pattern(self) -> re.Pattern[str]: + return re.compile(rf"{re.escape('https://stapi.example.com/v')}(.*){re.escape(self.value)}") diff --git a/pystapi-client/src/pystapi_client/exceptions.py b/pystapi-client/src/pystapi_client/exceptions.py new file mode 100644 index 0000000..431a2dc --- /dev/null +++ b/pystapi-client/src/pystapi_client/exceptions.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from httpx import Response + + +class APIError(Exception): + """Raised when unexpected server error.""" + + status_code: int | None + + @classmethod + def from_response(cls, response: Response) -> APIError: + error = cls(response.text) + error.status_code = response.status_code + return error + + +class ParametersError(Exception): + """Raised when invalid parameters are used in a query""" diff --git a/pystapi-client/src/pystapi_client/py.typed b/pystapi-client/src/pystapi_client/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/pystapi-client/src/pystapi_client/stapi_api_io.py b/pystapi-client/src/pystapi_client/stapi_api_io.py new file mode 100644 index 0000000..957337e --- /dev/null +++ b/pystapi-client/src/pystapi_client/stapi_api_io.py @@ -0,0 +1,203 @@ +import json +import logging +import urllib +import urllib.parse +from collections.abc import Callable, Iterator +from typing import Any + +import httpx +from httpx import Client as Session +from httpx import Request +from pydantic import AnyUrl +from stapi_pydantic import Link + +from .exceptions import APIError + +logger = logging.getLogger(__name__) + + +class StapiIO: + def __init__( + self, + root_url: AnyUrl, + headers: dict[str, str] | None = None, + parameters: dict[str, Any] | None = None, + request_modifier: Callable[[Request], Request] | None = None, + timeout: httpx._types.TimeoutTypes | None = None, + max_retries: int | None = 5, + ): + """Initialize class for API IO + + Args: + headers : Optional dictionary of headers to include in all requests + parameters: Optional dictionary of query string parameters to + include in all requests. + request_modifier: Optional callable that can be used to modify Request + objects before they are sent. If provided, the callable receives a + `request.Request` and must either modify the object directly or return + a new / modified request instance. + timeout: Optional float or (float, float) tuple following the semantics + defined by `Requests + `__. + max_retries: The number of times to retry requests. Set to ``None`` to + disable retries. + + Return: + StapiIO : StapiIO instance + """ + self.root_url = root_url + transport = None + if max_retries is not None: + transport = httpx.HTTPTransport(retries=max_retries) + + self.session = Session(transport=transport, timeout=timeout) + self.update( + headers=headers, + parameters=parameters, + request_modifier=request_modifier, + ) + + def update( + self, + headers: dict[str, str] | None = None, + parameters: dict[str, Any] | None = None, + request_modifier: Callable[[Request], Request] | None = None, + ) -> None: + """Updates this Stapi's headers, parameters, and/or request_modifier. + + Args: + headers : Optional dictionary of headers to include in all requests + parameters: Optional dictionary of query string parameters to + include in all requests. + request_modifier: Optional callable that can be used to modify Request + objects before they are sent. If provided, the callable receives a + `request.Request` and must either modify the object directly or return + a new / modified request instance. + timeout: Optional float or (float, float) tuple following the semantics + defined by `Requests + `__. + """ + self.session.headers.update(headers or {}) + self.session.params.merge(parameters or {}) + self._req_modifier = request_modifier + + def _read_text( + self, + href: str, + method: str = "GET", + headers: dict[str, str] | None = None, + parameters: dict[str, Any] | None = None, + ) -> str: + """Read text from the given URI. + + Overwrites the default method for reading text from a URL or file to allow + :class:`urllib.request.Request` instances as input. This method also raises + any :exc:`urllib.error.HTTPError` exceptions rather than catching + them to allow us to handle different response status codes as needed. + """ + + return self.request(href, method=method, headers=headers, parameters=parameters if parameters else None) + + def request( + self, + href: str, + method: str, + headers: dict[str, str] | None = None, + parameters: dict[str, Any] | None = None, + ) -> str: + """Makes a request to an http endpoint + + Args: + href (str): The request URL + method (Optional[str], optional): The http method to use, 'GET' or 'POST'. + Defaults to None, which will result in 'GET' being used. + headers (Optional[Dict[str, str]], optional): Additional headers to include + in request. Defaults to None. + parameters (Optional[Dict[str, Any]], optional): parameters to send with + request. Defaults to None. + + Raises: + APIError: raised if the server returns an error response + + Return: + str: The decoded response from the endpoint + """ + if method == "POST": + request = Request(method=method, url=href, headers=headers, json=parameters) + else: + request = Request(method=method, url=href, headers=headers, params=parameters if parameters else None) + + modified = self._req_modifier(request) if self._req_modifier else request + + # Log the request details + # NOTE can we mask header values? + msg = f"{modified.method} {modified.url} Headers: {modified.headers}" + if method == "POST" and hasattr(modified, "json"): + msg += f" Payload: {json.dumps(modified.json)}" + logger.debug(msg) + + try: + resp = self.session.send(modified) + except Exception as err: + logger.debug(err) + raise APIError.from_response(resp) + + # NOTE what about other successful status codes? + if resp.status_code != 200: + raise APIError.from_response(resp) + + try: + return resp.text + except Exception as err: + raise APIError(str(err)) + + def read_json(self, endpoint: str, method: str = "GET", parameters: dict[str, Any] | None = None) -> dict[str, Any]: + """Read JSON from a URL. + + Args: + url: The URL to read from + method: The HTTP method to use + parameters: Parameters to include in the request + + Returns: + The parsed JSON response + """ + href = urllib.parse.urljoin(str(self.root_url), endpoint) + text = self._read_text(href, method=method, parameters=parameters) + return json.loads(text) # type: ignore[no-any-return] + + def get_pages( + self, + url: str, + method: str = "GET", + parameters: dict[str, Any] | None = None, + lookup_key: str | None = None, + ) -> Iterator[dict[str, Any]]: + """Iterator that yields dictionaries for each page at a STAPI paging + endpoint. + + # TODO update endpoint examples + + Return: + dict[str, Any] : JSON content from a single page + """ + # TODO update this + + if not lookup_key: + lookup_key = "features" + + page = self.read_json(url, method=method, parameters=parameters) + if not (page.get(lookup_key)): + return None + yield page + + next_link = next((link for link in page.get("links", []) if link["rel"] == "next"), None) + while next_link: + link = Link.model_validate(next_link) + page = self.read_json(str(link.href), method=link.method or "GET") + if not (page.get(lookup_key)): + return None + yield page + + # get the next link and make the next request + next_link = next((link for link in page.get("links", []) if link["rel"] == "next"), None) diff --git a/pystapi-client/src/pystapi_client/version.py b/pystapi-client/src/pystapi_client/version.py new file mode 100644 index 0000000..f102a9c --- /dev/null +++ b/pystapi-client/src/pystapi_client/version.py @@ -0,0 +1 @@ +__version__ = "0.0.1" diff --git a/pystapi-client/src/pystapi_client/warnings.py b/pystapi-client/src/pystapi_client/warnings.py new file mode 100644 index 0000000..ecdb6dc --- /dev/null +++ b/pystapi-client/src/pystapi_client/warnings.py @@ -0,0 +1,96 @@ +import warnings +from collections.abc import Iterator +from contextlib import contextmanager + + +class PystapiClientWarning(UserWarning): + """Base warning class""" + + ... + + +class NoConformsTo(PystapiClientWarning): + """Inform user when client does not have "conformsTo" set""" + + def __str__(self) -> str: + return "Server does not advertise any conformance classes." + + +class DoesNotConformTo(PystapiClientWarning): + """Inform user when client does not conform to extension""" + + def __str__(self) -> str: + return "Server does not conform to {}".format(", ".join(self.args)) + + +class MissingLink(PystapiClientWarning): + """Inform user when link is not found""" + + def __str__(self) -> str: + return "No link with rel='{}' could be found on this {}.".format(*self.args) + + +class FallbackToPystapi(PystapiClientWarning): + """Inform user when falling back to Pystapi implementation""" + + def __str__(self) -> str: + return "Falling back to Pystapi. This might be slow." + + +@contextmanager +def strict() -> Iterator[None]: + """Context manager for raising all Pystapi-client warnings as errors + + For more fine-grained control or to filter warnings in the whole + python session, use the :py:mod:`warnings` module directly. + + Examples: + + >>> from pystapi_client import Client + >>> from pystapi_client.warnings import strict + >>> with strict(): + ... Client.open("https://perfect-api.test") + + For finer-grained control: + + >>> import warnings + >>> from pystapi_client import Client + >>> from pystapi_client.warnings import MissingLink + >>> warnings.filterwarnings("error", category=FallbackToPystapi) + >>> Client.open("https://imperfect-api.test") + """ + + warnings.filterwarnings("error", category=PystapiClientWarning) + try: + yield + finally: + warnings.filterwarnings("default", category=PystapiClientWarning) + + +@contextmanager +def ignore() -> Iterator[None]: + """Context manager for ignoring all pystapi-client warnings + + For more fine-grained control or to set filter warnings in the whole + python session, use the ``warnings`` module directly. + + Examples: + + >>> from pystapi_client import Client + >>> from pystapi_client.warnings import ignore + >>> with ignore(): + ... Client.open("https://perfect-api.test") + + For finer-grained control: + + >>> import warnings + >>> from pystapi_client import Client + >>> from pystapi_client.warnings import MissingLink + >>> warnings.filterwarnings("ignore", category=MissingLink) + >>> Client.open("https://imperfect-api.test") + """ + warnings.filterwarnings("ignore", category=PystapiClientWarning) + try: + yield + finally: + warnings.filterwarnings("default", category=PystapiClientWarning) diff --git a/stapi-fastapi/src/stapi_fastapi/models/product.py b/stapi-fastapi/src/stapi_fastapi/models/product.py index e7a34c3..4fe505d 100644 --- a/stapi-fastapi/src/stapi_fastapi/models/product.py +++ b/stapi-fastapi/src/stapi_fastapi/models/product.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING, Any +from stapi_pydantic import Constraints, OpportunityProperties, OrderParameters from stapi_pydantic import Product as BaseProduct if TYPE_CHECKING: @@ -19,6 +20,11 @@ class Product(BaseProduct): _search_opportunities_async: SearchOpportunitiesAsync | None _get_opportunity_collection: GetOpportunityCollection | None + # we don't want to include these in the model fields + _constraints: type[Constraints] + _opportunity_properties: type[OpportunityProperties] + _order_parameters: type[OrderParameters] + def __init__( self, *args: Any, @@ -26,6 +32,9 @@ def __init__( search_opportunities: SearchOpportunities | None = None, search_opportunities_async: SearchOpportunitiesAsync | None = None, get_opportunity_collection: GetOpportunityCollection | None = None, + constraints: type[Constraints], + opportunity_properties: type[OpportunityProperties], + order_parameters: type[OrderParameters], **kwargs: Any, ) -> None: super().__init__(*args, **kwargs) @@ -40,6 +49,9 @@ def __init__( self._search_opportunities = search_opportunities self._search_opportunities_async = search_opportunities_async self._get_opportunity_collection = get_opportunity_collection + self._constraints = constraints + self._opportunity_properties = opportunity_properties + self._order_parameters = order_parameters @property def create_order(self) -> CreateOrder: @@ -70,3 +82,15 @@ def supports_opportunity_search(self) -> bool: @property def supports_async_opportunity_search(self) -> bool: return self._search_opportunities_async is not None and self._get_opportunity_collection is not None + + @property + def constraints(self) -> type[Constraints]: + return self._constraints + + @property + def opportunity_properties(self) -> type[OpportunityProperties]: + return self._opportunity_properties + + @property + def order_parameters(self) -> type[OrderParameters]: + return self._order_parameters diff --git a/stapi-pydantic/src/stapi_pydantic/__init__.py b/stapi-pydantic/src/stapi_pydantic/__init__.py index bad9aad..2eb24a8 100644 --- a/stapi-pydantic/src/stapi_pydantic/__init__.py +++ b/stapi-pydantic/src/stapi_pydantic/__init__.py @@ -1,4 +1,5 @@ from .conformance import Conformance +from .constraints import Constraints from .datetime_interval import DatetimeInterval from .json_schema_model import JsonSchemaModel from .opportunity import ( @@ -29,6 +30,7 @@ __all__ = [ "Conformance", + "Constraints", "DatetimeInterval", "JsonSchemaModel", "Link", diff --git a/stapi-pydantic/src/stapi_pydantic/product.py b/stapi-pydantic/src/stapi_pydantic/product.py index 4a01415..533a55d 100644 --- a/stapi-pydantic/src/stapi_pydantic/product.py +++ b/stapi-pydantic/src/stapi_pydantic/product.py @@ -3,8 +3,6 @@ from pydantic import AnyHttpUrl, BaseModel, Field -from .opportunity import OpportunityProperties -from .order import OrderParameters from .shared import Link type Constraints = BaseModel @@ -40,37 +38,6 @@ class Product(BaseModel): providers: list[Provider] = Field(default_factory=list) links: list[Link] = Field(default_factory=list) - # we don't want to include these in the model fields - _constraints: type[Constraints] - _opportunity_properties: type[OpportunityProperties] - _order_parameters: type[OrderParameters] - - def __init__( - self, - *args: Any, - constraints: type[Constraints], - opportunity_properties: type[OpportunityProperties], - order_parameters: type[OrderParameters], - **kwargs: Any, - ) -> None: - super().__init__(*args, **kwargs) - - self._constraints = constraints - self._opportunity_properties = opportunity_properties - self._order_parameters = order_parameters - - @property - def constraints(self) -> type[Constraints]: - return self._constraints - - @property - def opportunity_properties(self) -> type[OpportunityProperties]: - return self._opportunity_properties - - @property - def order_parameters(self) -> type[OrderParameters]: - return self._order_parameters - def with_links(self, links: list[Link] | None = None) -> Self: if not links: return self diff --git a/stapi-pydantic/src/stapi_pydantic/py.typed b/stapi-pydantic/src/stapi_pydantic/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/uv.lock b/uv.lock index 931b6dc..8067e61 100644 --- a/uv.lock +++ b/uv.lock @@ -5,6 +5,7 @@ requires-python = ">=3.12" [manifest] members = [ "pystapi", + "pystapi-client", "pystapi-validator", "stapi-fastapi", "stapi-pydantic", @@ -486,15 +487,15 @@ wheels = [ [[package]] name = "hypothesis" -version = "6.130.6" +version = "6.130.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "sortedcontainers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/82/26/01514ec525bbeb2cd166347e5c33e2aa5ec39406fcf24d91a884a6d72e96/hypothesis-6.130.6.tar.gz", hash = "sha256:994abd048924e365dbb8c21e3ea44c2ac576e02fa3d34b04288281768ae83b53", size = 427182 } +sdist = { url = "https://files.pythonhosted.org/packages/aa/70/62c6a31686d23d62aecf3e3ff9a5b8f14f5ef1398a36b5d313709583e5fc/hypothesis-6.130.7.tar.gz", hash = "sha256:5aadc7b661745db0d3e063ae7794c9f14b3647c0f472fb1792006edcba0fe082", size = 427465 } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/18/153434ef2449be66c618695f74eeaec5db438e5a5d1f1ddfbf075d3ffd7b/hypothesis-6.130.6-py3-none-any.whl", hash = "sha256:fe457a716797dbcc9c3dd77fe4164d65f77296010ce5b931910276242a90ed0e", size = 491956 }, + { url = "https://files.pythonhosted.org/packages/67/80/8ca30a540a103a8d745a9a86ad2e88f1b8d3def247fd2179a22fee8218ac/hypothesis-6.130.7-py3-none-any.whl", hash = "sha256:c9f665f43b6b8546e5bf77e38b5ab246b182ac2618b9d9b849c49d4d35add817", size = 492183 }, ] [[package]] @@ -836,56 +837,56 @@ wheels = [ [[package]] name = "multidict" -version = "6.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/99/68/42bf1fb4272959aa7c0775caf53265c1a0da9d77f2d4e76326296c943811/multidict-6.3.0.tar.gz", hash = "sha256:2cf3e0781febf9f093eff3eca2d6dd7954ef2969ff46f6cd95173a4db8397fd8", size = 85840 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/82/60/6905d85104d39092a9f6b21123f1d2df85d30b22dbd01e795b47c9b6eb32/multidict-6.3.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2a83896925e3a2ddec736c48255572e6f15a95339024c02b800dab28e63461a3", size = 61868 }, - { url = "https://files.pythonhosted.org/packages/e6/db/9223dcc59aa26f48e3915e0ce9c31a989a8225e3c794e0d6390772de6f9c/multidict-6.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74e45386528db2d9346618276e8746507ec6ccb91512c5f02d2049ccafb0576e", size = 36835 }, - { url = "https://files.pythonhosted.org/packages/21/1b/f7080011d022bfc68c2b29c329012f2b3d19c446e77a0dc34e0cdfaed768/multidict-6.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bc15e81587b652bbeba9d4d7d3707bcaaa1d7b4aab2024c14be477662003a211", size = 35737 }, - { url = "https://files.pythonhosted.org/packages/91/59/33391241661176e1691add307a72757faecf5ae035e16b93c527f52a1d51/multidict-6.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a716344c6e92551bdf842b999c7d7b34a13e32facf3e6c5942690c9883d45e3a", size = 245422 }, - { url = "https://files.pythonhosted.org/packages/44/4a/e4f70e767067c9fd31e45d625490c128da4d63689ad99342e5e87599e5a9/multidict-6.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:299070f503a9a99e4af38d07da784ced28377cc62b678084b9e2e48fa51c57d3", size = 255425 }, - { url = "https://files.pythonhosted.org/packages/2f/18/6c37d3e336e270cbe1deecf2b5669edde93dfabb15796c3e19362f06da0e/multidict-6.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e87a8635a7db577617b50bd2f2080744ed20e556750b97e4f9988e6d20d3941", size = 251939 }, - { url = "https://files.pythonhosted.org/packages/3b/47/76f9e21ad746262b70bc5992e69c840aec8f1501d3a974cc46678f334cf5/multidict-6.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ab4ea5f49166b990411c522c1f5f901014932ead15a463616ec93e10fff2c05", size = 246197 }, - { url = "https://files.pythonhosted.org/packages/88/4f/52b26ef6fef9b7dcc9083c5c9e3e621f300a06c7bb9a67aa0a167618ddc0/multidict-6.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2cef02d3878804909283549bc10d4789a14c610fcd286f17cd94a195b21fe469", size = 231438 }, - { url = "https://files.pythonhosted.org/packages/09/dc/425c013d902ccad1b666bd06bfd7e2124a7be473e812ab812318858e5194/multidict-6.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a466c14574152b7caed5d76f1e46ae2963d33f5b0eb2dd915fa33870183b0f26", size = 249942 }, - { url = "https://files.pythonhosted.org/packages/18/cb/277fe0a3d83e094716ee436d39a6707496d77c255fd235b0aed1e71deddc/multidict-6.3.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:883c07b404229e98d30b1ef64f07d4e3422d431ecd727c2ebba8549f70b2ba16", size = 243881 }, - { url = "https://files.pythonhosted.org/packages/28/eb/3087bc61a29d62c9637699ed4f6dfc98759bc8e54a899477b1a309a17afb/multidict-6.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:767ed12b1662fac32d84eb43707e4f778b12554fc12ce3c8f7793a02111a9e32", size = 256525 }, - { url = "https://files.pythonhosted.org/packages/10/a7/25f50ca151999ec32f1ade23be06cffe088e7ffe3fe826e05c1dbd84db6d/multidict-6.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:34ef116022119d3053ecce8ba62109c8b54a650a537b183e79de1db4078894a8", size = 252166 }, - { url = "https://files.pythonhosted.org/packages/c6/4d/ac24c83fa6fe19b05b03fb7a3ca283eed5f74c26d238a41411fcd90a0c3a/multidict-6.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ad737617d38c55c0b72cd3ea01bf5cbab0d75123de5e52c39643ddc6465cb5f", size = 248461 }, - { url = "https://files.pythonhosted.org/packages/55/a9/cc2ec0dbdcb252ca461711363089d14a6ae80f97d8f0dcaffa025d1d5d4c/multidict-6.3.0-cp312-cp312-win32.whl", hash = "sha256:3d783be54d076843f58bf061fdaf1071789fb924fb35a0eb84dbc2c8b68499a2", size = 34640 }, - { url = "https://files.pythonhosted.org/packages/9a/20/ad6bf2fd522c0ab35a942e35c8b21bc8197fad6890f66d725eebf03a8770/multidict-6.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:6fbe184451480c17f1f8dac160c9f3f6d243010fdb8416de4d3d7ee69ea65aa4", size = 37969 }, - { url = "https://files.pythonhosted.org/packages/9b/de/988a79bc03f03a191458d938236fb06fa7ba2e03e1fec6ce53c86ababd8a/multidict-6.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90c3c60985e7da13e44aeaaf2e1c10fe1b7825788a18c82b0f9eaeb6c4e9d9c6", size = 61608 }, - { url = "https://files.pythonhosted.org/packages/b8/30/a8e15a3ac94fba52c8a6eb85dc8552b39e60112002317f7542c890dbff15/multidict-6.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:80935f26af0eec490c4e52381a28f39b08c2bc4ef4562448890027e4a4cfa3a4", size = 36724 }, - { url = "https://files.pythonhosted.org/packages/06/49/88d4971e61d98b208c98eec56ae13af6fb128d73fee18e9bb568a7a0415a/multidict-6.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e18db3b38ac30d8cc1ddbdfc7f6d3d814c1abb8936c57bd1c09c5da02873c8c4", size = 35611 }, - { url = "https://files.pythonhosted.org/packages/e6/1b/08ba37f64d4eacfceec12cc11aecd0a6482cca2c57a94dafef41ed66dd0a/multidict-6.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c8836280abc310ea6260dac93126645d21284885008c317c2ac4281a90f1398", size = 245274 }, - { url = "https://files.pythonhosted.org/packages/bb/d9/f5c5a381cffef4bf500e710ca73d6ef00a2de9647abf7bcd0a9f032dd408/multidict-6.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5737f40ffb1926b8bf38d32fdac48a27c441b757f1bf44cbaa100dafef049a01", size = 256891 }, - { url = "https://files.pythonhosted.org/packages/17/23/553528d531fd8d93834365d8e6b7c0bda25c787a8b5ae738099266f34bd7/multidict-6.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6df1b707adab4858dfaa74877f60f07d9d6be01a57015cf4e62aa8f00f3576b", size = 253116 }, - { url = "https://files.pythonhosted.org/packages/07/d0/79229446cb1507ff5f83ae372a4648e703fda7a4f7729332da0858d47e4e/multidict-6.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:162af9c2e0743465a47e2b32d9b0a7c260b7843629b5e6f0a8a92819b5a40d27", size = 245941 }, - { url = "https://files.pythonhosted.org/packages/d4/c9/91b09bda811e212816776967a3232f8776aa846af4c44e0e9139cf73fc60/multidict-6.3.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc4e82c5db4e65703c842b0947699dd958a7262a8b854d1c19bbfe2d27be9333", size = 232343 }, - { url = "https://files.pythonhosted.org/packages/c4/94/2941f8605a3ff8aaaef31c1c8adfd7c889d78763efc4e7f963fbca96a6c4/multidict-6.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5a922601cb94f427dd96eec3a7c776537ce7490f2beb69e88a81859e537343e4", size = 249610 }, - { url = "https://files.pythonhosted.org/packages/e3/8b/04a18732ab7d29db3d6009d8cab1a737c3262cd06ba1764756edb66d9a96/multidict-6.3.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3da30fb560b37cede31306b890c982213a23fa7335d368cdc16cf7034170860b", size = 244832 }, - { url = "https://files.pythonhosted.org/packages/67/27/4fafdc178bb7c5a870ca449e922a0e069b77fda1ba4e1729fde385ca6314/multidict-6.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5a1996d963016e6486b6a672f64f868e6b4e7e9e2caf710994df11b04790903e", size = 256546 }, - { url = "https://files.pythonhosted.org/packages/a6/09/f2c0d6974d1b3ac922834eb159d39f4a7f61b4560373821e5028623645a1/multidict-6.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9584441b7a603a12aa382adf8c093ddc5a22328a108dabc6a4a112fa5b4facae", size = 252193 }, - { url = "https://files.pythonhosted.org/packages/e1/6b/7b2ec53aea30c3729ac6bd92bcc620584b08e1333621e0fe48dc5dc36fdb/multidict-6.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:71a8ce430f6c341725aefc0031626c484e0858fd95d1bf3625e0d27919d8edca", size = 247797 }, - { url = "https://files.pythonhosted.org/packages/fa/07/086ac59a24e05ba5748abc57298a27705bab824f47842494dfa4b50bff15/multidict-6.3.0-cp313-cp313-win32.whl", hash = "sha256:b7d3053742a9a559dda8598a52e0c1bcbd18258cc199cba52137ce8c8e92c537", size = 34662 }, - { url = "https://files.pythonhosted.org/packages/cf/a3/5e0b74e8c1507623b7564fa8bfd07e626d45fc05fbb03f6248902c00c749/multidict-6.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:6b7e30e4246c2cd5e6c2c907486d235e4f3e8a39979744e9e0b8090629c62da4", size = 37826 }, - { url = "https://files.pythonhosted.org/packages/33/0c/e92a3398e80339e356e7aa8b2566d075ed876f5c12e9ad08704c49301a1d/multidict-6.3.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:be034aa2841c57ea43a55bc123d8f3f41cc2d3d102a22390c863791ba6bf23f1", size = 66383 }, - { url = "https://files.pythonhosted.org/packages/02/23/21ea785c2bbd36ad832e4365ac518bc7c14c72cc8be117fccb853ac3ee1f/multidict-6.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:168a02178b7e980899f3475ff008436eaab035d50e39cb8f7de641bbe9bbc3a6", size = 38709 }, - { url = "https://files.pythonhosted.org/packages/42/7b/52f65ed679b25e16a453bbacc06892622710ad3fc31cfa5c61f862af99fd/multidict-6.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c4a6ec469c30f4162745d6485b778432c497032b8a2cb3a0332eea9d3b6aef6", size = 38314 }, - { url = "https://files.pythonhosted.org/packages/2e/5b/923e8b22268e53be4f11d0abe3f7b091e5ee7d213e7ca837f215cbc22bdb/multidict-6.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b293804f4b53be6297bc2afdeaf15aff76c1b6be69f3a3da785143eebdfb656", size = 244220 }, - { url = "https://files.pythonhosted.org/packages/1f/9c/d0c515f234f2958771c7463f6a03383d36e4074f1eb00459ec3c7190e8dd/multidict-6.3.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5eb2a8415a891564054447a327a7ef7ec90e69e0e63d85d1ffb03f82e102c740", size = 243508 }, - { url = "https://files.pythonhosted.org/packages/64/41/9a1a3308b4b99302a4502758baba6bb79c853332f26ef5a90968737d4563/multidict-6.3.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db61c6ae9ee7268dc69a078ea22deaaf861350ad2e4c194c70798b8ec9789131", size = 238985 }, - { url = "https://files.pythonhosted.org/packages/40/68/fba5926f53ff3e7b19799399bd82e27cb3df5d569839d07b6b42827194f1/multidict-6.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88548dec2f6f69e547252fe3113bf1b3b08c0879f5b44633927be07ed18c5cc0", size = 238675 }, - { url = "https://files.pythonhosted.org/packages/f2/c6/3a5160331b9842905a3e8ae81527068318c9f6ebddfe7ed07853b97ba216/multidict-6.3.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:864684b36e0488715aac531785abe7514e872bfe83ecc768828e9ddaadeed320", size = 225747 }, - { url = "https://files.pythonhosted.org/packages/ed/0d/cc98fde65ee79beb0632c8d2f7a9e639e5175885d7c7ac2400f56bc78f73/multidict-6.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:71d92e846541669ae1d11e00906c408c1dc9890196e13f11d4d62bd235ac9ddb", size = 240611 }, - { url = "https://files.pythonhosted.org/packages/f6/73/580793855a587663b2e26aa9fa2fba3d16dbce26aff4cb92d48ae4814ff0/multidict-6.3.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c5223ab45639ce93c7233d518f6c3199422b49dbd0ebfb1d7917b5da2636712e", size = 227815 }, - { url = "https://files.pythonhosted.org/packages/90/a6/ba293045efd338e4131726d7226c9d0870568486a6d025ec20dbf79f3972/multidict-6.3.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:56225de73d69f5ee6b783648eb1936e1bbe874a529cb1e15d64038904c54efb2", size = 239895 }, - { url = "https://files.pythonhosted.org/packages/4b/4a/ecda417af238696daafe921fbbdc96fa7e829656206442a785174377c61d/multidict-6.3.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:66c108d8e02da2febb0aa7d7002e14c4a0571460993c9edf8249393cdae7eeef", size = 233297 }, - { url = "https://files.pythonhosted.org/packages/73/90/8cea643f4e9b7f9c73b72032aa38f765e96db07636ea4b00f0420d9f6a5f/multidict-6.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b95d96a0640decaa24cd9cf386fd4d8a96c973aafa42dd9c65609f9f0d66cc34", size = 232030 }, - { url = "https://files.pythonhosted.org/packages/1b/37/8d42820299fbfbc774ed8247a75b16dfe2f09c4e0d1ae62ac751b6c25397/multidict-6.3.0-cp313-cp313t-win32.whl", hash = "sha256:6b25953a1d6a97746becbd663b49e3b436a5001c995a62662d65835a2ba996a7", size = 41166 }, - { url = "https://files.pythonhosted.org/packages/74/63/44bb663fdd4da768d55fb0406daa50e2ea1904da014a0972068e6a7e44d1/multidict-6.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d9c2b1ca98e5454b78cd434f29fc33eb8f8a2f343efc5f975225d92070b9f7f6", size = 44929 }, - { url = "https://files.pythonhosted.org/packages/65/66/730bb6dbfbf87df8a341707ebd468044ea6c530605d41b3f31b494f03d6a/multidict-6.3.0-py3-none-any.whl", hash = "sha256:9ca652d9c6f68535537d75502b549ed0ca07fa6d3908f84f29f92148ec7310f2", size = 10266 }, +version = "6.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/4e0e25aabd94f289b7d47da8293a3563e73ac1a4f7e9caddf11b6eeaf52d/multidict-6.3.1.tar.gz", hash = "sha256:3e18d6afe3f855736022748606def2000af18e90253fb8b4d698b51f61e21283", size = 86832 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/74/acf7ecc3eb1195b3930008cfaae9ffbc54c71a5582a0c86db291307dd7cf/multidict-6.3.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9679106633695b132ebc9191ec6230bfb1415d37c483833fcef2b35a2e8665ec", size = 62598 }, + { url = "https://files.pythonhosted.org/packages/d3/78/544442c1bb61986e5cbd529bfb7038b38f88fc39fe17962bc63bb0013199/multidict-6.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:73a43b3b2409aa395cce91b7471cff6b45814548063b18412162ba2222084201", size = 37323 }, + { url = "https://files.pythonhosted.org/packages/f5/d1/8260a5da38ac65e885a0adbd40b50113e6f9e8eeb94b91aeeac08e4ad36e/multidict-6.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1ce924e24c4f1c014f2ed8782e82a5232d5f61293fc5c204d8569f451191ffa8", size = 36053 }, + { url = "https://files.pythonhosted.org/packages/e7/51/cceca6f30954620b9b03f96e06a54f908367cf86ae0c61a3472aa3de9363/multidict-6.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:123b1d48eeed2ac1126be078deb88006f871559787cefc8759a884442a6f2cdc", size = 244641 }, + { url = "https://files.pythonhosted.org/packages/08/df/fb512545fa3cbf20f870175a4698c6ba58abf261ab19faf28d293e39621d/multidict-6.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d98447906885e7f0f90456cde1d14ff41f30d9d7e127ab7140a45e784a0ff1b", size = 255743 }, + { url = "https://files.pythonhosted.org/packages/65/fb/a7648f5764e25e0a18e7d3bdda9fd67e86bb7e0c70a6ffee0348e1fb493f/multidict-6.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5072a9efe7f7f79d3dff1f26ac41e4893478f85ce55fe5318625f7eb703d76f8", size = 252217 }, + { url = "https://files.pythonhosted.org/packages/c0/98/32ef5e26956ee8cd7af8d7367b9da11f5a211438498e854bd5385e8eb812/multidict-6.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbc825b34595fe43966242e30b54d29617013e51b4310134aa2c16c3b3d00c91", size = 245195 }, + { url = "https://files.pythonhosted.org/packages/4d/e1/a8cf44bef56bee949ec68d993ecbc4b713338b3137fa42416f0e34a46c48/multidict-6.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:baec41c191855f92507f9e0bb182eea7eea5992d649f9c712c96a38076e59d00", size = 232457 }, + { url = "https://files.pythonhosted.org/packages/08/7c/81e91ef84b5df88d4780bcd03b08df423668b61ada7b387e0482ac19690a/multidict-6.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:eacd4036bb3d632828702a076458d660b53d12e049155eaeb7d11a91242d67b8", size = 252077 }, + { url = "https://files.pythonhosted.org/packages/6c/00/32b94a1b060f602aa9056189febefddfaf6cece4c6f4c5873668011fd67b/multidict-6.3.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:284737db826cc00fbd5292225717492f037afa404a2ddfea812cfbef7a3f0e93", size = 247211 }, + { url = "https://files.pythonhosted.org/packages/9c/78/9994ab4cc9b18c48e089f08c85028ea8c60ffa0d5868d7e42c842b9ca80e/multidict-6.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd121433f5d8707379f4fc0e6b4bf67b0b7cd1a7132e097ead2713c8d661a41", size = 260515 }, + { url = "https://files.pythonhosted.org/packages/86/aa/d40c0bce043fa2903e7d3f9e5a2402fd55850933bc81f86a08efe78e303d/multidict-6.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f94d64672487570c7c2bbcff74311055066e013545714b938786843eb54ef8", size = 255476 }, + { url = "https://files.pythonhosted.org/packages/86/1b/20580f901b260c2d6733e5ec3e1e227e04330de966b567b3f3e102567bd0/multidict-6.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:739fe3fde8b8aca7219048f8bda17901afb8710c93307dc0d740014d3481b36b", size = 251311 }, + { url = "https://files.pythonhosted.org/packages/9c/44/db7cb84b604ee1912d5ba908def729adc60413448e789247db98992c149f/multidict-6.3.1-cp312-cp312-win32.whl", hash = "sha256:891a94a056de2d904cc30f40ec1d111aebb09abd33089a34631ff5a19e0167b2", size = 35031 }, + { url = "https://files.pythonhosted.org/packages/32/bb/2931b3d6a2b57b5a7dbb819c2b5c55d3170c54098009224872d4b6ae40d3/multidict-6.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:d9844e0f93405a9c5bc2106d48cf82e022e18685baebea74cc5057ca2009799e", size = 38453 }, + { url = "https://files.pythonhosted.org/packages/da/00/ceaaf722b2c1fc29ba43ef52849573bbd9433e806209c1fd47c267b4c2be/multidict-6.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8f2c9b2d2084efab0ef8b694dab55ab3675359e00136e6707bef96be19db5ed9", size = 62288 }, + { url = "https://files.pythonhosted.org/packages/29/66/7ad79357e23eb5fa8cd2e2bde7f0c246da4e778e0962f9429dfa6176ae7b/multidict-6.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9678f5ecb8dc7380c11342ea3f9c2a43b90d1c2a081f22eb428bb923816ca94e", size = 37203 }, + { url = "https://files.pythonhosted.org/packages/2d/16/7adaab04efcf147654c798c9a46ec2803069679de741eba1c45a01e3dd2a/multidict-6.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6c242643c6c6a256ce220da3058c7c6074b62769136a8b463556f06ba88274b", size = 35889 }, + { url = "https://files.pythonhosted.org/packages/62/9a/dc7352fb5c8cbc4cddedef03bcc6623df85d11381984d438a288102e08fa/multidict-6.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:398f80e44df494d9a2319417cb560e86aab10f1feea48099bed0fdbd2e17d640", size = 245361 }, + { url = "https://files.pythonhosted.org/packages/05/f8/5cc4cc199822d5c17c7f937af54017423d13f8fba86ec377b485dc0f53c7/multidict-6.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fdc33b92c558bb3f037e638ad4607e2765ceab5345762c82c4562328066ea4d8", size = 254804 }, + { url = "https://files.pythonhosted.org/packages/43/4f/04d751772bc648b6dfe2d326205c30435608940b9d6a49273ab574c65e29/multidict-6.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da49eb28c5961dc0f66e020218be69681b2d0b04abfbce1d7541f2ed15321be0", size = 250507 }, + { url = "https://files.pythonhosted.org/packages/a0/e9/93f9dd0c9a4ab78e3ecb795949548e44090df7c29ed05b4b891d57838f95/multidict-6.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a5359269504f9ca8e88d68ce25eb523c9b599c843039708ca0eb1575cf55a9d", size = 246170 }, + { url = "https://files.pythonhosted.org/packages/6d/ef/405e6285f21ced0a96064fc7911dd1c1ad012df1fbcdcb26f88d93ae69e3/multidict-6.3.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1dcbca567085f19605e0c613b4ecca9bb1fa65325e4ace41b00cd383fb1c9573", size = 231247 }, + { url = "https://files.pythonhosted.org/packages/e5/94/ae85d369a85cb53bb3d02ca81a70b11089e0855dfc9098beab2e83412aef/multidict-6.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2552809ad6b6e3b3ac5ce4b16d1b641ad4f33f309747a7ba79bb433505796a6d", size = 248485 }, + { url = "https://files.pythonhosted.org/packages/23/9d/fa577db8c531b67934605e1661e50bdd1a36abe0fb6974bd600e333c0a8c/multidict-6.3.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d31c72dd409ffc18ca255241fb928575a6921b3876220c3602487ebf32cecf29", size = 244693 }, + { url = "https://files.pythonhosted.org/packages/a0/5d/3eaf0a7636bc040f22d727aa7c0e5fe2167ea066ac1720beb38f2e4d830c/multidict-6.3.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c712dbaaa609f57100c475904ebd14dde27040bb52b2f08d0393da059e35ec2a", size = 257817 }, + { url = "https://files.pythonhosted.org/packages/c0/d2/89bc641897b4702f69933b8ca9ce8470bf321db54b535a458a443210f83b/multidict-6.3.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c5821bebe862283dc349f2198a617085b7179d9324971c13c7692f5da8951c2e", size = 253148 }, + { url = "https://files.pythonhosted.org/packages/05/d4/04954d1c1d9ea2db24fe9925a3b91d593545c1548b71a9fa6ba096f71b8e/multidict-6.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a566f0b8788ad5c5cc42a0b228bf1cd8f20053ff8cb8149f610763750aa6590f", size = 248573 }, + { url = "https://files.pythonhosted.org/packages/4e/ad/595c9924c8eb3ebc3fc20bc4c47127b0bcbe6f04ee0bd835107b756079f0/multidict-6.3.1-cp313-cp313-win32.whl", hash = "sha256:0c2f11f250355d1dea7b1f09b8245c3b7a0ca99bc089670d072960a6d66d86e4", size = 35136 }, + { url = "https://files.pythonhosted.org/packages/08/74/3c7741a5b5163930411ca8ad9175523867a453ea4bbac5a3b0391739b91d/multidict-6.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:3ed86bec95a958fa789ed824cd756df0e44c2a137947c9baff9d6127c69dd697", size = 38400 }, + { url = "https://files.pythonhosted.org/packages/76/ca/c47712942358a90ee165d767659dd0cc894f27650e150e2ff4c2e4e4fe40/multidict-6.3.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:231c371ff30162f1c4019e252147c6d25b3bb5412c5ef1574edafea360a2ad63", size = 67360 }, + { url = "https://files.pythonhosted.org/packages/97/19/be38c395a46b9a1ca916eeea0d0d8076f672e0aab7e5d76a1bdb41e1ca94/multidict-6.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2fb4c7b9204ba463f8377ba3dc5990803a89ee34b34d3dd81c6f94a25f8b4791", size = 39338 }, + { url = "https://files.pythonhosted.org/packages/10/5b/3c520efe98e8beb2ca15f8e704587785fe401258ebb504d840498a1a4341/multidict-6.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c8e12b28f045392ec9a97eedcd8518ef060571df393f7c7e31a1fb59a902030e", size = 38755 }, + { url = "https://files.pythonhosted.org/packages/0c/7a/94433201cacef2d034193154d0519c8b89bd2c1d86c4da381ff9c5787484/multidict-6.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4587038ad21ac72e5ab3abba56aa6f5b3fe6039135a8041d15e18db036900a4d", size = 245611 }, + { url = "https://files.pythonhosted.org/packages/6d/47/9e697aa84193ed804f6990a1e77f6f403128ea1cf99a75a0b830d5947029/multidict-6.3.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:729d427085527fd1a016347847221b9628eeb91843c0531ba46e773a80f13b14", size = 250127 }, + { url = "https://files.pythonhosted.org/packages/ba/b4/b40b9ebd338ce5fdd0fc7de58a8c22efc884f65e15efa436739ae2007578/multidict-6.3.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:358edbfb2977965fd04b60d82a852ed080a150580723445580e11926ec887453", size = 245484 }, + { url = "https://files.pythonhosted.org/packages/0d/79/9e26356052d7eb8c854ed9ef30dc42aa576b565aca295bc78e576b3379a2/multidict-6.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc1df4fa44282961591b7db3858be878a99f42f9354b3aa0175764070c885fe2", size = 241555 }, + { url = "https://files.pythonhosted.org/packages/ff/73/284e8dc637065bd80709615674d34c87413d0e960718afa1f6349ea72f31/multidict-6.3.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d323eda78c74991efa910cac3034146f5e5012ac052d148e9bb60da177b9eda", size = 228361 }, + { url = "https://files.pythonhosted.org/packages/97/32/ec59821658f9b91cd049c1caf898dea767c1f1b4c48e0c8945f7b4a37633/multidict-6.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:badb5a21861bfa8865f2883339a218ee6268fe98e22c9c6f44b72a2c3e9fa5df", size = 247852 }, + { url = "https://files.pythonhosted.org/packages/1f/b6/526f9002d4ec28c87f1ffe12b6e23bdb17af6a5ec86a49311849bff07cd8/multidict-6.3.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4de22e401b2d55643585d97e5b91940fb590f4d6fdd63da35519ee1e58c7b892", size = 236858 }, + { url = "https://files.pythonhosted.org/packages/b0/07/a07edec8bf2b1195bb7f69a795a2e912acb47c056811bfbb48b389135f54/multidict-6.3.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:8b5bf828804b5bd20425432e61ff6d9e782943e8e52550541735882945447c72", size = 250400 }, + { url = "https://files.pythonhosted.org/packages/12/4c/a812796d3685c6698ceb68c09d354638f9b4a2c67965d42c19d3fc4cbb51/multidict-6.3.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:c7ef102290eb42190b6c155f76eb5824a2f015ad57e64e828b5960fa86dc5e12", size = 244087 }, + { url = "https://files.pythonhosted.org/packages/04/10/079e62a566d7ff190988ac0e3030f283280e18ec2d2fe99a9e8380336a6e/multidict-6.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b4bc8042eef9222e8379cb0663c1213062d9e92183274acc7fd0ca5cdc0af670", size = 242495 }, + { url = "https://files.pythonhosted.org/packages/ac/7b/26a0a8e39dafbac038aed0bc59bc59a5f6e650f23c23746ddfacbbf78cc3/multidict-6.3.1-cp313-cp313t-win32.whl", hash = "sha256:d839731885fe00068d570f815e28c2712d0221db1dfb7b70239b303a56c54410", size = 41754 }, + { url = "https://files.pythonhosted.org/packages/eb/bd/9b1b9266b93ab238ab5afdbaaeb7f78621877429b178dc34818409afb10c/multidict-6.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:ea0db870c1729a85a98a34f92e5a4806521ac8af9b0ebd9cd3dbf9d5e493657f", size = 45441 }, + { url = "https://files.pythonhosted.org/packages/57/99/624da94f4deb41a75b5e08752270ecdb1ff871c1b1539705c0aef02aa7a2/multidict-6.3.1-py3-none-any.whl", hash = "sha256:2d45b070b33fa1d0a9a7650469997713e3a4f5cd9eb564332d5d0206cf61efc5", size = 10348 }, ] [[package]] @@ -1233,6 +1234,7 @@ name = "pystapi" version = "0.0.0" source = { virtual = "." } dependencies = [ + { name = "pystapi-client" }, { name = "pystapi-validator" }, { name = "stapi-fastapi" }, { name = "stapi-pydantic" }, @@ -1255,6 +1257,7 @@ docs = [ [package.metadata] requires-dist = [ + { name = "pystapi-client", editable = "pystapi-client" }, { name = "pystapi-validator", editable = "pystapi-validator" }, { name = "stapi-fastapi", editable = "stapi-fastapi" }, { name = "stapi-pydantic", editable = "stapi-pydantic" }, @@ -1276,6 +1279,23 @@ docs = [ { name = "mkdocstrings-python", specifier = ">=1.16.8" }, ] +[[package]] +name = "pystapi-client" +version = "0.0.1" +source = { editable = "pystapi-client" } +dependencies = [ + { name = "httpx" }, + { name = "python-dateutil" }, + { name = "stapi-pydantic" }, +] + +[package.metadata] +requires-dist = [ + { name = "httpx", specifier = ">=0.28.1" }, + { name = "python-dateutil", specifier = ">=2.8.2" }, + { name = "stapi-pydantic", editable = "stapi-pydantic" }, +] + [[package]] name = "pystapi-validator" version = "0.1.0"