piccolo 1.5.2__py3-none-any.whl → 1.7.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/engine/sqlite.py CHANGED
@@ -9,6 +9,7 @@ import typing as t
9
9
  import uuid
10
10
  from dataclasses import dataclass
11
11
  from decimal import Decimal
12
+ from functools import partial, wraps
12
13
 
13
14
  from piccolo.engine.base import Batch, Engine, validate_savepoint_name
14
15
  from piccolo.engine.exceptions import TransactionError
@@ -35,14 +36,14 @@ if t.TYPE_CHECKING: # pragma: no cover
35
36
  # In
36
37
 
37
38
 
38
- def convert_numeric_in(value):
39
+ def convert_numeric_in(value: Decimal) -> float:
39
40
  """
40
41
  Convert any Decimal values into floats.
41
42
  """
42
43
  return float(value)
43
44
 
44
45
 
45
- def convert_uuid_in(value) -> str:
46
+ def convert_uuid_in(value: uuid.UUID) -> str:
46
47
  """
47
48
  Converts the UUID value being passed into sqlite.
48
49
  """
@@ -56,7 +57,7 @@ def convert_time_in(value: datetime.time) -> str:
56
57
  return value.isoformat()
57
58
 
58
59
 
59
- def convert_date_in(value: datetime.date):
60
+ def convert_date_in(value: datetime.date) -> str:
60
61
  """
61
62
  Converts the date value being passed into sqlite.
62
63
  """
@@ -74,122 +75,235 @@ def convert_datetime_in(value: datetime.datetime) -> str:
74
75
  return str(value)
75
76
 
76
77
 
77
- def convert_timedelta_in(value: datetime.timedelta):
78
+ def convert_timedelta_in(value: datetime.timedelta) -> float:
78
79
  """
79
80
  Converts the timedelta value being passed into sqlite.
80
81
  """
81
82
  return value.total_seconds()
82
83
 
83
84
 
84
- def convert_array_in(value: list):
85
+ def convert_array_in(value: list) -> str:
85
86
  """
86
- Converts a list value into a string.
87
+ Converts a list value into a string (it handles nested lists, and type like
88
+ dateime/ time / date which aren't usually JSON serialisable.).
89
+
87
90
  """
88
- if value and type(value[0]) not in [str, int, float, list]:
89
- raise ValueError("Can only serialise str, int, float, and list.")
90
91
 
91
- return dump_json(value)
92
+ def serialise(data: list):
93
+ output = []
94
+
95
+ for item in data:
96
+ if isinstance(item, list):
97
+ output.append(serialise(item))
98
+ elif isinstance(
99
+ item, (datetime.datetime, datetime.time, datetime.date)
100
+ ):
101
+ if adapter := ADAPTERS.get(type(item)):
102
+ output.append(adapter(item))
103
+ else:
104
+ raise ValueError("The adapter wasn't found.")
105
+ elif item is None or isinstance(item, (str, int, float, list)):
106
+ # We can safely JSON serialise these.
107
+ output.append(item)
108
+ else:
109
+ raise ValueError("We can't currently serialise this value.")
110
+
111
+ return output
112
+
113
+ return dump_json(serialise(value))
114
+
115
+
116
+ ###############################################################################
117
+
118
+ # Register adapters
119
+
120
+ ADAPTERS: t.Dict[t.Type, t.Callable[[t.Any], t.Any]] = {
121
+ Decimal: convert_numeric_in,
122
+ uuid.UUID: convert_uuid_in,
123
+ datetime.time: convert_time_in,
124
+ datetime.date: convert_date_in,
125
+ datetime.datetime: convert_datetime_in,
126
+ datetime.timedelta: convert_timedelta_in,
127
+ list: convert_array_in,
128
+ }
92
129
 
130
+ for value_type, adapter in ADAPTERS.items():
131
+ sqlite3.register_adapter(value_type, adapter)
132
+
133
+ ###############################################################################
93
134
 
94
135
  # Out
