piccolo 1.21.0__py3-none-any.whl → 1.22.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 CHANGED
@@ -1 +1 @@
1
- __VERSION__ = "1.21.0"
1
+ __VERSION__ = "1.22.0"
@@ -233,6 +233,11 @@ def populate():
233
233
  RecordingStudio.facilities: {
234
234
  "restaurant": True,
235
235
  "mixing_desk": True,
236
+ "instruments": {"electric_guitars": 10, "drum_kits": 2},
237
+ "technicians": [
238
+ {"name": "Alice Jones"},
239
+ {"name": "Bob Williams"},
240
+ ],
236
241
  },
237
242
  }
238
243
  )
@@ -244,6 +249,10 @@ def populate():
244
249
  RecordingStudio.facilities: {
245
250
  "restaurant": False,
246
251
  "mixing_desk": True,
252
+ "instruments": {"electric_guitars": 6, "drum_kits": 3},
253
+ "technicians": [
254
+ {"name": "Frank Smith"},
255
+ ],
247
256
  },
248
257
  },
249
258
  )
@@ -70,6 +70,10 @@ from piccolo.utils.warnings import colored_warning
70
70
 
71
71
  if t.TYPE_CHECKING: # pragma: no cover
72
72
  from piccolo.columns.base import ColumnMeta
73
+ from piccolo.query.operators.json import (
74
+ GetChildElement,
75
+ GetElementFromPath,
76
+ )
73
77
  from piccolo.table import Table
74
78
 
75
79
 
@@ -2319,6 +2323,76 @@ class JSON(Column):
2319
2323
  else:
2320
2324
  return "JSON"
2321
2325
 
2326
+ ###########################################################################
2327
+
2328
+ def arrow(self, key: t.Union[str, int, QueryString]) -> GetChildElement:
2329
+ """
2330
+ Allows a child element of the JSON structure to be returned - for
2331
+ example::
2332
+
2333
+ >>> await RecordingStudio.select(
2334
+ ... RecordingStudio.facilities.arrow("restaurant")
2335
+ ... )
2336
+
2337
+ """
2338
+ from piccolo.query.operators.json import GetChildElement
2339
+
2340
+ alias = self._alias or self._meta.get_default_alias()
2341
+ return GetChildElement(identifier=self, key=key, alias=alias)
2342
+
2343
+ def __getitem__(
2344
+ self, value: t.Union[str, int, QueryString]
2345
+ ) -> GetChildElement:
2346
+ """
2347
+ A shortcut for the ``arrow`` method, used for retrieving a child
2348
+ element.
2349
+
2350
+ For example:
2351
+
2352
+ .. code-block:: python
2353
+
2354
+ >>> await RecordingStudio.select(
2355
+ ... RecordingStudio.facilities["restaurant"]
2356
+ ... )
2357
+
2358
+ """
2359
+ return self.arrow(key=value)
2360
+
2361
+ def from_path(
2362
+ self,
2363
+ path: t.List[t.Union[str, int]],
2364
+ ) -> GetElementFromPath:
2365
+ """
2366
+ Allows an element of the JSON structure to be returned, which can be
2367
+ arbitrarily deep. For example::
2368
+
2369
+ >>> await RecordingStudio.select(
2370
+ ... RecordingStudio.facilities.from_path([
2371
+ ... "technician",
2372
+ ... 0,
2373
+ ... "first_name"
2374
+ ... ])
2375
+ ... )
2376
+
2377
+ It's the same as calling ``arrow`` multiple times, but is more
2378
+ efficient / convenient if extracting highly nested data::
2379
+
2380
+ >>> await RecordingStudio.select(
2381
+ ... RecordingStudio.facilities.arrow(
2382
+ ... "technician"
2383
+ ... ).arrow(
2384
+ ... 0
2385
+ ... ).arrow(
2386
+ ... "first_name"
2387
+ ... )
2388
+ ... )
2389
+
2390
+ """
2391
+ from piccolo.query.operators.json import GetElementFromPath
2392
+
2393
+ alias = self._alias or self._meta.get_default_alias()
2394
+ return GetElementFromPath(identifier=self, path=path, alias=alias)
2395
+
2322
2396
  ###########################################################################
2323
2397
  # Descriptors
2324
2398
 
@@ -2337,10 +2411,10 @@ class JSON(Column):
2337
2411
 
2338
2412
  class JSONB(JSON):
