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
@@ -0,0 +1,66 @@
1
+ import datetime
2
+ from unittest import TestCase
3
+
4
+ from tests.base import engines_only
5
+ from tests.example_apps.music.tables import Band
6
+
7
+
8
+ @engines_only("postgres", "cockroach")
9
+ class TestArrayPostgres(TestCase):
10
+
11
+ def test_string(self):
12
+ self.assertEqual(
13
+ Band.name.get_sql_value(["a", "b", "c"]),
14
+ '\'{"a","b","c"}\'',
15
+ )
16
+
17
+ def test_int(self):
18
+ self.assertEqual(
19
+ Band.name.get_sql_value([1, 2, 3]),
20
+ "'{1,2,3}'",
21
+ )
22
+
23
+ def test_nested(self):
24
+ self.assertEqual(
25
+ Band.name.get_sql_value([1, 2, 3, [4, 5, 6]]),
26
+ "'{1,2,3,{4,5,6}}'",
27
+ )
28
+
29
+ def test_time(self):
30
+ self.assertEqual(
31
+ Band.name.get_sql_value([datetime.time(hour=8, minute=0)]),
32
+ "'{\"08:00:00\"}'",
33
+ )
34
+
35
+
36
+ @engines_only("sqlite")
37
+ class TestArraySQLite(TestCase):
38
+ """
39
+ Note, we use ``.replace(" ", "")`` because we serialise arrays using
40
+ Python's json library, and there is inconsistency between Python versions
41
+ (some output ``["a", "b", "c"]``, and others ``["a","b","c"]``).
42
+ """
43
+
44
+ def test_string(self):
45
+ self.assertEqual(
46
+ Band.name.get_sql_value(["a", "b", "c"]).replace(" ", ""),
47
+ '\'["a","b","c"]\'',
48
+ )
49
+
50
+ def test_int(self):
51
+ self.assertEqual(
52
+ Band.name.get_sql_value([1, 2, 3]).replace(" ", ""),
53
+ "'[1,2,3]'",
54
+ )
55
+
56
+ def test_nested(self):
57
+ self.assertEqual(
58
+ Band.name.get_sql_value([1, 2, 3, [4, 5, 6]]).replace(" ", ""),
59
+ "'[1,2,3,[4,5,6]]'",
60
+ )
61
+
62
+ def test_time(self):
63
+ self.assertEqual(
64
+ Band.name.get_sql_value([datetime.time(hour=8, minute=0)]),
65
+ "'[\"08:00:00\"]'",
66
+ )
tests/conf/test_apps.py CHANGED
@@ -85,7 +85,7 @@ class TestAppConfig(TestCase):
85
85
  config = AppConfig(
86
86
  app_name="music", migrations_folder_path=pathlib.Path(__file__)
87
87
  )
88
- self.assertEqual(config.migrations_folder_path, __file__)
88
+ self.assertEqual(config.resolved_migrations_folder_path, __file__)
89
89
 
90
90
  def test_get_table_with_name(self):
