Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/pystapi-client/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# API

::: pystapi-client
1 change: 1 addition & 0 deletions docs/pystapi-client/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# pystapi-client
13 changes: 0 additions & 13 deletions docs/stapi-client/index.md

This file was deleted.

2 changes: 1 addition & 1 deletion docs/stapi-pydantic/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
!!! note

This repository intentionally has no input/output (IO) functionality.
For making requests to a STAPI API, use **[pystapi-client](../stapi-client/index.md)**.
For making requests to a STAPI API, use **[pystapi-client](../pystapi-client/index.md)**.
5 changes: 3 additions & 2 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ theme:

nav:
- index.md
- pystapi-client:
- pystapi-client/index.md
- pystapi-client/api.md
- stapi-pydantic:
- stapi-pydantic/index.md
- stapi-pydantic/api.md
- stapi-client:
- stapi-client/index.md

plugins:
- mkdocstrings:
Expand Down
30 changes: 30 additions & 0 deletions pystapi-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,40 @@ A Python client for working with [STAPI](https://stapi-spec.github.io/pystapi/)
Install from PyPi.
Other than [stapi-pydantic](https://stapi-spec.github.io/pystapi/stapi-pydantic/) itself, the only dependencies for **pystapi-client** are the Python [httpx](https://www.python-httpx.org/) and [python-dateutil](https://dateutil.readthedocs.io) libraries.

```shell
python -m pip install pystapi-client
```

## Development

See the instructions in the [pystapi monorepo](https://github.com/stapi-spec/pystapi?tab=readme-ov-file#development).

## Documentation

See the [documentation page](https://stapi-spec.github.io/pystapi/stapi-client/) for the latest docs.

## Usage Example

The `pystapi_client.Client` class is the main interface for working with services that conform to the STAPI API spec.

```python
from pystapi_client import Client

# Initialize client
client = Client.open("https://api.example.com/stapi")

# List all products
products = list(client.get_products())

# Get specific product
product = client.get_product("test-spotlight")

# List all Opportunities for a Product
opportunities = client.get_product_opportunities("test-spotlight")

# List orders
orders = client.get_orders()

# Get specific order
order = client.get_order("test-order")
```
3 changes: 2 additions & 1 deletion pystapi-client/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ description = "Python library for searching Satellite Tasking API (STAPI) APIs."
readme = "README.md"
authors = [
{ name = "Kaveh Karimi-Asli", email = "[email protected]" },
{ name = "Philip Weiss", email = "[email protected]" }
{ name = "Philip Weiss", email = "[email protected]" },
{ name = "Stella Reinhardt", email = "[email protected]"}
]
maintainers = [{ name = "Pete Gadomski", email = "[email protected]" }]
keywords = ["stapi"]
Expand Down
104 changes: 74 additions & 30 deletions pystapi-client/src/pystapi_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,31 +67,33 @@ def open(
request_modifier: Callable[[Request], Request] | None = None,
timeout: TimeoutTypes | None = None,
) -> "Client":
"""Opens a STAPI API 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.
url: The URL of a STAPI API
headers: Optional 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
include in all requests
request_modifier: Optional callable that 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.
of :class:`httpx.Request`.
If the callable returns a `httpx.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
<https://requests.readthedocs.io/en/latest/api/#main-interface>`__.
and return `None`
timeout: Optional timeout configuration. Can be:
- None to disable timeouts
- float for a default timeout
- tuple of (connect, read, write, pool) timeouts, each being float or None
- httpx.Timeout instance for fine-grained control
See `httpx timeouts <https://www.python-httpx.org/advanced/timeouts/>`__
for details

Return:
client : A :class:`Client` instance for this STAPI API
Returns:
Client: A :class:`Client` instance for this STAPI API
"""
stapi_io = StapiIO(
root_url=AnyUrl(url),
Expand All @@ -118,11 +120,11 @@ def get_single_link(
"""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.
rel: Optional relationship filter. If set, only links matching this
relationship are considered.
media_type: Optional media type filter. If set, only links matching
this media type are considered. Can be a single value or an
iterable of values.

Returns:
:class:`~stapi_pydantic.Link` | None: First link that matches ``rel``
Expand Down Expand Up @@ -161,6 +163,15 @@ def read_links(self) -> None:
]

def read_conformance(self) -> None:
"""Read the API conformance from the root of the STAPI API.

The conformance is stored in `Client.conforms_to`. This method attempts to read
from "/conformance" endpoint first, then falls back to the root endpoint "/".

Note:
This method silently continues if endpoints return APIError, no exceptions
are raised.
"""
conformance: list[str] = []
for endpoint in ["/conformance", "/"]:
try:
Expand All @@ -173,22 +184,26 @@ def read_conformance(self) -> None:
self.set_conforms_to(conformance)

def has_conforms_to(self) -> bool:
"""Whether server contains list of ``"conformsTo"`` URIs"""
"""Whether server contains list of ``"conformsTo"`` URIs

Return:
Whether the 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
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
conformance_uris: URIs indicating what the server conforms to
"""
self.conforms_to = conformance_uris

Expand All @@ -204,7 +219,7 @@ def add_conforms_to(self, name: str) -> None:
"""Add ``"conformsTo"`` by name.

Args:
name : name of :py:class:`ConformanceClasses` keys to add.
name: Name of :py:class:`ConformanceClasses` keys to add.
"""
conformance_class = ConformanceClasses.get_by_name(name)

Expand All @@ -215,7 +230,7 @@ def remove_conforms_to(self, name: str) -> None:
"""Remove ``"conformsTo"`` by name.

Args:
name : name of :py:class:`ConformanceClasses` keys to remove.
name: Name of :py:class:`ConformanceClasses` keys to remove.
"""
conformance_class = ConformanceClasses.get_by_name(name)

Expand All @@ -230,21 +245,24 @@ def has_conformance(self, conformance_class: ConformanceClasses | str) -> bool:
provides such an endpoint.

Args:
name : name of :py:class:`ConformanceClasses` keys to check
conformance against.
conformance_class: Either a ConformanceClasses enum member or a
string name of a conformance class to check against


Return:
bool: Indicates if the API conforms to the given spec or URI.
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:
"""Check if the API supports opportunities"""
return self.has_conformance(ConformanceClasses.OPPORTUNITIES)

def _supports_async_opportunities(self) -> bool:
"""Check if the API supports asynchronous opportunities"""
return self.has_conformance(ConformanceClasses.ASYNC_OPPORTUNITIES)

def get_products(self, limit: int | None = None) -> Iterator[Product]:
Expand Down Expand Up @@ -346,6 +364,17 @@ def create_product_order(self, product_id: str, order_parameters: OrderPayload)
return Order.model_validate(product_order_json)

def _get_products_href(self, product_id: str | None = None, subpath: str | None = None) -> str:
"""Get the href for the products endpoint

Args:
product_id: Optional product ID to get the href for

Returns:
str: The href for the products endpoint

Raises:
ValueError: When no products link is found in the API
"""
product_link = self.get_single_link("products")
if product_link is None:
raise ValueError("No products link found")
Expand All @@ -368,6 +397,9 @@ def get_orders(self, limit: int | None = None) -> Iterator[Order]: # type: igno
# TODO Update return type after the pydantic model generic type is fixed
"""Get orders from this STAPI API

Args:
limit: Optional limit on the number of orders to return

Returns:
Iterator[Order]: An iterator of STAPI Orders
"""
Expand Down Expand Up @@ -395,7 +427,7 @@ def get_order(self, order_id: str) -> Order: # type: ignore[type-arg]
Order: A STAPI Order

Raises:
ValueError if order_id does not exist.
ValueError: When the specified order_id does not exist
"""

order_endpoint = self._get_orders_href(order_id)
Expand All @@ -407,6 +439,18 @@ def get_order(self, order_id: str) -> Order: # type: ignore[type-arg]
return Order.model_validate(order_json)

def _get_orders_href(self, order_id: str | None = None) -> str:
"""Get the href for the orders endpoint

Args:
order_id: Optional order ID to get the href for

Returns:
The href for the orders endpoint

Raises:
ValueError: When no orders link is found in the API
"""

order_link = self.get_single_link("orders")
if order_link is None:
raise ValueError("No orders link found")
Expand Down
Loading