2339
2413
  """
2340
- Used for storing JSON strings - Postgres only. The data is stored in a
2341
- binary format, and can be queried. Insertion can be slower (as it needs to
2342
- be converted to the binary format). The benefits of JSONB generally
2343
- outweigh the downsides.
2414
+ Used for storing JSON strings - Postgres / CochroachDB only. The data is
2415
+ stored in a binary format, and can be queried more efficiently. Insertion
2416
+ can be slower (as it needs to be converted to the binary format). The
2417
+ benefits of JSONB generally outweigh the downsides.
2344
2418
 
2345
2419
  :param default:
2346
2420
  Either a JSON string can be provided, or a Python ``dict`` or ``list``
@@ -2352,41 +2426,6 @@ class JSONB(JSON):
2352
2426
  def column_type(self):
2353
2427
  return "JSONB" # Must be defined, we override column_type() in JSON()
2354
2428
 
2355
- def arrow(self, key: str) -> JSONB:
2356
- """
2357
- Allows part of the JSON structure to be returned - for example,
2358
- for {"a": 1}, and a key value of "a", then 1 will be returned.
2359
- """
2360
- instance = t.cast(JSONB, self.copy())
2361
- instance.json_operator = f"-> '{key}'"
2362
- return instance
2363
-
2364
- def get_select_string(
2365
- self, engine_type: str, with_alias: bool = True
2366
- ) -> QueryString:
2367
- select_string = self._meta.get_full_name(with_alias=False)
2368
-
2369
- if self.json_operator is not None:
2370
- select_string += f" {self.json_operator}"
2371
-
2372
- if with_alias:
2373
- alias = self._alias or self._meta.get_default_alias()
2374
- select_string += f' AS "{alias}"'
2375
-
2376
- return QueryString(select_string)
2377
-
2378
- def eq(self, value) -> Where:
2379
- """
2380
- See ``Boolean.eq`` for more details.
2381
- """
2382
- return self.__eq__(value)
2383
-
2384
- def ne(self, value) -> Where:
2385
- """
2386
- See ``Boolean.ne`` for more details.
2387
- """
2388
- return self.__ne__(value)
2389
-
2390
2429
  ###########################################################################
2391
2430
  # Descriptors
2392
2431
 
piccolo/query/base.py CHANGED
@@ -6,6 +6,7 @@ from time import time
6
6
  from piccolo.columns.column_types import JSON, JSONB
7
7
  from piccolo.custom_types import QueryResponseType, TableInstance
8
8
  from piccolo.query.mixins import ColumnsDelegate
9
+ from piccolo.query.operators.json import JSONQueryString
9
10
  from piccolo.querystring import QueryString
10
11
  from piccolo.utils.encoding import load_json
11
12
  from piccolo.utils.objects import make_nested_object
@@ -65,16 +66,20 @@ class Query(t.Generic[TableInstance, QueryResponseType]):
65
66
  self, "columns_delegate", None
66
67
  )
67
68
 
69
+ json_column_names: t.List[str] = []
70
+
68
71
  if columns_delegate is not None:
69
- json_columns = [
70
- i
71
- for i in columns_delegate.selected_columns
72
- if isinstance(i, (JSON, JSONB))
73
- ]
72
+ json_columns: t.List[t.Union[JSON, JSONB]] = []
73
+
74
+ for column in columns_delegate.selected_columns:
75
+ if isinstance(column, (JSON, JSONB)):
76
+ json_columns.append(column)
77
+ elif isinstance(column, JSONQueryString):
78
+ if alias := column._alias:
79
+ json_column_names.append(alias)
74
80
  else:
75
81
  json_columns = self.table._meta.json_columns
76
82
 
77
- json_column_names = []
78
83
  for column in json_columns:
79
84
  if column._alias is not None:
80
85
  json_column_names.append(column._alias)
File without changes
@@ -0,0 +1,111 @@
1
+ from __future__ import annotations
2
+
3
+ import typing as t
4
+
5
+ from piccolo.querystring import QueryString
6
+ from piccolo.utils.encoding import dump_json
7
+
8
+ if t.TYPE_CHECKING:
9
+ from piccolo.columns.column_types import JSON
10
+
11
+
12
+ class JSONQueryString(QueryString):
13
+
14
+ def clean_value(self, value: t.Any):
15
+ if not isinstance(value, (str, QueryString)):
16
+ value = dump_json(value)
17
+ return value
18
+
19
+ def __eq__(self, value) -> QueryString: # type: ignore[override]
20
+ value = self.clean_value(value)
21
+ return QueryString("{} = {}", self, value)
22
+
23
+ def __ne__(self, value) -> QueryString: # type: ignore[override]
24
+ value = self.clean_value(value)
25
+ return QueryString("{} != {}", self, value)
26
+
27
+ def eq(self, value) -> QueryString:
28
+ return self.__eq__(value)
29
+
30
+ def ne(self, value) -> QueryString:
31
+ return self.__ne__(value)
32
+
33
+
34
+ class GetChildElement(JSONQueryString):
35
+ """
36
+ Allows you to get a child element from a JSON object.
37
+
38
+ You can access this via the ``arrow`` function on ``JSON`` and ``JSONB``
39
+ columns.
40
+
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ identifier: t.Union[JSON, QueryString],
46
+ key: t.Union[str, int, QueryString],
47
+ alias: t.Optional[str] = None,
48
+ ):
49
+ if isinstance(key, int):
50
+ # asyncpg only accepts integer keys if we explicitly mark it as an
51
+ # int.
52
+ key = QueryString("{}::int", key)
53
+
54
+ super().__init__("{} -> {}", identifier, key, alias=alias)
55
+
56
+ def arrow(self, key: t.Union[str, int, QueryString]) -> GetChildElement:
57
+ """
58
+ This allows you to drill multiple levels deep into a JSON object if
59
+ needed.
60
+
61
+ For example::
62
+
63
+ >>> await RecordingStudio.select(
64
+ ... RecordingStudio.name,
65
+ ... RecordingStudio.facilities.arrow(
66
+ ... "instruments"
67
+ ... ).arrow(
68
+ ... "drum_kits"
69
+ ... ).as_alias("drum_kits")
70
+ ... ).output(load_json=True)
71
+ [
72
+ {'name': 'Abbey Road', 'drum_kits': 2},
73
+ {'name': 'Electric Lady', 'drum_kits': 3}
74
+ ]
75
+
76
+ """
77
+ return GetChildElement(identifier=self, key=key, alias=self._alias)
78
+
79
+ def __getitem__(
80
+ self, value: t.Union[str, int, QueryString]
81
+ ) -> GetChildElement:
82
+ return GetChildElement(identifier=self, key=value, alias=self._alias)
83
+
84
+
85
+ class GetElementFromPath(JSONQueryString):
86
+ """
87
+ Allows you to retrieve an element from a JSON object by specifying a path.
88
+ It can be several levels deep.
89
+
90
+ You can access this via the ``from_path`` function on ``JSON`` and
91
+ ``JSONB`` columns.
92
+
93
+ """
94
+
95
+ def __init__(
96
+ self,
97
+ identifier: t.Union[JSON, QueryString],
98
+ path: t.List[t.Union[str, int]],
99
+ alias: t.Optional[str] = None,
100
+ ):
101
+ """
102
+ :param path:
103
+ For example: ``["technician", 0, "name"]``.
104
+
105
+ """
106
+ super().__init__(
107
+ "{} #> {}",
108
+ identifier,
109
+ [str(i) if isinstance(i, int) else i for i in path],
110
+ alias=alias,
111
+ )
piccolo/querystring.py CHANGED
@@ -259,10 +259,22 @@ class QueryString(Selectable):
259
259
  # Basic logic
