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 CHANGED
@@ -1 +1 @@
1
- __VERSION__ = "1.14.0"
1
+ __VERSION__ = "1.15.0"
@@ -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("The password is too short.")
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("The password is too long.")
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
 
@@ -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
- instance: Table
29
- columns: t.Optional[t.Sequence[Column]] = None
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
- updated_values = (
75
- await instance.__class__.select(*columns)
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 updated_values is None:
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
- for key, value in updated_values.items():
87
- setattr(instance, key, value)
168
+ self._update_instance(instance=instance, data_dict=data_dict)
88
169
 
89
170
  return instance
90
171
 
@@ -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, columns: t.Optional[t.Sequence[Column]] = None
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
- return orjson.loads(data) if ORJSON else json.loads(data) # type: ignore
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,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: piccolo
3
- Version: 1.14.0
3
+ Version: 1.15.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
@@ -1,10 +1,10 @@
1
- piccolo/__init__.py,sha256=vQd7XyktulZvg_rtkaOpG-2W02Gb_wFd8aev4i-MHeA,23
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=IfXT9rtm1sBN-u_A8-_0gj6fJf3_RGSPtWMJa-FX9jw,49569
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=ZSXtOC9OANCKC6cq5ZCj3CqRwraibFDwtf1VVIBfmKQ,8954
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=2eyfgqdzl5bKQBRpmxil_HoXpCEDGGrh84q62ZmfBNk,14680
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=P1Eo_HYU_L7kcGM_cvDDgyLi1boCXY7Pc4tv_eDAzvc,2769
172
- piccolo/query/methods/select.py,sha256=KDbLRkADM9xNQsoNYN_eNSwapFACQCUNiCvw0B5tmko,21315
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=W34oj1F2f8zeirMceHZnAnJL2T8rPoiqXt-DJ-hzRGk,835
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=E9mGAOiDjVtIJYqZzlvPU_JFz6RZzGAVeE0Lk5VjGb4,9523
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=LtNsHQ8xAzBFLiZVZhWEB56zu25FnaWtzJ62FZH3heI,12647
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=ZXGLGHeMZcWnhZPB4eCasv1RkojPt6nUbxaE7WlyJbo,2804
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.14.0.dist-info/LICENSE,sha256=zFIpi-16uIJ420UMIG75NU0JbDBykvrdnXcj5U_EYBI,1059
369
- piccolo-1.14.0.dist-info/METADATA,sha256=4rDBz-Wbqp-MdHwcZd3LaxJToiNqW5hVFQkCOkCD8LU,5178
370
- piccolo-1.14.0.dist-info/WHEEL,sha256=rWxmBtp7hEUqVLOnTaDOPpR-cZpCDkzhhcBce-Zyd5k,91
371
- piccolo-1.14.0.dist-info/entry_points.txt,sha256=SJPHET4Fi1bN5F3WqcKkv9SClK3_F1I7m4eQjk6AFh0,46
372
- piccolo-1.14.0.dist-info/top_level.txt,sha256=-SR74VGbk43VoPy1HH-mHm97yoGukLK87HE5kdBW6qM,24
373
- piccolo-1.14.0.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (71.0.4)
2
+ Generator: setuptools (72.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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
- "The password is too short.",
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__(), "The password is too short."
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__(), "The password is too long."
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):
@@ -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),
@@ -1,5 +1,13 @@
1
- from tests.base import DBTestCase
2
- from tests.example_apps.music.tables import Band
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.assertTrue(band.name == "Pythonistas!!!")
28
- self.assertTrue(band.popularity == 8000)
29
- self.assertTrue(band.id == initial_data["id"])
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.assertTrue(band.name == "Pythonistas!!!")
54
- self.assertTrue(band.popularity == initial_data["popularity"])
55
- self.assertTrue(band.id == initial_data["id"])
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
+ )