Register Resources¶
A resource is a json document that is served by a service The example below, that has been copy paste from the test suite show how to register a resource.
Full example of blacksmith regitration¶
from enum import Enum
from typing import Optional
from blacksmith import (
PathInfoField,
PostBodyField,
QueryStringField,
Request,
Response,
register,
)
class SizeEnum(str, Enum):
s = "S"
m = "M"
l = "L"
class PartialItem(Response):
name: str = ""
class Item(Response):
name: str = ""
size: SizeEnum = SizeEnum.m
class CreateItem(Request):
name: str = PostBodyField()
size: SizeEnum = PostBodyField(SizeEnum.m)
class ListItem(Request):
name: Optional[str] = QueryStringField(None)
class GetItem(Request):
item_name: str = PathInfoField()
class UpdateItem(GetItem):
name: Optional[str] = PostBodyField(None)
size: Optional[SizeEnum] = PostBodyField(None)
DeleteItem = GetItem
register(
"api",
"item",
"api",
None,
collection_path="/items",
collection_contract={
"GET": (ListItem, PartialItem),
"POST": (CreateItem, None),
},
path="/items/{item_name}",
contract={
"GET": (GetItem, Item),
"PATCH": (UpdateItem, None),
"DELETE": (DeleteItem, None),
},
)
Request parameters¶
The first things to do is to create models that represent every routes.
To represent a request parameter, the base class blacksmith.Request
has to be overridden, with special fields
blacksmith.HeaderField
,
blacksmith.PathInfoField
,
blacksmith.PostBodyField
or
blacksmith.QueryStringField
.
For instance:
class CreateItem(Request):
name: str = PostBodyField()
size: SizeEnum = PostBodyField(SizeEnum.m)
Response¶
The response represent only the json body of a response. When no schema is passed (explicitly None), then, the raw response is returned.
Note
Note that for collection_contract
, the response of the GET
method,
does not have to be a list. Elements of the list are validated by the schema
one by one.
This is the only difference between collection_contract
and contract
,
and it is the only schema that behave like this.
class Item(Response):
name: str = ""
size: SizeEnum = SizeEnum.m
Note
Both Request and Response are Pydantic models. So you can use all the Pydantic validation you want.
Registration¶
The client_name is the name to access to the resource using the client factory. Everytime the client_name is used, it must always match the same (service, version) otherwise an exception will be raised during the load of the application.
This is a design decision to avoid to register client with service and version, then resources. But the client name reprent an internal name for a service.
By the way, sometime, it may be usefull to register the same resource of a service under different client_name by registering different parameter. The idea here is to register a client for a specific usage and you may have different schema for that.
Lastly, the resource will be accessible as a property of the client that will be manipulable using methods where the Request define the parameter type of the method, and the Response define the response type.
blacksmith.register(
client_name="api",
resource="item",
service="api",
version=None,
collection_path="/items",
collection_contract={
"GET": (ListItem, Item),
"POST": (CreateItem, None),
},
path="/items/{item_name}",
contract={
"GET": (GetItem, Item),
"PATCH": (UpdateItem, None),
"DELETE": (DeleteItem, None),
},
)
Not that you can only declare the path and collection_path consumed.
This is completely valid to register only a single route.
import blacksmith
class SearchItem(blacksmith.Request):
name_like = blacksmith.QueryStringField(alias="~name")
class Item(blacksmith.Response):
name: str
blacksmith.register(
client_name="api",
resource="item",
service="datastore",
version="v1",
path="/search",
collection_contract={
"GET": (SearchItem, Item),
},
)
or event a collection to bind an api that return a list.
blacksmith.register(
client_name="api",
resource="item",
service="datastore",
version="v1",
path="/search",
collection_contract={
"GET": (SearchItem, Item),
},
)
Note
An exception will be raised if a path or an http method has not been declared. No http request will be made.
To improve your request typing, you may use to have a set of distinct parameters, such as in the example above. This is usefull to deal with exclusive parameters.
from typing import Union
import blacksmith
class SearchItem(blacksmith.Request):
name_like = blacksmith.QueryStringField(alias="~name")
class SearchUser(blacksmith.Request):
first_name_like = blacksmith.QueryStringField(alias="~first_name")
class Item(blacksmith.Response):
name: str
blacksmith.register(
client_name="api",
resource="item",
service="datastore",
version="v1",
path="/search",
collection_contract={
"GET": (Union[SearchItem, SearchUser], Item),
},
)
Scanning resources¶
To keep the code clean, a good practice is to have a module named resources
and one submodule per services, then to have one submodule per per resources.
Something like this:
mypkg/resources
mypkg/resources/__init__.py
mypkg/resources/serviceA/__init__.py
mypkg/resources/serviceA/resourceA.py
mypkg/resources/serviceA/resourceB.py
mypkg/resources/serviceB/__init__.py
mypkg/resources/serviceB/resourceC.py
mypkg/resources/serviceB/resourceD.py
Then to load all the resources, use the blacksmith.scan()
method:
import blacksmith
# Fully load the registry with all resources
blacksmith.scan("mypkg.resources")
Important
There is no difference in the resources declaration for asynchronous and synchronous API. Resources declaration define what to consume, not how it will be consumed at the runtime.