Skip to content

Commit 97c760c

Browse files
philvarnerPhil Varner
andauthored
add support for numberMatched in Orders response (#123)
## What I'm changing - RootRouter Callable `get_orders` now requires an additional value in the result tuple that is a count of the total number of orders that will be returned from pagination. When this returns `Some(int)`, the value is used for the `numberMatched` field in FeatureCollection returned from the /orders endpoint. If this feature is not desired, providing a function that returns `Nothing` will exclude the `numberMatched` field in the response. - Removed dependency on `pyrfc3339` library, since only one function from it was used in tests and that function has been removed in newer versions of the library. - pydantic >= 2.12 is now required. - bump stapi-pydantic to 0.1.0, bump stapi-fastapi to 0.8.0 ## How I did it - Changed the signature on get_orders instead of adding another callable like get_orders_count. This makes it a bit cleaner. The 3-tuple returned by get_orders should be changed to a class if any more values are added. ## Checklist - [X] Tests pass: `uv run pytest` - [X] Checks pass: `uv run pre-commit run --all-files` - [X] CHANGELOG is updated (if necessary) --------- Co-authored-by: Phil Varner <phil.varner@orbitalsidekick.com>
1 parent 6a42083 commit 97c760c

File tree

11 files changed

+1680
-1258
lines changed

11 files changed

+1680
-1258
lines changed

stapi-fastapi/CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
66

7+
## [0.8.0] - 2025-12-03
8+
9+
### Added
10+
11+
- RootRouter Callable `get_orders` now requires an additional value in the result tuple that is a count of the total number of orders
12+
that will be returned from pagination. When this returns `Some(int)`, the value is used for the `numberMatched` field in
13+
FeatureCollection returned from the /orders endpoint. If this feature is not desired, providing a function that returns
14+
`Nothing` will exclude the `numberMatched` field in the response.
15+
16+
### Removed
17+
18+
- removed dependency on `pyrfc3339` library, since only one function from it was used in tests and that function has
19+
been removed in newer versions of the library.
20+
721
## [0.7.1] - 2025-04-25
822

923
### Fixed
@@ -126,6 +140,7 @@ Initial release
126140
- Add links `opportunities` and `create-order` to Product
127141
- Add link `create-order` to OpportunityCollection
128142

143+
[0.8.0]: https://github.com/stapi-spec/stapi-fastapi/tree/v0.8.0
129144
[0.7.1]: https://github.com/stapi-spec/stapi-fastapi/tree/v0.7.1
130145
[0.7.0]: https://github.com/stapi-spec/stapi-fastapi/tree/v0.7.0
131146
[0.6.0]: https://github.com/stapi-spec/stapi-fastapi/tree/v0.6.0

stapi-fastapi/pyproject.toml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "stapi-fastapi"
3-
version = "0.7.1"
3+
version = "0.8.0"
44
description = "Sensor Tasking API (STAPI) with FastAPI"
55
authors = [
66
{ name = "Christian Wygoda", email = "christian.wygoda@wygoda.net" },
@@ -20,8 +20,6 @@ dependencies = [
2020
"returns>=0.23",
2121
"nox>=2024.4.15",
2222
"pydantic-settings>=2.2.1",
23-
"pyrfc3339>=1.1",
24-
"types-pyRFC3339>=1.1.1",
2523
"uvicorn>=0.29.0",
2624
"stapi-pydantic>=0.0.3",
2725
]

stapi-fastapi/src/stapi_fastapi/backends/root_backend.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
GetOrders = Callable[
1515
[str | None, int, Request],
16-
Coroutine[Any, Any, ResultE[tuple[list[Order[OrderStatus]], Maybe[str]]]],
16+
Coroutine[Any, Any, ResultE[tuple[list[Order[OrderStatus]], Maybe[str], Maybe[int]]]],
1717
]
1818
"""
1919
Type alias for an async function that returns a list of existing Orders.

stapi-fastapi/src/stapi_fastapi/routers/root_router.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -251,19 +251,25 @@ def get_products(self, request: Request, next: str | None = None, limit: int = 1
251251
links=links,
252252
)
253253

254-
async def get_orders(
254+
async def get_orders( # noqa: C901
255255
self, request: Request, next: str | None = None, limit: int = 10
256256
) -> OrderCollection[OrderStatus]:
257257
links: list[Link] = []
258+
orders_count: int | None = None
258259
match await self._get_orders(next, limit, request):
259-
case Success((orders, maybe_pagination_token)):
260+
case Success((orders, maybe_pagination_token, maybe_orders_count)):
260261
for order in orders:
261262
order.links.extend(self.order_links(order, request))
262263
match maybe_pagination_token:
263264
case Some(x):
264265
links.append(self.pagination_link(request, x, limit))
265266
case Maybe.empty:
266267
pass
268+
match maybe_orders_count:
269+
case Some(x):
270+
orders_count = x
271+
case Maybe.empty:
272+
pass
267273
case Failure(ValueError()):
268274
raise NotFoundError(detail="Error finding pagination token")
269275
case Failure(e):
@@ -277,7 +283,12 @@ async def get_orders(
277283
)
278284
case _:
279285
raise AssertionError("Expected code to be unreachable")
280-
return OrderCollection(features=orders, links=links)
286+
287+
return OrderCollection(
288+
features=orders,
289+
links=links,
290+
number_matched=orders_count,
291+
)
281292

282293
async def get_order(self, order_id: str, request: Request) -> Order[OrderStatus]:
283294
"""
@@ -374,8 +385,12 @@ def order_statuses_link(self, request: Request, order_id: str) -> Link:
374385
)
375386

376387
def pagination_link(self, request: Request, pagination_token: str, limit: int) -> Link:
388+
href = str(request.url.include_query_params(next=pagination_token, limit=limit)).replace(
389+
str(request.url_for(f"{self.name}:{ROOT}")), self.url_for(request, f"{self.name}:{ROOT}"), 1
390+
)
391+
377392
return Link(
378-
href=str(request.url.include_query_params(next=pagination_token, limit=limit)),
393+
href=href,
379394
rel="next",
380395
type=TYPE_JSON,
381396
)

stapi-fastapi/tests/backends.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,15 @@
2121
)
2222