260
260
 
261
261
  def __eq__(self, value) -> QueryString: # type: ignore[override]
262
- return QueryString("{} = {}", self, value)
262
+ if value is None:
263
+ return QueryString("{} IS NULL", self)
264
+ else:
265
+ return QueryString("{} = {}", self, value)
263
266
 
264
267
  def __ne__(self, value) -> QueryString: # type: ignore[override]
265
- return QueryString("{} != {}", self, value)
268
+ if value is None:
269
+ return QueryString("{} IS NOT NULL", self, value)
270
+ else:
271
+ return QueryString("{} != {}", self, value)
272
+
273
+ def eq(self, value) -> QueryString:
274
+ return self.__eq__(value)
275
+
276
+ def ne(self, value) -> QueryString:
277
+ return self.__ne__(value)
266
278
 
267
279
  def __add__(self, value) -> QueryString:
268
280
  return QueryString("{} + {}", self, value)
@@ -8,6 +8,8 @@ import typing as t
8
8
  from dataclasses import dataclass
9
9
 
10
10
  from piccolo.apps.schema.commands.generate import get_output_schema
11
+ from piccolo.engine import engine_finder
12
+ from piccolo.engine.base import Engine
11
13
  from piccolo.table import Table
12
14
 
13
15
 
@@ -78,9 +80,16 @@ class TableStorage(metaclass=Singleton):
78
80
  works with Postgres.
79
81
  """
80
82
 
81
- def __init__(self):
83
+ def __init__(self, engine: t.Optional[Engine] = None):
84
+ """
85
+ :param engine:
86
+ Which engine to use to make the database queries. If not specified,
87
+ we try importing an engine from ``piccolo_conf.py``.
88
+
89
+ """
90
+ self.engine = engine or engine_finder()
82
91
  self.tables = ImmutableDict()
83
- self._schema_tables = {}
92
+ self._schema_tables: t.Dict[str, t.List[str]] = {}
84
93
 
85
94
  async def reflect(
86
95
  self,
@@ -120,10 +129,13 @@ class TableStorage(metaclass=Singleton):
120
129
  exclude_list = self._to_list(exclude)
121
130
 
122
131
  if keep_existing:
123
- exclude += self._schema_tables.get(schema_name, [])
132
+ exclude_list += self._schema_tables.get(schema_name, [])
124
133
 
125
134
  output_schema = await get_output_schema(
126
- schema_name=schema_name, include=include_list, exclude=exclude_list
135
+ schema_name=schema_name,
136
+ include=include_list,
137
+ exclude=exclude_list,
138
+ engine=self.engine,
127
139
  )
128
140
  add_tables = [
129
141
  self._add_table(schema_name=schema_name, table=table)
@@ -177,7 +189,7 @@ class TableStorage(metaclass=Singleton):
177
189
 
178
190
  def _add_to_schema_tables(self, schema_name: str, table_name: str) -> None:
179
191
  """
