add other valid field types, better parse model fields to pydantic model with optional values
This commit is contained in:
@ -8,4 +8,11 @@ coverage:
|
|||||||
patch: yes
|
patch: yes
|
||||||
changes: yes
|
changes: yes
|
||||||
|
|
||||||
comment: off
|
comment:
|
||||||
|
layout: "reach, diff, flags, files"
|
||||||
|
behavior: default
|
||||||
|
require_changes: false # if true: only post the comment if coverage changes
|
||||||
|
require_base: no # [yes :: must have a base report to post]
|
||||||
|
require_head: yes # [yes :: must have a head report to post]
|
||||||
|
branches: # branch names that can post comment
|
||||||
|
- "master"
|
||||||
@ -1,4 +1,4 @@
|
|||||||
# ORM
|
# Async-ORM
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a href="https://travis-ci.org/encode/orm">
|
<a href="https://travis-ci.org/encode/orm">
|
||||||
@ -10,6 +10,7 @@
|
|||||||
<a href="https://pypi.org/project/orm/">
|
<a href="https://pypi.org/project/orm/">
|
||||||
<img src="https://badge.fury.io/py/orm.svg" alt="Package version">
|
<img src="https://badge.fury.io/py/orm.svg" alt="Package version">
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://www.codefactor.io/repository/github/collerek/async-orm"><img src="https://www.codefactor.io/repository/github/collerek/async-orm/badge" alt="CodeFactor" /></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
The `async-orm` package is an async ORM for Python, with support for Postgres,
|
The `async-orm` package is an async ORM for Python, with support for Postgres,
|
||||||
@ -156,7 +157,7 @@ assert len(tracks) == 1
|
|||||||
The following keyword arguments are supported on all field types.
|
The following keyword arguments are supported on all field types.
|
||||||
|
|
||||||
* `primary_key`
|
* `primary_key`
|
||||||
* `allow_null`
|
* `nullable`
|
||||||
* `default`
|
* `default`
|
||||||
* `index`
|
* `index`
|
||||||
* `unique`
|
* `unique`
|
||||||
@ -167,7 +168,7 @@ All fields are required unless one of the following is set:
|
|||||||
* `allow_blank` - Allow empty strings to validate. Sets the default to `""`.
|
* `allow_blank` - Allow empty strings to validate. Sets the default to `""`.
|
||||||
* `default` - Set a default value for the field.
|
* `default` - Set a default value for the field.
|
||||||
|
|
||||||
* `orm.String(max_length)`
|
* `orm.String(length)`
|
||||||
* `orm.Text()`
|
* `orm.Text()`
|
||||||
* `orm.Boolean()`
|
* `orm.Boolean()`
|
||||||
* `orm.Integer()`
|
* `orm.Integer()`
|
||||||
|
|||||||
@ -1,3 +1,7 @@
|
|||||||
|
import datetime
|
||||||
|
import decimal
|
||||||
|
|
||||||
|
import pydantic
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
|
|
||||||
from orm.exceptions import ModelDefinitionError
|
from orm.exceptions import ModelDefinitionError
|
||||||
@ -66,8 +70,75 @@ class String(BaseField):
|
|||||||
class Integer(BaseField):
|
class Integer(BaseField):
|
||||||
__type__ = int
|
__type__ = int
|
||||||
|
|
||||||
|
def get_column_type(self):
|
||||||
|
return sqlalchemy.Integer()
|
||||||
|
|
||||||
|
|
||||||
|
class Text(BaseField):
|
||||||
|
__type__ = str
|
||||||
|
|
||||||
|
def get_column_type(self):
|
||||||
|
return sqlalchemy.Text()
|
||||||
|
|
||||||
|
|
||||||
|
class Float(BaseField):
|
||||||
|
__type__ = float
|
||||||
|
|
||||||
|
def get_column_type(self):
|
||||||
|
return sqlalchemy.Float()
|
||||||
|
|
||||||
|
|
||||||
|
class Boolean(BaseField):
|
||||||
|
__type__ = bool
|
||||||
|
|
||||||
|
def get_column_type(self):
|
||||||
|
return sqlalchemy.Boolean()
|
||||||
|
|
||||||
|
|
||||||
|
class DateTime(BaseField):
|
||||||
|
__type__ = datetime.datetime
|
||||||
|
|
||||||
|
def get_column_type(self):
|
||||||
|
return sqlalchemy.DateTime()
|
||||||
|
|
||||||
|
|
||||||
|
class Date(BaseField):
|
||||||
|
__type__ = datetime.date
|
||||||
|
|
||||||
|
def get_column_type(self):
|
||||||
|
return sqlalchemy.Date()
|
||||||
|
|
||||||
|
|
||||||
|
class Time(BaseField):
|
||||||
|
__type__ = datetime.time
|
||||||
|
|
||||||
|
def get_column_type(self):
|
||||||
|
return sqlalchemy.Time()
|
||||||
|
|
||||||
|
|
||||||
|
class JSON(BaseField):
|
||||||
|
__type__ = pydantic.Json
|
||||||
|
|
||||||
|
def get_column_type(self):
|
||||||
|
return sqlalchemy.JSON()
|
||||||
|
|
||||||
|
|
||||||
|
class BigInteger(BaseField):
|
||||||
|
__type__ = int
|
||||||
|
|
||||||
|
def get_column_type(self):
|
||||||
|
return sqlalchemy.BigInteger()
|
||||||
|
|
||||||
|
|
||||||
|
class Decimal(BaseField):
|
||||||
|
__type__ = decimal.Decimal
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
assert 'precision' in kwargs, 'precision is required'
|
||||||
|
assert 'length' in kwargs, 'length is required'
|
||||||
|
self.length = kwargs.pop('length')
|
||||||
|
self.precision = kwargs.pop('precision')
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def get_column_type(self):
|
def get_column_type(self):
|
||||||
return sqlalchemy.Integer()
|
return sqlalchemy.DECIMAL(self.length, self.precision)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
from typing import Any
|
from typing import Any, Optional
|
||||||
|
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
from pydantic import create_model
|
from pydantic import create_model
|
||||||
@ -6,6 +6,17 @@ from pydantic import create_model
|
|||||||
from orm.fields import BaseField
|
from orm.fields import BaseField
|
||||||
|
|
||||||
|
|
||||||
|
def parse_pydantic_field_from_model_fields(object_dict: dict):
|
||||||
|
pydantic_fields = {field_name: (
|
||||||
|
base_field.__type__,
|
||||||
|
... if (not base_field.nullable and not base_field.default) else (
|
||||||
|
base_field.default() if callable(base_field.default) else base_field.default)
|
||||||
|
)
|
||||||
|
for field_name, base_field in object_dict.items()
|
||||||
|
if isinstance(base_field, BaseField)}
|
||||||
|
return pydantic_fields
|
||||||
|
|
||||||
|
|
||||||
class ModelMetaclass(type):
|
class ModelMetaclass(type):
|
||||||
def __new__(
|
def __new__(
|
||||||
mcs: type, name: str, bases: Any, attrs: dict
|
mcs: type, name: str, bases: Any, attrs: dict
|
||||||
@ -28,9 +39,7 @@ class ModelMetaclass(type):
|
|||||||
pkname = field_name
|
pkname = field_name
|
||||||
columns.append(field.get_column(field_name))
|
columns.append(field.get_column(field_name))
|
||||||
|
|
||||||
pydantic_fields = {field_name: (base_field.__type__, base_field.default or ...)
|
pydantic_fields = parse_pydantic_field_from_model_fields(new_model.__dict__)
|
||||||
for field_name, base_field in new_model.__dict__.items()
|
|
||||||
if isinstance(base_field, BaseField)}
|
|
||||||
|
|
||||||
new_model.__table__ = sqlalchemy.Table(tablename, metadata, *columns)
|
new_model.__table__ = sqlalchemy.Table(tablename, metadata, *columns)
|
||||||
new_model.__columns__ = columns
|
new_model.__columns__ = columns
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
|
import datetime
|
||||||
|
|
||||||
|
import pydantic
|
||||||
import pytest
|
import pytest
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
|
|
||||||
@ -12,20 +15,34 @@ class ExampleModel(Model):
|
|||||||
__tablename__ = "example"
|
__tablename__ = "example"
|
||||||
__metadata__ = metadata
|
__metadata__ = metadata
|
||||||
test = fields.Integer(primary_key=True)
|
test = fields.Integer(primary_key=True)
|
||||||
test2 = fields.String(length=250)
|
test_string = fields.String(length=250)
|
||||||
|
test_text = fields.Text()
|
||||||
|
test_bool = fields.Boolean(nullable=False)
|
||||||
|
test_float = fields.Float()
|
||||||
|
test_datetime = fields.DateTime(default=datetime.datetime.now)
|
||||||
|
test_date = fields.Date(default=datetime.date.today)
|
||||||
|
test_time = fields.Time(default=datetime.time)
|
||||||
|
test_json = fields.JSON(default={})
|
||||||
|
test_bigint = fields.BigInteger(default=0)
|
||||||
|
test_decimal = fields.Decimal(length=10, precision=2)
|
||||||
|
|
||||||
|
|
||||||
class ExampleModel2(Model):
|
class ExampleModel2(Model):
|
||||||
__tablename__ = "example2"
|
__tablename__ = "example2"
|
||||||
__metadata__ = metadata
|
__metadata__ = metadata
|
||||||
test = fields.Integer(name='test12', primary_key=True)
|
test = fields.Integer(name='test12', primary_key=True)
|
||||||
test2 = fields.String('test22', length=250)
|
test_string = fields.String('test_string2', length=250)
|
||||||
|
|
||||||
|
|
||||||
|
def test_not_nullable_field_is_required():
|
||||||
|
with pytest.raises(pydantic.error_wrappers.ValidationError):
|
||||||
|
ExampleModel(test=1, test_string='test')
|
||||||
|
|
||||||
|
|
||||||
def test_model_attribute_access():
|
def test_model_attribute_access():
|
||||||
example = ExampleModel(test=1, test2='test')
|
example = ExampleModel(test=1, test_string='test', test_bool=True)
|
||||||
assert example.test == 1
|
assert example.test == 1
|
||||||
assert example.test2 == 'test'
|
assert example.test_string == 'test'
|
||||||
|
|
||||||
example.test = 12
|
example.test = 12
|
||||||
assert example.test == 12
|
assert example.test == 12
|
||||||
@ -35,7 +52,7 @@ def test_model_attribute_access():
|
|||||||
|
|
||||||
|
|
||||||
def test_primary_key_access_and_setting():
|
def test_primary_key_access_and_setting():
|
||||||
example = ExampleModel(pk=1, test2='test')
|
example = ExampleModel(pk=1, test_string='test', test_bool=True)
|
||||||
assert example.pk == 1
|
assert example.pk == 1
|
||||||
example.pk = 2
|
example.pk = 2
|
||||||
|
|
||||||
@ -49,4 +66,4 @@ def test_wrong_model_definition():
|
|||||||
__tablename__ = "example3"
|
__tablename__ = "example3"
|
||||||
__metadata__ = metadata
|
__metadata__ = metadata
|
||||||
test = fields.Integer(name='test12', primary_key=True)
|
test = fields.Integer(name='test12', primary_key=True)
|
||||||
test2 = fields.String('test22', name='test22', length=250)
|
test_string = fields.String('test_string2', name='test_string2', length=250)
|
||||||
|
|||||||
Reference in New Issue
Block a user