2323

24-
async def mock_get_orders(next: str | None, limit: int, request: Request) -> ResultE[tuple[list[Order], Maybe[str]]]:
24+
async def mock_get_orders(
25+
next: str | None,
26+
limit: int,
27+
request: Request,
28+
) -> ResultE[tuple[list[Order], Maybe[str], Maybe[int]]]:
2529
"""
2630
Return orders from backend. Handle pagination/limit if applicable
2731
"""
32+
count = 314
2833
try:
2934
start = 0
3035
limit = min(limit, 100)
@@ -37,8 +42,8 @@ async def mock_get_orders(next: str | None, limit: int, request: Request) -> Res
3742
orders = [request.state._orders_db.get_order(order_id) for order_id in ids]
3843

3944
if end > 0 and end < len(order_ids):
40-
return Success((orders, Some(request.state._orders_db._orders[order_ids[end]].id)))
41-
return Success((orders, Nothing))
45+
return Success((orders, Some(request.state._orders_db._orders[order_ids[end]].id), Some(count)))
46+
return Success((orders, Nothing, Some(count)))
4247
except Exception as e:
4348
return Failure(e)
4449

stapi-fastapi/tests/test_datetime_interval.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from zoneinfo import ZoneInfo
44

55
from pydantic import BaseModel, ValidationError
6-
from pyrfc3339.utils import format_timezone
76
from pytest import mark, raises
87
from stapi_pydantic import DatetimeInterval
98

@@ -14,6 +13,27 @@ class Model(BaseModel):
1413
datetime: DatetimeInterval
1514

1615

