Skip to content
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

Merged
merged 9 commits into from
Mar 5, 2024
12 changes: 12 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ jobs:
build:
docker:
- image: cimg/python:3.9
- image: cimg/postgres:13.3
environment:
POSTGRES_USER: postgres
POSTGRES_DB: xocto-dev
steps:
- checkout
- run:
Expand Down Expand Up @@ -31,6 +35,14 @@ jobs:
name: Check linting
command: make lint_check
when: always
- run:
name: install dockerize
command: wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz && sudo tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz && rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz
environment:
DOCKERIZE_VERSION: v0.6.1
- run:
name: Wait for db
command: dockerize -wait tcp://localhost:5432 -timeout 1m
- run:
name: Run tests
command: make test
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,6 @@ docs/_static

# Ctags
tags

# Editor configs
*.code-workspace
1 change: 0 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ repos:
args: [ --fix ]
# Run the formatter.
- id: ruff-format
args: [ --fix ]
Copy link
Contributor Author

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.


- repo: local
hooks:
Expand Down
3 changes: 2 additions & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ PyPI detail page: https://pypi.python.org/pypi/xocto
xocto/events
xocto/health
xocto/localtime
xocto/model_fields
xocto/numbers
xocto/ranges
xocto/pact_testing
xocto/pact_testing
xocto/settlement_periods
xocto/storage
xocto/tracing
Expand Down
161 changes: 161 additions & 0 deletions docs/xocto/model_fields.md
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)
```
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ dev = [
"mypy==1.8.0",
"numpy==1.22.2",
"pre-commit>=3.2.0",
"psycopg2>=2.8.4",
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 fields folder into a fields/postgres structure so folks don't unintentionally import something they can't use?

Copy link
Contributor

@delfick delfick Mar 1, 2024

Choose a reason for hiding this comment

The 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

don't think there's any real value in running tests against both PG and sqlite.

mmmkay

"pyarrow-stubs==10.0.1.6",
"pytest-django==4.8.0",
"pytest-mock==3.12.0",
Expand Down
2 changes: 1 addition & 1 deletion tests/events/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Empty file added tests/fields/__init__.py
Empty file.
Empty file.
Loading