Skip to main content

Tools to convert Django ORM models to Pydantic models

Project description

dantico

Tools to convert Django ORM models to Pydantic models.

GitHub Actions (Test) Codecov PyPI version Downloads PyPI Supported Python Versions PyPI Supported Django Versions

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

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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

dantico-0.0.9.tar.gz (23.5 kB view details)

Uploaded Source

Built Distribution

dantico-0.0.9-py3-none-any.whl (17.4 kB view details)

Uploaded Python 3

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

Hashes for dantico-0.0.9.tar.gz
Algorithm Hash digest
SHA256 627ca09b48ef0c6d0cf99f765e7084e69deb444d9a95126cae24639b915036f3
MD5 505b70133e25c442dd98d966b9b5e3ca
BLAKE2b-256 07efada98d055e995fb319479c848af8f6cbca39d0e3a440a3519b230999cbd0

See more details on using hashes here.

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

Hashes for dantico-0.0.9-py3-none-any.whl
Algorithm Hash digest
SHA256 a1463d57b2c5ddf37a3c31d90cd0bd14ba42ee2c019647a45e7d4b606f1dc076
MD5 67904b6f9f1a0f8ac959fecd99ec2ce0
BLAKE2b-256 7fde48b56d6a4556a691f3aff866bf75271dccd2fc0a0e0d23cecf81303e2e01

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page