180
- We keep record of schemas and their tables for easy use. This method
192
+ We keep a record of schemas and their tables for easy use. This method
181
193
  adds a table to its schema.
182
194
 
183
195
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: piccolo
3
- Version: 1.21.0
3
+ Version: 1.22.0
4
4
  Summary: A fast, user friendly ORM and query builder which supports asyncio.
5
5
  Home-page: https://github.com/piccolo-orm/piccolo
6
6
  Author: Daniel Townsend
@@ -9,19 +9,20 @@ License: MIT
9
9
  Project-URL: Documentation, https://piccolo-orm.readthedocs.io/en/latest/index.html
10
10
  Project-URL: Source, https://github.com/piccolo-orm/piccolo
11
11
  Project-URL: Tracker, https://github.com/piccolo-orm/piccolo/issues
12
+ Platform: UNKNOWN
12
13
  Classifier: License :: OSI Approved :: MIT License
13
14
  Classifier: Programming Language :: Python
14
15
  Classifier: Programming Language :: Python :: 3
15
- Classifier: Programming Language :: Python :: 3.8
16
16
  Classifier: Programming Language :: Python :: 3.9
17
17
  Classifier: Programming Language :: Python :: 3.10
18
18
  Classifier: Programming Language :: Python :: 3.11
19
19
  Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
20
21
  Classifier: Programming Language :: Python :: Implementation :: CPython
21
22
  Classifier: Framework :: AsyncIO
22
23
  Classifier: Typing :: Typed
23
24
  Classifier: Topic :: Database
24
- Requires-Python: >=3.8.0
25
+ Requires-Python: >=3.9.0
25
26
  Description-Content-Type: text/markdown
26
27
  License-File: LICENSE
27
28
  Requires-Dist: black
@@ -34,7 +35,7 @@ Requires-Dist: pydantic[email] (==2.*)
34
35
  Provides-Extra: all
35
36
  Requires-Dist: orjson (>=3.5.1) ; extra == 'all'
36
37
  Requires-Dist: ipython ; extra == 'all'
37
- Requires-Dist: asyncpg (>=0.21.0) ; extra == 'all'
38
+ Requires-Dist: asyncpg (>=0.30.0) ; extra == 'all'
38
39
  Requires-Dist: aiosqlite (>=0.16.0) ; extra == 'all'
39
40
  Requires-Dist: uvloop (>=0.12.0) ; (sys_platform != "win32") and extra == 'all'
40
41
  Provides-Extra: orjson
@@ -42,7 +43,7 @@ Requires-Dist: orjson (>=3.5.1) ; extra == 'orjson'
42
43
  Provides-Extra: playground
43
44
  Requires-Dist: ipython ; extra == 'playground'
44
45
  Provides-Extra: postgres
45
- Requires-Dist: asyncpg (>=0.21.0) ; extra == 'postgres'
46
+ Requires-Dist: asyncpg (>=0.30.0) ; extra == 'postgres'
46
47
  Provides-Extra: sqlite
47
48
  Requires-Dist: aiosqlite (>=0.16.0) ; extra == 'sqlite'
48
49
  Provides-Extra: uvloop
