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.
Files changed (52) 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/column_types.py +56 -39
  14. piccolo/columns/defaults/base.py +1 -1
  15. piccolo/columns/defaults/date.py +9 -1
  16. piccolo/columns/defaults/interval.py +1 -0
  17. piccolo/columns/defaults/time.py +9 -1
  18. piccolo/columns/defaults/timestamp.py +1 -0
  19. piccolo/columns/defaults/uuid.py +1 -1
  20. piccolo/columns/m2m.py +7 -7
  21. piccolo/columns/operators/comparison.py +4 -0
  22. piccolo/conf/apps.py +9 -4
  23. piccolo/engine/base.py +69 -20
  24. piccolo/engine/cockroach.py +2 -3
  25. piccolo/engine/postgres.py +33 -19
  26. piccolo/engine/sqlite.py +27 -22
  27. piccolo/query/functions/__init__.py +11 -1
  28. piccolo/query/functions/datetime.py +260 -0
  29. piccolo/query/functions/string.py +45 -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/schema.py +18 -11
  36. piccolo/table.py +22 -21
  37. piccolo/utils/encoding.py +5 -3
  38. {piccolo-1.9.0.dist-info → piccolo-1.11.0.dist-info}/METADATA +1 -1
  39. {piccolo-1.9.0.dist-info → piccolo-1.11.0.dist-info}/RECORD +52 -50
  40. tests/apps/migrations/auto/integration/test_migrations.py +1 -1
  41. tests/columns/test_array.py +28 -0
  42. tests/conf/test_apps.py +1 -1
  43. tests/engine/test_nested_transaction.py +2 -0
  44. tests/engine/test_transaction.py +1 -2
  45. tests/query/functions/test_datetime.py +114 -0
  46. tests/query/functions/test_string.py +34 -2
  47. tests/table/test_indexes.py +4 -2
  48. tests/utils/test_pydantic.py +70 -29
  49. {piccolo-1.9.0.dist-info → piccolo-1.11.0.dist-info}/LICENSE +0 -0
  50. {piccolo-1.9.0.dist-info → piccolo-1.11.0.dist-info}/WHEEL +0 -0
  51. {piccolo-1.9.0.dist-info → piccolo-1.11.0.dist-info}/entry_points.txt +0 -0
  52. {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
- from piccolo.query.functions.string import Upper
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 TestUpperFunction(BandTest):
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()
@@ -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
 
@@ -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
  """