diff --git a/.codecov.yml b/.codecov.yml index 033aafd..6d50415 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -8,4 +8,11 @@ coverage: patch: yes changes: yes -comment: off \ No newline at end of file +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" \ No newline at end of file diff --git a/.coverage b/.coverage index 922eaa1..edf7fc3 100644 Binary files a/.coverage and b/.coverage differ diff --git a/README.md b/README.md index f3e6026..2c6b6d0 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ORM +# Async-ORM
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. * `primary_key` -* `allow_null` +* `nullable` * `default` * `index` * `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 `""`. * `default` - Set a default value for the field. -* `orm.String(max_length)` +* `orm.String(length)` * `orm.Text()` * `orm.Boolean()` * `orm.Integer()` diff --git a/orm/fields.py b/orm/fields.py index 221c9a2..1b64610 100644 --- a/orm/fields.py +++ b/orm/fields.py @@ -1,3 +1,7 @@ +import datetime +import decimal + +import pydantic import sqlalchemy from orm.exceptions import ModelDefinitionError @@ -66,8 +70,75 @@ class String(BaseField): class Integer(BaseField): __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): + 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) def get_column_type(self): - return sqlalchemy.Integer() + return sqlalchemy.DECIMAL(self.length, self.precision) diff --git a/orm/models.py b/orm/models.py index 605b23a..50f2eb9 100644 --- a/orm/models.py +++ b/orm/models.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, Optional import sqlalchemy from pydantic import create_model @@ -6,6 +6,17 @@ from pydantic import create_model 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): def __new__( mcs: type, name: str, bases: Any, attrs: dict @@ -28,9 +39,7 @@ class ModelMetaclass(type): pkname = field_name columns.append(field.get_column(field_name)) - pydantic_fields = {field_name: (base_field.__type__, base_field.default or ...) - for field_name, base_field in new_model.__dict__.items() - if isinstance(base_field, BaseField)} + pydantic_fields = parse_pydantic_field_from_model_fields(new_model.__dict__) new_model.__table__ = sqlalchemy.Table(tablename, metadata, *columns) new_model.__columns__ = columns diff --git a/tests/test_columns.py b/tests/test_columns.py index c687ace..04cc8fa 100644 --- a/tests/test_columns.py +++ b/tests/test_columns.py @@ -1,3 +1,6 @@ +import datetime + +import pydantic import pytest import sqlalchemy @@ -12,20 +15,34 @@ class ExampleModel(Model): __tablename__ = "example" __metadata__ = metadata 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): __tablename__ = "example2" __metadata__ = metadata 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(): - example = ExampleModel(test=1, test2='test') + example = ExampleModel(test=1, test_string='test', test_bool=True) assert example.test == 1 - assert example.test2 == 'test' + assert example.test_string == 'test' example.test = 12 assert example.test == 12 @@ -35,7 +52,7 @@ def test_model_attribute_access(): 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 example.pk = 2 @@ -49,4 +66,4 @@ def test_wrong_model_definition(): __tablename__ = "example3" __metadata__ = metadata 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)