Skip to content
This repository was archived by the owner on Apr 2, 2025. It is now read-only.

Commit e1f5d79

Browse files
philvarnerPhil Varner
andauthored
Add Conformance endpoints (#101)
* update ruff to v0.7.3 and format code with it * enable mypy, and fix code warnings produced by it * run lint with python 3.12 * add conformance endpoints * remove init method from Conformance * refactor use of assert_link --------- Co-authored-by: Phil Varner <[email protected]>
1 parent 435d0b1 commit e1f5d79

File tree

9 files changed

+111
-24
lines changed

9 files changed

+111
-24
lines changed

CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Conformance endpoint `/conformance` and root body `conformsTo` attribute.
13+
14+
### Changed
15+
16+
none
17+
18+
### Deprecated
19+
20+
none
21+
22+
### Removed
23+
24+
none
25+
26+
### Fixed
27+
28+
none
29+
30+
### Security
31+
32+
none
33+
1034
## [v0.1.0] - 2024-10-23
1135

1236
Initial release

bin/server.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from stapi_fastapi.backends.product_backend import ProductBackend
66
from stapi_fastapi.backends.root_backend import RootBackend
77
from stapi_fastapi.exceptions import ConstraintsException, NotFoundException
8+
from stapi_fastapi.models.conformance import CORE
89
from stapi_fastapi.models.opportunity import (
910
Opportunity,
1011
OpportunityPropertiesBase,
@@ -104,7 +105,7 @@ class TestSpotlightProperties(OpportunityPropertiesBase):
104105
backend=product_backend,
105106
)
106107

107-
root_router = RootRouter(root_backend)
108+
root_router = RootRouter(root_backend, conformances=[CORE])
108109
root_router.add_product(product)
109110
app: FastAPI = FastAPI()
110111
app.include_router(root_router, prefix="")
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from pydantic import BaseModel, Field
2+
3+
CORE = "https://stapi.example.com/v0.1.0/core"
4+
OPPORTUNITIES = "https://stapi.example.com/v0.1.0/opportunities"
5+
6+
7+
class Conformance(BaseModel):
8+
conforms_to: list[str] = Field(
9+
default_factory=list, serialization_alias="conformsTo"
10+
)

src/stapi_fastapi/models/root.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
from pydantic import BaseModel
1+
from pydantic import BaseModel, Field
22

33
from stapi_fastapi.models.shared import Link
44

55

66
class RootResponse(BaseModel):
7-
links: list[Link]
7+
id: str
8+
conformsTo: list[str] = Field(default_factory=list)
9+
title: str = ""
10+
description: str = ""
11+
links: list[Link] = Field(default_factory=list)

src/stapi_fastapi/routers/root_router.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from stapi_fastapi.backends.root_backend import RootBackend
77
from stapi_fastapi.constants import TYPE_GEOJSON, TYPE_JSON
8+
from stapi_fastapi.models.conformance import CORE, Conformance
89
from stapi_fastapi.models.order import Order, OrderCollection
910
from stapi_fastapi.models.product import Product, ProductsCollection
1011
from stapi_fastapi.models.root import RootResponse
@@ -17,15 +18,17 @@ class RootRouter(APIRouter):
1718
def __init__(
1819
self,
1920
backend: RootBackend,
21+
conformances: list[str] = [CORE],
2022
name: str = "root",
21-
openapi_endpoint_name="openapi",
22-
docs_endpoint_name="swagger_ui_html",
23+
openapi_endpoint_name: str = "openapi",
24+
docs_endpoint_name: str = "swagger_ui_html",
2325
*args,
2426
**kwargs,
2527
) -> None:
2628
super().__init__(*args, **kwargs)
2729
self.backend = backend
2830
self.name = name
31+
self.conformances = conformances
2932
self.openapi_endpoint_name = openapi_endpoint_name
3033
self.docs_endpoint_name = docs_endpoint_name
3134

@@ -43,6 +46,14 @@ def __init__(
4346
tags=["Root"],
4447
)
4548

49+
self.add_api_route(
50+
"/conformance",
51+
self.get_conformance,
52+
methods=["GET"],
53+
name=f"{self.name}:conformance",
54+
tags=["Conformance"],
55+
)
56+
4657
self.add_api_route(
4758
"/products",
4859
self.get_products,
@@ -71,12 +82,19 @@ def __init__(
7182

7283
def get_root(self, request: Request) -> RootResponse:
7384
return RootResponse(
85+
id="STAPI API",
86+
conformsTo=self.conformances,
7487
links=[
7588
Link(
7689
href=str(request.url_for(f"{self.name}:root")),
7790
rel="self",
7891
type=TYPE_JSON,
7992
),
93+
Link(
94+
href=str(request.url_for(f"{self.name}:conformance")),
95+
rel="conformance",
96+
type=TYPE_JSON,
97+
),
8098
Link(
8199
href=str(request.url_for(f"{self.name}:list-products")),
82100
rel="products",
@@ -97,9 +115,12 @@ def get_root(self, request: Request) -> RootResponse:
97115
rel="service-docs",
98116
type="text/html",
99117
),
100-
]
118+
],
101119
)
102120

121+
def get_conformance(self, request: Request) -> Conformance:
122+
return Conformance(conforms_to=self.conformances)
123+
103124
def get_products(self, request: Request) -> ProductsCollection:
104125
return ProductsCollection(
105126
products=[pr.get_product(request) for pr in self.product_routers.values()],

tests/conformance_test.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from fastapi import status
2+
from fastapi.testclient import TestClient
3+
4+
from stapi_fastapi.models.conformance import CORE
5+
6+
7+
def test_conformance(stapi_client: TestClient) -> None:
8+
res = stapi_client.get("/conformance")
9+
10+
assert res.status_code == status.HTTP_200_OK
11+
assert res.headers["Content-Type"] == "application/json"
12+
13+
body = res.json()
14+
15+
assert body["conformsTo"] == [CORE]

tests/conftest.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from collections.abc import Iterator
22
from datetime import UTC, datetime, timedelta, timezone
3-
from typing import Callable
3+
from typing import Any, Callable
44
from urllib.parse import urljoin
55
from uuid import uuid4
66

@@ -19,6 +19,7 @@
1919
from stapi_fastapi.routers.root_router import RootRouter
2020

2121
from .backends import MockOrderDB, MockProductBackend, MockRootBackend
22+
from .utils import find_link
2223

2324

2425
class TestSpotlightProperties(OpportunityPropertiesBase):
@@ -87,6 +88,23 @@ def url_for(value: str) -> str:
8788
yield url_for
8889

8990

91+
@pytest.fixture
92+
def assert_link(url_for) -> Callable:
93+
def _assert_link(
94+
req: str,
95+
body: dict[str, Any],
96+
rel: str,
97+
path: str,
98+
media_type: str = "application/json",
99+
):
100+
link = find_link(body["links"], rel)
101+
assert link, f"{req} Link[rel={rel}] should exist"
102+
assert link["type"] == media_type
103+
assert link["href"] == url_for(path)
104+
105+
return _assert_link
106+
107+
90108
@pytest.fixture
91109
def products(mock_product_test_spotlight: Product) -> list[Product]:
92110
return [mock_product_test_spotlight]

tests/root_test.py

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,22 @@
11
from fastapi import status
22
from fastapi.testclient import TestClient
33

4-
from .utils import find_link
4+
from stapi_fastapi.models.conformance import CORE
55

66

7-
def test_root(stapi_client: TestClient, url_for) -> None:
7+
def test_root(stapi_client: TestClient, assert_link) -> None:
88
res = stapi_client.get("/")
99

1010
assert res.status_code == status.HTTP_200_OK
1111
assert res.headers["Content-Type"] == "application/json"
1212

13-
data = res.json()
13+
body = res.json()
1414

15-
link = find_link(data["links"], "self")
16-
assert link, "GET / Link[rel=self] should exist"
17-
assert link["type"] == "application/json"
18-
assert link["href"] == url_for("/")
15+
assert body["conformsTo"] == [CORE]
1916

20-
link = find_link(data["links"], "service-description")
21-
assert link, "GET / Link[rel=service-description] should exist"
22-
assert link["type"] == "application/json"
23-
assert str(link["href"]) == url_for("/openapi.json")
24-
25-
link = find_link(data["links"], "service-docs")
26-
assert link, "GET / Link[rel=service-docs] should exist"
27-
assert link["type"] == "text/html"
28-
assert str(link["href"]) == url_for("/docs")
17+
assert_link("GET /", body, "self", "/")
18+
assert_link("GET /", body, "service-description", "/openapi.json")
19+
assert_link("GET /", body, "service-docs", "/docs", media_type="text/html")
20+
assert_link("GET /", body, "conformance", "/conformance")
21+
assert_link("GET /", body, "products", "/products")
22+
assert_link("GET /", body, "orders", "/orders")

tests/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import Any
22

3-
link_dict = dict[str, Any]
3+
type link_dict = dict[str, Any]
44

55

66
def find_link(links: list[link_dict], rel: str) -> link_dict | None:

0 commit comments

Comments
 (0)