-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add new date and datetime based range fields that work with xocto.range types #136
Changes from all commits
853f663
ca0572b
08c6d1b
a537ddc
4ca8cfd
3100050
0828edc
93c37c9
7558f71
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,3 +29,6 @@ docs/_static | |
|
||
# Ctags | ||
tags | ||
|
||
# Editor configs | ||
*.code-workspace |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -34,7 +34,6 @@ repos: | |
args: [ --fix ] | ||
# Run the formatter. | ||
- id: ruff-format | ||
args: [ --fix ] | ||
|
||
- repo: local | ||
hooks: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
# Model Fields | ||
|
||
Custom [Django model fields](https://docs.djangoproject.com/en/dev/ref/models/fields/). | ||
|
||
|
||
## Postgres specific fields | ||
|
||
Fields that can only be used with a Postgres database, enhancing the functionality | ||
provided by [django.contrib.postgres.fields](https://docs.djangoproject.com/en/dev/ref/contrib/postgres/fields/). | ||
|
||
### Ranges | ||
|
||
Provides [range fields](https://docs.djangoproject.com/en/dev/ref/contrib/postgres/fields/#range-fields) that work | ||
with the `Range` classes from [xocto.ranges](./ranges.md). | ||
|
||
The underlying Range type exposed by the Django ORM is `psycopg2.extras.Range` which is awkward to | ||
use throughout an application, and requires lots of boilerplate code to work with correctly. | ||
|
||
Alternatively, `xocto.ranges` are designed to be used throughout an application. These fields | ||
make it possible to interact with them directly from the Django ORM. | ||
|
||
Migrating from the Django range types to these range types is straightforward and does | ||
not require any changes to the database schema. The migration file generated will refer | ||
to the new fields, but since the underlying database type is the same, the migration will | ||
be a no-op. | ||
|
||
The standard Django query operators are almost the same as for the built-in types. | ||
They accept `xocto.ranges` as arguments, but don't support passing in a tuple of values: | ||
|
||
```python | ||
|
||
assert SalesPeriod.objects.filter( | ||
period__contains=ranges.FiniteDateRange( | ||
start=datetime.date(2020, 1, 10), | ||
end=datetime.date(2020, 1, 20), | ||
) | ||
).exists() | ||
|
||
assert SalesPeriod.objects.filter( | ||
period__overlaps=ranges.FiniteDateRange( | ||
start=datetime.date(2020, 1, 10), | ||
end=datetime.date(2020, 1, 20), | ||
) | ||
).exists() | ||
|
||
# ERROR! This will raise a TypeError | ||
SalesPeriod.objects.filter(period__overlaps=(datetime.date(2020, 1, 10), datetime.date(2020, 1, 20))) | ||
|
||
``` | ||
|
||
#### FiniteDateRangeField | ||
|
||
Module: `xocto.fields.postgres.ranges.FiniteDateRangeField`\ | ||
Bounds: `[]`\ | ||
Type: [xocto.ranges.FiniteDateRange](xocto.ranges.FiniteDateRange) | ||
|
||
A field that represents an inclusive-inclusive `[]` ranges of dates. The start | ||
and end of the range are inclusive and must not be `None`. | ||
|
||
```python | ||
import datetime | ||
from django.db import models | ||
from xocto import ranges | ||
from xocto.fields.postgres import ranges as db_ranges | ||
|
||
class SalesPeriod(models.Model): | ||
... | ||
period = db_ranges.FiniteDateRangeField() | ||
a-musing-moose marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
sales_period = SalesPeriod.objects.create( | ||
period=ranges.FiniteDateRange( | ||
start=datetime.date(2020, 1, 1), | ||
end=datetime.date(2020, 1, 31) | ||
), | ||
... | ||
) | ||
|
||
assert sales_period.period == ranges.FiniteDateRange( | ||
start=datetime.date(2020, 1, 1), | ||
end=datetime.date(2020, 1, 31) | ||
) | ||
assert sales_period.period.start == datetime.date(2020, 1, 1) | ||
``` | ||
|
||
|
||
#### FiniteDateTimeRangeField | ||
|
||
Module: `xocto.fields.postgres.ranges.FiniteDateTimeRangeField`\ | ||
Bounds: `[)`\ | ||
Type: [xocto.ranges.FiniteDatetimeRange](xocto.ranges.FiniteDatetimeRange) | ||
|
||
A field that represents an inclusive-exclusive `[)` ranges of timezone-aware | ||
datetimes. Both the start and end of the range must not be `None`. | ||
|
||
The values returned from the database will always be converted to the local timezone | ||
as per the `TIME_ZONE` setting in `settings.py`. | ||
|
||
```python | ||
import datetime | ||
from django.db import models | ||
from xocto import ranges, localtime | ||
from xocto.fields.postgres import ranges as db_ranges | ||
|
||
class CalendarEntry(models.Model): | ||
... | ||
event_time = db_ranges.FiniteDateTimeRangeField() | ||
|
||
calendar_entry = CalendarEntry.objects.create( | ||
event_time=ranges.FiniteDatetimeRange( | ||
start=localtime.datetime(2020, 1, 1, 14, 30), | ||
end=localtime.datetime(2020, 1, 1, 15, 30) | ||
), | ||
... | ||
) | ||
|
||
assert calendar_entry.event_time == ranges.FiniteDatetimeRange( | ||
start=localtime.datetime(2020, 1, 1, 14, 30), | ||
end=localtime.datetime(2020, 1, 1, 15, 30) | ||
) | ||
assert calendar_entry.event_time.start == localtime.datetime(2020, 1, 1, 14, 30) | ||
``` | ||
|
||
|
||
#### HalfFiniteDateTimeRangeField | ||
|
||
Module: `xocto.fields.postgres.ranges.HalfFiniteDateTimeRangeField`\ | ||
Bounds: `[)`\ | ||
Type: [xocto.ranges.HalfFiniteDatetimeRange](xocto.ranges.HalfFiniteRange) | ||
|
||
> **_NOTE:_** docs can not link directly to `HalfFiniteDatetimeRange` at this stage as it's a type alias | ||
|
||
A field that represents an inclusive-exclusive `[)` ranges of timezone-aware | ||
datetimes. The end of the range may be open-ended, represented by `None`. | ||
|
||
The values returned from the database will always be converted to the local timezone | ||
as per the `TIME_ZONE` setting in `settings.py`. | ||
|
||
```python | ||
import datetime | ||
from django.db import models | ||
from xocto import ranges, localtime | ||
from xocto.fields.postgres import ranges as db_ranges | ||
|
||
class Agreement(models.Model): | ||
... | ||
period = db_ranges.HalfFiniteDateTimeRangeField() | ||
|
||
agreement = Agreement.objects.create( | ||
period=ranges.HalfFiniteDatetimeRange( | ||
start=localtime.datetime(2020, 1, 1, 14, 30), | ||
end=None, | ||
), | ||
... | ||
) | ||
|
||
assert agreement.period == ranges.HalfFiniteDatetimeRange( | ||
start=localtime.datetime(2020, 1, 1, 14, 30), | ||
end=None | ||
) | ||
assert agreement.period.start == localtime.datetime(2020, 1, 1, 14, 30) | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -49,6 +49,7 @@ dev = [ | |
"mypy==1.8.0", | ||
"numpy==1.22.2", | ||
"pre-commit>=3.2.0", | ||
"psycopg2>=2.8.4", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. given it's an open source library, should we make the postgres stuff optional? and then also make the CI run against both postgres and sqlite? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is dependency is optional. I don't think there's any real value in running tests against both PG and sqlite. We don't have that much in xocto that depends on the database, but these fields absolutely depend on postgres. It'll be noted in the docs (once written). Is there value in namespacing the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah it is too, sorry, didn't see which list it was in. The namespacing sounds like a reasonable idea
mmmkay |
||
"pyarrow-stubs==10.0.1.6", | ||
"pytest-django==4.8.0", | ||
"pytest-mock==3.12.0", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,4 +4,4 @@ | |
def test_timer(): | ||
with utils.Timer() as t: | ||
pass | ||
assert 0 < t.duration_in_ms < 1 | ||
assert 0 <= t.duration_in_ms < 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was incorrectly added in a previous change but didn't complain for some reason.