Skip to content

Commit d0d7a59

Browse files
committed
added Interval column type
1 parent e1c1535 commit d0d7a59

File tree

5 files changed

+210
-2
lines changed

5 files changed

+210
-2
lines changed

docs/src/piccolo/schema/column_types.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,12 @@ Date
117117

118118
.. autoclass:: Date
119119

120+
========
121+
Interval
122+
========
123+
124+
.. autoclass:: Interval
125+
120126
====
121127
Time
122128
====

piccolo/columns/column_types.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22
import copy
3-
from datetime import datetime, date, time
3+
from datetime import datetime, date, time, timedelta
44
import decimal
55
import typing as t
66
import uuid
@@ -9,6 +9,7 @@
99
from piccolo.columns.operators.string import ConcatPostgres, ConcatSQLite
1010
from piccolo.columns.defaults.date import DateArg, DateNow, DateCustom
1111
from piccolo.columns.defaults.time import TimeArg, TimeNow, TimeCustom
12+
from piccolo.columns.defaults.interval import IntervalArg, IntervalCustom
1213
from piccolo.columns.defaults.timestamp import (
1314
TimestampArg,
1415
TimestampNow,
@@ -612,6 +613,45 @@ def __init__(self, default: TimeArg = TimeNow(), **kwargs) -> None:
612613
super().__init__(**kwargs)
613614

614615

616+
class Interval(Column):
617+
"""
618+
Used for storing timedeltas. Uses the ``timedelta`` type for values.
619+
620+
**Example**
621+
622+
.. code-block:: python
623+
624+
from datetime import timedelta
625+
626+
class Concert(Table):
627+
duration = Interval()
628+
629+
# Create
630+
>>> Concert(
631+
>>> duration=timedelta(hours=2)
632+
>>> ).save().run_sync()
633+
634+
# Query
635+
>>> Concert.select(Concert.duration).run_sync()
636+
{'duration': datetime.timedelta(seconds=7200)}
637+
638+
"""
639+
640+
value_type = timedelta
641+
642+
def __init__(
643+
self, default: IntervalArg = IntervalCustom(), **kwargs
644+
) -> None:
645+
self._validate_default(default, IntervalArg.__args__) # type: ignore
646+
647+
if isinstance(default, timedelta):
648+
default = IntervalCustom.from_timedelta(default)
649+
650+
self.default = default
651+
kwargs.update({"default": default})
652+
super().__init__(**kwargs)
653+
654+
615655
###############################################################################
616656

617657

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
from __future__ import annotations
2+
import datetime
3+
import typing as t
4+
5+
from .base import Default
6+
7+
8+
class IntervalCustom(Default):
9+
def __init__(
10+
self,
11+
weeks: int = 0,
12+
days: int = 0,
13+
hours: int = 0,
14+
minutes: int = 0,
15+
seconds: int = 0,
16+
milliseconds: int = 0,
17+
microseconds: int = 0,
18+
):
19+
self.weeks = weeks
20+
self.days = days
21+
self.hours = hours
22+
self.minutes = minutes
23+
self.seconds = seconds
24+
self.milliseconds = milliseconds
25+
self.microseconds = microseconds
26+
27+
@property
28+
def timedelta(self):
29+
return datetime.timedelta(
30+
weeks=self.weeks,
31+
days=self.days,
32+
hours=self.hours,
33+
minutes=self.minutes,
34+
seconds=self.seconds,
35+
milliseconds=self.milliseconds,
36+
microseconds=self.microseconds,
37+
)
38+
39+
@property
40+
def postgres(self):
41+
value = self.get_postgres_interval_string(
42+
attributes=[
43+
"weeks",
44+
"days",
45+
"hours",
46+
"minutes",
47+
"seconds",
48+
"milliseconds",
49+
"microseconds",
50+
]
51+
)
52+
return f"'{value}'"
53+
54+
@property
55+
def sqlite(self):
56+
value = self.get_sqlite_interval_string(
57+
attributes=[
58+
"weeks",
59+
"days",
60+
"hours",
61+
"minutes",
62+
"seconds",
63+
"milliseconds",
64+
"microseconds",
65+
]
66+
).replace("'", "")
67+
return f"'{value}'"
68+
69+
def python(self):
70+
return self.timedelta
71+
72+
@classmethod
73+
def from_timedelta(cls, instance: datetime.timedelta):
74+
return cls(
75+
days=instance.days,
76+
seconds=instance.seconds,
77+
microseconds=instance.microseconds,
78+
)
79+
80+
81+
###############################################################################
82+
83+
IntervalArg = t.Union[
84+
IntervalCustom, None, datetime.timedelta,
85+
]
86+
87+
88+
__all__ = [
89+
"IntervalArg",
90+
"IntervalCustom",
91+
]

piccolo/engine/sqlite.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import aiosqlite
1212
from aiosqlite import Cursor, Connection
1313

14+
from piccolo.columns.defaults.interval import IntervalCustom
1415
from piccolo.engine.base import Batch, Engine
1516
from piccolo.engine.exceptions import TransactionError
1617
from piccolo.query.base import Query
@@ -51,6 +52,15 @@ def convert_date_in(value):
5152
return value.isoformat()
5253

5354

55+
def convert_timedelta_in(value):
56+
"""
57+
Converts the timedelta value being passed into sqlite.
58+
"""
59+
return IntervalCustom.from_timedelta(instance=value).sqlite.replace(
60+
"'", ""
61+
)
62+
63+
5464
def convert_numeric_out(value: bytes) -> Decimal:
5565
"""
5666
Convert float values into Decimals.
@@ -78,21 +88,37 @@ def convert_date_out(value: bytes) -> datetime.date:
7888

7989
def convert_time_out(value: bytes) -> datetime.time:
8090
"""
81-
If the value is a uuid, convert it to a UUID instance.
91+
If the value is a time, convert it to a UUID instance.
8292
"""
8393
return datetime.time.fromisoformat(value.decode("utf8"))
8494

8595

96+
def convert_interval_out(value: bytes) -> datetime.timedelta:
97+
"""
98+
If the value is an interval, convert it to a timedelta instance.
99+
"""
100+
unit_value_strings = [i.strip() for i in value.decode("utf8").split(",")]
101+
kwargs = {}
102+
103+
for unit_value_string in unit_value_strings:
104+
value, key = unit_value_string.split(" ")
105+
kwargs[key] = int(value)
106+
107+
return datetime.timedelta(**kwargs)
108+
109+
86110
sqlite3.register_converter("Numeric", convert_numeric_out)
87111
sqlite3.register_converter("Integer", convert_int_out)
88112
sqlite3.register_converter("UUID", convert_uuid_out)
89113
sqlite3.register_converter("Date", convert_date_out)
90114
sqlite3.register_converter("Time", convert_time_out)
115+
sqlite3.register_converter("Interval", convert_interval_out)
91116

92117
sqlite3.register_adapter(Decimal, convert_numeric_in)
93118
sqlite3.register_adapter(uuid.UUID, convert_uuid_in)
94119
sqlite3.register_adapter(datetime.time, convert_time_in)
95120
sqlite3.register_adapter(datetime.date, convert_date_in)
121+
sqlite3.register_adapter(datetime.timedelta, convert_timedelta_in)
96122

97123
###############################################################################
98124

tests/columns/test_interval.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import datetime
2+
from unittest import TestCase
3+
4+
from piccolo.table import Table
5+
from piccolo.columns.column_types import Interval
6+
from piccolo.columns.defaults.interval import IntervalCustom
7+
8+
9+
class MyTable(Table):
10+
interval = Interval()
11+
12+
13+
class MyTableDefault(Table):
14+
interval = Interval(default=IntervalCustom(days=1))
15+
16+
17+
class TestInterval(TestCase):
18+
def setUp(self):
19+
MyTable.create_table().run_sync()
20+
21+
def tearDown(self):
22+
MyTable.alter().drop_table().run_sync()
23+
24+
def test_interval(self):
25+
interval = datetime.timedelta(hours=2)
26+
row = MyTable(interval=interval)
27+
row.save().run_sync()
28+
29+
result = MyTable.objects().first().run_sync()
30+
self.assertEqual(result.interval, interval)
31+
32+
33+
class TestIntervalDefault(TestCase):
34+
def setUp(self):
35+
MyTableDefault.create_table().run_sync()
36+
37+
def tearDown(self):
38+
MyTableDefault.alter().drop_table().run_sync()
39+
40+
def test_interval(self):
41+
row = MyTableDefault()
42+
row.save().run_sync()
43+
44+
result = MyTableDefault.objects().first().run_sync()
45+
self.assertTrue(result.interval.days == 1)

0 commit comments

Comments
 (0)