piccolo 1.8.0__py3-none-any.whl → 1.10.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.
Files changed (60) hide show
  1. piccolo/__init__.py +1 -1
  2. piccolo/apps/fixtures/commands/load.py +1 -1
  3. piccolo/apps/migrations/auto/__init__.py +8 -0
  4. piccolo/apps/migrations/auto/migration_manager.py +2 -1
  5. piccolo/apps/migrations/commands/backwards.py +3 -1
  6. piccolo/apps/migrations/commands/base.py +1 -1
  7. piccolo/apps/migrations/commands/check.py +1 -1
  8. piccolo/apps/migrations/commands/clean.py +1 -1
  9. piccolo/apps/migrations/commands/forwards.py +3 -1
  10. piccolo/apps/migrations/commands/new.py +4 -2
  11. piccolo/apps/schema/commands/generate.py +2 -2
  12. piccolo/apps/shell/commands/run.py +1 -1
  13. piccolo/columns/base.py +55 -29
  14. piccolo/columns/column_types.py +28 -4
  15. piccolo/columns/defaults/base.py +6 -4
  16. piccolo/columns/defaults/date.py +9 -1
  17. piccolo/columns/defaults/interval.py +1 -0
  18. piccolo/columns/defaults/time.py +9 -1
  19. piccolo/columns/defaults/timestamp.py +1 -0
  20. piccolo/columns/defaults/uuid.py +1 -1
  21. piccolo/columns/m2m.py +7 -7
  22. piccolo/columns/operators/comparison.py +4 -0
  23. piccolo/conf/apps.py +9 -4
  24. piccolo/engine/base.py +69 -20
  25. piccolo/engine/cockroach.py +2 -3
  26. piccolo/engine/postgres.py +33 -19
  27. piccolo/engine/sqlite.py +27 -22
  28. piccolo/query/functions/__init__.py +5 -0
  29. piccolo/query/functions/math.py +48 -0
  30. piccolo/query/methods/create_index.py +1 -1
  31. piccolo/query/methods/drop_index.py +1 -1
  32. piccolo/query/methods/objects.py +7 -7
  33. piccolo/query/methods/select.py +13 -7
  34. piccolo/query/mixins.py +3 -10
  35. piccolo/querystring.py +18 -0
  36. piccolo/schema.py +20 -12
  37. piccolo/table.py +22 -21
  38. piccolo/utils/encoding.py +5 -3
  39. {piccolo-1.8.0.dist-info → piccolo-1.10.0.dist-info}/METADATA +1 -1
  40. {piccolo-1.8.0.dist-info → piccolo-1.10.0.dist-info}/RECORD +59 -52
  41. tests/apps/migrations/auto/integration/test_migrations.py +1 -1
  42. tests/columns/test_array.py +91 -19
  43. tests/columns/test_get_sql_value.py +66 -0
  44. tests/conf/test_apps.py +1 -1
  45. tests/engine/test_nested_transaction.py +2 -0
  46. tests/engine/test_transaction.py +1 -2
  47. tests/query/functions/__init__.py +0 -0
  48. tests/query/functions/base.py +34 -0
  49. tests/query/functions/test_functions.py +64 -0
  50. tests/query/functions/test_math.py +39 -0
  51. tests/query/functions/test_string.py +25 -0
  52. tests/query/functions/test_type_conversion.py +134 -0
  53. tests/query/test_querystring.py +136 -0
  54. tests/table/test_indexes.py +4 -2
  55. tests/utils/test_pydantic.py +70 -29
  56. tests/query/test_functions.py +0 -238
  57. {piccolo-1.8.0.dist-info → piccolo-1.10.0.dist-info}/LICENSE +0 -0
  58. {piccolo-1.8.0.dist-info → piccolo-1.10.0.dist-info}/WHEEL +0 -0
  59. {piccolo-1.8.0.dist-info → piccolo-1.10.0.dist-info}/entry_points.txt +0 -0
  60. {piccolo-1.8.0.dist-info → piccolo-1.10.0.dist-info}/top_level.txt +0 -0
