Skip to content

Commit 4a0293b

Browse files
phil-oskgadomski
andauthored
add cli with 5 commands: product, products, order, orders, opportunities (#88)
## What I'm changing add cli with 5 commands: product, products, order, orders, opportunities --------- Co-authored-by: Pete Gadomski <[email protected]>
1 parent 00492f4 commit 4a0293b

File tree

7 files changed

+161
-4
lines changed

7 files changed

+161
-4
lines changed

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ dependencies = [
88
"pystapi-client",
99
"pystapi-validator",
1010
"stapi-pydantic",
11-
"stapi-fastapi"
11+
"stapi-fastapi",
1212
]
1313

1414
[dependency-groups]
@@ -21,6 +21,7 @@ dev = [
2121
"pre-commit>=4.2.0",
2222
"pre-commit-hooks>=5.0.0",
2323
"fastapi[standard]>=0.115.12",
24+
"types-click>=7.1.8",
2425
"pygithub>=2.6.1",
2526
"respx>=0.22.0",
2627
]

pystapi-client/pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ dependencies = [
1616
"httpx>=0.28.1",
1717
"stapi-pydantic",
1818
"python-dateutil>=2.8.2",
19+
"click>=8.1.8",
1920
]
2021

2122
[project.scripts]
22-
stapi-client = "pystapi_client.cli:cli"
23+
stapi = "pystapi_client.scripts.cli:cli"
2324

2425
[tool.uv.sources]
2526
stapi-pydantic = { workspace = true }

pystapi-client/src/pystapi_client/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from pystapi_client.conformance import ConformanceClasses
2626
from pystapi_client.exceptions import APIError
2727
from pystapi_client.stapi_api_io import StapiIO
28-
from pystapi_client.warnings import NoConformsTo
28+
from pystapi_client.warns import NoConformsTo
2929

3030
DEFAULT_LINKS = [
3131
{
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import itertools
2+
import json
3+
4+
import click
5+
from pystapi_client.client import Client
6+
from pystapi_client.exceptions import APIError
7+
8+
CONTEXT_SETTINGS = dict(default_map={"cli": {"url": "http://localhost:8000"}})
9+
10+
11+
@click.group(context_settings=CONTEXT_SETTINGS)
12+
@click.option("--url", type=str, required=True, help="Base URL for STAPI server")
13+
@click.pass_context
14+
def cli(ctx: click.Context, url: str) -> None:
15+
"""Command line interface for STAPI client. Group ensures client is created."""
16+
17+
client = Client.open(url)
18+
ctx.obj = {"client": client}
19+
20+
21+
@click.command()
22+
@click.pass_context
23+
@click.option("--max-items", "max_items", type=click.IntRange(min=1), help="Max number of products to display")
24+
@click.option("--limit", type=click.IntRange(min=1), help="Limit number of products to request")
25+
def products(ctx: click.Context, limit: int | None, max_items: int | None) -> None:
26+
"""List products."""
27+
28+
client: Client = ctx.obj["client"]
29+
30+
products_iter = client.get_products(limit=limit)
31+
32+
if max_items:
33+
products_iter = itertools.islice(products_iter, max_items)
34+
35+
products_list = list(products_iter)
36+
if len(products_list) == 0:
37+
click.echo("No products found.")
38+
return
39+
40+
# Serialize the products list into JSON format and output it
41+
click.echo(json.dumps([p.model_dump(mode="json") for p in products_list]))
42+
43+
44+
@click.command()
45+
@click.pass_context
46+
@click.option("--id", type=str, required=True, help="Product ID to retrieve")
47+
def product(ctx: click.Context, id: str) -> None:
48+
"""Get product by ID."""
49+
50+
client: Client = ctx.obj["client"]
51+
52+
product = client.get_product(product_id=id)
53+
if not product:
54+
click.echo(f"Product {id} not found.", err=True)
55+
return
56+
57+
click.echo(product.model_dump_json())
58+
59+
60+
@click.command()
61+
@click.pass_context
62+
@click.option("--max-items", "max_items", type=click.IntRange(min=1), help="Max number of products to display")
63+
@click.option("--limit", type=click.IntRange(min=1), help="Limit number of products to request")
64+
def orders(ctx: click.Context, max_items: int | None, limit: int | None) -> None:
65+
"""List orders."""
66+
67+
client: Client = ctx.obj["client"]
68+
69+
orders_iter = client.get_orders(limit=limit)
70+
71+
if max_items:
72+
orders_iter = itertools.islice(orders_iter, max_items)
73+
74+
orders_list = list(orders_iter)
75+
if len(orders_list) == 0:
76+
click.echo("No orders found.", err=True)
77+
return
78+
79+
# Serialize the orders list into JSON format and output it
80+
click.echo(json.dumps([o.model_dump(mode="json") for o in orders_list]))
81+
82+
83+
@click.command()
84+
@click.pass_context
85+
@click.option("--id", type=str, required=True, help="Order ID to retrieve")
86+
def order(ctx: click.Context, id: str) -> None:
87+
"""Get order by ID."""
88+
89+
client: Client = ctx.obj["client"]
90+
91+
try:
92+
order = client.get_order(order_id=id)
93+
click.echo(order.model_dump_json())
94+
except APIError as e:
95+
if e.status_code == 404:
96+
click.echo(f"Order {id} not found.", err=True)
97+
else:
98+
raise e
99+
100+
101+
@click.command()
102+
@click.pass_context
103+
@click.option("--product-id", "product_id", type=str, required=True, help="Product ID for opportunities")
104+
@click.option("--max-items", "max_items", type=click.IntRange(min=1), help="Max number of opportunities to display")
105+
@click.option("--limit", type=click.IntRange(min=1), default=10, help="Max number of opportunities to display")
106+
def opportunities(ctx: click.Context, product_id: str, limit: int, max_items: None) -> None:
107+
"""List opportunities for a product."""
108+
109+
client: Client = ctx.obj["client"]
110+
111+
date_range = ("2025-01-03T15:18:11Z", "2025-04-03T15:18:11Z")
112+
geometry = {"type": "Point", "coordinates": [-122.4194, 37.7749]}
113+
114+
opportunities_iter = client.get_product_opportunities(
115+
product_id=product_id, geometry=geometry, date_range=date_range, limit=limit
116+
)
117+
118+
if max_items:
119+
opportunities_iter = itertools.islice(opportunities_iter, max_items)
120+
121+
opportunities_list = list(opportunities_iter)
122+
if len(opportunities_list) == 0:
123+
click.echo("No opportunities found.", err=True)
124+
return
125+
126+
click.echo(json.dumps([o.model_dump(mode="json") for o in opportunities_list]))
127+
128+
129+
cli.add_command(products)
130+
cli.add_command(product)
131+
cli.add_command(opportunities)
132+
cli.add_command(orders)
133+
cli.add_command(order)
134+
135+
if __name__ == "__main__":
136+
try:
137+
cli()
138+
except Exception as e:
139+
click.echo(f"Error: {e=}", err=True)
140+
raise e

pystapi-client/src/pystapi_client/stapi_api_io.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ def request(
147147
resp = self.session.send(modified)
148148
except Exception as err:
149149
logger.debug(err)
150-
raise APIError(f"Error sending request: {err}")
150+
raise APIError(f"Error sending request: {err=}")
151151

152152
# NOTE what about other successful status codes?
153153
if resp.status_code != 200:
File renamed without changes.

uv.lock

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)