Skip to content

Commit

Permalink
add conformance endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
Phil Varner committed Nov 11, 2024
1 parent 9b4340e commit 87770e4
Show file tree
Hide file tree
Showing 8 changed files with 114 additions and 22 deletions.
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

### Added

- Conformance endpoint `/conformance` and root body `conformsTo` attribute.

### Changed

none

### Deprecated

none

### Removed

none

### Fixed

none

### Security

none

## [v0.1.0] - 2024-10-23

Initial release
Expand Down
3 changes: 2 additions & 1 deletion bin/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from stapi_fastapi.backends.product_backend import ProductBackend
from stapi_fastapi.backends.root_backend import RootBackend
from stapi_fastapi.exceptions import ConstraintsException, NotFoundException
from stapi_fastapi.models.conformance import CORE
from stapi_fastapi.models.opportunity import (
Opportunity,
OpportunityPropertiesBase,
Expand Down Expand Up @@ -104,7 +105,7 @@ class TestSpotlightProperties(OpportunityPropertiesBase):
backend=product_backend,
)

root_router = RootRouter(root_backend)
root_router = RootRouter(root_backend, conformances=[CORE])
root_router.add_product(product)
app: FastAPI = FastAPI()
app.include_router(root_router, prefix="")
17 changes: 17 additions & 0 deletions src/stapi_fastapi/models/conformance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from pydantic import BaseModel, Field

CORE = "https://stapi.example.com/v0.1.0/core"
OPPORTUNITIES = "https://stapi.example.com/v0.1.0/opportunities"


class Conformance(BaseModel):
conforms_to: list[str] = Field(default_factory=list, alias="conformsTo")

def __init__(
self,
*args,
conforms_to: list[str],
**kwargs,
) -> None:
super().__init__(*args, **kwargs)
self.conforms_to = conforms_to
8 changes: 6 additions & 2 deletions src/stapi_fastapi/models/root.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
from pydantic import BaseModel
from pydantic import BaseModel, Field

from stapi_fastapi.models.shared import Link


class RootResponse(BaseModel):
links: list[Link]
id: str
conformsTo: list[str] = Field(default_factory=list)
title: str = ""
description: str = ""
links: list[Link] = Field(default_factory=list)
27 changes: 24 additions & 3 deletions src/stapi_fastapi/routers/root_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from stapi_fastapi.backends.root_backend import RootBackend
from stapi_fastapi.constants import TYPE_GEOJSON, TYPE_JSON
from stapi_fastapi.models.conformance import CORE, Conformance
from stapi_fastapi.models.order import Order, OrderCollection
from stapi_fastapi.models.product import Product, ProductsCollection
from stapi_fastapi.models.root import RootResponse
Expand All @@ -17,15 +18,17 @@ class RootRouter(APIRouter):
def __init__(
self,
backend: RootBackend,
conformances: list[str] = [CORE],
name: str = "root",
openapi_endpoint_name="openapi",
docs_endpoint_name="swagger_ui_html",
openapi_endpoint_name: str = "openapi",
docs_endpoint_name: str = "swagger_ui_html",
*args,
**kwargs,
) -> None:
super().__init__(*args, **kwargs)
self.backend = backend
self.name = name
self.conformances = conformances
self.openapi_endpoint_name = openapi_endpoint_name
self.docs_endpoint_name = docs_endpoint_name

Expand All @@ -43,6 +46,14 @@ def __init__(
tags=["Root"],
)

self.add_api_route(
"/conformance",
self.get_conformance,
methods=["GET"],
name=f"{self.name}:conformance",
tags=["Conformance"],
)

self.add_api_route(
"/products",
self.get_products,
Expand Down Expand Up @@ -71,12 +82,19 @@ def __init__(

def get_root(self, request: Request) -> RootResponse:
return RootResponse(
id="STAPI API",
conformsTo=self.conformances,
links=[
Link(
href=str(request.url_for(f"{self.name}:root")),
rel="self",
type=TYPE_JSON,
),
Link(
href=str(request.url_for(f"{self.name}:conformance")),
rel="conformance",
type=TYPE_JSON,
),
Link(
href=str(request.url_for(f"{self.name}:list-products")),
rel="products",
Expand All @@ -97,9 +115,12 @@ def get_root(self, request: Request) -> RootResponse:
rel="service-docs",
type="text/html",
),
]
],
)

def get_conformance(self, request: Request) -> Conformance:
return Conformance(conforms_to=self.conformances)

def get_products(self, request: Request) -> ProductsCollection:
return ProductsCollection(
products=[pr.get_product(request) for pr in self.product_routers.values()],
Expand Down
15 changes: 15 additions & 0 deletions tests/conformance_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from fastapi import status
from fastapi.testclient import TestClient

from stapi_fastapi.models.conformance import CORE


def test_conformance(stapi_client: TestClient) -> None:
res = stapi_client.get("/conformance")

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

body = res.json()

assert body["conformsTo"] == [CORE]
26 changes: 11 additions & 15 deletions tests/root_test.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from fastapi import status
from fastapi.testclient import TestClient

from .utils import find_link
from stapi_fastapi.models.conformance import CORE

from .utils import assert_link


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

data = res.json()

link = find_link(data["links"], "self")
assert link, "GET / Link[rel=self] should exist"
assert link["type"] == "application/json"
assert link["href"] == url_for("/")
body = res.json()

link = find_link(data["links"], "service-description")
assert link, "GET / Link[rel=service-description] should exist"
assert link["type"] == "application/json"
assert str(link["href"]) == url_for("/openapi.json")
assert body["conformsTo"] == [CORE]

link = find_link(data["links"], "service-docs")
assert link, "GET / Link[rel=service-docs] should exist"
assert link["type"] == "text/html"
assert str(link["href"]) == url_for("/docs")
assert_link("GET /", body, "self", "/", url_for)
assert_link("GET /", body, "service-description", "/openapi.json", url_for)
assert_link("GET /", body, "service-docs", "/docs", url_for, media_type="text/html")
assert_link("GET /", body, "conformance", "/conformance", url_for)
assert_link("GET /", body, "products", "/products", url_for)
assert_link("GET /", body, "orders", "/orders", url_for)
16 changes: 15 additions & 1 deletion tests/utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
from typing import Any
from typing import Any, Callable

link_dict = dict[str, Any]


def find_link(links: list[link_dict], rel: str) -> link_dict | None:
return next((link for link in links if link["rel"] == rel), None)


def assert_link(
req: str,
body: dict[str, Any],
rel: str,
path: str,
url_for: Callable[[str], str],
media_type: str = "application/json",
) -> None:
link = find_link(body["links"], rel)
assert link, f"{req} Link[rel={rel}] should exist"
assert link["type"] == media_type
assert link["href"] == url_for(path)

0 comments on commit 87770e4

Please sign in to comment.