95
136
 
96
137
 
97
- def convert_numeric_out(value: bytes) -> Decimal:
138
+ def decode_to_string(converter: t.Callable[[str], t.Any]):
139
+ """
140
+ This means we can use our converters with string and bytes. They are
141
+ passed bytes when used directly via SQLite, and are passed strings when
142
+ used by the array converters.
143
+ """
144
+
145
+ @wraps(converter)
146
+ def wrapper(value: t.Union[str, bytes]) -> t.Any:
147
+ if isinstance(value, bytes):
148
+ return converter(value.decode("utf8"))
149
+ elif isinstance(value, str):
150
+ return converter(value)
151
+ else:
152
+ raise ValueError("Unsupported type")
153
+
154
+ return wrapper
155
+
156
+
157
+ @decode_to_string
158
+ def convert_numeric_out(value: str) -> Decimal:
98
159
  """
99
160
  Convert float values into Decimals.
100
161
  """
101
- return Decimal(value.decode("ascii"))
162
+ return Decimal(value)
102
163
 
103
164
 
104
- def convert_int_out(value: bytes) -> int:
165
+ @decode_to_string
166
+ def convert_int_out(value: str) -> int:
105
167
  """
106
168
  Make sure Integer values are actually of type int.
107
169
  """
108
170
  return int(float(value))
109
171
 
110
172
 
111
- def convert_uuid_out(value: bytes) -> uuid.UUID:
173
+ @decode_to_string
174
+ def convert_uuid_out(value: str) -> uuid.UUID:
112
175
  """
113
176
  If the value is a uuid, convert it to a UUID instance.
114
177
  """
115
- return uuid.UUID(value.decode("utf8"))
178
+ return uuid.UUID(value)
116
179
 
117
180
 
118
- def convert_date_out(value: bytes) -> datetime.date:
119
- return datetime.date.fromisoformat(value.decode("utf8"))
181
+ @decode_to_string
182
+ def convert_date_out(value: str) -> datetime.date:
183
+ return datetime.date.fromisoformat(value)
120
184
 
121
185
 
122
- def convert_time_out(value: bytes) -> datetime.time:
186
+ @decode_to_string
187
+ def convert_time_out(value: str) -> datetime.time:
123
188
  """
124
189
  If the value is a time, convert it to a UUID instance.
125
190
  """
126
- return datetime.time.fromisoformat(value.decode("utf8"))
191
+ return datetime.time.fromisoformat(value)
127
192
 
128
193
 
129
- def convert_seconds_out(value: bytes) -> datetime.timedelta:
194
+ @decode_to_string
195
+ def convert_seconds_out(value: str) -> datetime.timedelta:
130
196
  """
131
197
  If the value is from a seconds column, convert it to a timedelta instance.
132
198
  """
133
- return datetime.timedelta(seconds=float(value.decode("utf8")))
199
+ return datetime.timedelta(seconds=float(value))
134
200
 
135
201
 
136
- def convert_boolean_out(value: bytes) -> bool:
202
+ @decode_to_string
203
+ def convert_boolean_out(value: str) -> bool:
137
204
  """
138
205
  If the value is from a boolean column, convert it to a bool value.
139
206
  """
140
- _value = value.decode("utf8")
141
- return _value == "1"
207
+ return value == "1"
142
208
 
143
209
 
144
- def convert_timestamp_out(value: bytes) -> datetime.datetime:
210
+ @decode_to_string
211
+ def convert_timestamp_out(value: str) -> datetime.datetime:
145
212
  """
146
213
  If the value is from a timestamp column, convert it to a datetime value.
147
214
  """
148
- return datetime.datetime.fromisoformat(value.decode("utf8"))
215
+ return datetime.datetime.fromisoformat(value)
149
216
 
150
217
 
151
- def convert_timestamptz_out(value: bytes) -> datetime.datetime:
218
+ @decode_to_string
219
+ def convert_timestamptz_out(value: str) -> datetime.datetime:
152
220
  """
153
221
  If the value is from a timestamptz column, convert it to a datetime value,
154
222
  with a timezone of UTC.
155
223
  """
