improve date handling

This commit is contained in:
collerek
2021-07-06 15:11:26 +02:00
parent 69fffdd2e2
commit cb4e5ea955
9 changed files with 206 additions and 27 deletions

View File

@ -108,14 +108,18 @@ You can use either `length` and `precision` parameters or `max_digits` and `deci
### Time
`Time()` has no required parameters.
`Time(timezone: bool = False)` has no required parameters.
You can pass `timezone=True` for timezone aware database column.
* Sqlalchemy column: `sqlalchemy.Time`
* Type (used for pydantic): `datetime.time`
### DateTime
`DateTime()` has no required parameters.
`DateTime(timezone: bool = False)` has no required parameters.
You can pass `timezone=True` for timezone aware database column.
* Sqlalchemy column: `sqlalchemy.DateTime`
* Type (used for pydantic): `datetime.datetime`

View File

@ -44,6 +44,22 @@ pip install ormar[sqlite]
Will install also `aiosqlite`.
### Orjson
```py
pip install ormar[orjson]
```
Will install also `orjson` that is much faster than builtin json parser.
### Crypto
```py
pip install ormar[crypto]
```
Will install also `cryptography` that is required to work with encrypted columns.
### Manual installation of dependencies
Of course, you can also install these requirements manually with `pip install asyncpg` etc.

View File

@ -1,3 +1,15 @@
# 0.10.14
## ✨ Features
* Allow passing `timezone:bool = False` parameter to `DateTime` and `Time` fields for timezone aware database columns [#264](https://github.com/collerek/ormar/issues/264)
* Allow passing datetime, date and time for filter on `DateTime`, `Time` and `Date` fields to allow filtering by datetimes instead of converting the value to string [#79](https://github.com/collerek/ormar/issues/79)
## 🐛 Fixes
* Fix dependencies from `psycopg2` to `psycopg2-binary` [#255](https://github.com/collerek/ormar/issues/255)
# 0.10.13
## ✨ Features

View File

@ -76,7 +76,7 @@ class UndefinedType: # pragma no cover
Undefined = UndefinedType()
__version__ = "0.10.13"
__version__ = "0.10.14"
__all__ = [
"Integer",
"BigInteger",

View File

@ -351,6 +351,19 @@ class DateTime(ModelFieldFactory, datetime.datetime):
_type = datetime.datetime
_sample = "datetime"
def __new__( # type: ignore # noqa CFQ002
cls, *, timezone: bool = False, **kwargs: Any
) -> BaseField: # type: ignore
kwargs = {
**kwargs,
**{
k: v
for k, v in locals().items()
if k not in ["cls", "__class__", "kwargs"]
},
}
return super().__new__(cls, **kwargs)
@classmethod
def get_column_type(cls, **kwargs: Any) -> Any:
"""
@ -362,7 +375,7 @@ class DateTime(ModelFieldFactory, datetime.datetime):
:return: initialized column with proper options
:rtype: sqlalchemy Column
"""
return sqlalchemy.DateTime()
return sqlalchemy.DateTime(timezone=kwargs.get("timezone", False))
class Date(ModelFieldFactory, datetime.date):
@ -395,6 +408,19 @@ class Time(ModelFieldFactory, datetime.time):
_type = datetime.time
_sample = "time"
def __new__( # type: ignore # noqa CFQ002
cls, *, timezone: bool = False, **kwargs: Any
) -> BaseField: # type: ignore
kwargs = {
**kwargs,
**{
k: v
for k, v in locals().items()
if k not in ["cls", "__class__", "kwargs"]
},
}
return super().__new__(cls, **kwargs)
@classmethod
def get_column_type(cls, **kwargs: Any) -> Any:
"""
@ -406,7 +432,7 @@ class Time(ModelFieldFactory, datetime.time):
:return: initialized column with proper options
:rtype: sqlalchemy Column
"""
return sqlalchemy.Time()
return sqlalchemy.Time(timezone=kwargs.get("timezone", False))
class JSON(ModelFieldFactory, pydantic.Json):

View File

@ -1,3 +1,4 @@
import datetime
from typing import Any, Dict, TYPE_CHECKING, Type
import sqlalchemy
@ -138,6 +139,19 @@ class FilterAction(QueryAction):
if isinstance(self.filter_value, ormar.Model):
self.filter_value = self.filter_value.pk
if isinstance(
self.filter_value, (datetime.date, datetime.time, datetime.datetime)
):
self.filter_value = self.filter_value.isoformat()
if isinstance(self.filter_value, (list, tuple, set)):
self.filter_value = [
x.isoformat()
if isinstance(x, (datetime.date, datetime.time, datetime.datetime))
else x
for x in self.filter_value
]
op_attr = FILTER_OPERATORS[self.operator]
if self.operator == "isnull":
op_attr = "is_" if self.filter_value else "isnot"

View File

@ -69,7 +69,7 @@ setup(
"typing_extensions>=3.7,<=3.7.4.3",
],
extras_require={
"postgresql": ["asyncpg", "psycopg2"],
"postgresql": ["asyncpg", "psycopg2-binary"],
"mysql": ["aiomysql", "pymysql"],
"sqlite": ["aiosqlite"],
"orjson": ["orjson"],

View File

@ -0,0 +1,118 @@
from datetime import timezone, timedelta, datetime, date, time
import databases
import pytest
import sqlalchemy
import ormar
from tests.settings import DATABASE_URL
database = databases.Database(DATABASE_URL, force_rollback=True)
metadata = sqlalchemy.MetaData()
class DateFieldsModel(ormar.Model):
class Meta:
database = database
metadata = metadata
id: int = ormar.Integer(primary_key=True)
created_date: datetime = ormar.DateTime(
default=datetime.now(tz=timezone(timedelta(hours=3))), timezone=True
)
updated_date: datetime = ormar.DateTime(
default=datetime.now(tz=timezone(timedelta(hours=3))),
name="modification_date",
timezone=True,
)
class SampleModel(ormar.Model):
class Meta:
database = database
metadata = metadata
id: int = ormar.Integer(primary_key=True)
updated_at: datetime = ormar.DateTime()
class TimeModel(ormar.Model):
class Meta:
database = database
metadata = metadata
id: int = ormar.Integer(primary_key=True)
elapsed: time = ormar.Time()
class DateModel(ormar.Model):
class Meta:
database = database
metadata = metadata
id: int = ormar.Integer(primary_key=True)
creation_date: date = ormar.Date()
@pytest.fixture(autouse=True, scope="module")
def create_test_database():
engine = sqlalchemy.create_engine(DATABASE_URL)
metadata.drop_all(engine)
metadata.create_all(engine)
yield
metadata.drop_all(engine)
@pytest.mark.asyncio
async def test_model_crud_with_timezone():
async with database:
datemodel = await DateFieldsModel().save()
assert datemodel.created_date is not None
assert datemodel.updated_date is not None
@pytest.mark.asyncio
async def test_query_with_datetime_in_filter():
async with database:
creation_dt = datetime(2021, 5, 18, 0, 0, 0, 0)
sample = await SampleModel.objects.create(updated_at=creation_dt)
current_dt = datetime(2021, 5, 19, 0, 0, 0, 0)
outdated_samples = await SampleModel.objects.filter(
updated_at__lt=current_dt
).all()
assert outdated_samples[0] == sample
@pytest.mark.asyncio
async def test_query_with_date_in_filter():
async with database:
sample = await TimeModel.objects.create(elapsed=time(0, 20, 20))
await TimeModel.objects.create(elapsed=time(0, 12, 0))
await TimeModel.objects.create(elapsed=time(0, 19, 55))
sample4 = await TimeModel.objects.create(elapsed=time(0, 21, 15))
threshold = time(0, 20, 0)
samples = await TimeModel.objects.filter(TimeModel.elapsed >= threshold).all()
assert len(samples) == 2
assert samples[0] == sample
assert samples[1] == sample4
@pytest.mark.asyncio
async def test_query_with_time_in_filter():
async with database:
await DateModel.objects.create(creation_date=date(2021, 5, 18))
sample2 = await DateModel.objects.create(creation_date=date(2021, 5, 19))
sample3 = await DateModel.objects.create(creation_date=date(2021, 5, 20))
outdated_samples = await DateModel.objects.filter(
creation_date__in=[date(2021, 5, 19), date(2021, 5, 20)]
).all()
assert len(outdated_samples) == 2
assert outdated_samples[0] == sample2
assert outdated_samples[1] == sample3

View File

@ -185,25 +185,14 @@ def test_combining_groups_together():
)
# @pytest.mark.asyncio
# async def test_filtering_by_field_access():
# async with database:
# async with database.transaction(force_rollback=True):
# category = await Category(name='Toys').save()
# product1 = await Product(name="G.I Joe",
# rating=4.7,
# category=category).save()
# product2 = await Product(name="My Little Pony",
# rating=3.8,
# category=category).save()
#
# check = Product.object.get(Product.name == "My Little Pony")
# assert check == product2
@pytest.mark.asyncio
async def test_filtering_by_field_access():
async with database:
async with database.transaction(force_rollback=True):
category = await Category(name="Toys").save()
product2 = await Product(
name="My Little Pony", rating=3.8, category=category
).save()
# TODO: Finish implementation
# * overload operators and add missing functions that return FilterAction (V)
# * return OrderAction for desc() and asc() (V)
# * create filter groups for & and | (and ~ - NOT?) (V)
# * accept args in all functions that accept filters? or only filter and exclude? (V)
# all functions: delete, first, get, get_or_none, get_or_create, all, filter, exclude
# * accept OrderActions in order_by (V)
check = await Product.objects.get(Product.name == "My Little Pony")
assert check == product2