-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* Add basic Siren classes * Add Siren Example based on HAL's * Add Support for Siren Links * Add Support for Siren Actions * Add Field type conversion to HTML Types * Prepopulare Siren Fields with values from the serialized object * Ruff Formatting * Add integration tests for Items * Add Siren Integration Tests * Fix bug with conditioned actions and links * Simplify integration tests * Enable optional population of fields * Use Model Objects instead of dicts * Refactor Actions and Link factory * Ruff formatting * Simplify parse_uri logic * Dereference schemas to avoid missing definitions * Add some SirenLinkFor tests * Add more unit tests for Links, Actions and Fields * Increase test coverage with unit tests to 100% * Fix edge case with None Actions/Links * Add Response validation against official jsonschema * Fix test for non-rendering actions * Fix Pylint and Mypy warnings * Remove unnecessary aliases for links and actions * Unify import statements * Add Python 3.8 compatibility * Use Ruff verbose mode * Use Python 3.8 compatible types * Avoid fixing issues in ruff check * Fix RUF022 * Sort imports in __all__ * Fix RUF022 check * Add poetry.lock to git to avoid version mismatches * Remove HAL's method and description
- Loading branch information
Showing
23 changed files
with
4,103 additions
and
58 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
from examples.hal.app import Item, ItemSummary, Person, app | ||
from examples.hal.data import curies, items, people | ||
|
||
__all__ = ["ItemSummary", "Item", "Person", "app", "items", "people", "curies"] | ||
__all__ = ["Item", "ItemSummary", "Person", "app", "curies", "items", "people"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
from examples.siren.app import Item, ItemSummary, Person, app | ||
from examples.siren.data import items, people | ||
|
||
__all__ = ["Item", "ItemSummary", "Person", "app", "items", "people"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import uvicorn | ||
|
||
if __name__ == "__main__": | ||
uvicorn.run("examples.siren.app:app", host="127.0.0.1", port=8000, reload=True) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
from typing import Any, Optional, Sequence, cast | ||
|
||
from fastapi import FastAPI, HTTPException | ||
from pydantic.main import BaseModel | ||
|
||
from examples.siren.data import Item as ItemData | ||
from examples.siren.data import Person as PersonData | ||
from examples.siren.data import items, people | ||
from fastapi_hypermodel import ( | ||
SirenActionFor, | ||
SirenHyperModel, | ||
SirenLinkFor, | ||
SirenResponse, | ||
) | ||
|
||
|
||
class ItemSummary(SirenHyperModel): | ||
name: str | ||
id_: str | ||
|
||
links: Sequence[SirenLinkFor] = ( | ||
SirenLinkFor("read_item", {"id_": "<id_>"}, rel=["self"]), | ||
) | ||
|
||
actions: Sequence[SirenActionFor] = ( | ||
SirenActionFor("update_item", {"id_": "<id_>"}, name="update"), | ||
) | ||
|
||
|
||
class Item(ItemSummary): | ||
description: Optional[str] = None | ||
price: float | ||
|
||
|
||
class ItemUpdate(BaseModel): | ||
name: Optional[str] = None | ||
description: Optional[str] = None | ||
price: Optional[float] = None | ||
|
||
|
||
class ItemCreate(BaseModel): | ||
id_: str | ||
|
||
|
||
class ItemCollection(SirenHyperModel): | ||
items: Sequence[Item] | ||
|
||
links: Sequence[SirenLinkFor] = (SirenLinkFor("read_items", rel=["self"]),) | ||
|
||
actions: Sequence[SirenActionFor] = ( | ||
SirenActionFor("read_item", templated=True, name="find"), | ||
SirenActionFor("update_item", templated=True, name="update"), | ||
) | ||
|
||
|
||
class Person(SirenHyperModel): | ||
name: str | ||
id_: str | ||
is_locked: bool | ||
|
||
items: Sequence[Item] | ||
|
||
links: Sequence[SirenLinkFor] = ( | ||
SirenLinkFor("read_person", {"id_": "<id_>"}, rel=["self"]), | ||
) | ||
|
||
actions: Sequence[SirenActionFor] = ( | ||
SirenActionFor("update_person", {"id_": "<id_>"}, name="update"), | ||
SirenActionFor( | ||
"put_person_items", | ||
{"id_": "<id_>"}, | ||
description="Add an item to this person and the items list", | ||
condition=lambda values: not values["is_locked"], | ||
name="add_item", | ||
populate_fields=False, | ||
), | ||
) | ||
|
||
|
||
class PersonCollection(SirenHyperModel): | ||
people: Sequence[Person] | ||
|
||
links: Sequence[SirenLinkFor] = (SirenLinkFor("read_people", rel=["self"]),) | ||
|
||
actions: Sequence[SirenActionFor] = ( | ||
SirenActionFor( | ||
"read_person", | ||
description="Get a particular person", | ||
templated=True, | ||
name="find", | ||
), | ||
SirenActionFor( | ||
"update_person", | ||
description="Update a particular person", | ||
templated=True, | ||
name="update", | ||
), | ||
) | ||
|
||
|
||
class PersonUpdate(BaseModel): | ||
name: Optional[str] = None | ||
is_locked: Optional[bool] = None | ||
|
||
|
||
app = FastAPI() | ||
SirenHyperModel.init_app(app) | ||
|
||
|
||
@app.get("/items", response_model=ItemCollection, response_class=SirenResponse) | ||
def read_items() -> Any: | ||
return items | ||
|
||
|
||
@app.get("/items/{id_}", response_model=Item, response_class=SirenResponse) | ||
def read_item(id_: str) -> Any: | ||
return next(item for item in items["items"] if item["id_"] == id_) | ||
|
||
|
||
@app.put("/items/{id_}", response_model=Item, response_class=SirenResponse) | ||
def update_item(id_: str, item: ItemUpdate) -> Any: | ||
base_item = next(item for item in items["items"] if item["id_"] == id_) | ||
update_item = cast(ItemData, item.model_dump(exclude_none=True)) | ||
base_item.update(update_item) | ||
return base_item | ||
|
||
|
||
@app.get("/people", response_model=PersonCollection, response_class=SirenResponse) | ||
def read_people() -> Any: | ||
return people | ||
|
||
|
||
@app.get("/people/{id_}", response_model=Person, response_class=SirenResponse) | ||
def read_person(id_: str) -> Any: | ||
return next(person for person in people["people"] if person["id_"] == id_) | ||
|
||
|
||
@app.put("/people/{id_}", response_model=Person, response_class=SirenResponse) | ||
def update_person(id_: str, person: PersonUpdate) -> Any: | ||
base_person = next(person for person in people["people"] if person["id_"] == id_) | ||
update_person = cast(PersonData, person.model_dump(exclude_none=True)) | ||
base_person.update(update_person) | ||
return base_person | ||
|
||
|
||
@app.put("/people/{id_}/items", response_model=Person, response_class=SirenResponse) | ||
def put_person_items(id_: str, item: ItemCreate) -> Any: | ||
complete_item = next( | ||
(item_ for item_ in items["items"] if item_["id_"] == item.id_), | ||
None, | ||
) | ||
if not complete_item: | ||
raise HTTPException(status_code=404, detail=f"No item found with id {item.id_}") | ||
|
||
base_person = next(person for person in people["people"] if person["id_"] == id_) | ||
|
||
base_person_items = base_person["items"] | ||
base_person_items.append(complete_item) | ||
return base_person |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
from typing import List | ||
|
||
from typing_extensions import NotRequired, TypedDict | ||
|
||
|
||
class Item(TypedDict): | ||
id_: str | ||
name: str | ||
price: float | ||
description: NotRequired[str] | ||
|
||
|
||
class Items(TypedDict): | ||
items: List[Item] | ||
|
||
|
||
items: Items = { | ||
"items": [ | ||
{"id_": "item01", "name": "Foo", "price": 10.2}, | ||
{ | ||
"id_": "item02", | ||
"name": "Bar", | ||
"description": "The Bar fighters", | ||
"price": 62, | ||
}, | ||
{ | ||
"id_": "item03", | ||
"name": "Baz", | ||
"description": "There goes my baz", | ||
"price": 50.2, | ||
}, | ||
{ | ||
"id_": "item04", | ||
"name": "Doe", | ||
"description": "There goes my Doe", | ||
"price": 5, | ||
}, | ||
] | ||
} | ||
|
||
|
||
class Person(TypedDict): | ||
id_: str | ||
name: str | ||
is_locked: bool | ||
items: List[Item] | ||
|
||
|
||
class People(TypedDict): | ||
people: List[Person] | ||
|
||
|
||
people: People = { | ||
"people": [ | ||
{ | ||
"id_": "person01", | ||
"name": "Alice", | ||
"is_locked": False, | ||
"items": items["items"][:2], | ||
}, | ||
{ | ||
"id_": "person02", | ||
"name": "Bob", | ||
"is_locked": True, | ||
"items": items["items"][2:], | ||
}, | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,4 @@ | ||
from examples.url_for.app import Item, ItemSummary, Person, app | ||
from examples.url_for.data import items, people | ||
|
||
__all__ = [ | ||
"items", | ||
"people", | ||
"Person", | ||
"ItemSummary", | ||
"Item", | ||
"app", | ||
] | ||
__all__ = ["Item", "ItemSummary", "Person", "app", "items", "people"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.