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'...))
Note
Before Python 3.8, typing_extensions.Annotated
can be used to annotate
the fields.
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.