16+
# format_timezone was removed from pyrfc3339 (MIT) in v2.1, so included here now
17+
def format_timezone(utcoffset):
18+
"""
19+
Return a string representing the timezone offset.
20+
Remaining seconds are rounded to the nearest minute.
21+
22+
>>> format_timezone(3600)
23+
'+01:00'
24+
>>> format_timezone(5400)
25+
'+01:30'
26+
>>> format_timezone(-28800)
27+
'-08:00'
28+
"""
29+
30+
hours, seconds = divmod(abs(utcoffset), 3600)
31+
minutes = round(float(seconds) / 60)
32+
sign = "+" if utcoffset >= 0 else "-"
33+
34+
return f"{sign}{int(hours):02d}:{int(minutes):02d}"
35+
36+
1737
def rfc3339_strftime(dt: datetime, format: str) -> str:
1838
tds = int(round(dt.tzinfo.utcoffset(dt).total_seconds())) # type: ignore
1939
long = format_timezone(tds)

stapi-fastapi/tests/test_order.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,9 @@
1717

1818
def test_empty_order(stapi_client: TestClient):
1919
res = stapi_client.get("/orders")
20-
default_orders = {"type": "FeatureCollection", "features": [], "links": []}
2120
assert res.status_code == status.HTTP_200_OK
2221
assert res.headers["Content-Type"] == "application/geo+json"
23-
assert res.json() == default_orders
22+
assert res.json() == {"type": "FeatureCollection", "features": [], "links": [], "numberMatched": 314}
2423

2524

2625
@pytest.fixture

stapi-pydantic/CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ All notable changes to this project will be documented in this file.
66

77
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
88

9-
## [Unreleased]
9+
## [0.1.0] - 2025-07-17
10+
11+
### Changed
12+
13+
- pydantic >= 2.12 is now required.
1014

1115
### Added
1216

@@ -46,6 +50,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
4650
Initial release.
4751

4852
[unreleased]: https://github.com/stapi-spec/pystapi/compare/stac-pydantic/stapi-pydantic%2Fv0.0.4...main
53+
[0.1.0]: https://github.com/stapi-spec/pystapi/compare/stac-pydantic/stapi-pydantic%2Fv0.0.4...stapi-pydantic%2Fv0.1.0
4954
[0.0.4]: https://github.com/stapi-spec/pystapi/compare/stac-pydantic/stapi-pydantic%2Fv0.0.3...stapi-pydantic%2Fv0.0.4
5055
[0.0.3]: https://github.com/stapi-spec/pystapi/compare/stac-pydantic/stapi-pydantic%2Fv0.0.2...stapi-pydantic%2Fv0.0.3
5156
[0.0.2]: https://github.com/stapi-spec/pystapi/compare/stac-pydantic/stapi-pydantic%2Fv0.0.1...stapi-pydantic%2Fv0.0.2

stapi-pydantic/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
[project]
22
name = "stapi-pydantic"
3-
version = "0.0.4"
3+
version = "0.1.0"
44
description = "Pydantic models for Satellite Tasking API (STAPI) Specification"
55
readme = "README.md"
66
authors = [
77
{ name = "Phil Varner", email = "philvarner@gmail.com" },
88
{ name = "Pete Gadomski", email = "pete.gadomski@gmail.com" },
99
]
1010
requires-python = ">=3.11"
11-
dependencies = ["cql2>=0.3.6", "geojson-pydantic>=1.2.0"]
11+
dependencies = ["pydantic>=2.12", "cql2>=0.3.6", "geojson-pydantic>=1.2.0"]
1212

1313
[project.scripts]
1414
stapi-pydantic = "stapi_pydantic:main"

stapi-pydantic/src/stapi_pydantic/order.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@ class OrderCollection(_GeoJsonBase, Generic[T]):
129129
type: Literal["FeatureCollection"] = "FeatureCollection"
130130
features: list[Order[T]]
131131
links: list[Link] = Field(default_factory=list)
132+
number_matched: int | None = Field(
133+
serialization_alias="numberMatched", default=None, exclude_if=lambda x: x is None
134+
)
132135

133136
def __iter__(self) -> Iterator[Order[T]]: # type: ignore [override]
134137
"""iterate over features"""

0 commit comments

Comments
 (0)