Skip to content

Commit 7f31f6b

Browse files
authored
Setup client testing (#89)
## What I'm changing - <!-- a list of changes, including any issues this might close or reference --> ## How I did it - <!-- more detail on decisions and choices --> ## Checklist - [ ] Tests pass: `uv run pytest` - [ ] Checks pass: `uv run pre-commit --all-files` - [ ] CHANGELOG is updated (if necessary)
1 parent 07d4513 commit 7f31f6b

File tree

8 files changed

+492
-160
lines changed

8 files changed

+492
-160
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ dev = [
2222
"pre-commit-hooks>=5.0.0",
2323
"fastapi[standard]>=0.115.12",
2424
"pygithub>=2.6.1",
25+
"respx>=0.22.0",
2526
]
2627
docs = [
2728
"mkdocs-material>=9.6.11",

pystapi-client/src/pystapi_client/client.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -432,10 +432,6 @@ def get_order(self, order_id: str) -> Order: # type: ignore[type-arg]
432432

433433
order_endpoint = self._get_orders_href(order_id)
434434
order_json = self.stapi_io.read_json(order_endpoint)
435-
436-
if order_json is None:
437-
raise ValueError(f"Order {order_id} not found")
438-
439435
return Order.model_validate(order_json)
440436

441437
def _get_orders_href(self, order_id: str | None = None) -> str:

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.from_response(resp)
150+
raise APIError(f"Error sending request: {err}")
151151

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

pystapi-client/tests/conftest.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import json
2+
from collections.abc import Generator
3+
from copy import deepcopy
4+
from pathlib import Path
5+
from typing import Any
6+
7+
import httpx
8+
import pytest
9+
import respx
10+
from httpx import Response
11+
12+
WORKING_DIR = Path(__file__).parent
13+
14+
15+
def load_fixture(name: str) -> dict[str, Any]:
16+
with open(WORKING_DIR / "fixtures" / f"{name}.json") as f:
17+
return json.load(f) # type: ignore[no-any-return]
18+
19+
20+
@pytest.fixture
21+
def mocked_api() -> Generator[respx.MockRouter, None, None]:
22+
landing_page_data = load_fixture("landing_page")
23+
products = load_fixture("products")
24+
25+
with respx.mock(base_url="https://stapi.example.com", assert_all_called=False) as respx_mock:
26+
landing_page = respx_mock.get("/")
27+
landing_page.return_value = Response(200, json=landing_page_data)
28+
29+
conformance_route = respx_mock.get("/conformance")
30+
conformance_route.return_value = Response(200, json={"conformsTo": landing_page_data["conformsTo"]})
31+
32+
# products_route.return_value = Response(200, json=products)
33+
34+
def mock_products_response(request: httpx.Request) -> httpx.Response:
35+
products_limited = deepcopy(products)
36+
limit = request.url.params.get("limit")
37+
page = int(request.url.params.get("page", 1))
38+
if limit is not None:
39+
start_index = (page - 1) * int(limit)
40+
end_index = start_index + int(limit)
41+
products_limited["products"] = products_limited["products"][start_index:end_index]
42+
has_next_page = end_index < len(products_limited["products"]) + 1
43+
if has_next_page:
44+
products_limited["links"].append(
45+
{
46+
"href": "https://stapi.example.com/products?limit=1&page=2",
47+
"method": "GET",
48+
"rel": "next",
49+
}
50+
)
51+
return Response(200, json=products_limited)
52+
53+
respx_mock.get("/products").mock(side_effect=mock_products_response)
54+
respx_mock.get("/products", params={"limit": 1}).mock(side_effect=mock_products_response)
55+
56+
yield respx_mock
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
2+
{
3+
"id": "example-stapi",
4+
"title": "A simple STAPI Example",
5+
"description": "This API demonstrated the landing page for a SpatioTemporal Asset Tasking API",
6+
"conformsTo" : [
7+
"https://stapi.example.com/v0.1.0/core",
8+
"https://geojson.org/schema/Point.json",
9+
"https://geojson.org/schema/Polygon.json"
10+
],
11+
"links": [
12+
{
13+
"rel": "conformance",
14+
"type": "application/json",
15+
"href": "https://stapi.example.com/conformance",
16+
"title": "Conformance classes implemented by this API"
17+
},
18+
{
19+
"rel": "orders",
20+
"type": "application/json",
21+
"href": "https://stapi.example.com/orders",
22+
"title": "List of existing orders"
23+
},
24+
{
25+
"rel": "products",
26+
"type": "application/json",
27+
"href": "https://stapi.example.com/products",
28+
"title": "List of available products"
29+
},
30+
{
31+
"rel": "service-desc",
32+
"type": "application/vnd.oai.openapi+json;version=3.0",
33+
"href": "https://stapi.example.com/api",
34+
"title": "The machine-readable API definition"
35+
36+
},
37+
{
38+
"rel": "service-doc",
39+
"type": "text/html",
40+
"href": "https://stapi.example.com/api.html",
41+
"title": "The human-readable API documentation"
42+
43+
}
44+
]
45+
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
{
2+
"products": [
3+
{
4+
"type": "Collection",
5+
"id": "multispectral",
6+
"title": "Multispectral",
7+
"description": "Full color EO image",
8+
"keywords": [
9+
"EO",
10+
"color"
11+
],
12+
"license": "license",
13+
"providers": [
14+
{
15+
"name": "planet",
16+
"description": "planet description",
17+
"roles": [
18+
"producer"
19+
],
20+
"url": "https://planet.com"
21+
}
22+
],
23+
"links": [
24+
{
25+
"href": "https://stapi.example.com/",
26+
"rel": "latest-version",
27+
"type": "media type",
28+
"title": "title"
29+
}
30+
],
31+
"queryables": {
32+
"gsd": {
33+
"minimum": 0.5,
34+
"maximum": 10.0
35+
},
36+
"target_elevation": {
37+
"minimum": 30.0,
38+
"maximum": 90.0
39+
},
40+
"target_azimuth": {
41+
"minimum": -360.0,
42+
"maximum": 360.0
43+
},
44+
"view:sun_elevation": {
45+
"minimum": 10.0,
46+
"maximum": 90.0
47+
},
48+
"view:sun_azimuth": {
49+
"minimum": -360.0,
50+
"maximum": 360.0
51+
},
52+
"view:off_nadir": {
53+
"minimum": 0.0,
54+
"maximum": 30.0
55+
},
56+
"cloud_coverage_prediction_max": {
57+
"type": "number",
58+
"minimum": 0,
59+
"maximum": 100,
60+
"multipleOf": 0.01
61+
}
62+
},
63+
"parameters": {
64+
"eo:cloud_cover": {
65+
"type": "number",
66+
"minimum": 0,
67+
"maximum": 100,
68+
"multipleOf": 0.01
69+
}
70+
},
71+
"properties": {
72+
"eo:bands": [
73+
{
74+
"name": "band1",
75+
"common_name": "blue",
76+
"center_wavelength": 0.47,
77+
"full_width_half_max": 0.07,
78+
"solar_illumination": 1959.66
79+
},
80+
{
81+
"name": "band2",
82+
"common_name": "green",
83+
"center_wavelength": 0.56,
84+
"full_width_half_max": 0.08,
85+
"solar_illumination": 1823.24
86+
},
87+
{
88+
"name": "band3",
89+
"common_name": "red",
90+
"center_wavelength": 0.645,
91+
"full_width_half_max": 0.09,
92+
"solar_illumination": 1512.06
93+
},
94+
{
95+
"name": "band4",
96+
"common_name": "nir",
97+
"center_wavelength": 0.8,
98+
"full_width_half_max": 0.152,
99+
"solar_illumination": 1041.63
100+
}
101+
]
102+
}
103+
},
104+
{
105+
"type": "Collection",
106+
"id": "spotlight",
107+
"title": "Spotlight",
108+
"description": "SAR Spotlight frame",
109+
"keywords": [
110+
"SAR",
111+
"spotlight"
112+
],
113+
"license": "license",
114+
"providers": [
115+
{
116+
"name": "planet",
117+
"description": "planet description",
118+
"roles": [
119+
"producer"
120+
],
121+
"url": "https://planet.com"
122+
}
123+
],
124+
"links": [
125+
{
126+
"href": "https://stapi.example.com/",
127+
"rel": "latest-version",
128+
"type": "media type",
129+
"title": "title"
130+
}
131+
],
132+
"queryables": {
133+
"sar:resolution_range": {
134+
"minimum": 0.5,
135+
"maximum": 5.0
136+
},
137+
"sar:resolution_azimuth": {
138+
"minimum": 0.5,
139+
"maximum": 5.0
140+
},
141+
"grazing": {
142+
"minimum": 20.0,
143+
"maximum": 40.0
144+
},
145+
"target_azimuth": {
146+
"minimum": -360.0,
147+
"maximum": 360.0
148+
},
149+
"squint": {
150+
"minimum": -5.0,
151+
"maximum": 5.0
152+
}
153+
},
154+
"parameters": {
155+
"sar:polarizarions": [
156+
"HH",
157+
"VV",
158+
"HV"
159+
]
160+
},
161+
"properties": {
162+
"sar:product_type": "SSC",
163+
"sar:frequency_band": "X"
164+
}
165+
}
166+
],
167+
"links": []
168+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import respx
2+
from pystapi_client.client import Client
3+
from stapi_pydantic import Link
4+
5+
6+
def test_get_products(mocked_api: respx.MockRouter) -> None:
7+
client = Client.open(url="https://stapi.example.com")
8+
9+
products = list(client.get_products())
10+
assert len(products) == 2
11+
12+
13+
def test_get_products_paginated(mocked_api: respx.MockRouter) -> None:
14+
client = Client.open(url="https://stapi.example.com")
15+
16+
products = list(client.get_products(limit=1))
17+
assert len(products) == 2
18+
19+
20+
def test_pagination(mocked_api: respx.MockRouter) -> None:
21+
client = Client.open(url="https://stapi.example.com")
22+
23+
products_link = Link(href="https://stapi.example.com/products", method="GET", body={"limit": 1}, rel="")
24+
for products_collection in client.stapi_io.get_pages(products_link, "products"):
25+
assert len(products_collection["products"]) == 1

0 commit comments

Comments
 (0)