Advanced topics
Configuring model key
By default, the primary key of a model inheriting from
hrefs.BaseReferrableModel is called id. This can be changed by
annotating another field with hrefs.PrimaryKey.
from typing import Annotated
from hrefs import Href, BaseReferrableModel, PrimaryKey
from pydantic import parse_obj_as
class MyModel(BaseReferrableModel):
my_id: Annotated[int, PrimaryKey]
class Config:
details_view = "get_my_model"
@app.get("/my_models/{my_id}")
def get_my_model(my_id: int) -> MyModel:
...
Then MyModel.my_id is the key used by Href[MyModel].
>>> model = MyModel(my_id=1)
>>> model.get_key()
1
>>> parse_obj_as(Href[MyModel], model)
Href(key=1, url=AnyHttpUrl(...'http://example.com/my_models/1'...))
Composite keys
It is also possible to annotate multiple fields with PrimaryKey. When using
composite keys with FastAPI or Starlette models, each
part of the key must appear in the route template.
from typing import Annotated
from hrefs import Href, BaseReferrableModel, PrimaryKey
from pydantic import parse_obj_as
class Page(BaseReferrableModel):
book_id: Annotated[int, PrimaryKey]
page_number: Annotated[int, PrimaryKey]
class Config:
details_view = "get_page"
@app.get("/books/{book_id}/pages/{page_number}")
def get_page(book_id: int, page_number: int) -> Page:
...
The primary key of the model will be a named tuple of the annotated parts.
>>> page = Page(book_id=1, page_number=123)
>>> page.get_key()
key(book_id=1, page_number=123)
>>> parse_obj_as(Href[Page], page)
Href(key=key(book_id=1, page_number=123), url=AnyHttpUrl(...'http://example.com/books/1/pages/123'...))
Hyperlinks as keys
A model can also have a hrefs.Href object as (a part of) its model key.
Modifying the example from the previous section we have:
from typing import Annotated
from hrefs import Href, BaseReferrableModel, PrimaryKey
from pydantic import parse_obj_as
class Book(BaseReferrableModel):
id: int
class Config:
details_view = "get_book"
class Page(BaseReferrableModel):
book: Annotated[Href[Book], PrimaryKey]
page_number: Annotated[int, PrimaryKey]
class Config:
details_view = "get_page"
@app.get("/books/{id}")
def get_book(id: int) -> Book:
...
@app.get("/books/{book_id}/pages/{page_number}")
def get_page(book_id: int, page_number: int) -> Page:
...
Note that the path parameter in the get_page route handler is called
book_id, which is the hyperlink name book joined to id – the model
key of Book. The key is automatically unwrapped when it appears in route
handler. In the model itself the name and type of book are preserved:
>>> page = Page(book=1, page_number=123)
>>> page.get_key()
key(book=Href(key=1, url=AnyHttpUrl(...'http://example.com/books/1'...)), page_number=123)
>>> href = parse_obj_as(Href[Page], page)
>>> href.key
key(book=Href(key=1, url=AnyHttpUrl(...'http://example.com/books/1'...)), page_number=123)
>>> href.url
AnyHttpUrl(...'http://example.com/books/1/pages/123'...)
Self hyperlinks
It is possible to have a hyperlink to the model itself as a primary key. Expanding the idea in Hyperlinks as keys, we can have:
from typing import Annotated
from hrefs import Href, BaseReferrableModel, PrimaryKey
from pydantic import parse_obj_as
class Book(BaseReferrableModel):
self: Annotated[Href["Book"], PrimaryKey(type_=int, name="id")]
class Config:
details_view = "get_book"
Book.update_forward_refs()
@app.get("/books/{id}")
def get_book(id: int) -> Book:
...
Note
Pydantic v2 supports postponed annotations, and
Model.update_forward_refs() is unnecessary. In fact, the method is
deprecated.
Note the need to use forward reference "Book" inside the body of the
class. That is because the name Book is not yet available in the class
body. Also the PrimaryKey annotation now includes the type_ argument to
indicate that the underlying key type is int. Without explicit type the
primary key definition would be circular, and the library would have no way of
knowing the actual key type.
The key name in the route handler is again unwrapped renamed to id. The
renaming is done with the name argument of the PrimaryKey. It is not
advisable to have self as an argument name in a route handler, because it
creates ambiguity with the self parameter Python uses in instance methods.
The unwrapping and renaming applies to the route handler. In the model fields
the name and type of self are preserved:
>>> book = Book(self=1)
>>> book.get_key()
1
>>> book
Book(self=Href(key=1, url=AnyHttpUrl(...'http://example.com/books/1'...)))
>>> parse_obj_as(Href[Book], book)
Href(key=1, url=AnyHttpUrl(...'http://example.com/books/1'...))
Having both id and self
It is possible to have self hyperlink without it being a primary key. A
common pattern in APIs is to include both id primary key and the self
hyperlink. A recipe to achieve that is:
from hrefs import Href, BaseReferrableModel
from pydantic import root_validator, parse_obj_as
class Book(BaseReferrableModel):
id: int
self: Href["Book"]
@root_validator(pre=True, allow_reuse=True)
def populate_self(cls, values):
values["self"] = values["id"]
return values
class Config:
details_view = "get_book"
Book.update_forward_refs()
@app.get("/books/{id}")
def get_book(id: int) -> Book:
...
In the above example, id is the primary key by the virtue of being called
id. self is just a regular field that happens to be a hyperlink to the
Book model itself. The Book.populate_self() validator runs on the whole
model before any other validation takes place, and takes care of populating the
self field from id.
>>> book = Book(id=1)
>>> book
Book(id=1, self=Href(key=1, url=AnyHttpUrl(...'http://example.com/books/1'...)))
Inheritance
It is possible for a referrable model to inherit another:
from hrefs import Href, BaseReferrableModel
from pydantic import parse_obj_as
class Book(BaseReferrableModel):
id: int
title: str
class Textbook(Book):
subject: str
class Config:
details_view = "get_textbook"
@app.get("/textbooks/{id}")
def get_textbook(id: int) -> Textbook:
...
The derived model Textbook inherits the key id and details view
"get_book" from its parent Book.
>>> textbook = Textbook(id=1, title="Introduction to hrefs", subject="hrefs")
>>> textbook.get_key()
1
>>> parse_obj_as(Href[Textbook], textbook)
Href(key=1, url=AnyHttpUrl(...'http://example.com/textbooks/1'...))
Primary key annotations are not composable across inheritance. it is not possible to define a part of the model key in the parent and another part in the derived model. Model key definitions — whether implicit or explicit — should only exist in one class of the inheritance tree.