156
- _value = datetime.datetime.fromisoformat(value.decode("utf8"))
157
- _value = _value.replace(tzinfo=datetime.timezone.utc)
158
- return _value
224
+ return datetime.datetime.fromisoformat(value).replace(
225
+ tzinfo=datetime.timezone.utc
226
+ )
159
227
 
160
228
 
161
- def convert_array_out(value: bytes) -> t.List:
229
+ @decode_to_string
230
+ def convert_array_out(value: str) -> t.List:
162
231
  """
163
232
  If the value if from an array column, deserialise the string back into a
164
233
  list.
165
234
  """
166
- return load_json(value.decode("utf8"))
167
-
168
-
169
- def convert_M2M_out(value: bytes) -> t.List:
170
- _value = value.decode("utf8")
171
- return _value.split(",")
172
-
173
-
174
- sqlite3.register_converter("Numeric", convert_numeric_out)
175
- sqlite3.register_converter("Integer", convert_int_out)
176
- sqlite3.register_converter("UUID", convert_uuid_out)
177
- sqlite3.register_converter("Date", convert_date_out)
178
- sqlite3.register_converter("Time", convert_time_out)
179
- sqlite3.register_converter("Seconds", convert_seconds_out)
180
- sqlite3.register_converter("Boolean", convert_boolean_out)
181
- sqlite3.register_converter("Timestamp", convert_timestamp_out)
182
- sqlite3.register_converter("Timestamptz", convert_timestamptz_out)
183
- sqlite3.register_converter("Array", convert_array_out)
184
- sqlite3.register_converter("M2M", convert_M2M_out)
185
-
186
- sqlite3.register_adapter(Decimal, convert_numeric_in)
187
- sqlite3.register_adapter(uuid.UUID, convert_uuid_in)
188
- sqlite3.register_adapter(datetime.time, convert_time_in)
189
- sqlite3.register_adapter(datetime.date, convert_date_in)
190
- sqlite3.register_adapter(datetime.datetime, convert_datetime_in)
191
- sqlite3.register_adapter(datetime.timedelta, convert_timedelta_in)
192
- sqlite3.register_adapter(list, convert_array_in)
235
+ return load_json(value)
236
+
237
+
238
+ def convert_complex_array_out(value: bytes, converter: t.Callable):
239
+ """
240
+ This is used to handle arrays of things like timestamps, which we can't
241
+ just load from JSON without doing additional work to convert the elements
242
+ back into Python objects.
243
+ """
244
+ parsed = load_json(value.decode("utf8"))
245
+
246
+ def convert_list(list_value: t.List):
247
+ output = []
248
+
249
+ for value in list_value:
250
+ if isinstance(value, list):
251
+ # For nested arrays
252
+ output.append(convert_list(value))
253
+ elif isinstance(value, str):
254
+ output.append(converter(value))
255
+ else:
256
+ output.append(value)
257
+
258
+ return output
259
+
260
+ if isinstance(parsed, list):
261
+ return convert_list(parsed)
262
+ else:
263
+ return parsed
264
+
265
+
266
+ @decode_to_string
267
+ def convert_M2M_out(value: str) -> t.List:
268
+ return value.split(",")
269
+
270
+
271
+ ###############################################################################
272
+ # Register the basic converters
273
+
274
+ CONVERTERS = {
275
+ "NUMERIC": convert_numeric_out,
276
+ "INTEGER": convert_int_out,
277
+ "UUID": convert_uuid_out,
278
+ "DATE": convert_date_out,
279
+ "TIME": convert_time_out,
280
+ "SECONDS": convert_seconds_out,
281
+ "BOOLEAN": convert_boolean_out,
282
+ "TIMESTAMP": convert_timestamp_out,
283
+ "TIMESTAMPTZ": convert_timestamptz_out,
284
+ "M2M": convert_M2M_out,
285
+ }
286
+
287
+ for column_name, converter in CONVERTERS.items():
288
+ sqlite3.register_converter(column_name, converter)
289
+
290
+ ###############################################################################
291
+ # Register the array converters
292
+
293
+ # The ARRAY column type handles values which can be easily serialised to and
294
+ # from JSON.
295
+ sqlite3.register_converter("ARRAY", convert_array_out)
296
+
297
+ # We have special column types for arrays of timestamps etc, as simply loading
298
+ # the JSON isn't sufficient.
299
+ for column_name in ("TIMESTAMP", "TIMESTAMPTZ", "DATE", "TIME"):
300
+ sqlite3.register_converter(
301
+ f"ARRAY_{column_name}",
302
+ partial(
303
+ convert_complex_array_out,
304
+ converter=CONVERTERS[column_name],
305
+ ),
306
+ )
193
307
 
