piccolo 1.9.0__py3-none-any.whl → 1.11.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- piccolo/__init__.py +1 -1
- piccolo/apps/fixtures/commands/load.py +1 -1
- piccolo/apps/migrations/auto/__init__.py +8 -0
- piccolo/apps/migrations/auto/migration_manager.py +2 -1
- piccolo/apps/migrations/commands/backwards.py +3 -1
- piccolo/apps/migrations/commands/base.py +1 -1
- piccolo/apps/migrations/commands/check.py +1 -1
- piccolo/apps/migrations/commands/clean.py +1 -1
- piccolo/apps/migrations/commands/forwards.py +3 -1
- piccolo/apps/migrations/commands/new.py +4 -2
- piccolo/apps/schema/commands/generate.py +2 -2
- piccolo/apps/shell/commands/run.py +1 -1
- piccolo/columns/column_types.py +56 -39
- piccolo/columns/defaults/base.py +1 -1
- piccolo/columns/defaults/date.py +9 -1
- piccolo/columns/defaults/interval.py +1 -0
- piccolo/columns/defaults/time.py +9 -1
- piccolo/columns/defaults/timestamp.py +1 -0
- piccolo/columns/defaults/uuid.py +1 -1
- piccolo/columns/m2m.py +7 -7
- piccolo/columns/operators/comparison.py +4 -0
- piccolo/conf/apps.py +9 -4
- piccolo/engine/base.py +69 -20
- piccolo/engine/cockroach.py +2 -3
- piccolo/engine/postgres.py +33 -19
- piccolo/engine/sqlite.py +27 -22
- piccolo/query/functions/__init__.py +11 -1
- piccolo/query/functions/datetime.py +260 -0
- piccolo/query/functions/string.py +45 -0
- piccolo/query/methods/create_index.py +1 -1
- piccolo/query/methods/drop_index.py +1 -1
- piccolo/query/methods/objects.py +7 -7
- piccolo/query/methods/select.py +13 -7
- piccolo/query/mixins.py +3 -10
- piccolo/schema.py +18 -11
- piccolo/table.py +22 -21
- piccolo/utils/encoding.py +5 -3
- {piccolo-1.9.0.dist-info → piccolo-1.11.0.dist-info}/METADATA +1 -1
- {piccolo-1.9.0.dist-info → piccolo-1.11.0.dist-info}/RECORD +52 -50
- tests/apps/migrations/auto/integration/test_migrations.py +1 -1
- tests/columns/test_array.py +28 -0
- tests/conf/test_apps.py +1 -1
- tests/engine/test_nested_transaction.py +2 -0
- tests/engine/test_transaction.py +1 -2
- tests/query/functions/test_datetime.py +114 -0
- tests/query/functions/test_string.py +34 -2
- tests/table/test_indexes.py +4 -2
- tests/utils/test_pydantic.py +70 -29
- {piccolo-1.9.0.dist-info → piccolo-1.11.0.dist-info}/LICENSE +0 -0
- {piccolo-1.9.0.dist-info → piccolo-1.11.0.dist-info}/WHEEL +0 -0
- {piccolo-1.9.0.dist-info → piccolo-1.11.0.dist-info}/entry_points.txt +0 -0
- {piccolo-1.9.0.dist-info → piccolo-1.11.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,114 @@
|
|
1
|
+
import datetime
|
2
|
+
|
3
|
+
from piccolo.columns import Timestamp
|
4
|
+
from piccolo.query.functions.datetime import (
|
5
|
+
Day,
|
6
|
+
Extract,
|
7
|
+
Hour,
|
8
|
+
Minute,
|
9
|
+
Month,
|
10
|
+
Second,
|
11
|
+
Strftime,
|
12
|
+
Year,
|
13
|
+
)
|
14
|
+
from piccolo.table import Table
|
15
|
+
from tests.base import engines_only, sqlite_only
|
16
|
+
|
17
|
+
from .base import FunctionTest
|
18
|
+
|
19
|
+
|
20
|
+
class Concert(Table):
|
21
|
+
starts = Timestamp()
|
22
|
+
|
23
|
+
|
24
|
+
class DatetimeTest(FunctionTest):
|
25
|
+
tables = [Concert]
|
26
|
+
|
27
|
+
def setUp(self) -> None:
|
28
|
+
super().setUp()
|
29
|
+
self.concert = Concert(
|
30
|
+
{
|
31
|
+
Concert.starts: datetime.datetime(
|
32
|
+
year=2024, month=6, day=14, hour=23, minute=46, second=10
|
33
|
+
)
|
34
|
+
}
|
35
|
+
)
|
36
|
+
self.concert.save().run_sync()
|
37
|
+
|
38
|
+
|
39
|
+
@engines_only("postgres", "cockroach")
|
40
|
+
class TestExtract(DatetimeTest):
|
41
|
+
def test_extract(self):
|
42
|
+
self.assertEqual(
|
43
|
+
Concert.select(
|
44
|
+
Extract(Concert.starts, "year", alias="starts_year")
|
45
|
+
).run_sync(),
|
46
|
+
[{"starts_year": self.concert.starts.year}],
|
47
|
+
)
|
48
|
+
|
49
|
+
def test_invalid_format(self):
|
50
|
+
with self.assertRaises(ValueError):
|
51
|
+
Extract(
|
52
|
+
Concert.starts,
|
53
|
+
"abc123", # type: ignore
|
54
|
+
alias="starts_year",
|
55
|
+
)
|
56
|
+
|
57
|
+
|
58
|
+
@sqlite_only
|
59
|
+
class TestStrftime(DatetimeTest):
|
60
|
+
def test_strftime(self):
|
61
|
+
self.assertEqual(
|
62
|
+
Concert.select(
|
63
|
+
Strftime(Concert.starts, "%Y", alias="starts_year")
|
64
|
+
).run_sync(),
|
65
|
+
[{"starts_year": str(self.concert.starts.year)}],
|
66
|
+
)
|
67
|
+
|
68
|
+
|
69
|
+
class TestDatabaseAgnostic(DatetimeTest):
|
70
|
+
def test_year(self):
|
71
|
+
self.assertEqual(
|
72
|
+
Concert.select(
|
73
|
+
Year(Concert.starts, alias="starts_year")
|
74
|
+
).run_sync(),
|
75
|
+
[{"starts_year": self.concert.starts.year}],
|
76
|
+
)
|
77
|
+
|
78
|
+
def test_month(self):
|
79
|
+
self.assertEqual(
|
80
|
+
Concert.select(
|
81
|
+
Month(Concert.starts, alias="starts_month")
|
82
|
+
).run_sync(),
|
83
|
+
[{"starts_month": self.concert.starts.month}],
|
84
|
+
)
|
85
|
+
|
86
|
+
def test_day(self):
|
87
|
+
self.assertEqual(
|
88
|
+
Concert.select(Day(Concert.starts, alias="starts_day")).run_sync(),
|
89
|
+
[{"starts_day": self.concert.starts.day}],
|
90
|
+
)
|
91
|
+
|
92
|
+
def test_hour(self):
|
93
|
+
self.assertEqual(
|
94
|
+
Concert.select(
|
95
|
+
Hour(Concert.starts, alias="starts_hour")
|
96
|
+
).run_sync(),
|
97
|
+
[{"starts_hour": self.concert.starts.hour}],
|
98
|
+
)
|
99
|
+
|
100
|
+
def test_minute(self):
|
101
|
+
self.assertEqual(
|
102
|
+
Concert.select(
|
103
|
+
Minute(Concert.starts, alias="starts_minute")
|
104
|
+
).run_sync(),
|
105
|
+
[{"starts_minute": self.concert.starts.minute}],
|
106
|
+
)
|
107
|
+
|
108
|
+
def test_second(self):
|
109
|
+
self.assertEqual(
|
110
|
+
Concert.select(
|
111
|
+
Second(Concert.starts, alias="starts_second")
|
112
|
+
).run_sync(),
|
113
|
+
[{"starts_second": self.concert.starts.second}],
|
114
|
+
)
|
@@ -1,10 +1,13 @@
|
|
1
|
-
|
1
|
+
import pytest
|
2
|
+
|
3
|
+
from piccolo.query.functions.string import Concat, Upper
|
4
|
+
from tests.base import engine_version_lt, is_running_sqlite
|
2
5
|
from tests.example_apps.music.tables import Band
|
3
6
|
|
4
7
|
from .base import BandTest
|
5
8
|
|
6
9
|
|
7
|
-
class
|
10
|
+
class TestUpper(BandTest):
|
8
11
|
|
9
12
|
def test_column(self):
|
10
13
|
"""
|
@@ -23,3 +26,32 @@ class TestUpperFunction(BandTest):
|
|
23
26
|
"""
|
24
27
|
response = Band.select(Upper(Band.manager._.name)).run_sync()
|
25
28
|
self.assertListEqual(response, [{"upper": "GUIDO"}])
|
29
|
+
|
30
|
+
|
31
|
+
@pytest.mark.skipif(
|
32
|
+
is_running_sqlite() and engine_version_lt(3.44),
|
33
|
+
reason="SQLite version not supported",
|
34
|
+
)
|
35
|
+
class TestConcat(BandTest):
|
36
|
+
|
37
|
+
def test_column_and_string(self):
|
38
|
+
response = Band.select(
|
39
|
+
Concat(Band.name, "!!!", alias="name")
|
40
|
+
).run_sync()
|
41
|
+
self.assertListEqual(response, [{"name": "Pythonistas!!!"}])
|
42
|
+
|
43
|
+
def test_column_and_column(self):
|
44
|
+
response = Band.select(
|
45
|
+
Concat(Band.name, Band.popularity, alias="name")
|
46
|
+
).run_sync()
|
47
|
+
self.assertListEqual(response, [{"name": "Pythonistas1000"}])
|
48
|
+
|
49
|
+
def test_join(self):
|
50
|
+
response = Band.select(
|
51
|
+
Concat(Band.name, "-", Band.manager._.name, alias="name")
|
52
|
+
).run_sync()
|
53
|
+
self.assertListEqual(response, [{"name": "Pythonistas-Guido"}])
|
54
|
+
|
55
|
+
def test_min_args(self):
|
56
|
+
with self.assertRaises(ValueError):
|
57
|
+
Concat()
|
tests/table/test_indexes.py
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
+
import typing as t
|
1
2
|
from unittest import TestCase
|
2
3
|
|
4
|
+
from piccolo.columns.base import Column
|
3
5
|
from piccolo.columns.column_types import Integer
|
4
6
|
from piccolo.table import Table
|
5
7
|
from tests.example_apps.music.tables import Manager
|
@@ -45,12 +47,12 @@ class TestProblematicColumnName(TestCase):
|
|
45
47
|
def tearDown(self):
|
46
48
|
Concert.alter().drop_table().run_sync()
|
47
49
|
|
48
|
-
def test_problematic_name(self):
|
50
|
+
def test_problematic_name(self) -> None:
|
49
51
|
"""
|
50
52
|
Make sure we can add an index to a column with a problematic name
|
51
53
|
(which clashes with a SQL keyword).
|
52
54
|
"""
|
53
|
-
columns = [Concert.order]
|
55
|
+
columns: t.List[Column] = [Concert.order]
|
54
56
|
Concert.create_index(columns=columns).run_sync()
|
55
57
|
index_name = Concert._get_index_name([i._meta.name for i in columns])
|
56
58
|
|
tests/utils/test_pydantic.py
CHANGED
@@ -274,6 +274,7 @@ class TestUUIDColumn(TestCase):
|
|
274
274
|
# We'll also fetch it from the DB in case the database adapter's UUID
|
275
275
|
# is used.
|
276
276
|
ticket_from_db = Ticket.objects().first().run_sync()
|
277
|
+
assert ticket_from_db is not None
|
277
278
|
|
278
279
|
for ticket_ in (ticket, ticket_from_db):
|
279
280
|
json = pydantic_model(**ticket_.to_dict()).model_dump_json()
|
@@ -368,8 +369,8 @@ class TestJSONColumn(TestCase):
|
|
368
369
|
json_string = '{"code": 12345}'
|
369
370
|
|
370
371
|
model_instance = pydantic_model(meta=json_string, meta_b=json_string)
|
371
|
-
self.assertEqual(model_instance.meta, json_string)
|
372
|
-
self.assertEqual(model_instance.meta_b, json_string)
|
372
|
+
self.assertEqual(model_instance.meta, json_string) # type: ignore
|
373
|
+
self.assertEqual(model_instance.meta_b, json_string) # type: ignore
|
373
374
|
|
374
375
|
def test_deserialize_json(self):
|
375
376
|
class Movie(Table):
|
@@ -384,8 +385,8 @@ class TestJSONColumn(TestCase):
|
|
384
385
|
output = {"code": 12345}
|
385
386
|
|
386
387
|
model_instance = pydantic_model(meta=json_string, meta_b=json_string)
|
387
|
-
self.assertEqual(model_instance.meta, output)
|
388
|
-
self.assertEqual(model_instance.meta_b, output)
|
388
|
+
self.assertEqual(model_instance.meta, output) # type: ignore
|
389
|
+
self.assertEqual(model_instance.meta_b, output) # type: ignore
|
389
390
|
|
390
391
|
def test_validation(self):
|
391
392
|
class Movie(Table):
|
@@ -428,8 +429,8 @@ class TestJSONColumn(TestCase):
|
|
428
429
|
pydantic_model = create_pydantic_model(table=Movie)
|
429
430
|
movie = pydantic_model(meta=None, meta_b=None)
|
430
431
|
|
431
|
-
self.assertIsNone(movie.meta)
|
432
|
-
self.assertIsNone(movie.meta_b)
|
432
|
+
self.assertIsNone(movie.meta) # type: ignore
|
433
|
+
self.assertIsNone(movie.meta_b) # type: ignore
|
433
434
|
|
434
435
|
|
435
436
|
class TestExcludeColumns(TestCase):
|
@@ -490,7 +491,7 @@ class TestExcludeColumns(TestCase):
|
|
490
491
|
with self.assertRaises(ValueError):
|
491
492
|
create_pydantic_model(
|
492
493
|
Computer,
|
493
|
-
exclude_columns=("CPU",),
|
494
|
+
exclude_columns=("CPU",), # type: ignore
|
494
495
|
)
|
495
496
|
|
496
497
|
def test_invalid_column_different_table(self):
|
@@ -629,7 +630,10 @@ class TestNestedModel(TestCase):
|
|
629
630
|
|
630
631
|
#######################################################################
|
631
632
|
|
632
|
-
ManagerModel =
|
633
|
+
ManagerModel = t.cast(
|
634
|
+
t.Type[pydantic.BaseModel],
|
635
|
+
BandModel.model_fields["manager"].annotation,
|
636
|
+
)
|
633
637
|
self.assertTrue(issubclass(ManagerModel, pydantic.BaseModel))
|
634
638
|
self.assertEqual(
|
635
639
|
[i for i in ManagerModel.model_fields.keys()], ["name", "country"]
|
@@ -637,7 +641,10 @@ class TestNestedModel(TestCase):
|
|
637
641
|
|
638
642
|
#######################################################################
|
639
643
|
|
640
|
-
CountryModel =
|
644
|
+
CountryModel = t.cast(
|
645
|
+
t.Type[pydantic.BaseModel],
|
646
|
+
ManagerModel.model_fields["country"].annotation,
|
647
|
+
)
|
641
648
|
self.assertTrue(issubclass(CountryModel, pydantic.BaseModel))
|
642
649
|
self.assertEqual(
|
643
650
|
[i for i in CountryModel.model_fields.keys()], ["name"]
|
@@ -674,7 +681,10 @@ class TestNestedModel(TestCase):
|
|
674
681
|
|
675
682
|
BandModel = create_pydantic_model(table=Band, nested=(Band.manager,))
|
676
683
|
|
677
|
-
ManagerModel =
|
684
|
+
ManagerModel = t.cast(
|
685
|
+
t.Type[pydantic.BaseModel],
|
686
|
+
BandModel.model_fields["manager"].annotation,
|
687
|
+
)
|
678
688
|
self.assertTrue(issubclass(ManagerModel, pydantic.BaseModel))
|
679
689
|
self.assertEqual(
|
680
690
|
[i for i in ManagerModel.model_fields.keys()], ["name", "country"]
|
@@ -690,22 +700,29 @@ class TestNestedModel(TestCase):
|
|
690
700
|
# Test two levels deep
|
691
701
|
|
692
702
|
BandModel = create_pydantic_model(
|
693
|
-
table=Band, nested=(Band.manager.country,)
|
703
|
+
table=Band, nested=(Band.manager._.country,)
|
694
704
|
)
|
695
705
|
|
696
|
-
ManagerModel =
|
706
|
+
ManagerModel = t.cast(
|
707
|
+
t.Type[pydantic.BaseModel],
|
708
|
+
BandModel.model_fields["manager"].annotation,
|
709
|
+
)
|
697
710
|
self.assertTrue(issubclass(ManagerModel, pydantic.BaseModel))
|
698
711
|
self.assertEqual(
|
699
712
|
[i for i in ManagerModel.model_fields.keys()], ["name", "country"]
|
700
713
|
)
|
701
714
|
self.assertEqual(ManagerModel.__qualname__, "Band.manager")
|
702
715
|
|
703
|
-
AssistantManagerType =
|
704
|
-
|
705
|
-
|
716
|
+
AssistantManagerType = t.cast(
|
717
|
+
t.Type[pydantic.BaseModel],
|
718
|
+
BandModel.model_fields["assistant_manager"].annotation,
|
719
|
+
)
|
706
720
|
self.assertIs(AssistantManagerType, t.Optional[int])
|
707
721
|
|
708
|
-
CountryModel =
|
722
|
+
CountryModel = t.cast(
|
723
|
+
t.Type[pydantic.BaseModel],
|
724
|
+
ManagerModel.model_fields["country"].annotation,
|
725
|
+
)
|
709
726
|
self.assertTrue(issubclass(CountryModel, pydantic.BaseModel))
|
710
727
|
self.assertEqual(
|
711
728
|
[i for i in CountryModel.model_fields.keys()], ["name"]
|
@@ -716,13 +733,16 @@ class TestNestedModel(TestCase):
|
|
716
733
|
# Test three levels deep
|
717
734
|
|
718
735
|
ConcertModel = create_pydantic_model(
|
719
|
-
Concert, nested=(Concert.band_1.manager,)
|
736
|
+
Concert, nested=(Concert.band_1._.manager,)
|
720
737
|
)
|
721
738
|
|
722
739
|
VenueModel = ConcertModel.model_fields["venue"].annotation
|
723
740
|
self.assertIs(VenueModel, t.Optional[int])
|
724
741
|
|
725
|
-
BandModel =
|
742
|
+
BandModel = t.cast(
|
743
|
+
t.Type[pydantic.BaseModel],
|
744
|
+
ConcertModel.model_fields["band_1"].annotation,
|
745
|
+
)
|
726
746
|
self.assertTrue(issubclass(BandModel, pydantic.BaseModel))
|
727
747
|
self.assertEqual(
|
728
748
|
[i for i in BandModel.model_fields.keys()],
|
@@ -730,7 +750,10 @@ class TestNestedModel(TestCase):
|
|
730
750
|
)
|
731
751
|
self.assertEqual(BandModel.__qualname__, "Concert.band_1")
|
732
752
|
|
733
|
-
ManagerModel =
|
753
|
+
ManagerModel = t.cast(
|
754
|
+
t.Type[pydantic.BaseModel],
|
755
|
+
BandModel.model_fields["manager"].annotation,
|
756
|
+
)
|
734
757
|
self.assertTrue(issubclass(ManagerModel, pydantic.BaseModel))
|
735
758
|
self.assertEqual(
|
736
759
|
[i for i in ManagerModel.model_fields.keys()],
|
@@ -751,11 +774,14 @@ class TestNestedModel(TestCase):
|
|
751
774
|
|
752
775
|
MyConcertModel = create_pydantic_model(
|
753
776
|
Concert,
|
754
|
-
nested=(Concert.band_1.manager,),
|
777
|
+
nested=(Concert.band_1._.manager,),
|
755
778
|
model_name="MyConcertModel",
|
756
779
|
)
|
757
780
|
|
758
|
-
BandModel =
|
781
|
+
BandModel = t.cast(
|
782
|
+
t.Type[pydantic.BaseModel],
|
783
|
+
MyConcertModel.model_fields["band_1"].annotation,
|
784
|
+
)
|
759
785
|
self.assertEqual(BandModel.__qualname__, "MyConcertModel.band_1")
|
760
786
|
|
761
787
|
ManagerModel = BandModel.model_fields["manager"].annotation
|
@@ -763,7 +789,7 @@ class TestNestedModel(TestCase):
|
|
763
789
|
ManagerModel.__qualname__, "MyConcertModel.band_1.manager"
|
764
790
|
)
|
765
791
|
|
766
|
-
def test_cascaded_args(self):
|
792
|
+
def test_cascaded_args(self) -> None:
|
767
793
|
"""
|
768
794
|
Make sure that arguments passed to ``create_pydantic_model`` are
|
769
795
|
cascaded to nested models.
|
@@ -784,14 +810,20 @@ class TestNestedModel(TestCase):
|
|
784
810
|
table=Band, nested=True, include_default_columns=True
|
785
811
|
)
|
786
812
|
|
787
|
-
ManagerModel =
|
813
|
+
ManagerModel = t.cast(
|
814
|
+
t.Type[pydantic.BaseModel],
|
815
|
+
BandModel.model_fields["manager"].annotation,
|
816
|
+
)
|
788
817
|
self.assertTrue(issubclass(ManagerModel, pydantic.BaseModel))
|
789
818
|
self.assertEqual(
|
790
819
|
[i for i in ManagerModel.model_fields.keys()],
|
791
820
|
["id", "name", "country"],
|
792
821
|
)
|
793
822
|
|
794
|
-
CountryModel =
|
823
|
+
CountryModel = t.cast(
|
824
|
+
t.Type[pydantic.BaseModel],
|
825
|
+
ManagerModel.model_fields["country"].annotation,
|
826
|
+
)
|
795
827
|
self.assertTrue(issubclass(CountryModel, pydantic.BaseModel))
|
796
828
|
self.assertEqual(
|
797
829
|
[i for i in CountryModel.model_fields.keys()], ["id", "name"]
|
@@ -823,13 +855,22 @@ class TestRecursionDepth(TestCase):
|
|
823
855
|
table=Concert, nested=True, max_recursion_depth=2
|
824
856
|
)
|
825
857
|
|
826
|
-
VenueModel =
|
858
|
+
VenueModel = t.cast(
|
859
|
+
t.Type[pydantic.BaseModel],
|
860
|
+
ConcertModel.model_fields["venue"].annotation,
|
861
|
+
)
|
827
862
|
self.assertTrue(issubclass(VenueModel, pydantic.BaseModel))
|
828
863
|
|
829
|
-
BandModel =
|
864
|
+
BandModel = t.cast(
|
865
|
+
t.Type[pydantic.BaseModel],
|
866
|
+
ConcertModel.model_fields["band"].annotation,
|
867
|
+
)
|
830
868
|
self.assertTrue(issubclass(BandModel, pydantic.BaseModel))
|
831
869
|
|
832
|
-
ManagerModel =
|
870
|
+
ManagerModel = t.cast(
|
871
|
+
t.Type[pydantic.BaseModel],
|
872
|
+
BandModel.model_fields["manager"].annotation,
|
873
|
+
)
|
833
874
|
self.assertTrue(issubclass(ManagerModel, pydantic.BaseModel))
|
834
875
|
|
835
876
|
# We should have hit the recursion depth:
|
@@ -851,7 +892,7 @@ class TestDBColumnName(TestCase):
|
|
851
892
|
|
852
893
|
model = BandModel(regrettable_column_name="test")
|
853
894
|
|
854
|
-
self.assertEqual(model.name, "test")
|
895
|
+
self.assertEqual(model.name, "test") # type: ignore
|
855
896
|
|
856
897
|
|
857
898
|
class TestJSONSchemaExtra(TestCase):
|
@@ -885,7 +926,7 @@ class TestPydanticExtraFields(TestCase):
|
|
885
926
|
config: pydantic.config.ConfigDict = {"extra": "forbid"}
|
886
927
|
model = create_pydantic_model(Band, pydantic_config=config)
|
887
928
|
|
888
|
-
self.assertEqual(model.model_config
|
929
|
+
self.assertEqual(model.model_config.get("extra"), "forbid")
|
889
930
|
|
890
931
|
def test_pydantic_invalid_extra_fields(self) -> None:
|
891
932
|
"""
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|