Skip to content
This repository was archived by the owner on Aug 19, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions docs/declaring_models.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,23 @@ All fields are required unless one of the following is set:
* `allow_blank` - A boolean. Determine if empty strings are allowed. Sets the default to `""`.
* `default` - A value or a callable (function).

Special keyword arguments for `DateTime` and `Date` fields:

* `auto_now` - Automatically set the field to now every time the object is saved. Useful for “last-modified” timestamps.
* `auto_now_add` - Automatically set the field to now when the object is first created. Useful for creation of timestamps.

Default=`datetime.date.today()` for `DateField` and `datetime.datetime.now()` for `DateTimeField`.

!!! note
Setting `auto_now` or `auto_now_add` to True will cause the field to be read_only.

The following column types are supported.
See `TypeSystem` for [type-specific validation keyword arguments][typesystem-fields].

* `orm.BigInteger()`
* `orm.Boolean()`
* `orm.Date()`
* `orm.DateTime()`
* `orm.Date(auto_now, auto_now_add)`
* `orm.DateTime(auto_now, auto_now_add)`
* `orm.Decimal()`
* `orm.Email(max_length)`
* `orm.Enum()`
Expand Down
20 changes: 18 additions & 2 deletions orm/fields.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import typing
from datetime import date, datetime

import sqlalchemy
import typesystem
Expand Down Expand Up @@ -100,16 +101,31 @@ def get_column_type(self):
return sqlalchemy.Boolean()


class DateTime(ModelField):
class AutoNowMixin(ModelField):
def __init__(self, auto_now=False, auto_now_add=False, **kwargs):
self.auto_now = auto_now
self.auto_now_add = auto_now_add
if auto_now_add and auto_now:
raise ValueError("auto_now and auto_now_add cannot be both True")
if auto_now_add or auto_now:
kwargs["read_only"] = True
super().__init__(**kwargs)


class DateTime(AutoNowMixin):
def get_validator(self, **kwargs) -> typesystem.Field:
if self.auto_now_add or self.auto_now:
kwargs["default"] = datetime.now
return typesystem.DateTime(**kwargs)

def get_column_type(self):
return sqlalchemy.DateTime()


class Date(ModelField):
class Date(AutoNowMixin):
def get_validator(self, **kwargs) -> typesystem.Field:
if self.auto_now_add or self.auto_now:
kwargs["default"] = date.today
return typesystem.Date(**kwargs)

def get_column_type(self):
Expand Down
21 changes: 14 additions & 7 deletions orm/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from sqlalchemy.ext.asyncio import create_async_engine

from orm.exceptions import MultipleMatches, NoMatch
from orm.fields import String, Text
from orm.fields import Date, DateTime, String, Text

FILTER_OPERATORS = {
"exact": "__eq__",
Expand All @@ -21,6 +21,15 @@
}


def _update_auto_now_fields(values, fields):
for key, value in fields.items():
if isinstance(value, DateTime) and value.auto_now:
values[key] = value.validator.get_default_value()
elif isinstance(value, Date) and value.auto_now:
values[key] = value.validator.get_default_value()
return values


class ModelRegistry:
def __init__(self, database: databases.Database) -> None:
self.database = database
Expand Down Expand Up @@ -400,7 +409,6 @@ async def create(self, **kwargs):
fields={key: value.validator for key, value in fields.items()}
)
kwargs = validator.validate(kwargs)

for key, value in fields.items():
if value.validator.read_only and value.validator.has_default():
kwargs[key] = value.validator.get_default_value()
Expand Down Expand Up @@ -429,8 +437,9 @@ async def update(self, **kwargs) -> None:
if key in kwargs
}
validator = typesystem.Schema(fields=fields)
kwargs = validator.validate(kwargs)

kwargs = _update_auto_now_fields(
validator.validate(kwargs), self.model_cls.fields
)
expr = self.table.update().values(**kwargs)

for filter_clause in self.filter_clauses:
Expand Down Expand Up @@ -513,11 +522,9 @@ async def update(self, **kwargs):
key: field.validator for key, field in self.fields.items() if key in kwargs
}
validator = typesystem.Schema(fields=fields)
kwargs = validator.validate(kwargs)

kwargs = _update_auto_now_fields(validator.validate(kwargs), self.fields)
pk_column = getattr(self.table.c, self.pkname)
expr = self.table.update().values(**kwargs).where(pk_column == self.pk)

await self.database.execute(expr)

# Update the model instance.
Expand Down
30 changes: 29 additions & 1 deletion tests/test_columns.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ class Product(orm.Model):
"created": orm.DateTime(default=datetime.datetime.now),
"created_day": orm.Date(default=datetime.date.today),
"created_time": orm.Time(default=time),
"created_date": orm.Date(auto_now_add=True),
"created_datetime": orm.DateTime(auto_now_add=True),
"updated_datetime": orm.DateTime(auto_now=True),
"updated_date": orm.Date(auto_now=True),
"data": orm.JSON(default={}),
"description": orm.Text(allow_blank=True),
"huge_number": orm.BigInteger(default=0),
Expand Down Expand Up @@ -69,10 +73,13 @@ async def rollback_transactions():

async def test_model_crud():
product = await Product.objects.create()

product = await Product.objects.get(pk=product.pk)
assert product.created.year == datetime.datetime.now().year
assert product.created_day == datetime.date.today()
assert product.created_date == datetime.date.today()
assert product.created_datetime.date() == datetime.datetime.now().date()
assert product.updated_date == datetime.date.today()
assert product.updated_datetime.date() == datetime.datetime.now().date()
assert product.data == {}
assert product.description == ""
assert product.huge_number == 0
Expand All @@ -96,6 +103,8 @@ async def test_model_crud():
assert product.price == decimal.Decimal("999.99")
assert product.uuid == uuid.UUID("01175cde-c18f-4a13-a492-21bd9e1cb01b")

last_updated_datetime = product.updated_datetime
last_updated_date = product.updated_date
user = await User.objects.create()
assert isinstance(user.pk, uuid.UUID)

Expand All @@ -114,3 +123,22 @@ async def test_model_crud():
user = await User.objects.get()
assert isinstance(user.ipaddress, (ipaddress.IPv4Address, ipaddress.IPv6Address))
assert user.url == "https://encode.io"
# Test auto_now update
await product.update(
data={"foo": 1234},
)
assert product.updated_datetime != last_updated_datetime
assert product.updated_date == last_updated_date


async def test_both_auto_now_and_auto_now_add_raise_error():
with pytest.raises(ValueError):

class Product(orm.Model):
registry = models
fields = {
"id": orm.Integer(primary_key=True),
"created_datetime": orm.DateTime(auto_now_add=True, auto_now=True),
}

await Product.objects.create()