diff --git a/pyproject.toml b/pyproject.toml index a7af602..0d227cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ dependencies = [ "pystapi-client", "pystapi-validator", "stapi-pydantic", - "stapi-fastapi" + "stapi-fastapi", ] [dependency-groups] @@ -21,6 +21,7 @@ dev = [ "pre-commit>=4.2.0", "pre-commit-hooks>=5.0.0", "fastapi[standard]>=0.115.12", + "types-click>=7.1.8", "pygithub>=2.6.1", "respx>=0.22.0", ] diff --git a/pystapi-client/pyproject.toml b/pystapi-client/pyproject.toml index 729a05a..ee8a3cb 100644 --- a/pystapi-client/pyproject.toml +++ b/pystapi-client/pyproject.toml @@ -16,10 +16,11 @@ dependencies = [ "httpx>=0.28.1", "stapi-pydantic", "python-dateutil>=2.8.2", + "click>=8.1.8", ] [project.scripts] -stapi-client = "pystapi_client.cli:cli" +stapi = "pystapi_client.scripts.cli:cli" [tool.uv.sources] stapi-pydantic = { workspace = true } diff --git a/pystapi-client/src/pystapi_client/client.py b/pystapi-client/src/pystapi_client/client.py index aecad50..d38c452 100644 --- a/pystapi-client/src/pystapi_client/client.py +++ b/pystapi-client/src/pystapi_client/client.py @@ -25,7 +25,7 @@ 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 +from pystapi_client.warns import NoConformsTo DEFAULT_LINKS = [ { diff --git a/pystapi-client/src/pystapi_client/scripts/cli.py b/pystapi-client/src/pystapi_client/scripts/cli.py new file mode 100644 index 0000000..8d66154 --- /dev/null +++ b/pystapi-client/src/pystapi_client/scripts/cli.py @@ -0,0 +1,140 @@ +import itertools +import json + +import click +from pystapi_client.client import Client +from pystapi_client.exceptions import APIError + +CONTEXT_SETTINGS = dict(default_map={"cli": {"url": "http://localhost:8000"}}) + + +@click.group(context_settings=CONTEXT_SETTINGS) +@click.option("--url", type=str, required=True, help="Base URL for STAPI server") +@click.pass_context +def cli(ctx: click.Context, url: str) -> None: + """Command line interface for STAPI client. Group ensures client is created.""" + + client = Client.open(url) + ctx.obj = {"client": client} + + +@click.command() +@click.pass_context +@click.option("--max-items", "max_items", type=click.IntRange(min=1), help="Max number of products to display") +@click.option("--limit", type=click.IntRange(min=1), help="Limit number of products to request") +def products(ctx: click.Context, limit: int | None, max_items: int | None) -> None: + """List products.""" + + client: Client = ctx.obj["client"] + + products_iter = client.get_products(limit=limit) + + if max_items: + products_iter = itertools.islice(products_iter, max_items) + + products_list = list(products_iter) + if len(products_list) == 0: + click.echo("No products found.") + return + + # Serialize the products list into JSON format and output it + click.echo(json.dumps([p.model_dump(mode="json") for p in products_list])) + + +@click.command() +@click.pass_context +@click.option("--id", type=str, required=True, help="Product ID to retrieve") +def product(ctx: click.Context, id: str) -> None: + """Get product by ID.""" + + client: Client = ctx.obj["client"] + + product = client.get_product(product_id=id) + if not product: + click.echo(f"Product {id} not found.", err=True) + return + + click.echo(product.model_dump_json()) + + +@click.command() +@click.pass_context +@click.option("--max-items", "max_items", type=click.IntRange(min=1), help="Max number of products to display") +@click.option("--limit", type=click.IntRange(min=1), help="Limit number of products to request") +def orders(ctx: click.Context, max_items: int | None, limit: int | None) -> None: + """List orders.""" + + client: Client = ctx.obj["client"] + + orders_iter = client.get_orders(limit=limit) + + if max_items: + orders_iter = itertools.islice(orders_iter, max_items) + + orders_list = list(orders_iter) + if len(orders_list) == 0: + click.echo("No orders found.", err=True) + return + + # Serialize the orders list into JSON format and output it + click.echo(json.dumps([o.model_dump(mode="json") for o in orders_list])) + + +@click.command() +@click.pass_context +@click.option("--id", type=str, required=True, help="Order ID to retrieve") +def order(ctx: click.Context, id: str) -> None: + """Get order by ID.""" + + client: Client = ctx.obj["client"] + + try: + order = client.get_order(order_id=id) + click.echo(order.model_dump_json()) + except APIError as e: + if e.status_code == 404: + click.echo(f"Order {id} not found.", err=True) + else: + raise e + + +@click.command() +@click.pass_context +@click.option("--product-id", "product_id", type=str, required=True, help="Product ID for opportunities") +@click.option("--max-items", "max_items", type=click.IntRange(min=1), help="Max number of opportunities to display") +@click.option("--limit", type=click.IntRange(min=1), default=10, help="Max number of opportunities to display") +def opportunities(ctx: click.Context, product_id: str, limit: int, max_items: None) -> None: + """List opportunities for a product.""" + + client: Client = ctx.obj["client"] + + date_range = ("2025-01-03T15:18:11Z", "2025-04-03T15:18:11Z") + geometry = {"type": "Point", "coordinates": [-122.4194, 37.7749]} + + opportunities_iter = client.get_product_opportunities( + product_id=product_id, geometry=geometry, date_range=date_range, limit=limit + ) + + if max_items: + opportunities_iter = itertools.islice(opportunities_iter, max_items) + + opportunities_list = list(opportunities_iter) + if len(opportunities_list) == 0: + click.echo("No opportunities found.", err=True) + return + + click.echo(json.dumps([o.model_dump(mode="json") for o in opportunities_list])) + + +cli.add_command(products) +cli.add_command(product) +cli.add_command(opportunities) +cli.add_command(orders) +cli.add_command(order) + +if __name__ == "__main__": + try: + cli() + except Exception as e: + click.echo(f"Error: {e=}", err=True) + raise e diff --git a/pystapi-client/src/pystapi_client/stapi_api_io.py b/pystapi-client/src/pystapi_client/stapi_api_io.py index 57b2305..0509b98 100644 --- a/pystapi-client/src/pystapi_client/stapi_api_io.py +++ b/pystapi-client/src/pystapi_client/stapi_api_io.py @@ -147,7 +147,7 @@ def request( resp = self.session.send(modified) except Exception as err: logger.debug(err) - raise APIError(f"Error sending request: {err}") + raise APIError(f"Error sending request: {err=}") # NOTE what about other successful status codes? if resp.status_code != 200: diff --git a/pystapi-client/src/pystapi_client/warnings.py b/pystapi-client/src/pystapi_client/warns.py similarity index 100% rename from pystapi-client/src/pystapi_client/warnings.py rename to pystapi-client/src/pystapi_client/warns.py diff --git a/uv.lock b/uv.lock index f6ce27c..c3c472a 100644 --- a/uv.lock +++ b/uv.lock @@ -1508,6 +1508,7 @@ dependencies = [ { name = "pystapi-validator" }, { name = "stapi-fastapi" }, { name = "stapi-pydantic" }, + { name = "types-click" }, ] [package.dev-dependencies] @@ -1521,6 +1522,7 @@ dev = [ { name = "pytest" }, { name = "respx" }, { name = "ruff" }, + { name = "types-click" }, ] docs = [ { name = "mkdocs-material" }, @@ -1533,6 +1535,7 @@ requires-dist = [ { name = "pystapi-validator", editable = "pystapi-validator" }, { name = "stapi-fastapi", editable = "stapi-fastapi" }, { name = "stapi-pydantic", editable = "stapi-pydantic" }, + { name = "types-click", specifier = ">=7.1.8" }, ] [package.metadata.requires-dev] @@ -1547,6 +1550,7 @@ dev = [ { name = "pytest", specifier = ">=8.3.5" }, { name = "respx", specifier = ">=0.22.0" }, { name = "ruff", specifier = ">=0.11.2" }, + { name = "types-click", specifier = ">=7.1.8" }, ] docs = [ { name = "mkdocs-material", specifier = ">=9.6.11" }, @@ -1558,6 +1562,7 @@ name = "pystapi-client" version = "0.0.1" source = { editable = "pystapi-client" } dependencies = [ + { name = "click" }, { name = "httpx" }, { name = "python-dateutil" }, { name = "stapi-pydantic" }, @@ -1565,6 +1570,7 @@ dependencies = [ [package.metadata] requires-dist = [ + { name = "click", specifier = ">=8.1.8" }, { name = "httpx", specifier = ">=0.28.1" }, { name = "python-dateutil", specifier = ">=2.8.2" }, { name = "stapi-pydantic", editable = "stapi-pydantic" }, @@ -2243,6 +2249,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061 }, ] +[[package]] +name = "types-click" +version = "7.1.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/ff/0e6a56108d45c80c61cdd4743312d0304d8192482aea4cce96c554aaa90d/types-click-7.1.8.tar.gz", hash = "sha256:b6604968be6401dc516311ca50708a0a28baa7a0cb840efd7412f0dbbff4e092", size = 10015 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/ad/607454a5f991c5b3e14693a7113926758f889138371058a5f72f567fa131/types_click-7.1.8-py3-none-any.whl", hash = "sha256:8cb030a669e2e927461be9827375f83c16b8178c365852c060a34e24871e7e81", size = 12929 }, +] + [[package]] name = "types-pyrfc3339" version = "2.0.1.20241107"