Tools to convert Django ORM models to Pydantic models
Project description
dantico
Tools to convert Django ORM models to Pydantic models.
The key features are:
-
Custom Field Support: Create Pydantic Schemas from Django Models with default field type validations out of the box.
-
Field Validator: Fields can be validated with Pydantic validator or root_validator.
Requirements
- Python 3.7+
- Django 3.0+
- Pydantic 1.6+
Installation
pip install dantico
Contents
- Basic usage
- Excluding and including model fields
- Optional model fields
- Introspect the related objects
- Schema customization
- Field validator
Usage
Here are a few examples of what you can do with dantico:
Basic
Assume we have the following user model definition:
# models.py
from django.db import models
class User(models.Model):
GENDER_MALE = "male"
GENDER_FEMALE = "female"
GENDER_OTHER = "other"
GENDER_CHOICES = (
(GENDER_MALE, "Male"),
(GENDER_FEMALE, "Female"),
(GENDER_OTHER, "Other"),
)
username = models.CharField(max_length=20)
age = models.IntegerField()
gender = models.CharField(
choices=GENDER_CHOICES,
max_length=10,
blank=True,
)
password = models.CharField(max_length=100)
company = models.ForeignKey(
Company,
on_delete=models.CASCADE,
)
languages = models.ManyToManyField(Language)
def __str__(self):
return self.name
Using the ModelSchema
class will automatically generate schemas from our User
model.
# schemas.py
from dantico import ModelSchema
from users.models import User
class UserSchema(ModelSchema):
class Config:
model = User
json_output = json.dumps(UserSchema.schema(), indent=4)
print(json_output)
# Output:
{
"title": "UserSchema",
"type": "object",
"properties": {
"id": {
"title": "Id",
"extra": {},
"type": "integer"
},
"Username": {
"title": "Username",
"maxLength": 20,
"type": "string"
},
"age": {
"title": "Age",
"type": "integer"
},
"gender": {
"title": "Gender",
"allOf": [
{
"$ref": "#/definitions/GenderEnum"
}
]
},
"password": {
"title": "Password",
"maxLength": 100,
"type": "string"
},
"company_id": {
"title": "Company",
"type": "integer"
},
"languages": {
"title": "Languages",
"type": "array",
"items": {
"type": "integer"
}
}
},
"required": [
"Username",
"age",
"password",
"company_id",
"languages"
],
"definitions": {
"GenderEnum": {
"title": "GenderEnum",
"description": "An enumeration.",
"enum": [
"male",
"female",
"other"
]
}
}
}
Excluding and including model fields
By default dantico include all the fields from the Django model. As a rule of thumb, always use the include
or exclude
attribute to explicitly define a list of fields that you want to be visible in your API. Note that you cannot use both at the same time.
# schemas.py
from dantico import ModelSchema
from users.models import User
class UserSchema(ModelSchema):
class Config:
model = User
exclude = ["password", "age"]
json_output = json.dumps(UserSchema.schema(), indent=4)
print(json_output)
# Output:
{
"title": "UserSchema",
"type": "object",
"properties": {
"id": {
"title": "Id",
"extra": {},
"type": "integer"
},
"username": {
"title": "Username",
"maxLength": 20,
"type": "string"
},
"gender": {
"title": "Gender",
"allOf": [
{
"$ref": "#/definitions/GenderEnum"
}
]
},
"company_id": {
"title": "Company",
"type": "integer"
},
"languages": {
"title": "Languages",
"type": "array",
"items": {
"type": "integer"
}
}
},
"required": [
"username",
"company_id",
"languages"
],
"definitions": {
"GenderEnum": {
"title": "GenderEnum",
"description": "An enumeration.",
"enum": [
"male",
"female",
"other"
]
}
}
}
An example of using include
:
# schemas.py
from dantico import ModelSchema
from users.models import User
class UserSchema(ModelSchema):
class Config:
model = User
include = ["username", "age", "company"]
json_output = json.dumps(UserSchema.schema(), indent=4)
print(json_output)
# Output:
{
"title": "UserSchema",
"type": "object",
"properties": {
"username": {
"title": "Username",
"maxLength": 20,
"type": "string"
},
"age": {
"title": "Age",
"type": "integer"
},
"company_id": {
"title": "Company",
"type": "integer"
}
},
"required": [
"Username",
"age",
"company_id"
]
}
Optional model fields
We can also specify model fields to mark as optional
.
# schemas.py
from dantico import ModelSchema
from users.models import User
class UserSchema(ModelSchema):
class Config:
model = User
exclude = ["password", "languages"]
optional = ["age"] # 'age' schema field is now optional
json_output = json.dumps(UserSchema.schema(), indent=4)
print(json_output)
# Output:
{
"title": "UserSchema",
"type": "object",
"properties": {
"id": {
"title": "Id",
"extra": {},
"type": "integer"
},
"username": {
"title": "Username",
"maxLength": 20,
"type": "string"
},
"age": {
"title": "Age",
"extra": {},
"type": "integer"
},
"gender": {
"title": "Gender",
"allOf": [
{
"$ref": "#/definitions/GenderEnum"
}
]
},
"company_id": {
"title": "Company",
"type": "integer"
}
},
"required": [
"username",
"company_id"
],
"definitions": {
"GenderEnum": {
"title": "GenderEnum",
"description": "An enumeration.",
"enum": [
"male",
"female",
"other"
]
}
}
}
Introspect the related objects
The depth
attribute lets us look into the Django model relations (many to one, one to one, many to many).
Consider the following models definitions:
# models.py
from django.db import models
class Company(models.Model):
name = models.CharField(max_length=20)
location = models.CharField(max_length=20)
date_created = models.DateField()
def __str__(self):
return self.name
class Language(models.Model):
name = models.CharField(max_length=20)
creator = models.CharField(max_length=20)
paradigm = models.CharField(max_length=20)
date_created = models.DateField()
def __str__(self):
return self.name
class User(models.Model):
GENDER_MALE = "male"
GENDER_FEMALE = "female"
GENDER_OTHER = "other"
GENDER_CHOICES = (
(GENDER_MALE, "Male"),
(GENDER_FEMALE, "Female"),
(GENDER_OTHER, "Other"),
)
username = models.CharField(max_length=20)
age = models.IntegerField()
gender = models.CharField(
choices=GENDER_CHOICES,
max_length=10,
blank=True,
)
password = models.CharField(max_length=100)
company = models.ForeignKey(
Company,
on_delete=models.CASCADE,
)
languages = models.ManyToManyField(Language)
def __str__(self):
return self.name
Now let's add the depth
attribute:
# schemas.py
from dantico import ModelSchema
from users.models import User
class UserSchema(ModelSchema):
class Config:
model = User
exclude = ["password", "age", "gender"]
optional = ["age"]
depth = 1 # by default, depth = 0
json_output = json.dumps(UserSchema.schema(), indent=4)
print(json_output)
# Output:
{
"title": "UserSchema",
"type": "object",
"properties": {
"id": {
"title": "Id",
"extra": {},
"type": "integer"
},
"username": {
"title": "Username",
"maxLength": 20,
"type": "string"
},
"company": {
"title": "Company",
"allOf": [
{
"$ref": "#/definitions/Company"
}
]
},
"languages": {
"title": "Languages",
"type": "array",
"items": {
"$ref": "#/definitions/Language"
}
}
},
"required": [
"username",
"company",
"languages"
],
"definitions": {
"Company": {
"title": "Company",
"type": "object",
"properties": {
"id": {
"title": "Id",
"extra": {},
"type": "integer"
},
"name": {
"title": "Name",
"maxLength": 20,
"type": "string"
},
"location": {
"title": "Location",
"maxLength": 20,
"type": "string"
},
"date_created": {
"title": "Date Created",
"type": "string",
"format": "date"
}
},
"required": [
"name",
"location",
"date_created"
]
},
"Language": {
"title": "Language",
"type": "object",
"properties": {
"id": {
"title": "Id",
"extra": {},
"type": "integer"
},
"name": {
"title": "Name",
"maxLength": 20,
"type": "string"
},
"creator": {
"title": "Creator",
"maxLength": 20,
"type": "string"
},
"paradigm": {
"title": "Paradigm",
"maxLength": 20,
"type": "string"
},
"date_created": {
"title": "Date Created",
"type": "string",
"format": "date"
}
},
"required": [
"name",
"creator",
"paradigm",
"date_created"
]
}
}
}
Schema customization
Docstrings and titles can be used as descriptive text in the schema output.
# schemas.py
from pydantic import Field
from dantico import ModelSchema
from users.models import User
class UserSchema(ModelSchema):
"""My user model schema"""
username: str = Field(
title="The user's username",
description="This is the user's username",
)
age: int = Field(
None,
title="The user's age",
description="This is the user's age",
)
class Config:
model = User
exclude = ["password", "gender", "languages"]
title = "User schema"
json_output = json.dumps(UserSchema.schema(), indent=4)
print(json_output)
# Output:
{
"title": "User schema",
"description": "My user model schema",
"type": "object",
"properties": {
"username": {
"title": "The user's username",
"description": "This is the user's username",
"type": "string"
},
"age": {
"title": "The user's age",
"description": "This is the user's age",
"type": "integer"
},
"id": {
"title": "Id",
"extra": {},
"type": "integer"
},
"company_id": {
"title": "Company",
"type": "integer"
}
},
"required": [
"username",
"company_id"
]
}
Field validator
Custom validation is not an easy task in this case. But we still can achieve it by using threads.
Because we define a validator to validate fields on inheriting models, we should set check_fields=False
on the validator. More information can be found here.
# schemas.py
import asyncio
import concurrent.futures
from asgiref.sync import sync_to_async
from dantico import ModelSchema
from pydantic import validator
from users.models import User
class UserSchema(ModelSchema):
class Config:
model = User
exclude = ["password"]
@validator("username", check_fields=False)
def validate_username(cls, v):
"""
Here we are using async method as validator. Because there is
already an event loop (using FastAPI), we need to start another thread.
:param cls: Access the class of the object that is being validated
:param v: Validate the value
:return: The result of the username_must_be_unique function
"""
@sync_to_async
def username_must_be_unique():
if User.objects.filter(username__icontains=v).exists():
raise ValueError("username already exists")
return v
# A way to run async code in a sync environment.
pool = concurrent.futures.ThreadPoolExecutor(1)
result = pool.submit(asyncio.run, username_must_be_unique()).result()
return result
License
This project is licensed under the terms of the MIT license.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file dantico-0.0.9.tar.gz
.
File metadata
- Download URL: dantico-0.0.9.tar.gz
- Upload date:
- Size: 23.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/4.0.1 CPython/3.9.14
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 627ca09b48ef0c6d0cf99f765e7084e69deb444d9a95126cae24639b915036f3 |
|
MD5 | 505b70133e25c442dd98d966b9b5e3ca |
|
BLAKE2b-256 | 07efada98d055e995fb319479c848af8f6cbca39d0e3a440a3519b230999cbd0 |
File details
Details for the file dantico-0.0.9-py3-none-any.whl
.
File metadata
- Download URL: dantico-0.0.9-py3-none-any.whl
- Upload date:
- Size: 17.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/4.0.1 CPython/3.9.14
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | a1463d57b2c5ddf37a3c31d90cd0bd14ba42ee2c019647a45e7d4b606f1dc076 |
|
MD5 | 67904b6f9f1a0f8ac959fecd99ec2ce0 |
|
BLAKE2b-256 | 7fde48b56d6a4556a691f3aff866bf75271dccd2fc0a0e0d23cecf81303e2e01 |