91
91
  """
@@ -45,10 +45,12 @@ class TestDifferentDB(TestCase):
45
45
 
46
46
  self.assertTrue(await Musician.table_exists().run())
47
47
  musician = await Musician.select("name").first().run()
48
+ assert musician is not None
48
49
  self.assertEqual(musician["name"], "Bob")
49
50
 
50
51
  self.assertTrue(await Roadie.table_exists().run())
51
52
  roadie = await Roadie.select("name").first().run()
53
+ assert roadie is not None
52
54
  self.assertEqual(roadie["name"], "Dave")
53
55
 
54
56
  def test_nested(self):
@@ -4,7 +4,6 @@ from unittest import TestCase
4
4
 
5
5
  import pytest
6
6
 
7
- from piccolo.engine.postgres import Atomic
8
7
  from piccolo.engine.sqlite import SQLiteEngine, TransactionType
9
8
  from piccolo.table import drop_db_tables_sync
10
9
  from piccolo.utils.sync import run_sync
@@ -58,7 +57,7 @@ class TestAtomic(TestCase):
58
57
  engine = Band._meta.db
59
58
  await engine.start_connection_pool()
60
59
 
61
- atomic: Atomic = engine.atomic()
60
+ atomic = engine.atomic()
62
61
  atomic.add(
63
62
  Manager.create_table(),
64
63
  Band.create_table(),
File without changes
@@ -0,0 +1,34 @@
1
+ import typing as t
2
+ from unittest import TestCase
3
+
4
+ from piccolo.table import Table, create_db_tables_sync, drop_db_tables_sync
5
+ from tests.example_apps.music.tables import Band, Manager
6
+
7
+
8
+ class FunctionTest(TestCase):
9
+ tables: t.List[t.Type[Table]]
10
+
11
+ def setUp(self) -> None:
12
+ create_db_tables_sync(*self.tables)
13
+
14
+ def tearDown(self) -> None:
15
+ drop_db_tables_sync(*self.tables)
16
+
17
+
18
+ class BandTest(FunctionTest):
19
+ tables = [Band, Manager]
20
+
21
+ def setUp(self) -> None:
22
+ super().setUp()
23
+
24
+ manager = Manager({Manager.name: "Guido"})
25
+ manager.save().run_sync()
26
+
27
+ band = Band(
28
+ {
29
+ Band.name: "Pythonistas",
30
+ Band.manager: manager,
31
+ Band.popularity: 1000,
32
+ }
33
+ )
34
+ band.save().run_sync()
@@ -0,0 +1,64 @@
1
+ from piccolo.query.functions import Reverse, Upper
2
+ from piccolo.querystring import QueryString
3
+ from tests.base import engines_skip
4
+ from tests.example_apps.music.tables import Band
5
+
6
+ from .base import BandTest
7
+
8
+
9
+ @engines_skip("sqlite")
10
+ class TestNested(BandTest):
11
+ """
12
+ Skip the the test for SQLite, as it doesn't support ``Reverse``.
13
+ """
14
+
15
+ def test_nested(self):
16
+ """
17
+ Make sure we can nest functions.
18
+ """
19
+ response = Band.select(Upper(Reverse(Band.name))).run_sync()
20
+ self.assertListEqual(response, [{"upper": "SATSINOHTYP"}])
21
+
22
+ def test_nested_with_joined_column(self):
23
+ """
24
+ Make sure nested functions can be used on a column from a joined table.
25
+ """
26
+ response = Band.select(Upper(Reverse(Band.manager._.name))).run_sync()
27
+ self.assertListEqual(response, [{"upper": "ODIUG"}])
28
+
29
+ def test_nested_within_querystring(self):
30
+ """
31
+ If we wrap a function in a custom QueryString - make sure the columns
32
+ are still accessible, so joins are successful.
33
+ """
34
+ response = Band.select(
35
+ QueryString("CONCAT({}, '!')", Upper(Band.manager._.name)),
36
+ ).run_sync()
37
+
38
+ self.assertListEqual(response, [{"concat": "GUIDO!"}])
39
+
40
+
41
+ class TestWhereClause(BandTest):
42
+
43
+ def test_where(self):
44
+ """
45
+ Make sure where clauses work with functions.
46
+ """
47
+ response = (
48
+ Band.select(Band.name)
49
+ .where(Upper(Band.name) == "PYTHONISTAS")
50
+ .run_sync()
51
+ )
52
+ self.assertListEqual(response, [{"name": "Pythonistas"}])
53
+
54
+ def test_where_with_joined_column(self):
55
+ """
56
+ Make sure where clauses work with functions, when a joined column is
57
+ used.
58
+ """
59
+ response = (
60
+ Band.select(Band.name)
61
+ .where(Upper(Band.manager._.name) == "GUIDO")
62
+ .run_sync()
63
+ )
64
+ self.assertListEqual(response, [{"name": "Pythonistas"}])
@@ -0,0 +1,39 @@
1
+ import decimal
2
+
3
+ from piccolo.columns import Numeric
4
+ from piccolo.query.functions.math import Abs, Ceil, Floor, Round
5
+ from piccolo.table import Table
6
+
7
+ from .base import FunctionTest
8
+
9
+
10
+ class Ticket(Table):
11
+ price = Numeric(digits=(5, 2))
12
+
13
+
14
+ class TestMath(FunctionTest):
15
+
16
+ tables = [Ticket]
17
+
18
+ def setUp(self):
19
+ super().setUp()
20
+ self.ticket = Ticket({Ticket.price: decimal.Decimal("36.50")})
21
+ self.ticket.save().run_sync()
22
+
23
+ def test_floor(self):
24
+ response = Ticket.select(Floor(Ticket.price, alias="price")).run_sync()
25
+ self.assertListEqual(response, [{"price": decimal.Decimal("36.00")}])
26
+
27
+ def test_ceil(self):
28
+ response = Ticket.select(Ceil(Ticket.price, alias="price")).run_sync()
29
+ self.assertListEqual(response, [{"price": decimal.Decimal("37.00")}])
30
+
31
+ def test_abs(self):
32
+ self.ticket.price = decimal.Decimal("-1.50")
33
+ self.ticket.save().run_sync()
34
+ response = Ticket.select(Abs(Ticket.price, alias="price")).run_sync()
35
+ self.assertListEqual(response, [{"price": decimal.Decimal("1.50")}])
36
+
37
+ def test_round(self):
38
+ response = Ticket.select(Round(Ticket.price, alias="price")).run_sync()
39
+ self.assertListEqual(response, [{"price": decimal.Decimal("37.00")}])
@@ -0,0 +1,25 @@
1
+ from piccolo.query.functions.string import Upper
2
+ from tests.example_apps.music.tables import Band
3
+
4
+ from .base import BandTest
5
+
6
+
7
+ class TestUpperFunction(BandTest):
8
+
9
+ def test_column(self):
10
+ """
11
+ Make sure we can uppercase a column's value.
12
+ """
13
+ response = Band.select(Upper(Band.name)).run_sync()
14
+ self.assertListEqual(response, [{"upper": "PYTHONISTAS"}])
15
+
16
+ def test_alias(self):
17
+ response = Band.select(Upper(Band.name, alias="name")).run_sync()
18
+ self.assertListEqual(response, [{"name": "PYTHONISTAS"}])
19
+
20
+ def test_joined_column(self):
21
+ """
22
+ Make sure we can uppercase a column's value from a joined table.
23
+ """
24
+ response = Band.select(Upper(Band.manager._.name)).run_sync()
25
+ self.assertListEqual(response, [{"upper": "GUIDO"}])
@@ -0,0 +1,134 @@
1
+ from piccolo.columns import Integer, Text, Varchar
2
+ from piccolo.query.functions import Cast, Length
3
+ from tests.example_apps.music.tables import Band, Manager
4
+
5
+ from .base import BandTest
6
+
7
+
8
+ class TestCast(BandTest):
9
+ def test_varchar(self):
10
+ """
11
+ Make sure that casting to ``Varchar`` works.
12
+ """
13
+ response = Band.select(
14
+ Cast(
15
+ Band.popularity,
16
+ as_type=Varchar(),
17
+ )
18
+ ).run_sync()
19
+
20
+ self.assertListEqual(
21
+ response,
22
+ [{"popularity": "1000"}],
23
+ )
24
+
25
+ def test_text(self):
26
+ """
27
+ Make sure that casting to ``Text`` works.
28
+ """
29
+ response = Band.select(
30
+ Cast(
31
+ Band.popularity,
32
+ as_type=Text(),
33
+ )
34
+ ).run_sync()
35
+
36
+ self.assertListEqual(
37
+ response,
38
+ [{"popularity": "1000"}],
39
+ )
40
+
41
+ def test_integer(self):
42
+ """
43
+ Make sure that casting to ``Integer`` works.
44
+ """
45
+ Band.update({Band.name: "1111"}, force=True).run_sync()
46
+
47
+ response = Band.select(
48
+ Cast(
49
+ Band.name,
50
+ as_type=Integer(),
51
+ )
52
+ ).run_sync()
53
+
54
+ self.assertListEqual(
55
+ response,
56
+ [{"name": 1111}],
57
+ )
58
+
59
+ def test_join(self):
60
+ """
61
+ Make sure that casting works with joins.
62
+ """
63
+ Manager.update({Manager.name: "1111"}, force=True).run_sync()
64
+
65
+ response = Band.select(
66
+ Band.name,
67
+ Cast(
68
+ Band.manager.name,
69
+ as_type=Integer(),
70
+ ),
71
+ ).run_sync()
72
+
73
+ self.assertListEqual(
74
+ response,
75
+ [
76
+ {
77
+ "name": "Pythonistas",
78
+ "manager.name": 1111,
79
+ }
80
+ ],
81
+ )
82
+
83
+ def test_nested_inner(self):
84
+ """
85
+ Make sure ``Cast`` can be passed into other functions.
86
+ """
87
+ Band.update({Band.name: "1111"}, force=True).run_sync()
88
+
89
+ response = Band.select(
90
+ Length(
91
+ Cast(
92
+ Band.popularity,
93
+ as_type=Varchar(),
94
+ )
95
+ )
96
+ ).run_sync()
97
+
98
+ self.assertListEqual(
99
+ response,
100
+ [{"length": 4}],
101
+ )
102
+
103
+ def test_nested_outer(self):
104
+ """
105
+ Make sure a querystring can be passed into ``Cast`` (meaning it can be
106
+ nested).
107
+ """
108
+ response = Band.select(
109
+ Cast(
110
+ Length(Band.name),
111
+ as_type=Varchar(),
112
+ alias="length",
113
+ )
114
+ ).run_sync()
115
+
116
+ self.assertListEqual(
117
+ response,
118
+ [{"length": str(len("Pythonistas"))}],
119
+ )
120
+
121
+ def test_where_clause(self):
122
+ """
123
+ Make sure ``Cast`` works in a where clause.
124
+ """
125
+ response = (
126
+ Band.select(Band.name, Band.popularity)
127
+ .where(Cast(Band.popularity, Varchar()) == "1000")
128
+ .run_sync()
129
+ )
130
+
131
+ self.assertListEqual(
132
+ response,
133
+ [{"name": "Pythonistas", "popularity": 1000}],
134
+ )
@@ -1,6 +1,7 @@
1
1
  from unittest import TestCase
2
2
 
3
3
  from piccolo.querystring import QueryString
4
+ from tests.base import postgres_only
4
5
 
5
6
 
6
7
  # TODO - add more extensive tests (increased nesting and argument count).
@@ -28,3 +29,138 @@ class TestQueryString(TestCase):
28
29
  def test_querystring_with_no_args(self):
29
30
  qs = QueryString("SELECT name FROM band")
30
31
  self.assertEqual(qs.compile_string(), ("SELECT name FROM band", []))
32
+
33
+
34
+ @postgres_only
35
+ class TestQueryStringOperators(TestCase):
36
+ """
37
+ Make sure basic operations can be used on ``QueryString``.
38
+ """
39
+
40
+ def test_add(self):
41
+ query = QueryString("SELECT price") + 1
42
+ self.assertIsInstance(query, QueryString)
43
+ self.assertEqual(
44
+ query.compile_string(),
45
+ ("SELECT price + $1", [1]),
46
+ )
47
+
48
+ def test_multiply(self):
49
+ query = QueryString("SELECT price") * 2
50
+ self.assertIsInstance(query, QueryString)
51
+ self.assertEqual(
52
+ query.compile_string(),
53
+ ("SELECT price * $1", [2]),
54
+ )
55
+
56
+ def test_divide(self):
57
+ query = QueryString("SELECT price") / 1
58
+ self.assertIsInstance(query, QueryString)
59
+ self.assertEqual(
60
+ query.compile_string(),
61
+ ("SELECT price / $1", [1]),
62
+ )
63
+
64
+ def test_power(self):
65
+ query = QueryString("SELECT price") ** 2
66
+ self.assertIsInstance(query, QueryString)
67
+ self.assertEqual(
68
+ query.compile_string(),
69
+ ("SELECT price ^ $1", [2]),
70
+ )
71
+
72
+ def test_subtract(self):
73
+ query = QueryString("SELECT price") - 1
74
+ self.assertIsInstance(query, QueryString)
75
+ self.assertEqual(
76
+ query.compile_string(),
77
+ ("SELECT price - $1", [1]),
78
+ )
79
+
80
+ def test_modulus(self):
81
+ query = QueryString("SELECT price") % 1
82
+ self.assertIsInstance(query, QueryString)
83
+ self.assertEqual(
84
+ query.compile_string(),
85
+ ("SELECT price % $1", [1]),
86
+ )
87
+
88
+ def test_like(self):
89
+ query = QueryString("strip(name)").like("Python%")
90
+ self.assertIsInstance(query, QueryString)
91
+ self.assertEqual(
92
+ query.compile_string(),
93
+ ("strip(name) LIKE $1", ["Python%"]),
94
+ )
95
+
96
+ def test_ilike(self):
97
+ query = QueryString("strip(name)").ilike("Python%")
98
+ self.assertIsInstance(query, QueryString)
99
+ self.assertEqual(
100
+ query.compile_string(),
101
+ ("strip(name) ILIKE $1", ["Python%"]),
102
+ )
103
+
104
+ def test_greater_than(self):
105
+ query = QueryString("SELECT price") > 10
106
+ self.assertIsInstance(query, QueryString)
107
+ self.assertEqual(
108
+ query.compile_string(),
109
+ ("SELECT price > $1", [10]),
110
+ )
111
+
112
+ def test_greater_equal_than(self):
113
+ query = QueryString("SELECT price") >= 10
114
+ self.assertIsInstance(query, QueryString)
115
+ self.assertEqual(
116
+ query.compile_string(),
117
+ ("SELECT price >= $1", [10]),
118
+ )
119
+
120
+ def test_less_than(self):
121
+ query = QueryString("SELECT price") < 10
122
+ self.assertIsInstance(query, QueryString)
123
+ self.assertEqual(
124
+ query.compile_string(),
125
+ ("SELECT price < $1", [10]),
126
+ )
127
+
128
+ def test_less_equal_than(self):
129
+ query = QueryString("SELECT price") <= 10
130
+ self.assertIsInstance(query, QueryString)
131
+ self.assertEqual(
132
+ query.compile_string(),
133
+ ("SELECT price <= $1", [10]),
134
+ )
135
+
136
+ def test_equals(self):
137
+ query = QueryString("SELECT price") == 10
138
+ self.assertIsInstance(query, QueryString)
139
+ self.assertEqual(
140
+ query.compile_string(),
141
+ ("SELECT price = $1", [10]),
142
+ )
143
+
144
+ def test_not_equals(self):
145
+ query = QueryString("SELECT price") != 10
146
+ self.assertIsInstance(query, QueryString)
147
+ self.assertEqual(
148
+ query.compile_string(),
149
+ ("SELECT price != $1", [10]),
150
+ )
151
+
152
+ def test_is_in(self):
153
+ query = QueryString("SELECT price").is_in([10, 20, 30])
154
+ self.assertIsInstance(query, QueryString)
155
+ self.assertEqual(
156
+ query.compile_string(),
157
+ ("SELECT price IN $1", [[10, 20, 30]]),
158
+ )
159
+
160
+ def test_not_in(self):
161
+ query = QueryString("SELECT price").not_in([10, 20, 30])
162
+ self.assertIsInstance(query, QueryString)
163
+ self.assertEqual(
164
+ query.compile_string(),
165
+ ("SELECT price NOT IN $1", [[10, 20, 30]]),
166
+ )
@@ -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