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

Commit 87770e4

Browse files
author
Phil Varner
committed
add conformance endpoints
1 parent 9b4340e commit 87770e4

File tree

8 files changed

+114
-22
lines changed

8 files changed

+114
-22
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: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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(default_factory=list, alias="conformsTo")
9+
10+
def __init__(
11+
self,
12+
*args,
13+
conforms_to: list[str],
14+
**kwargs,
15+
) -> None:
16+
super().__init__(*args, **kwargs)
17+
self.conforms_to = conforms_to

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/root_test.py

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
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
5+
6+
from .utils import assert_link
57

68

79
def test_root(stapi_client: TestClient, url_for) -> None:
@@ -10,19 +12,13 @@ def test_root(stapi_client: TestClient, url_for) -> None:
1012
assert res.status_code == status.HTTP_200_OK
1113
assert res.headers["Content-Type"] == "application/json"
1214

13-
data = res.json()
14-
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+
body = res.json()
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")
17+
assert body["conformsTo"] == [CORE]
2418

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")
19+
assert_link("GET /", body, "self", "/", url_for)
20+
assert_link("GET /", body, "service-description", "/openapi.json", url_for)
21+
assert_link("GET /", body, "service-docs", "/docs", url_for, media_type="text/html")
22+
assert_link("GET /", body, "conformance", "/conformance", url_for)
23+
assert_link("GET /", body, "products", "/products", url_for)
24+
assert_link("GET /", body, "orders", "/orders", url_for)

tests/utils.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,21 @@
1-
from typing import Any
1+
from typing import Any, Callable
22

33
link_dict = dict[str, Any]
44

55

66
def find_link(links: list[link_dict], rel: str) -> link_dict | None:
77
return next((link for link in links if link["rel"] == rel), None)
8+
9+
10+
def assert_link(
11+
req: str,
12+
body: dict[str, Any],
13+
rel: str,
14+
path: str,
15+
url_for: Callable[[str], str],
16+
media_type: str = "application/json",
17+
) -> None:
18+
link = find_link(body["links"], rel)
19+
assert link, f"{req} Link[rel={rel}] should exist"
20+
assert link["type"] == media_type
21+
assert link["href"] == url_for(path)

0 commit comments

Comments
 (0)