piccolo 1.14.0__py3-none-any.whl → 1.15.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 +1 -1
- piccolo/apps/user/tables.py +6 -2
- piccolo/query/base.py +1 -3
- piccolo/query/methods/refresh.py +91 -10
- piccolo/query/methods/select.py +3 -0
- piccolo/table.py +8 -2
- piccolo/utils/encoding.py +42 -1
- {piccolo-1.14.0.dist-info → piccolo-1.15.0.dist-info}/METADATA +1 -1
- {piccolo-1.14.0.dist-info → piccolo-1.15.0.dist-info}/RECORD +16 -16
- {piccolo-1.14.0.dist-info → piccolo-1.15.0.dist-info}/WHEEL +1 -1
- tests/apps/user/test_tables.py +12 -4
- tests/columns/m2m/test_m2m.py +7 -0
- tests/table/test_refresh.py +218 -8
- {piccolo-1.14.0.dist-info → piccolo-1.15.0.dist-info}/LICENSE +0 -0
- {piccolo-1.14.0.dist-info → piccolo-1.15.0.dist-info}/entry_points.txt +0 -0
- {piccolo-1.14.0.dist-info → piccolo-1.15.0.dist-info}/top_level.txt +0 -0
piccolo/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__VERSION__ = "1.
|
1
|
+
__VERSION__ = "1.15.0"
|
piccolo/apps/user/tables.py
CHANGED
@@ -91,10 +91,14 @@ class BaseUser(Table, tablename="piccolo_user"):
|
|
91
91
|
raise ValueError("A password must be provided.")
|
92
92
|
|
93
93
|
if len(password) < cls._min_password_length:
|
94
|
-
raise ValueError(
|
94
|
+
raise ValueError(
|
95
|
+
f"The password is too short. (min {cls._min_password_length})"
|
96
|
+
)
|
95
97
|
|
96
98
|
if len(password) > cls._max_password_length:
|
97
|
-
raise ValueError(
|
99
|
+
raise ValueError(
|
100
|
+
f"The password is too long. (max {cls._max_password_length})"
|
101
|
+
)
|
98
102
|
|
99
103
|
if password.startswith("pbkdf2_sha256"):
|
100
104
|
logger.warning(
|
piccolo/query/base.py
CHANGED
@@ -79,9 +79,7 @@ class Query(t.Generic[TableInstance, QueryResponseType]):
|
|
79
79
|
if column._alias is not None:
|
80
80
|
json_column_names.append(column._alias)
|
81
81
|
elif len(column._meta.call_chain) > 0:
|
82
|
-
json_column_names.append(
|
83
|
-
column._meta.get_default_alias().replace("$", ".")
|
84
|
-
)
|
82
|
+
json_column_names.append(column._meta.get_default_alias())
|
85
83
|
else:
|
86
84
|
json_column_names.append(column._meta.name)
|
87
85
|
|
piccolo/query/methods/refresh.py
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import typing as t
|
4
|
-
from dataclasses import dataclass
|
5
4
|
|
5
|
+
from piccolo.utils.encoding import JSONDict
|
6
6
|
from piccolo.utils.sync import run_sync
|
7
7
|
|
8
8
|
if t.TYPE_CHECKING: # pragma: no cover
|
@@ -10,7 +10,6 @@ if t.TYPE_CHECKING: # pragma: no cover
|
|
10
10
|
from piccolo.table import Table
|
11
11
|
|
12
12
|
|
13
|
-
@dataclass
|
14
13
|
class Refresh:
|
15
14
|
"""
|
16
15
|
Used to refresh :class:`Table <piccolo.table.Table>` instances with the
|
@@ -22,11 +21,33 @@ class Refresh:
|
|
22
21
|
:param columns:
|
23
22
|
Which columns to refresh - it not specified, then all columns are
|
24
23
|
refreshed.
|
24
|
+
:param load_json:
|
25
|
+
Whether to load ``JSON`` / ``JSONB`` columns as objects, instead of
|
26
|
+
just a string.
|
25
27
|
|
26
28
|
"""
|
27
29
|
|
28
|
-
|
29
|
-
|
30
|
+
def __init__(
|
31
|
+
self,
|
32
|
+
instance: Table,
|
33
|
+
columns: t.Optional[t.Sequence[Column]] = None,
|
34
|
+
load_json: bool = False,
|
35
|
+
):
|
36
|
+
self.instance = instance
|
37
|
+
|
38
|
+
if columns:
|
39
|
+
for column in columns:
|
40
|
+
if len(column._meta.call_chain) > 0:
|
41
|
+
raise ValueError(
|
42
|
+
"We can't currently selectively refresh certain "
|
43
|
+
"columns on child objects (e.g. Concert.band_1.name). "
|
44
|
+
"Please just specify top level columns (e.g. "
|
45
|
+
"Concert.band_1), and the entire child object will be "
|
46
|
+
"refreshed."
|
47
|
+
)
|
48
|
+
|
49
|
+
self.columns = columns
|
50
|
+
self.load_json = load_json
|
30
51
|
|
31
52
|
@property
|
32
53
|
def _columns(self) -> t.Sequence[Column]:
|
@@ -40,6 +61,63 @@ class Refresh:
|
|
40
61
|
i for i in self.instance._meta.columns if not i._meta.primary_key
|
41
62
|
]
|
42
63
|
|
64
|
+
def _get_columns(self, instance: Table, columns: t.Sequence[Column]):
|
65
|
+
"""
|
66
|
+
If `prefetch` was used on the object, for example::
|
67
|
+
|
68
|
+
>>> await Band.objects(Band.manager)
|
69
|
+
|
70
|
+
We should also update the prefetched object.
|
71
|
+
|
72
|
+
It works multiple level deep. If we refresh this::
|
73
|
+
|
74
|
+
>>> await Album.objects(Album.band.manager).first()
|
75
|
+
|
76
|
+
It will update the nested `band` object, and also the `manager`
|
77
|
+
object.
|
78
|
+
|
79
|
+
"""
|
80
|
+
from piccolo.columns.column_types import ForeignKey
|
81
|
+
from piccolo.table import Table
|
82
|
+
|
83
|
+
select_columns = []
|
84
|
+
|
85
|
+
for column in columns:
|
86
|
+
if isinstance(column, ForeignKey) and isinstance(
|
87
|
+
(child_instance := getattr(instance, column._meta.name)),
|
88
|
+
Table,
|
89
|
+
):
|
90
|
+
select_columns.extend(
|
91
|
+
self._get_columns(
|
92
|
+
child_instance,
|
93
|
+
# Fetch all columns (even the primary key, just in
|
94
|
+
# case the foreign key now references a different row).
|
95
|
+
column.all_columns(),
|
96
|
+
)
|
97
|
+
)
|
98
|
+
else:
|
99
|
+
select_columns.append(column)
|
100
|
+
|
101
|
+
return select_columns
|
102
|
+
|
103
|
+
def _update_instance(self, instance: Table, data_dict: t.Dict):
|
104
|
+
"""
|
105
|
+
Update the table instance. It is called recursively, if the instance
|
106
|
+
has child instances.
|
107
|
+
"""
|
108
|
+
for key, value in data_dict.items():
|
109
|
+
if isinstance(value, dict) and not isinstance(value, JSONDict):
|
110
|
+
# If the value is a dict, then it's a child instance.
|
111
|
+
if all(i is None for i in value.values()):
|
112
|
+
# If all values in the nested object are None, then we can
|
113
|
+
# safely assume that the object itself is null, as the
|
114
|
+
# primary key value must be null.
|
115
|
+
setattr(instance, key, None)
|
116
|
+
else:
|
117
|
+
self._update_instance(getattr(instance, key), value)
|
118
|
+
else:
|
119
|
+
setattr(instance, key, value)
|
120
|
+
|
43
121
|
async def run(
|
44
122
|
self, in_pool: bool = True, node: t.Optional[str] = None
|
45
123
|
) -> Table:
|
@@ -54,7 +132,6 @@ class Refresh:
|
|
54
132
|
Modifies the instance in place, but also returns it as a convenience.
|
55
133
|
|
56
134
|
"""
|
57
|
-
|
58
135
|
instance = self.instance
|
59
136
|
|
60
137
|
if not instance._exists_in_db:
|
@@ -71,20 +148,24 @@ class Refresh:
|
|
71
148
|
if not columns:
|
72
149
|
raise ValueError("No columns to fetch.")
|
73
150
|
|
74
|
-
|
75
|
-
|
151
|
+
select_columns = self._get_columns(
|
152
|
+
instance=self.instance, columns=columns
|
153
|
+
)
|
154
|
+
|
155
|
+
data_dict = (
|
156
|
+
await instance.__class__.select(*select_columns)
|
76
157
|
.where(pk_column == primary_key_value)
|
158
|
+
.output(nested=True, load_json=self.load_json)
|
77
159
|
.first()
|
78
160
|
.run(node=node, in_pool=in_pool)
|
79
161
|
)
|
80
162
|
|
81
|
-
if
|
163
|
+
if data_dict is None:
|
82
164
|
raise ValueError(
|
83
165
|
"The object doesn't exist in the database any more."
|
84
166
|
)
|
85
167
|
|
86
|
-
|
87
|
-
setattr(instance, key, value)
|
168
|
+
self._update_instance(instance=instance, data_dict=data_dict)
|
88
169
|
|
89
170
|
return instance
|
90
171
|
|
piccolo/query/methods/select.py
CHANGED
@@ -406,6 +406,9 @@ class Select(Query[TableInstance, t.List[t.Dict[str, t.Any]]]):
|
|
406
406
|
def output(self: Self, *, load_json: bool, as_list: bool) -> SelectJSON: # type: ignore # noqa: E501
|
407
407
|
...
|
408
408
|
|
409
|
+
@t.overload
|
410
|
+
def output(self: Self, *, load_json: bool, nested: bool) -> Self: ...
|
411
|
+
|
409
412
|
@t.overload
|
410
413
|
def output(self: Self, *, nested: bool) -> Self: ...
|
411
414
|
|
piccolo/table.py
CHANGED
@@ -541,7 +541,9 @@ class Table(metaclass=TableMetaclass):
|
|
541
541
|
)
|
542
542
|
|
543
543
|
def refresh(
|
544
|
-
self,
|
544
|
+
self,
|
545
|
+
columns: t.Optional[t.Sequence[Column]] = None,
|
546
|
+
load_json: bool = False,
|
545
547
|
) -> Refresh:
|
546
548
|
"""
|
547
549
|
Used to fetch the latest data for this instance from the database.
|
@@ -551,6 +553,10 @@ class Table(metaclass=TableMetaclass):
|
|
551
553
|
If you only want to refresh certain columns, specify them here.
|
552
554
|
Otherwise all columns are refreshed.
|
553
555
|
|
556
|
+
:param load_json:
|
557
|
+
Whether to load ``JSON`` / ``JSONB`` columns as objects, instead of
|
558
|
+
just a string.
|
559
|
+
|
554
560
|
Example usage::
|
555
561
|
|
556
562
|
# Get an instance from the database.
|
@@ -564,7 +570,7 @@ class Table(metaclass=TableMetaclass):
|
|
564
570
|
instance.refresh().run_sync()
|
565
571
|
|
566
572
|
"""
|
567
|
-
return Refresh(instance=self, columns=columns)
|
573
|
+
return Refresh(instance=self, columns=columns, load_json=load_json)
|
568
574
|
|
569
575
|
@t.overload
|
570
576
|
def get_related(
|
piccolo/utils/encoding.py
CHANGED
@@ -29,5 +29,46 @@ def dump_json(data: t.Any, pretty: bool = False) -> str:
|
|
29
29
|
return json.dumps(data, **params) # type: ignore
|
30
30
|
|
31
31
|
|
32
|
+
class JSONDict(dict):
|
33
|
+
"""
|
34
|
+
Once we have parsed a JSON string into a dictionary, we can't distinguish
|
35
|
+
it from other dictionaries.
|
36
|
+
|
37
|
+
Sometimes we might want to - for example::
|
38
|
+
|
39
|
+
>>> await Album.select(
|
40
|
+
... Album.all_columns(),
|
41
|
+
... Album.recording_studio.all_columns()
|
42
|
+
... ).output(
|
43
|
+
... nested=True,
|
44
|
+
... load_json=True
|
45
|
+
... )
|
46
|
+
|
47
|
+
[{
|
48
|
+
'id': 1,
|
49
|
+
'band': 1,
|
50
|
+
'name': 'Awesome album 1',
|
51
|
+
'recorded_at': {
|
52
|
+
'id': 1,
|
53
|
+
'facilities': {'restaurant': True, 'mixing_desk': True},
|
54
|
+
'name': 'Abbey Road'
|
55
|
+
},
|
56
|
+
'release_date': datetime.date(2021, 1, 1)
|
57
|
+
}]
|
58
|
+
|
59
|
+
Facilities could be mistaken for a table.
|
60
|
+
|
61
|
+
"""
|
62
|
+
|
63
|
+
...
|
64
|
+
|
65
|
+
|
32
66
|
def load_json(data: str) -> t.Any:
|
33
|
-
|
67
|
+
response = (
|
68
|
+
orjson.loads(data) if ORJSON else json.loads(data) # type: ignore
|
69
|
+
)
|
70
|
+
|
71
|
+
if isinstance(response, dict):
|
72
|
+
return JSONDict(**response)
|
73
|
+
|
74
|
+
return response
|
@@ -1,10 +1,10 @@
|
|
1
|
-
piccolo/__init__.py,sha256=
|
1
|
+
piccolo/__init__.py,sha256=PzCSZ5iBV_NCcXFBA13Mmkxa8tLr-WxcGYxYBnMCjhA,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
5
|
piccolo/querystring.py,sha256=yZdURtiVSlxkkEVJoFKAmL2OfNxrUr8xfsfuBBB7IuY,9662
|
6
6
|
piccolo/schema.py,sha256=qNNy4tG_HqnXR9t3hHMgYXtGxHabwQAhUpc6RKLJ_gE,7960
|
7
|
-
piccolo/table.py,sha256=
|
7
|
+
piccolo/table.py,sha256=nS3zuhGNPZ4H9s_E3ieFbrE_u1Tr6TenBVw_nrrtmRQ,49766
|
8
8
|
piccolo/table_reflection.py,sha256=jrN1nHerDJ4tU09GtNN3hz7ap-7rXnSUjljFO6LB2H0,7094
|
9
9
|
piccolo/apps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
10
|
piccolo/apps/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -104,7 +104,7 @@ piccolo/apps/tester/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NM
|
|
104
104
|
piccolo/apps/tester/commands/run.py,sha256=phFxim2ogARAviW-YT11y9F-L5SJxSioAIepUzQeAWU,2431
|
105
105
|
piccolo/apps/user/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
106
106
|
piccolo/apps/user/piccolo_app.py,sha256=yfw1J9FEnBWdgGdUdNMnqp06Wzm5_s9TO2r5KLchrLM,842
|
107
|
-
piccolo/apps/user/tables.py,sha256=
|
107
|
+
piccolo/apps/user/tables.py,sha256=KgPqONdl1SDV-3cGq5aS-za_v_5_TGHQqNFYXF0M3Jw,9082
|
108
108
|
piccolo/apps/user/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
109
109
|
piccolo/apps/user/commands/change_password.py,sha256=F7mlhtTUY7uENSK8vds5oibIDJTz7QBQdrl7EB-lF9Q,649
|
110
110
|
piccolo/apps/user/commands/change_permissions.py,sha256=LScsKJUMJqIi54cZ1SgS9Wb356KB0t7smu94FDXLfVk,1463
|
@@ -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=
|
149
|
+
piccolo/query/base.py,sha256=iI9Fv3oOw7T4ZWZvRKRwdtClvQtSaAepslH24vwxZVA,14616
|
150
150
|
piccolo/query/mixins.py,sha256=EFEFb9It4y1mR6_JXLn139h5M9KgeP750STYy5M4MLs,21951
|
151
151
|
piccolo/query/proxy.py,sha256=Yq4jNc7IWJvdeO3u7_7iPyRy2WhVj8KsIUcIYHBIi9Q,1839
|
152
152
|
piccolo/query/functions/__init__.py,sha256=pZkzOIh7Sg9HPNOeegOwAS46Oxt31ATlSVmwn-lxCbc,605
|
@@ -168,8 +168,8 @@ piccolo/query/methods/indexes.py,sha256=J-QUqaBJwpgahskUH0Cu0Mq7zEKcfVAtDsUVIVX-
|
|
168
168
|
piccolo/query/methods/insert.py,sha256=ssLJ_wn08KnOwwr7t-VILyn1P4hrvM63CfPIcAJWT5k,4701
|
169
169
|
piccolo/query/methods/objects.py,sha256=iahDUziUtlx7pJ2uBAhdm3hCTmg2AS9C8cal1my5KR0,11705
|
170
170
|
piccolo/query/methods/raw.py,sha256=VhYpCB52mZk4zqFTsqK5CHKTDGskUjISXTBV7UjohmA,600
|
171
|
-
piccolo/query/methods/refresh.py,sha256=
|
172
|
-
piccolo/query/methods/select.py,sha256=
|
171
|
+
piccolo/query/methods/refresh.py,sha256=wg1zghKfwz-VmqK4uWa4GNMiDtK-skTqow591Hb3ONM,5854
|
172
|
+
piccolo/query/methods/select.py,sha256=UH-y2g3Ub7bEowfLObrrhw0W-HepTXWCmuMPhk13roE,21406
|
173
173
|
piccolo/query/methods/table_exists.py,sha256=0yb3n6Jd2ovSBWlZ-gl00K4E7Jnbj7J8qAAX5d7hvNk,1259
|
174
174
|
piccolo/query/methods/update.py,sha256=LfWqIXEl1aecc0rkVssTFmwyD6wXGhlKcTrUVhtlEsw,3705
|
175
175
|
piccolo/testing/__init__.py,sha256=pRFSqRInfx95AakOq54atmvqoB-ue073q2aR8u8zR40,83
|
@@ -177,7 +177,7 @@ piccolo/testing/model_builder.py,sha256=lVEiEe71xrH8SSjzFc2l0s-VaCXHeg9Bo5oAYOEb
|
|
177
177
|
piccolo/testing/random_builder.py,sha256=0LkGpanQ7P1R82gLIMQyK9cm1LdZkPvxbShTEf3jeH4,2128
|
178
178
|
piccolo/utils/__init__.py,sha256=SDFFraauI9Op8dCRkreQv1dwUcab8Mi1eC-n0EwlTy8,36
|
179
179
|
piccolo/utils/dictionary.py,sha256=8vRPxgaXadDVhqihP1UxL7nUBgM6Gpe_Eu3xJq7zzGM,1886
|
180
|
-
piccolo/utils/encoding.py,sha256=
|
180
|
+
piccolo/utils/encoding.py,sha256=CtSODJOkT3TVHfGlTDXozDsClBCJbGGqluc6_UlJ-7c,1761
|
181
181
|
piccolo/utils/lazy_loader.py,sha256=T8GChEqtKWcBegn-6g_BQ7hOg2Xu1bedFh7Z8E7xcOY,1912
|
182
182
|
piccolo/utils/list.py,sha256=4hPGiksJWxL226W7gyYBcqVGgMTgVa2jP8zvalc3zw8,1541
|
183
183
|
piccolo/utils/naming.py,sha256=d7_mMscguK799RMhxFDifRgn8Ply5wiy2k1KkP22WUs,276
|
@@ -237,7 +237,7 @@ tests/apps/sql_shell/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5N
|
|
237
237
|
tests/apps/sql_shell/commands/test_run.py,sha256=6p0nqCoG_qNLrKeBuHspmer_SrMwEF-vfp9LbPj2W2E,425
|
238
238
|
tests/apps/tester/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
239
239
|
tests/apps/user/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
240
|
-
tests/apps/user/test_tables.py,sha256=
|
240
|
+
tests/apps/user/test_tables.py,sha256=5ImGAWpIKxlqweGeJRMiVOHaBJxRwSzsp3GZ7x6lzCo,9807
|
241
241
|
tests/apps/user/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
242
242
|
tests/apps/user/commands/test_change_password.py,sha256=IEIlnny68jt4UFOaUOwHk9C7WHED51wYoSNX-YIi7rU,1051
|
243
243
|
tests/apps/user/commands/test_change_permissions.py,sha256=uVKEiT1EKot3VA2TDETdQ1hsWL-83rLQrJl4jIxPgqo,2108
|
@@ -273,7 +273,7 @@ tests/columns/test_uuid.py,sha256=ifCyIrO6usYO33WM0FgwuPPhYzs4zVKD3MViIT61J7c,35
|
|
273
273
|
tests/columns/test_varchar.py,sha256=Oyt8t7msCfIiZdU9B23zjlV6atFv4sX3iMSDJc565Oc,681
|
274
274
|
tests/columns/m2m/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
275
275
|
tests/columns/m2m/base.py,sha256=uH92ECQuY5AjpQXPySwrlruZPZzB4LH2V2FUSXmHRLQ,14563
|
276
|
-
tests/columns/m2m/test_m2m.py,sha256=
|
276
|
+
tests/columns/m2m/test_m2m.py,sha256=0ObmIHUJF6CZoNBznc5xXVr5_BbGBqOmWwtpg8IcPt4,13055
|
277
277
|
tests/columns/m2m/test_m2m_schema.py,sha256=oxu7eAjFFpDjnq9Eq-5OTNmlnsEIMFWx18OItfpVs-s,339
|
278
278
|
tests/conf/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
279
279
|
tests/conf/example.py,sha256=K8sTttLpEac8rQlOLDY500IGkHj3P3NoyFbCMnT1EqY,347
|
@@ -336,7 +336,7 @@ tests/table/test_objects.py,sha256=bir86ks-Ngy8x9Eu9bekOrh6twBYdEkIgTdbBWY6x9s,8
|
|
336
336
|
tests/table/test_output.py,sha256=ZnpPbgVp79JcB6E_ooWQxOpOlhkwNUlMxC-1LSIEc2Y,4304
|
337
337
|
tests/table/test_raw.py,sha256=9PTvYngQi41nYd5lKzkJdTqsEcwrdOXcvZjq-W26CwQ,1683
|
338
338
|
tests/table/test_ref.py,sha256=eYNRnYHzNMXuMbV3B1ca5EidpIg4500q6hr1ccuVaso,269
|
339
|
-
tests/table/test_refresh.py,sha256=
|
339
|
+
tests/table/test_refresh.py,sha256=VqTNT0_UGifCYr0wWVEXqI53G51RlMu0-oaw8tUTTLs,9213
|
340
340
|
tests/table/test_repr.py,sha256=uahz3_GffGQrf2mDE-4-Pu4AmSLBAyso6-9rbohCl58,446
|
341
341
|
tests/table/test_select.py,sha256=jgeiahIlNFVijxYb3a54g1sJWVfH3llaYrsTBmdicrs,40390
|
342
342
|
tests/table/test_str.py,sha256=eztWNULcjARR1fr9X5n4tojhDNgDfatVyNHwuYrzHAo,1731
|
@@ -365,9 +365,9 @@ tests/utils/test_sql_values.py,sha256=vzxRmy16FfLZPH-sAQexBvsF9MXB8n4smr14qoEOS5
|
|
365
365
|
tests/utils/test_sync.py,sha256=9ytVo56y2vPQePvTeIi9lHIouEhWJbodl1TmzkGFrSo,799
|
366
366
|
tests/utils/test_table_reflection.py,sha256=SIzuat-IpcVj1GCFyOWKShI8YkhdOPPFH7qVrvfyPNE,3794
|
367
367
|
tests/utils/test_warnings.py,sha256=NvSC_cvJ6uZcwAGf1m-hLzETXCqprXELL8zg3TNLVMw,269
|
368
|
-
piccolo-1.
|
369
|
-
piccolo-1.
|
370
|
-
piccolo-1.
|
371
|
-
piccolo-1.
|
372
|
-
piccolo-1.
|
373
|
-
piccolo-1.
|
368
|
+
piccolo-1.15.0.dist-info/LICENSE,sha256=zFIpi-16uIJ420UMIG75NU0JbDBykvrdnXcj5U_EYBI,1059
|
369
|
+
piccolo-1.15.0.dist-info/METADATA,sha256=GGstF5gDdGfqb2J2-55eYcA9pbhIVN6W23Jir9GF4A4,5178
|
370
|
+
piccolo-1.15.0.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
|
371
|
+
piccolo-1.15.0.dist-info/entry_points.txt,sha256=SJPHET4Fi1bN5F3WqcKkv9SClK3_F1I7m4eQjk6AFh0,46
|
372
|
+
piccolo-1.15.0.dist-info/top_level.txt,sha256=-SR74VGbk43VoPy1HH-mHm97yoGukLK87HE5kdBW6qM,24
|
373
|
+
piccolo-1.15.0.dist-info/RECORD,,
|
tests/apps/user/test_tables.py
CHANGED
@@ -109,7 +109,7 @@ class TestLogin(TestCase):
|
|
109
109
|
BaseUser.update_password_sync(username, malicious_password)
|
110
110
|
self.assertEqual(
|
111
111
|
manager.exception.__str__(),
|
112
|
-
"The password is too long.",
|
112
|
+
f"The password is too long. (max {BaseUser._max_password_length})",
|
113
113
|
)
|
114
114
|
|
115
115
|
# Test short passwords
|
@@ -118,7 +118,10 @@ class TestLogin(TestCase):
|
|
118
118
|
BaseUser.update_password_sync(username, short_password)
|
119
119
|
self.assertEqual(
|
120
120
|
manager.exception.__str__(),
|
121
|
-
|
121
|
+
(
|
122
|
+
"The password is too short. (min "
|
123
|
+
f"{BaseUser._min_password_length})"
|
124
|
+
),
|
122
125
|
)
|
123
126
|
|
124
127
|
# Test no password
|
@@ -205,7 +208,11 @@ class TestCreateUser(TestCase):
|
|
205
208
|
BaseUser.create_user_sync(username="bob", password="abc")
|
206
209
|
|
207
210
|
self.assertEqual(
|
208
|
-
manager.exception.__str__(),
|
211
|
+
manager.exception.__str__(),
|
212
|
+
(
|
213
|
+
"The password is too short. (min "
|
214
|
+
f"{BaseUser._min_password_length})"
|
215
|
+
),
|
209
216
|
)
|
210
217
|
|
211
218
|
def test_long_password_error(self):
|
@@ -216,7 +223,8 @@ class TestCreateUser(TestCase):
|
|
216
223
|
)
|
217
224
|
|
218
225
|
self.assertEqual(
|
219
|
-
manager.exception.__str__(),
|
226
|
+
manager.exception.__str__(),
|
227
|
+
f"The password is too long. (max {BaseUser._max_password_length})",
|
220
228
|
)
|
221
229
|
|
222
230
|
def test_no_username_error(self):
|
tests/columns/m2m/test_m2m.py
CHANGED
@@ -4,6 +4,7 @@ import decimal
|
|
4
4
|
import uuid
|
5
5
|
from unittest import TestCase
|
6
6
|
|
7
|
+
from piccolo.utils.encoding import JSONDict
|
7
8
|
from tests.base import engines_skip
|
8
9
|
|
9
10
|
try:
|
@@ -376,6 +377,9 @@ class TestM2MComplexSchema(TestCase):
|
|
376
377
|
|
377
378
|
if isinstance(column, UUID):
|
378
379
|
self.assertIn(type(returned_value), (uuid.UUID, asyncpgUUID))
|
380
|
+
elif isinstance(column, (JSON, JSONB)):
|
381
|
+
self.assertEqual(type(returned_value), JSONDict)
|
382
|
+
self.assertEqual(original_value, returned_value)
|
379
383
|
else:
|
380
384
|
self.assertEqual(
|
381
385
|
type(original_value),
|
@@ -401,6 +405,9 @@ class TestM2MComplexSchema(TestCase):
|
|
401
405
|
if isinstance(column, UUID):
|
402
406
|
self.assertIn(type(returned_value), (uuid.UUID, asyncpgUUID))
|
403
407
|
self.assertEqual(str(original_value), str(returned_value))
|
408
|
+
elif isinstance(column, (JSON, JSONB)):
|
409
|
+
self.assertEqual(type(returned_value), JSONDict)
|
410
|
+
self.assertEqual(original_value, returned_value)
|
404
411
|
else:
|
405
412
|
self.assertEqual(
|
406
413
|
type(original_value),
|
tests/table/test_refresh.py
CHANGED
@@ -1,5 +1,13 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
import typing as t
|
2
|
+
|
3
|
+
from tests.base import DBTestCase, TableTest
|
4
|
+
from tests.example_apps.music.tables import (
|
5
|
+
Band,
|
6
|
+
Concert,
|
7
|
+
Manager,
|
8
|
+
RecordingStudio,
|
9
|
+
Venue,
|
10
|
+
)
|
3
11
|
|
4
12
|
|
5
13
|
class TestRefresh(DBTestCase):
|
@@ -24,9 +32,55 @@ class TestRefresh(DBTestCase):
|
|
24
32
|
# Refresh `band`, and make sure it has the correct data.
|
25
33
|
band.refresh().run_sync()
|
26
34
|
|
27
|
-
self.
|
28
|
-
self.
|
29
|
-
self.
|
35
|
+
self.assertEqual(band.name, "Pythonistas!!!")
|
36
|
+
self.assertEqual(band.popularity, 8000)
|
37
|
+
self.assertEqual(band.id, initial_data["id"])
|
38
|
+
|
39
|
+
def test_refresh_with_prefetch(self) -> None:
|
40
|
+
"""
|
41
|
+
Make sure ``refresh`` works, when the object used prefetch to get
|
42
|
+
nested objets (the nested objects should be updated too).
|
43
|
+
"""
|
44
|
+
band = (
|
45
|
+
Band.objects(Band.manager)
|
46
|
+
.where(Band.name == "Pythonistas")
|
47
|
+
.first()
|
48
|
+
.run_sync()
|
49
|
+
)
|
50
|
+
assert band is not None
|
51
|
+
|
52
|
+
# Modify the data in the database.
|
53
|
+
Manager.update({Manager.name: "Guido!!!"}).where(
|
54
|
+
Manager.name == "Guido"
|
55
|
+
).run_sync()
|
56
|
+
|
57
|
+
# Refresh `band`, and make sure it has the correct data.
|
58
|
+
band.refresh().run_sync()
|
59
|
+
|
60
|
+
self.assertEqual(band.manager.name, "Guido!!!")
|
61
|
+
|
62
|
+
def test_refresh_with_prefetch_multiple_layers_deep(self) -> None:
|
63
|
+
"""
|
64
|
+
Make sure ``refresh`` works, when the object used prefetch to get
|
65
|
+
nested objets (the nested objects should be updated too).
|
66
|
+
"""
|
67
|
+
band = (
|
68
|
+
Band.objects(Band.manager)
|
69
|
+
.where(Band.name == "Pythonistas")
|
70
|
+
.first()
|
71
|
+
.run_sync()
|
72
|
+
)
|
73
|
+
assert band is not None
|
74
|
+
|
75
|
+
# Modify the data in the database.
|
76
|
+
Manager.update({Manager.name: "Guido!!!"}).where(
|
77
|
+
Manager.name == "Guido"
|
78
|
+
).run_sync()
|
79
|
+
|
80
|
+
# Refresh `band`, and make sure it has the correct data.
|
81
|
+
band.refresh().run_sync()
|
82
|
+
|
83
|
+
self.assertEqual(band.manager.name, "Guido!!!")
|
30
84
|
|
31
85
|
def test_columns(self) -> None:
|
32
86
|
"""
|
@@ -50,9 +104,9 @@ class TestRefresh(DBTestCase):
|
|
50
104
|
)
|
51
105
|
query.run_sync()
|
52
106
|
|
53
|
-
self.
|
54
|
-
self.
|
55
|
-
self.
|
107
|
+
self.assertEqual(band.name, "Pythonistas!!!")
|
108
|
+
self.assertEqual(band.popularity, initial_data["popularity"])
|
109
|
+
self.assertEqual(band.id, initial_data["id"])
|
56
110
|
|
57
111
|
def test_error_when_not_in_db(self) -> None:
|
58
112
|
"""
|
@@ -85,3 +139,159 @@ class TestRefresh(DBTestCase):
|
|
85
139
|
"The instance's primary key value isn't defined.",
|
86
140
|
str(manager.exception),
|
87
141
|
)
|
142
|
+
|
143
|
+
|
144
|
+
class TestRefreshWithPrefetch(TableTest):
|
145
|
+
|
146
|
+
tables = [Manager, Band, Concert, Venue]
|
147
|
+
|
148
|
+
def setUp(self):
|
149
|
+
super().setUp()
|
150
|
+
|
151
|
+
self.manager = Manager({Manager.name: "Guido"})
|
152
|
+
self.manager.save().run_sync()
|
153
|
+
|
154
|
+
self.band = Band(
|
155
|
+
{Band.name: "Pythonistas", Band.manager: self.manager}
|
156
|
+
)
|
157
|
+
self.band.save().run_sync()
|
158
|
+
|
159
|
+
self.concert = Concert({Concert.band_1: self.band})
|
160
|
+
self.concert.save().run_sync()
|
161
|
+
|
162
|
+
def test_single_layer(self) -> None:
|
163
|
+
"""
|
164
|
+
Make sure ``refresh`` works, when the object used prefetch to get
|
165
|
+
nested objects (the nested objects should be updated too).
|
166
|
+
"""
|
167
|
+
band = (
|
168
|
+
Band.objects(Band.manager)
|
169
|
+
.where(Band.name == "Pythonistas")
|
170
|
+
.first()
|
171
|
+
.run_sync()
|
172
|
+
)
|
173
|
+
assert band is not None
|
174
|
+
|
175
|
+
# Modify the data in the database.
|
176
|
+
self.manager.name = "Guido!!!"
|
177
|
+
self.manager.save().run_sync()
|
178
|
+
|
179
|
+
# Refresh `band`, and make sure it has the correct data.
|
180
|
+
band.refresh().run_sync()
|
181
|
+
self.assertEqual(band.manager.name, "Guido!!!")
|
182
|
+
|
183
|
+
def test_multiple_layers(self) -> None:
|
184
|
+
"""
|
185
|
+
Make sure ``refresh`` works when ``prefetch`` was used to fetch objects
|
186
|
+
multiple layers deep.
|
187
|
+
"""
|
188
|
+
concert = (
|
189
|
+
Concert.objects(Concert.band_1._.manager)
|
190
|
+
.where(Concert.band_1._.name == "Pythonistas")
|
191
|
+
.first()
|
192
|
+
.run_sync()
|
193
|
+
)
|
194
|
+
assert concert is not None
|
195
|
+
|
196
|
+
# Modify the data in the database.
|
197
|
+
self.manager.name = "Guido!!!"
|
198
|
+
self.manager.save().run_sync()
|
199
|
+
|
200
|
+
concert.refresh().run_sync()
|
201
|
+
self.assertEqual(concert.band_1.manager.name, "Guido!!!")
|
202
|
+
|
203
|
+
def test_updated_foreign_key(self) -> None:
|
204
|
+
"""
|
205
|
+
If a foreign key now references a different row, make sure this
|
206
|
+
is refreshed correctly.
|
207
|
+
"""
|
208
|
+
band = (
|
209
|
+
Band.objects(Band.manager)
|
210
|
+
.where(Band.name == "Pythonistas")
|
211
|
+
.first()
|
212
|
+
.run_sync()
|
213
|
+
)
|
214
|
+
assert band is not None
|
215
|
+
|
216
|
+
# Assign a different manager to the band
|
217
|
+
new_manager = Manager({Manager.name: "New Manager"})
|
218
|
+
new_manager.save().run_sync()
|
219
|
+
Band.update({Band.manager: new_manager.id}, force=True).run_sync()
|
220
|
+
|
221
|
+
# Refresh `band`, and make sure it references the new manager.
|
222
|
+
band.refresh().run_sync()
|
223
|
+
self.assertEqual(band.manager.id, new_manager.id)
|
224
|
+
self.assertEqual(band.manager.name, "New Manager")
|
225
|
+
|
226
|
+
def test_foreign_key_set_to_null(self):
|
227
|
+
"""
|
228
|
+
Make sure that if the foreign key was set to null, that ``refresh``
|
229
|
+
sets the nested object to ``None``.
|
230
|
+
"""
|
231
|
+
band = (
|
232
|
+
Band.objects(Band.manager)
|
233
|
+
.where(Band.name == "Pythonistas")
|
234
|
+
.first()
|
235
|
+
.run_sync()
|
236
|
+
)
|
237
|
+
assert band is not None
|
238
|
+
|
239
|
+
# Remove the manager from band
|
240
|
+
Band.update({Band.manager: None}, force=True).run_sync()
|
241
|
+
|
242
|
+
# Refresh `band`, and make sure the foreign key value is now `None`,
|
243
|
+
# instead of a nested object.
|
244
|
+
band.refresh().run_sync()
|
245
|
+
self.assertIsNone(band.manager)
|
246
|
+
|
247
|
+
def test_exception(self) -> None:
|
248
|
+
"""
|
249
|
+
We don't currently let the user refresh specific fields from nested
|
250
|
+
objects - an exception should be raised.
|
251
|
+
"""
|
252
|
+
with self.assertRaises(ValueError):
|
253
|
+
self.concert.refresh(columns=[Concert.band_1._.manager]).run_sync()
|
254
|
+
|
255
|
+
# Shouldn't raise an exception:
|
256
|
+
self.concert.refresh(columns=[Concert.band_1]).run_sync()
|
257
|
+
|
258
|
+
|
259
|
+
class TestRefreshWithLoadJSON(TableTest):
|
260
|
+
|
261
|
+
tables = [RecordingStudio]
|
262
|
+
|
263
|
+
def setUp(self):
|
264
|
+
super().setUp()
|
265
|
+
|
266
|
+
self.recording_studio = RecordingStudio(
|
267
|
+
{RecordingStudio.facilities: {"piano": True}}
|
268
|
+
)
|
269
|
+
self.recording_studio.save().run_sync()
|
270
|
+
|
271
|
+
def test_load_json(self):
|
272
|
+
"""
|
273
|
+
Make sure we can refresh an object, and load the JSON as a Python
|
274
|
+
object.
|
275
|
+
"""
|
276
|
+
RecordingStudio.update(
|
277
|
+
{RecordingStudio.facilities: {"electric piano": True}},
|
278
|
+
force=True,
|
279
|
+
).run_sync()
|
280
|
+
|
281
|
+
# Refresh without load_json:
|
282
|
+
self.recording_studio.refresh().run_sync()
|
283
|
+
|
284
|
+
self.assertEqual(
|
285
|
+
# Remove the white space, because some versions of Python add
|
286
|
+
# whitespace around JSON, and some don't.
|
287
|
+
self.recording_studio.facilities.replace(" ", ""),
|
288
|
+
'{"electricpiano":true}',
|
289
|
+
)
|
290
|
+
|
291
|
+
# Refresh with load_json:
|
292
|
+
self.recording_studio.refresh(load_json=True).run_sync()
|
293
|
+
|
294
|
+
self.assertDictEqual(
|
295
|
+
t.cast(dict, self.recording_studio.facilities),
|
296
|
+
{"electric piano": True},
|
297
|
+
)
|
File without changes
|
File without changes
|
File without changes
|