@@ -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 = BandModel.model_fields["manager"].annotation
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 = ManagerModel.model_fields["country"].annotation
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 = BandModel.model_fields["manager"].annotation
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 = BandModel.model_fields["manager"].annotation
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 = BandModel.model_fields[
704
- "assistant_manager"
705
- ].annotation
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 = ManagerModel.model_fields["country"].annotation
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 = ConcertModel.model_fields["band_1"].annotation
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 = BandModel.model_fields["manager"].annotation
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 = MyConcertModel.model_fields["band_1"].annotation
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 = BandModel.model_fields["manager"].annotation
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 = ManagerModel.model_fields["country"].annotation
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 = ConcertModel.model_fields["venue"].annotation
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 = ConcertModel.model_fields["band"].annotation
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 = BandModel.model_fields["manager"].annotation
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["extra"], "forbid")
929
+ self.assertEqual(model.model_config.get("extra"), "forbid")
889
930
 
890
931
  def test_pydantic_invalid_extra_fields(self) -> None:
891
932
  """
@@ -1,238 +0,0 @@
1
- from unittest import TestCase
2
-
3
- from piccolo.columns import Integer, Text, Varchar
4
- from piccolo.query.functions import Cast, Length, Reverse, Upper
5
- from piccolo.querystring import QueryString
6
- from piccolo.table import create_db_tables_sync, drop_db_tables_sync
7
- from tests.base import engines_skip
8
- from tests.example_apps.music.tables import Band, Manager
9
-
10
-
11
- class FunctionTest(TestCase):
12
- tables = (Band, Manager)
13
-
14
- def setUp(self) -> None:
15
- create_db_tables_sync(*self.tables)
16
-
17
- manager = Manager({Manager.name: "Guido"})
18
- manager.save().run_sync()
19
-
20
- band = Band(
21
- {
22
- Band.name: "Pythonistas",
23
- Band.manager: manager,
24
- Band.popularity: 1000,
25
- }
26
- )
27
- band.save().run_sync()
28
-
29
- def tearDown(self) -> None:
30
- drop_db_tables_sync(*self.tables)
31
-
32
-
33
- class TestUpperFunction(FunctionTest):
34
-
35
- def test_column(self):
36
- """
37
- Make sure we can uppercase a column's value.
38
- """
39
- response = Band.select(Upper(Band.name)).run_sync()
40
- self.assertListEqual(response, [{"upper": "PYTHONISTAS"}])
41
-
42
- def test_alias(self):
43
- response = Band.select(Upper(Band.name, alias="name")).run_sync()
44
- self.assertListEqual(response, [{"name": "PYTHONISTAS"}])
45
-
46
- def test_joined_column(self):
47
- """
48
- Make sure we can uppercase a column's value from a joined table.
49
- """
50
- response = Band.select(Upper(Band.manager._.name)).run_sync()
51
- self.assertListEqual(response, [{"upper": "GUIDO"}])
52
-
53
-
54
- @engines_skip("sqlite")
55
- class TestNested(FunctionTest):
56
- """
57
- Skip the the test for SQLite, as it doesn't support ``Reverse``.
58
- """
59
-
60
- def test_nested(self):
61
- """
62
- Make sure we can nest functions.
63
- """
64
- response = Band.select(Upper(Reverse(Band.name))).run_sync()
65
- self.assertListEqual(response, [{"upper": "SATSINOHTYP"}])
66
-
67
- def test_nested_with_joined_column(self):
68
- """
69
- Make sure nested functions can be used on a column from a joined table.
70
- """
71
- response = Band.select(Upper(Reverse(Band.manager._.name))).run_sync()
72
- self.assertListEqual(response, [{"upper": "ODIUG"}])
73
-
74
- def test_nested_within_querystring(self):
75
- """
76
- If we wrap a function in a custom QueryString - make sure the columns
77
- are still accessible, so joins are successful.
78
- """
79
- response = Band.select(
80
- QueryString("CONCAT({}, '!')", Upper(Band.manager._.name)),
81
- ).run_sync()
82
-
83
- self.assertListEqual(response, [{"concat": "GUIDO!"}])
84
-
85
-
86
- class TestWhereClause(FunctionTest):
87
-
88
- def test_where(self):
89
- """
90
- Make sure where clauses work with functions.
91
- """
92
- response = (
93
- Band.select(Band.name)
94
- .where(Upper(Band.name) == "PYTHONISTAS")
95
- .run_sync()
96
- )
97
- self.assertListEqual(response, [{"name": "Pythonistas"}])
98
-
99
- def test_where_with_joined_column(self):
100
- """
101
- Make sure where clauses work with functions, when a joined column is
102
- used.
103
- """
104
- response = (
105
- Band.select(Band.name)
106
- .where(Upper(Band.manager._.name) == "GUIDO")
107
- .run_sync()
108
- )
109
- self.assertListEqual(response, [{"name": "Pythonistas"}])
110
-
111
-
112
- class TestCast(FunctionTest):
113
- def test_varchar(self):
114
- """
115
- Make sure that casting to ``Varchar`` works.
116
- """
117
- response = Band.select(
118
- Cast(
119
- Band.popularity,
120
- as_type=Varchar(),
121
- )
122
- ).run_sync()
123
-
124
- self.assertListEqual(
125
- response,
126
- [{"popularity": "1000"}],
127
- )
128
-
129
- def test_text(self):
130
- """
131
- Make sure that casting to ``Text`` works.
132
- """
133
- response = Band.select(
134
- Cast(
135
- Band.popularity,
136
- as_type=Text(),
137
- )
138
- ).run_sync()
139
-
140
- self.assertListEqual(
141
- response,
142
- [{"popularity": "1000"}],
143
- )
144
-
145
- def test_integer(self):
146
- """
147
- Make sure that casting to ``Integer`` works.
148
- """
149
- Band.update({Band.name: "1111"}, force=True).run_sync()
150
-
151
- response = Band.select(
152
- Cast(
153
- Band.name,
154
- as_type=Integer(),
155
- )
156
- ).run_sync()
157
-
158
- self.assertListEqual(
159
- response,
160
- [{"name": 1111}],
161
- )
162
-
163
- def test_join(self):
164
- """
165
- Make sure that casting works with joins.
166
- """
167
- Manager.update({Manager.name: "1111"}, force=True).run_sync()
168
-
169
- response = Band.select(
170
- Band.name,
171
- Cast(
172
- Band.manager.name,
173
- as_type=Integer(),
174
- ),
175
- ).run_sync()
176
-
177
- self.assertListEqual(
178
- response,
179
- [
180
- {
181
- "name": "Pythonistas",
182
- "manager.name": 1111,
183
- }
184
- ],
185
- )
186
-
187
- def test_nested_inner(self):
188
- """
189
- Make sure ``Cast`` can be passed into other functions.
190
- """
191
- Band.update({Band.name: "1111"}, force=True).run_sync()
192
-
193
- response = Band.select(
194
- Length(
195
- Cast(
196
- Band.popularity,
197
- as_type=Varchar(),
198
- )
199
- )
200
- ).run_sync()
201
-
202
- self.assertListEqual(
203
- response,
204
- [{"length": 4}],
205
- )
206
-
207
- def test_nested_outer(self):
208
- """
209
- Make sure a querystring can be passed into ``Cast`` (meaning it can be
210
- nested).
211
- """
212
- response = Band.select(
213
- Cast(
214
- Length(Band.name),
215
- as_type=Varchar(),
216
- alias="length",
217
- )
218
- ).run_sync()
219
-
220
- self.assertListEqual(
221
- response,
222
- [{"length": str(len("Pythonistas"))}],
223
- )
224
-
225
- def test_where_clause(self):
226
- """
227
- Make sure ``Cast`` works in a where clause.
228
- """
229
- response = (
230
- Band.select(Band.name, Band.popularity)
231
- .where(Cast(Band.popularity, Varchar()) == "1000")
232
- .run_sync()
233
- )
234
-
235
- self.assertListEqual(
236
- response,
237
- [{"name": "Pythonistas", "popularity": 1000}],
238
- )