Let's say I have a route that allows clients to create a new user
(pseudocode)
#app.route("POST")
def create_user(user: UserScheme, db: Session = Depends(get_db)) -> User:
...
and my UserScheme accepts a field such as an email. I would like to be able to set some settings (for example max_length) globally in a different model Settings. How do I access that inside a scheme? I'd like to access the db inside my scheme.
So basically my scheme should look something like this (the given code does not work):
class UserScheme(BaseModel):
email: str
#validator("email")
def validate_email(cls, value: str) -> str:
settings = get_settings(db) # `db` should be set somehow
if len(value) > settings.email_max_length:
raise ValueError("Your mail might not be that long")
return value
I couldn't find a way to somehow pass db to the scheme. I was thinking about validating such fields (that depend on db) inside my route. While this approach works somehow, the error message itself is not raised on the specific field but rather on the entire form, but it should report the error for the correct field so that frontends can display it correctly.
One option to accept arbitrary JSON objects as input, and then construct a UserScheme instance manually inside the route handler:
#app.route(
"POST",
response_model=User,
openapi_extra={
"requestBody": {
"content": {
"application/json": {
"schema": UserScheme.schema(ref_template="#/components/schemas/{model}")
}
}
}
},
)
def create_user(request: Request, db: Session = Depends(get_db)) -> User:
settings = get_settings(db)
user_data = request.json()
user_schema = UserScheme(settings, **user_data)
Note that this idea was borrowed from https://stackoverflow.com/a/68815913/2954547, and I have not tested it myself.
In order to facilitate the above, you might want to redesign this class so that the settings object itself as an attribute on the UserScheme model, which means that you don't ever need to perform database access or other effectful operations inside the validator, while also preventing you from instantiating a UserScheme without some kind of sensible settings in place, even if they are fallbacks or defaults.
class SystemSettings(BaseModel):
...
def get_settings(db: Session) -> SystemSettings:
...
EmailAddress = typing.NewType('EmailAddress', st)
class UserScheme(BaseModel):
settings: SystemSettings
if typing.TYPE_CHECKING:
email: EmailAddress
else:
email: str | EmailAddress
#validator("email")
def _validate_email(cls, value: str, values: dict[str, typing.Any]) -> EmailAddress:
if len(value) > values['settings'].max_email_length:
raise ValueError('...')
return EmailAddress(value)
The use of tyipng.NewType isn't necessary here, but I think it's a good tool in situations like this. Note that the typing.TYPE_CHECKING trick is required to make it work, as per https://github.com/pydantic/pydantic/discussions/4823.
Related
In ASP.NET Core you have the concept of adding a service/object as "scoped" which means that it is the same object within a request. We used this concept e.g. to get the user out of the Bearer token header and provide the user data project-wide over a "CurrentUserService".
I'm searching for a way to do this using Pythons FastAPI and Beanie framework. Here is the use-case:
I want to use the event-based actions of beanie to automatically set the user data before a database update. Here is the current state of the code:
class DataEntity(Document):
name: str
last_modified_by: str
#before_event(Replace)
def set_version_data(self):
#here the current user should be set into last_modified_by
So my question is: How can I set the last_modified_by field on this event level?
I already tried to use the FastAPI dependencies, because they look like a scoped wrap-around I searched for:
async def get_current_user(x_user: str = Header(None)) -> str:
return x_user
class DataEntity(Document):
name: str
last_modified_by: str
#before_event(Replace)
def set_version_data(self, current_user: str = Depends(get_current_user)):
self.last_modified_by = current_user
But this is not handled right and I get the error "pydantic.error_wrappers.ValidationError: 1 validation error for DataEntity: last_modified_by - str type expected (type=type_error.str)".
Using a ORM, I want to do a POST request letting some fields with a null value, which will be translated in the database for the default value specified there.
The problem is that OpenAPI (Swagger) docs, ignores the default None and still prompts a UUID by default.
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional
from uuid import UUID
import uvicorn
class Table(BaseModel):
# ID: Optional[UUID] # the docs show a example UUID, ok
ID: Optional[UUID] = None # the docs still shows a uuid, when it should show a null or valid None value.
app = FastAPI()
#app.post("/table/", response_model=Table)
def create_table(table: Table):
# here we call to sqlalchey orm etc.
return 'nothing important, the important thing is in the docs'
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
In the OpenAPI schema example (request body) which is at the docs we find:
{
"ID": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
This is not ok, because I specified that the default value is None,so I expected this instead:
{
"ID": null, # null is the equivalent of None here
}
Which will pass a null to the ID and finally will be parsed in the db to the default value (that is a new generated UUID).
When you declare Optional parameters, users shouldn't have to include those parameters in their request (specified with null value) in order to be null. The default value of the parameters will be null, unless the user specifies some other value when sending the request.
Hence, all you have to do is to declare a custom example for the Pydantic model using Config and schema_extra, as described in the documentation and as shown below. The below example will create an empty (i.e., {}) request body in OpenAPI (Swagger UI), which can be successfully submitted (as ID is the only attribute of the model and is optional).
class Table(BaseModel):
ID: Optional[UUID] = None
class Config:
schema_extra = {
"example": {
}
}
#app.post("/table/", response_model=Table)
def create_table(table: Table):
return table
If the Table model included some other required attributes, you could add example values for those, as demonstrated below:
class Table(BaseModel):
ID: Optional[UUID] = None
some_attr: str
class Config:
schema_extra = {
"example": {
"some_attr": "Foo"
}
}
If you would like to keep the auto-generated examples for the rest of the attributes except the one for the ID attribute, you could use the below to remove ID from the model's properties in the generated schema (inspired by Schema customization):
class Table(BaseModel):
ID: Optional[UUID] = None
some_attr: str
some_attr2: float
some_attr3: bool
class Config:
#staticmethod
def schema_extra(schema: Dict[str, Any], model: Type['Table']) -> None:
del schema.get('properties')['ID']
Also, if you would like to add custom example to some of the attributes, you could use Field() (as described here); for example, some_attr: str = Field(example="Foo").
Another possible solution would be to modify the generated OpenAPI schema, as described in Solution 3 of this answer. Though, the above solution is likely more suited to this case.
Note
ID: Optional[UUID] = None is the same as ID: UUID = None. As previously documented in FastAPI website (see this answer):
The Optional in Optional[str] is not used by FastAPI, but will allow
your editor to give you better support and detect errors.
Since then, FastAPI has revised their documentation with the following:
The Union in Union[str, None] will allow your editor to give you
better support and detect errors.
Hence, ID: Union[UUID, None] = None is the same as ID: Optional[UUID] = None and ID: UUID = None. In Python 3.10+, one could also use ID: UUID| None = None (see here).
As per FastAPI documentation (see Info section in the link provided):
Have in mind that the most important part to make a parameter optional
is the part:
= None
or the:
= Query(default=None)
as it will use that None as the default value, and that way make the
parameter not required.
The Union[str, None] part allows your editor to provide better
support, but it is not what tells FastAPI that this parameter is not
required.
I'm handling this request in my code (Python3.9, FastAPI, Pydantic):
https://myapi.com/api?params[A]=1¶ms[B]=2
I tried to make following model:
BaseModel for handling special get request
(for fastapi.Query and pydantic.Field is same)
I also set up aliases for it, but in swagger docs I see next field:
Snap of the swagger docs
There are fields that are specified as extra_data
So, if I specify query params in parameters of my endpoint like this:
#app.get('/')
def my_handler(a: str = Query(None, alias="params[A]")):
return None
Everything works fine. How can I fix it? I want to initialize my pydantic.BaseModel with speacial aliases using this way and avoid usage of query-params in
class MyModel(BaseModel):
a = Field(alias="params[A]")
b = Field(alias="params[B]")
def my_handler(model: MyModel = Depends()):
return model.dict()
I have a 'core' Django product that includes default implementations of common tasks, but I want to allow that implementation to be redefined (or customised if that makes it easier).
For example in the core product, I might have a view which allows a user to click a button to resend 'all notifications':
# in core/views.py
... imports etc...
from core.tasks import resend_notifications
def handle_user_resend_request(request, user_id):
user = get_object_or_404(id=user_id)
if request.method == 'POST':
for follower in user.followers:
resend_notifications(follower.id)
... etc etc ...
# in core/tasks.py
... imports etc...
def resend_notifications(id):
send_email(User.objects.get(id=id))
And then in some deployments of this product, perhaps the 'resend_notifications' needs to look like:
# in customer_specific/tasks.py
... imports etc ...
def resend_notifications(id):
person = User.objects.get(id=id)
if '#super-hack.email.com' in person.email:
# This is not a real email, send via the magic portal
send_via_magic(person)
else:
send_email(person)
# and send via fax for good measure
send_fax(person)
How do I get the resend_notifications function in the views.py file to point to the customer_specific version?
Should I be defining this in the Django config and sharing access that way? What if the tasks are actually Celery tasks?
NB: The tasks I have are actually defined as Celery tasks (I removed this extra detail because I think the question is more general). I have tried with a custom decorator tag that mutates a global object, but that is definitely not the way to go for a number of reasons.
PS: I feel like this is a dependency injection question, but that is not a common thing in Django.
In a similar situation I ended up going for a solution like so – I put this on my Organization model in the application (equiv of a GitHub organization).
#property
def forms(self):
if self.ldap:
from portal.ldap import forms
else:
from portal.users import forms
return forms
I essentially wanted to use different form classes if the organization the authenticated user belongs to has LDAP configured – and thus the create/invite user forms needed to be different.
I then overwrote get_form_class in the appropriate views like so:
def get_form_class(self):
return self.request.user.organization.forms.CreateUserForm
I imagine you might want to do something similar in your scenario, wrap your function(s) in a proxy abstraction that determines which version to use – be that based on env vars, settings or the request.
This ended up being solved via a Django settings object that can be reconfigured by the deployment config. It was largely inspired by the technique here: settings.py from django-rest-framework.
For example, I have a settings file like this in my project:
yourproject/settings.py
"""
Settings for <YOUR PROJECT> are all namespaced in the YOUR_PROJECT config option.
For example your project's config file (usually called `settings.py` or 'production.py') might look like this:
YOUR_PROJECT = {
'PROCESS_TASK': (
'your_project.tasks.process_task',
)
}
This module provides the `yourproject_settings` object, that is used
to access settings, checking for user settings first, then falling
back to the defaults.
"""
# This file was effectively borrow from https://github.com/tomchristie/django-rest-framework/blob/8385ae42c06b8e68a714cb67b7f0766afe316883/rest_framework/settings.py
from __future__ import unicode_literals
from django.conf import settings
from django.utils.module_loading import import_string
DEFAULTS = {
'RESEND_NOTIFICATIONS_TASK': 'core.tasks.resend_notifications',
}
# List of settings that may be in string import notation.
IMPORT_STRINGS = (
'RESEND_NOTIFICATIONS_TASK',
)
MANDATORY_SETTINGS = (
'RESEND_NOTIFICATIONS_TASK',
)
def perform_import(val, setting_name):
"""
If the given setting is a string import notation,
then perform the necessary import or imports.
"""
if val is None:
return None
if callable(val):
return val
if isinstance(val, (list, tuple)):
return [perform_import(item, setting_name) for item in val]
try:
return import_string(val)
except (ImportError, AttributeError) as e:
msg = "Could not import '%s' for setting '%s'. %s: %s." % (val, setting_name, e.__class__.__name__, e)
raise ImportError(msg)
class YourProjectSettings(object):
"""
A settings object, that allows settings to be accessed as properties.
For example:
from your_project.settings import yourproject_settings as the_settings
print(the_settings.RESEND_NOTIFICATIONS_TASK)
Any setting with string import paths will be automatically resolved
and return the class, rather than the string literal.
"""
namespace = 'YOUR_PROJECT'
def __init__(self, mandatory=None, defaults=None, import_strings=None):
self.mandatory = mandatory or MANDATORY_SETTINGS
self.defaults = defaults or DEFAULTS
self.import_strings = import_strings or IMPORT_STRINGS
self.__check_settings()
#property
def user_settings(self):
if not hasattr(self, '_user_settings'):
self._user_settings = getattr(settings, self.__class__.namespace, {})
return self._user_settings
def __getattr__(self, attr):
if attr not in self.defaults and attr not in self.mandatory:
raise AttributeError("Invalid Pyrite setting: '%s'" % attr)
try:
# Check if present in user settings
val = self.user_settings[attr]
except KeyError:
# Fall back to defaults
val = self.defaults[attr]
# Coerce import strings into classes
if attr in self.import_strings:
val = perform_import(val, attr)
# Cache the result
setattr(self, attr, val)
return val
def __check_settings(self):
for setting in self.mandatory:
if setting not in self.user_settings:
raise RuntimeError(
'The "{}" setting is required as part of the configuration for "{}", but has not been supplied.'.format(
setting, self.__class__.namespace))
yourproject_settings = YourProjectSettings(MANDATORY_SETTINGS, DEFAULTS, IMPORT_STRINGS)
This allows me to either:
Use the default value (i.e. 'core.tasks.resend_notications'); OR
To redefine the binding in my config file:
site_config/special.py
... other django settings like DB / DEBUG / Static files etc
YOUR_PROJECT = {
'RESEND_NOTIFICATIONS_TASK': 'customer_specific.tasks.resend_notifications',
}
... etc. ...
Then in my view function, I access the correct function via the settings:
core/views.py
... imports etc...
from yourproject.settings import yourproject_settings as my_settings
def handle_user_resend_request(request, user_id):
user = get_object_or_404(id=user_id)
if request.method == 'POST':
for follower in user.followers:
my_settings.RESEND_NOTIFICATIONS_TASK(follower.id)
... etc etc ...
I am facing a very weird issue today.
Here is my serializer class.
class Connectivity(serializers.Serializer):
device_type = serializers.CharField(max_length=100,required=True)
device_name = serializers.CharField(max_length=100,required=True)
class Connections(serializers.Serializer):
device_name = serializers.CharField(max_length=100,required=True)
connectivity = Connectivity(required = True, many = True)
class Topologyserializer(serializers.Serializer):
name = serializers.CharField(max_length=100,required=True, \
validators=[UniqueValidator(queryset=Topology.objects.all())])
json = Connections(required=True,many=True)
def create(self, validated_data):
return validated_data
I am calling Topologyserializer from a Django view and I am passing a json like:
{
"name":"tokpwol",
"json": [
]
}
As per my experience with DRF since I have mentioned required = True in json field it should not accept the above json.
But I am able to create record.
Can anyone suggest me why it is not validating the json field and how it accepting empty list as json field?
I am using django rest framework 3.0.3
DRF does not clearly state what required stands for for lists.
In its code, it appears that validation passes as long as a value is supplied, even if that value is an empty list.
If you want to ensure the list is not empty, you'll need to validate its content manually. You would do that by adding the following method on your TopologySerializer:
def validate_json(self, value):
if not value:
raise serializers.ValidationError("Connections list is empty")
return value
I cannot test it right now, but it should work.