194
308
  ###############################################################################
195
309
 
piccolo/query/__init__.py CHANGED
@@ -1,9 +1,9 @@
1
1
  from piccolo.columns.combination import WhereRaw
2
2
 
3
3
  from .base import Query
4
+ from .functions.aggregate import Avg, Max, Min, Sum
4
5
  from .methods import (
5
6
  Alter,
6
- Avg,
7
7
  Count,
8
8
  Create,
9
9
  CreateIndex,
@@ -11,12 +11,9 @@ from .methods import (
11
11
  DropIndex,
12
12
  Exists,
13
13
  Insert,
14
- Max,
15
- Min,
16
14
  Objects,
17
15
  Raw,
18
16
  Select,
19
- Sum,
20
17
  TableExists,
21
18
  Update,
22
19
  )
@@ -0,0 +1,16 @@
1
+ from .aggregate import Avg, Count, Max, Min, Sum
2
+ from .string import Length, Lower, Ltrim, Reverse, Rtrim, Upper
3
+
4
+ __all__ = (
5
+ "Avg",
6
+ "Count",
7
+ "Length",
8
+ "Lower",
9
+ "Ltrim",
10
+ "Max",
11
+ "Min",
12
+ "Reverse",
13
+ "Rtrim",
14
+ "Sum",
15
+ "Upper",
16
+ )
@@ -0,0 +1,179 @@
1
+ import typing as t
2
+
3
+ from piccolo.columns.base import Column
4
+ from piccolo.querystring import QueryString
5
+
6
+ from .base import Function
7
+
8
+
9
+ class Avg(Function):
10
+ """
11
+ ``AVG()`` SQL function. Column type must be numeric to run the query.
12
+
13
+ .. code-block:: python
14
+
15
+ await Band.select(Avg(Band.popularity)).run()
16
+
17
+ # We can use an alias. These two are equivalent:
18
+
19
+ await Band.select(
20
+ Avg(Band.popularity, alias="popularity_avg")
21
+ ).run()
22
+
23
+ await Band.select(
24
+ Avg(Band.popularity).as_alias("popularity_avg")
25
+ ).run()
26
+
27
+ """
28
+
29
+ function_name = "AVG"
30
+
31
+
32
+ class Count(QueryString):
33
+ """
34
+ Used in ``Select`` queries, usually in conjunction with the ``group_by``
35
+ clause::
36
+
37
+ >>> await Band.select(
38
+ ... Band.manager.name.as_alias('manager_name'),
39
+ ... Count(alias='band_count')
40
+ ... ).group_by(Band.manager)
41
+ [{'manager_name': 'Guido', 'count': 1}, ...]
42
+
43
+ It can also be used without the ``group_by`` clause (though you may prefer
44
+ to the :meth:`Table.count <piccolo.table.Table.count>` method instead, as
45
+ it's more convenient)::
46
+
47
+ >>> await Band.select(Count())
48
+ [{'count': 3}]
49
+
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ column: t.Optional[Column] = None,
55
+ distinct: t.Optional[t.Sequence[Column]] = None,
56
+ alias: str = "count",
57
+ ):
58
+ """
59
+ :param column:
60
+ If specified, the count is for non-null values in that column.
61
+ :param distinct:
62
+ If specified, the count is for distinct values in those columns.
63
+ :param alias:
64
+ The name of the value in the response::
65
+
66
+ # These two are equivalent:
67
+
68
+ await Band.select(
69
+ Band.name, Count(alias="total")
70
+ ).group_by(Band.name)
71
+
72
+ await Band.select(
73
+ Band.name,
74
+ Count().as_alias("total")
75
+ ).group_by(Band.name)
76
+
77
+ """
78
+ if distinct and column:
79
+ raise ValueError("Only specify `column` or `distinct`")
80
+
81
+ if distinct:
82
+ engine_type = distinct[0]._meta.engine_type
83
+ if engine_type == "sqlite":
84
+ # SQLite doesn't allow us to specify multiple columns, so
85
+ # instead we concatenate the values.
86
+ column_names = " || ".join("{}" for _ in distinct)
87
+ else:
88
+ column_names = ", ".join("{}" for _ in distinct)
89
+
90
+ return super().__init__(
91
+ f"COUNT(DISTINCT({column_names}))", *distinct, alias=alias
92
+ )
93
+ else:
94
+ if column:
95
+ return super().__init__("COUNT({})", column, alias=alias)
96
+ else:
97
+ return super().__init__("COUNT(*)", alias=alias)
98
+
99
+
100
+ class Min(Function):
101
+ """
102
+ ``MIN()`` SQL function.
103
+
104
+ .. code-block:: python
105
+
106
+ await Band.select(Min(Band.popularity)).run()
107
+
108
+ # We can use an alias. These two are equivalent:
109
+
110
+ await Band.select(
111
+ Min(Band.popularity, alias="popularity_min")
112
+ ).run()
113
+
114
+ await Band.select(
115
+ Min(Band.popularity).as_alias("popularity_min")
116
+ ).run()
117
+
118
+ """
119
+
120
+ function_name = "MIN"
121
+
122
+
123
+ class Max(Function):
124
+ """
125
+ ``MAX()`` SQL function.
126
+
127
+ .. code-block:: python
128
+
129
+ await Band.select(
130
+ Max(Band.popularity)
131
+ ).run()
132
+
133
+ # We can use an alias. These two are equivalent:
134
+
135
+ await Band.select(
136
+ Max(Band.popularity, alias="popularity_max")
137
+ ).run()
138
+
139
+ await Band.select(
140
+ Max(Band.popularity).as_alias("popularity_max")
141
+ ).run()
142
+
143
+ """
144
+
145
+ function_name = "MAX"
146
+
147
+
148
+ class Sum(Function):
149
+ """
150
+ ``SUM()`` SQL function. Column type must be numeric to run the query.
151
+
152
+ .. code-block:: python
153
+
154
+ await Band.select(
155
+ Sum(Band.popularity)
156
+ ).run()
157
+
158
+ # We can use an alias. These two are equivalent:
159
+
160
+ await Band.select(
161
+ Sum(Band.popularity, alias="popularity_sum")
162
+ ).run()
163
+
164
+ await Band.select(
165
+ Sum(Band.popularity).as_alias("popularity_sum")
166
+ ).run()
167
+
168
+ """
169
+
170
+ function_name = "SUM"
171
+
172
+
173
+ __all__ = (
174
+ "Avg",
175
+ "Count",
176
+ "Min",
177
+ "Max",
178
+ "Sum",
179
+ )
@@ -0,0 +1,21 @@
1
+ import typing as t
2
+
3
+ from piccolo.columns.base import Column
4
+ from piccolo.querystring import QueryString
5
+
6
+
7
+ class Function(QueryString):
8
+ function_name: str
9
+
10
+ def __init__(
11
+ self,
12
+ identifier: t.Union[Column, QueryString, str],
13
+ alias: t.Optional[str] = None,
14
+ ):
15
+ alias = alias or self.__class__.__name__.lower()
16
+
17
+ super().__init__(
18
+ f"{self.function_name}({{}})",
19
+ identifier,
20
+ alias=alias,
21
+ )
@@ -0,0 +1,73 @@
1
+ """
2
+ These functions mirror their counterparts in the Postgresql docs:
3
+
4
+ https://www.postgresql.org/docs/current/functions-string.html
5
+
6
+ """
7
+
8
+ from .base import Function
9
+
10
+
11
+ class Length(Function):
12
+ """
13
+ Returns the number of characters in the string.
14
+ """
15
+
16
+ function_name = "LENGTH"
17
+
18
+
19
+ class Lower(Function):
20
+ """
21
+ Converts the string to all lower case, according to the rules of the
22
+ database's locale.
23
+ """
24
+
25
+ function_name = "LOWER"
26
+
27
+
28
+ class Ltrim(Function):
29
+ """
30
+ Removes the longest string containing only characters in characters (a
31
+ space by default) from the start of string.
32
+ """
33
+
34
+ function_name = "LTRIM"
35
+
36
+
37
+ class Reverse(Function):
38
+ """
39
+ Return reversed string.
40
+
41
+ Not supported in SQLite.
42
+
43
+ """
44
+
45
+ function_name = "REVERSE"
46
+
47
+
48
+ class Rtrim(Function):
49
+ """
50
+ Removes the longest string containing only characters in characters (a
51
+ space by default) from the end of string.
52
+ """
53
+
54
+ function_name = "RTRIM"
55
+
56
+
57
+ class Upper(Function):
58
+ """
59
+ Converts the string to all upper case, according to the rules of the
60
+ database's locale.
61
+ """
62
+
63
+ function_name = "UPPER"
64
+
65
+
66
+ __all__ = (
67
+ "Length",
68
+ "Lower",
69
+ "Ltrim",
70
+ "Reverse",
71
+ "Rtrim",
72
+ "Upper",
73
+ )
@@ -9,6 +9,23 @@ from .insert import Insert
9
9
  from .objects import Objects
10
10
  from .raw import Raw
11
11
  from .refresh import Refresh
12
- from .select import Avg, Max, Min, Select, Sum
12
+ from .select import Select
13
13
  from .table_exists import TableExists
14
14
  from .update import Update
15
+
16
+ __all__ = (
17
+ "Alter",
18
+ "Count",
19
+ "Create",
20
+ "CreateIndex",
21
+ "Delete",
22
+ "DropIndex",
23
+ "Exists",
24
+ "Insert",
25
+ "Objects",
26
+ "Raw",
27
+ "Refresh",
28
+ "Select",
29
+ "TableExists",
30
+ "Update",
31
+ )
@@ -4,7 +4,7 @@ import typing as t
4
4
 
5
5
  from piccolo.custom_types import Combinable
6
6
  from piccolo.query.base import Query
7
- from piccolo.query.methods.select import Count as SelectCount
7
+ from piccolo.query.functions.aggregate import Count as CountFunction
8
8
  from piccolo.query.mixins import WhereDelegate
9
9
  from piccolo.querystring import QueryString
10
10
 
@@ -32,7 +32,7 @@ class Count(Query):
32
32
  ###########################################################################
33
33
  # Clauses
34
34
 
35
- def where(self: Self, *where: Combinable) -> Self:
35
+ def where(self: Self, *where: t.Union[Combinable, QueryString]) -> Self:
36
36
  self.where_delegate.where(*where)
37
37
  return self
38
38
 
@@ -50,7 +50,7 @@ class Count(Query):
50
50
  table: t.Type[Table] = self.table
51
51
 
52
52
  query = table.select(
53
- SelectCount(column=self.column, distinct=self._distinct)
53
+ CountFunction(column=self.column, distinct=self._distinct)
54
54
  )
55
55
 
56
56
  query.where_delegate._where = self.where_delegate._where