@@ -155,3 +156,5 @@ We have a handy page which shows the equivalent of [common Django queries in Pic
155
156
  Our documentation is on [Read the docs](https://piccolo-orm.readthedocs.io/en/latest/piccolo/getting_started/index.html).
156
157
 
157
158
  We also have some great [tutorial videos on YouTube](https://www.youtube.com/channel/UCE7x5nm1Iy9KDfXPNrNQ5lA).
159
+
160
+
@@ -1,11 +1,11 @@
1
- piccolo/__init__.py,sha256=lfg4osIcO7gnlgKb0zY1KT8t1Ngd2pATD-6YnBkIDcU,23
1
+ piccolo/__init__.py,sha256=aVoeQDKbkLtZX6_B3ZNdv8mqfI_4pHUT1Go9Ek88OVI,23
2
2
  piccolo/custom_types.py,sha256=7HMQAze-5mieNLfbQ5QgbRQgR2abR7ol0qehv2SqROY,604
3
3
  piccolo/main.py,sha256=1VsFV67FWTUikPTysp64Fmgd9QBVa_9wcwKfwj2UCEA,5117
4
4
  piccolo/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- piccolo/querystring.py,sha256=yZdURtiVSlxkkEVJoFKAmL2OfNxrUr8xfsfuBBB7IuY,9662
5
+ piccolo/querystring.py,sha256=kb7RYTvQZEyPsC4GH8vR2b_w35wnM-ita242S0_eyvQ,10013
6
6
  piccolo/schema.py,sha256=qNNy4tG_HqnXR9t3hHMgYXtGxHabwQAhUpc6RKLJ_gE,7960
7
7
  piccolo/table.py,sha256=UvEbagMYRkTbyFHTUwUshZlL_dC4UKDP7vUOwF8OXmg,50593
8
- piccolo/table_reflection.py,sha256=jrN1nHerDJ4tU09GtNN3hz7ap-7rXnSUjljFO6LB2H0,7094
8
+ piccolo/table_reflection.py,sha256=02baOSLX6f2LEo0kruFZYF_nPPTbIvaCTH_KPGe0DKw,7540
9
9
  piccolo/apps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  piccolo/apps/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  piccolo/apps/app/piccolo_app.py,sha256=8z2ITpxQQ-McxSYwQ5H_vyEnRXbY6cyAh2JSqhiylYk,340
@@ -77,7 +77,7 @@ piccolo/apps/migrations/commands/templates/migration.py.jinja,sha256=wMC8RTIcQj3
77
77
  piccolo/apps/playground/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
78
78
  piccolo/apps/playground/piccolo_app.py,sha256=zs6nGxt-lgUF8nEwI0uDTNZDKQqjZaNDH8le5RqrMNE,222
79
79
  piccolo/apps/playground/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
80
- piccolo/apps/playground/commands/run.py,sha256=S8osLV4s_mWdvvVYGn-49wel-d1SFRB17fO3ZtMcv8Y,8629
80
+ piccolo/apps/playground/commands/run.py,sha256=lpCbVmXabWBlsgwE-8cK4woIK_78E3nq9CTfA5hEJFI,9014
81
81
  piccolo/apps/project/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
82
82
  piccolo/apps/project/piccolo_app.py,sha256=mT3O0m3QcCfS0oOr3jt0QZ9TX6gUavGPjJeNn2C_fdM,220
83
83
  piccolo/apps/project/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -117,7 +117,7 @@ piccolo/apps/user/piccolo_migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeu
117
117
  piccolo/columns/__init__.py,sha256=OYhO_n9anMiU9nL-K6ATq9FhAtm8RyMpqYQ7fTVbhxI,1120
118
118
  piccolo/columns/base.py,sha256=_bg9yMWjMwE76Z7RDqi9iYSmtRuFx5bkx9uYJsFHKjQ,32487
119
119
  piccolo/columns/choices.py,sha256=-HNQuk9vMmVZIPZ5PMeXGTfr23o4nzKPSAkvcG1k0y8,723
120
- piccolo/columns/column_types.py,sha256=X_ZsA0C4WBNVonV2OsizRdt1osMshgtQ3Ob6JN-amfg,83485
120
+ piccolo/columns/column_types.py,sha256=XVI6qA_qsP1BD6bqVqQdj7a8dldoYz4VgOPI-wW696I,84747
121
121
  piccolo/columns/combination.py,sha256=vMXC2dfY7pvnCFhsT71XFVyb4gdQzfRsCMaiduu04Ss,6900
122
122
  piccolo/columns/indexes.py,sha256=NfNok3v_791jgDlN28KmhP9ZCjl6031BXmjxV3ovXJk,372
123
123
  piccolo/columns/m2m.py,sha256=QMeSOnm4DT2cG9U5jC6sOZ6z9DxCWwDyZMSqk0wR2q4,14682
@@ -146,7 +146,7 @@ piccolo/engine/finder.py,sha256=GjzBNtzRzH79fjtRn7OI3nZiOXE8JfoQWAvHVPrPNx4,507
146
146
  piccolo/engine/postgres.py,sha256=DekL3KafCdzSAEQ6_EgOiUB1ERXh2xpePYwI9QvmN-c,18955
147
147
  piccolo/engine/sqlite.py,sha256=KwJc3UttBP_8qSREbLJshqEfROF17ENf0Ju9BwI5_so,25236
148
148
  piccolo/query/__init__.py,sha256=bcsMV4813rMRAIqGv4DxI4eyO4FmpXkDv9dfTk5pt3A,699
149
- piccolo/query/base.py,sha256=iI9Fv3oOw7T4ZWZvRKRwdtClvQtSaAepslH24vwxZVA,14616
149
+ piccolo/query/base.py,sha256=sO5VyicbWjgYaQukr6jqUqUUrOctL6QJ1MjcsgDKHXM,14912
150
150
  piccolo/query/mixins.py,sha256=X9HEYnj6uOjgTkGr4vgqTwN_dokJPzVagwbFx385atQ,24468
151
151
  piccolo/query/proxy.py,sha256=Yq4jNc7IWJvdeO3u7_7iPyRy2WhVj8KsIUcIYHBIi9Q,1839
152
152
  piccolo/query/functions/__init__.py,sha256=pZkzOIh7Sg9HPNOeegOwAS46Oxt31ATlSVmwn-lxCbc,605
@@ -172,6 +172,8 @@ piccolo/query/methods/refresh.py,sha256=wg1zghKfwz-VmqK4uWa4GNMiDtK-skTqow591Hb3
172
172
  piccolo/query/methods/select.py,sha256=41OW-DIE_wr5VdxSusMKNT2aUhzQsCwK2Qh1XqgXHg0,22424
173
173
  piccolo/query/methods/table_exists.py,sha256=0yb3n6Jd2ovSBWlZ-gl00K4E7Jnbj7J8qAAX5d7hvNk,1259
174
174
  piccolo/query/methods/update.py,sha256=LfWqIXEl1aecc0rkVssTFmwyD6wXGhlKcTrUVhtlEsw,3705
175
+ piccolo/query/operators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
176
+ piccolo/query/operators/json.py,sha256=hdo1M6N9qTHJTJ0sRV9Bwt_iQZTgs4VdCKOPH1sXe-k,3168
175
177
  piccolo/testing/__init__.py,sha256=pRFSqRInfx95AakOq54atmvqoB-ue073q2aR8u8zR40,83
176
178
  piccolo/testing/model_builder.py,sha256=lVEiEe71xrH8SSjzFc2l0s-VaCXHeg9Bo5oAYOEbLrI,6545
177
179
  piccolo/testing/random_builder.py,sha256=0LkGpanQ7P1R82gLIMQyK9cm1LdZkPvxbShTEf3jeH4,2128
@@ -259,7 +261,7 @@ tests/columns/test_double_precision.py,sha256=7rhcSfDkb2fBh_zEG4UGwD_GW1sy6U9-8N
259
261
  tests/columns/test_get_sql_value.py,sha256=mKgsInN374jzV99y9mg_ZiG-AvnJgz36SZi89xL7RZM,1768
260
262
  tests/columns/test_interval.py,sha256=2M18pfoGxLLosEvwTmuC4zQkM6jWwU0Nv2fqViW3xOs,2780
261
263
  tests/columns/test_json.py,sha256=_cziJvw2uT8e_4u9lKhmU56lgQeE7bEqCXYf6AzfChA,3482
262
- tests/columns/test_jsonb.py,sha256=eJQoxpyuQ4yrX-GMJhRkZntyd4tX6M6RhAxYn2-ISII,6727
264
+ tests/columns/test_jsonb.py,sha256=KXPgJTchobzHNss86Gb0CeTDlaa5S3pQ8cM3D06-7J8,8592
263
265
  tests/columns/test_numeric.py,sha256=AkTvdvjSsfRsMM79tx4AskUpsTizGBLMY_tC2OII9U4,751
264
266
  tests/columns/test_primary_key.py,sha256=foNG9eTQUJ5yiEVQ7faIEMycW_VuZ7vgzknYXaZ-QXM,4886
265
267
  tests/columns/test_readable.py,sha256=xKVfJuxZcfyncNVKXNryl2WFREX655jwD9DxiLArQiU,758
@@ -313,6 +315,8 @@ tests/query/functions/test_type_conversion.py,sha256=WeYR9UfJnbidle07-akQ1g9hFCd
313
315
  tests/query/mixins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
314
316
  tests/query/mixins/test_columns_delegate.py,sha256=Zw9uaqOEb7kpPQzzO9yz0jhQEeCfoPSjsy-BCLg_8XU,2032
315
317
  tests/query/mixins/test_order_by_delegate.py,sha256=mOV3Gxs0XeliONxjWSOniI1z6lbZ_xTfcGYd53JLnaY,507
318
+ tests/query/operators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
319
+ tests/query/operators/test_json.py,sha256=SEYEdbyF0wB3nvONqyBGFlLe8OhgtSIvxx19P2uJ8Bw,1269
316
320
  tests/table/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
317
321
  tests/table/test_all_columns.py,sha256=wZ7i9mTT9wKWLE2BoQ9jDbPaqnBHfV-ZlsROp7SZq7k,667
318
322
  tests/table/test_alter.py,sha256=pMD38BIFfta1vxFqp8YoaRfMxdwxhQSwcxYO4erpUi8,12394
@@ -368,9 +372,9 @@ tests/utils/test_sql_values.py,sha256=vzxRmy16FfLZPH-sAQexBvsF9MXB8n4smr14qoEOS5
368
372
  tests/utils/test_sync.py,sha256=9ytVo56y2vPQePvTeIi9lHIouEhWJbodl1TmzkGFrSo,799
369
373
  tests/utils/test_table_reflection.py,sha256=SIzuat-IpcVj1GCFyOWKShI8YkhdOPPFH7qVrvfyPNE,3794
370
374
  tests/utils/test_warnings.py,sha256=NvSC_cvJ6uZcwAGf1m-hLzETXCqprXELL8zg3TNLVMw,269
371
- piccolo-1.21.0.dist-info/LICENSE,sha256=zFIpi-16uIJ420UMIG75NU0JbDBykvrdnXcj5U_EYBI,1059
372
- piccolo-1.21.0.dist-info/METADATA,sha256=CMfZm7IYerdMTQLFwzuyCPWZMlrGf1G_shqoq2LlnGM,5178
373
- piccolo-1.21.0.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
374
- piccolo-1.21.0.dist-info/entry_points.txt,sha256=SJPHET4Fi1bN5F3WqcKkv9SClK3_F1I7m4eQjk6AFh0,46
375
- piccolo-1.21.0.dist-info/top_level.txt,sha256=-SR74VGbk43VoPy1HH-mHm97yoGukLK87HE5kdBW6qM,24
376
- piccolo-1.21.0.dist-info/RECORD,,
375
+ piccolo-1.22.0.dist-info/LICENSE,sha256=zFIpi-16uIJ420UMIG75NU0JbDBykvrdnXcj5U_EYBI,1059
376
+ piccolo-1.22.0.dist-info/METADATA,sha256=eL8y2tQr2Vd5Wmmqe0ICPO145ccpTL_5cthuK_zXIVU,5199
377
+ piccolo-1.22.0.dist-info/WHEEL,sha256=00yskusixUoUt5ob_CiUp6LsnN5lqzTJpoqOFg_FVIc,92
378
+ piccolo-1.22.0.dist-info/entry_points.txt,sha256=zYhu-YNtMlh2N_8wptCS8YWKOgc81UPL3Ji5gly8ouc,47
379
+ piccolo-1.22.0.dist-info/top_level.txt,sha256=-SR74VGbk43VoPy1HH-mHm97yoGukLK87HE5kdBW6qM,24
380
+ piccolo-1.22.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.2.0)
2
+ Generator: bdist_wheel (0.38.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,2 +1,3 @@
1
1
  [console_scripts]
2
2
  piccolo = piccolo.main:main
3
+
@@ -1,6 +1,6 @@
1
1
  from piccolo.columns.column_types import JSONB, ForeignKey, Varchar
2
2
  from piccolo.table import Table
3
- from piccolo.testing.test_case import TableTest
3
+ from piccolo.testing.test_case import AsyncTableTest, TableTest
4
4
  from tests.base import engines_only, engines_skip
5
5
 
6
6
 
@@ -137,93 +137,150 @@ class TestJSONB(TableTest):
137
137
  [{"name": "Guitar", "studio_facilities": {"mixing_desk": True}}],
138
138
  )
139
139
 
140
- def test_arrow(self):
140
+
141
+ @engines_only("postgres", "cockroach")
142
+ class TestArrow(AsyncTableTest):
143
+ tables = [RecordingStudio, Instrument]
144
+
145
+ async def insert_row(self):
146
+ await RecordingStudio(
147
+ name="Abbey Road", facilities='{"mixing_desk": true}'
148
+ ).save()
149
+
150
+ async def test_arrow(self):
141
151
  """
142
152
  Test using the arrow function to retrieve a subset of the JSON.
143
153
  """
144
- RecordingStudio(
145
- name="Abbey Road", facilities='{"mixing_desk": true}'
146
- ).save().run_sync()
154
+ await self.insert_row()
147
155
 
148
- row = (
149
- RecordingStudio.select(
150
- RecordingStudio.facilities.arrow("mixing_desk")
151
- )
152
- .first()
153
- .run_sync()
154
- )
156
+ row = await RecordingStudio.select(
157
+ RecordingStudio.facilities.arrow("mixing_desk")
158
+ ).first()
155
159
  assert row is not None
156
160
  self.assertEqual(row["facilities"], "true")
157
161
 
158
- row = (
162
+ row = await (
159
163
  RecordingStudio.select(
160
164
  RecordingStudio.facilities.arrow("mixing_desk")
161
165
  )
162
166
  .output(load_json=True)
163
167
  .first()
164
- .run_sync()
165
168
  )
166
169
  assert row is not None
167
170
  self.assertEqual(row["facilities"], True)
168
171
 
169
- def test_arrow_as_alias(self):
172
+ async def test_arrow_as_alias(self):
170
173
  """
171
174
  Test using the arrow function to retrieve a subset of the JSON.
172
175
  """
173
- RecordingStudio(
174
- name="Abbey Road", facilities='{"mixing_desk": true}'
175
- ).save().run_sync()
176
+ await self.insert_row()
176
177
 
177
- row = (
178
- RecordingStudio.select(
179
- RecordingStudio.facilities.arrow("mixing_desk").as_alias(
180
- "mixing_desk"
181
- )
178
+ row = await RecordingStudio.select(
179
+ RecordingStudio.facilities.arrow("mixing_desk").as_alias(
180
+ "mixing_desk"
182
181
  )
183
- .first()
184
- .run_sync()
185
- )
182
+ ).first()
183
+ assert row is not None
184
+ self.assertEqual(row["mixing_desk"], "true")
185
+
186
+ async def test_square_brackets(self):
187
+ """
188
+ Make sure we can use square brackets instead of calling ``arrow``
189
+ explicitly.
190
+ """
191
+ await self.insert_row()
192
+
193
+ row = await RecordingStudio.select(
194
+ RecordingStudio.facilities["mixing_desk"].as_alias("mixing_desk")
195
+ ).first()
186
196
  assert row is not None
187
197
  self.assertEqual(row["mixing_desk"], "true")
188
198
 
189
- def test_arrow_where(self):
199
+ async def test_multiple_levels_deep(self):
200
+ """
201
+ Make sure elements can be extracted multiple levels deep, and using
202
+ array indexes.
203
+ """
204
+ await RecordingStudio(
205
+ name="Abbey Road",
206
+ facilities={
207
+ "technicians": [
208
+ {"name": "Alice Jones"},
209
+ {"name": "Bob Williams"},
210
+ ]
211
+ },
212
+ ).save()
213
+
214
+ response = await RecordingStudio.select(
215
+ RecordingStudio.facilities["technicians"][0]["name"].as_alias(
216
+ "technician_name"
217
+ )
218
+ ).output(load_json=True)
219
+ assert response is not None
220
+ self.assertListEqual(response, [{"technician_name": "Alice Jones"}])
221
+
222
+ async def test_arrow_where(self):
190
223
  """
191
224
  Make sure the arrow function can be used within a WHERE clause.
192
225
  """
193
- RecordingStudio(
194
- name="Abbey Road", facilities='{"mixing_desk": true}'
195
- ).save().run_sync()
226
+ await self.insert_row()
196
227
 
197
228
  self.assertEqual(
198
- RecordingStudio.count()
199
- .where(RecordingStudio.facilities.arrow("mixing_desk").eq(True))
200
- .run_sync(),
229
+ await RecordingStudio.count().where(
230
+ RecordingStudio.facilities.arrow("mixing_desk").eq(True)
231
+ ),
201
232
  1,
202
233
  )
203
234
 
204
235
  self.assertEqual(
205
- RecordingStudio.count()
206
- .where(RecordingStudio.facilities.arrow("mixing_desk").eq(False))
207
- .run_sync(),
236
+ await RecordingStudio.count().where(
237
+ RecordingStudio.facilities.arrow("mixing_desk").eq(False)
238
+ ),
208
239
  0,
209
240
  )
210
241
 
211
- def test_arrow_first(self):
242
+ async def test_arrow_first(self):
212
243
  """
213
244
  Make sure the arrow function can be used with the first clause.
214
245
  """
215
- RecordingStudio.insert(
246
+ await RecordingStudio.insert(
216
247
  RecordingStudio(facilities='{"mixing_desk": true}'),
217
248
  RecordingStudio(facilities='{"mixing_desk": false}'),
218
- ).run_sync()
249
+ )
219
250
 
220
251
  self.assertEqual(
221
- RecordingStudio.select(
252
+ await RecordingStudio.select(
222
253
  RecordingStudio.facilities.arrow("mixing_desk").as_alias(
223
254
  "mixing_desk"
224
255
  )
225
- )
226
- .first()
227
- .run_sync(),
256
+ ).first(),
228
257
  {"mixing_desk": "true"},
229
258
  )
259
+
260
+
261
+ @engines_only("postgres", "cockroach")
262
+ class TestFromPath(AsyncTableTest):
263
+
264
+ tables = [RecordingStudio, Instrument]
265
+
266
+ async def test_from_path(self):
267
+ """
268
+ Make sure ``from_path`` can be used for complex nested data.
269
+ """
270
+ await RecordingStudio(
271
+ name="Abbey Road",
272
+ facilities={
273
+ "technicians": [
274
+ {"name": "Alice Jones"},
275
+ {"name": "Bob Williams"},
276
+ ]
277
+ },
278
+ ).save()
279
+
280
+ response = await RecordingStudio.select(
281
+ RecordingStudio.facilities.from_path(
282
+ ["technicians", 0, "name"]
283
+ ).as_alias("technician_name")
284
+ ).output(load_json=True)
285
+ assert response is not None
286
+ self.assertListEqual(response, [{"technician_name": "Alice Jones"}])
File without changes
@@ -0,0 +1,52 @@
1
+ from unittest import TestCase
2
+
3
+ from piccolo.columns import JSONB
4
+ from piccolo.query.operators.json import GetChildElement, GetElementFromPath
5
+ from piccolo.table import Table
6
+ from tests.base import engines_skip
7
+
8
+
9
+ class RecordingStudio(Table):
10
+ facilities = JSONB(null=True)
11
+
12
+
13
+ @engines_skip("sqlite")
14
+ class TestGetChildElement(TestCase):
15
+
16
+ def test_query(self):
17
+ """
18
+ Make sure the generated SQL looks correct.
19
+ """
20
+ querystring = GetChildElement(
21
+ GetChildElement(RecordingStudio.facilities, "a"), "b"
22
+ )
23
+
24
+ sql, query_args = querystring.compile_string()
25
+
26
+ self.assertEqual(
27
+ sql,
28
+ '"recording_studio"."facilities" -> $1 -> $2',
29
+ )
30
+
31
+ self.assertListEqual(query_args, ["a", "b"])
32
+
33
+
34
+ @engines_skip("sqlite")
35
+ class TestGetElementFromPath(TestCase):
36
+
37
+ def test_query(self):
38
+ """
39
+ Make sure the generated SQL looks correct.
40
+ """
41
+ querystring = GetElementFromPath(
42
+ RecordingStudio.facilities, ["a", "b"]
43
+ )
44
+
45
+ sql, query_args = querystring.compile_string()
46
+
47
+ self.assertEqual(
48
+ sql,
49
+ '"recording_studio"."facilities" #> $1',
50
+ )
51
+
52
+ self.assertListEqual(query_args, [["a", "b"]])