clear-skies 1.21.0__py3-none-any.whl → 1.21.2__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.

Potentially problematic release.


This version of clear-skies might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: clear-skies
3
- Version: 1.21.0
3
+ Version: 1.21.2
4
4
  Summary: A framework for building backends in the cloud
5
5
  Home-page: https://github.com/cmancone/clearskies
6
6
  License: MIT
@@ -25,6 +25,7 @@ Requires-Dist: jose (>=1.0.0,<2.0.0) ; extra == "jose"
25
25
  Requires-Dist: jwcrypto (>=1.5.6,<2.0.0) ; extra == "jwcrypto"
26
26
  Requires-Dist: pymysql (>=1.1.0,<2.0.0) ; extra == "mysql"
27
27
  Requires-Dist: requests (>=2.31.0,<3.0.0)
28
+ Requires-Dist: typing-extensions (>=4.12.0,<5.0.0) ; python_version >= "3.10" and python_version < "3.11"
28
29
  Project-URL: Repository, https://github.com/cmancone/clearskies
29
30
  Description-Content-Type: text/markdown
30
31
 
@@ -59,7 +59,7 @@ clearskies/column_types/__init__.py,sha256=wofhLfyW00I6tb6o9DMsMx7j9hlbbqefhDzWf
59
59
  clearskies/column_types/audit.py,sha256=2YcrZVVElpOUdmxYTQ_6CshL1HVou6fz65dOOs_b8Gw,9659
60
60
  clearskies/column_types/belongs_to.py,sha256=tH1tbTOfjifSNuVjO-KbMF7GiUIoLfcDItrrS3TGGM8,11044
61
61
  clearskies/column_types/boolean.py,sha256=1yyM1CUfgD84pPE65c1OP1Qjf_J0Z45hjPrDR51AUkQ,1878
62
- clearskies/column_types/category_tree.py,sha256=PgNmzZPyqYS5NADH_QTCxLvDXZFxzv5ESKTkvPrrLXo,9140
62
+ clearskies/column_types/category_tree.py,sha256=5Wt3W2c2CvqiURqlFBORMvK0TK6qAhgzhx3wP8WfJSc,13167
63
63
  clearskies/column_types/column.py,sha256=ftuDFswjk-KE9Frxo1rhgkjr4sjSjnUc5ZtfNrnGLIc,15530
64
64
  clearskies/column_types/created.py,sha256=LIWSzPJ9rbHuk1u53pNvKtVCOG9y9XCn-mEEsSi97Zc,649
65
65
  clearskies/column_types/created_by_authorization_data.py,sha256=--1w1TOSo2CMwrpn6Y_iorl2RTqLgG8MbR8k27qreew,1108
@@ -68,7 +68,7 @@ clearskies/column_types/created_by_ip.py,sha256=wwCUoEwHEVGN89x4xP7NJ6QR85Aum6v3
68
68
  clearskies/column_types/created_by_routing_data.py,sha256=EhVorRaGV2OhEb0YSPwPmrsK2NQycYgGEd4ab8-qI2I,569
69
69
  clearskies/column_types/created_by_user_agent.py,sha256=sSYDRrqSjsCwcYlhF_s9NO-iDww3PaH6aO2ATp_SKGQ,419
70
70
  clearskies/column_types/created_micro.py,sha256=Y_YADPg-TYPfWCFuGbs1b7NeM1Ixi_oBP8OQfJgTI7s,670
71
- clearskies/column_types/datetime.py,sha256=VCVLDTxEN1yQ0h-BprejKWCYSOneQkvm7EeKy3c_T-Q,4339
71
+ clearskies/column_types/datetime.py,sha256=xtuZpUC9fA16i1oO80kPIx--8RDPuei9RdsDDqensbw,4340
72
72
  clearskies/column_types/datetime_micro.py,sha256=ewQSniCc2MmNIyX2XNuNcCIwh5Fpf1HcvpLfzB8lz8g,382
73
73
  clearskies/column_types/email.py,sha256=qq0Yo_C3KxUqT68q2HWXocBBR4xwMqjxcIdgZRv218U,584
74
74
  clearskies/column_types/float.py,sha256=j8jJeBueSOusPtAFCWgLHYBncfLnqT1U7bh1zcAkYiA,1332
@@ -120,7 +120,7 @@ clearskies/di/__init__.py,sha256=T7SgQNny2XAZQPeFkdmp1XxxmEVxtnpcRiGK8YflkwU,304
120
120
  clearskies/di/additional_config.py,sha256=jdoS_HWC0MAabori3WwLRAG1i5YKZmQfQ1o0hCoxsPs,526
121
121
  clearskies/di/additional_config_auto_import.py,sha256=m57IODPbnCAus9iDu3mDp42u4H87oPZvjAlBGoS8uRQ,111
122
122
  clearskies/di/di.py,sha256=g0U0PI73eNp0mkGH3KUN1fmqNic5eEUK-_IB8hQh-Kg,15511
123
- clearskies/di/standard_dependencies.py,sha256=fgD4GT4Z0bcGPBaPDrzCqCtblD0UP572XD5d0O_DJxE,4523
123
+ clearskies/di/standard_dependencies.py,sha256=NW8tNZ2Punwq70XaH_e9IhmQ7ggsFFvum8ymwinoD7U,4522
124
124
  clearskies/di/test_module/__init__.py,sha256=7YHQF7JHP0FdI7GdEGANSZ_t1EISQYhUNm1wqOg0NKw,88
125
125
  clearskies/di/test_module/another_module/__init__.py,sha256=8SRmHPDepLKGWTUSc1ucDF6U8mJPsNDsBDmBQCpzPWo,35
126
126
  clearskies/di/test_module/module_class.py,sha256=I_-wnMuHfbsvti-7d2Z4bXnr6deo__uvww9nds9qrlE,46
@@ -181,8 +181,8 @@ clearskies/input_requirements/unique.py,sha256=gpbm9uoXcy8WCHsuWqAotwockbjDfJOWi
181
181
  clearskies/mocks/__init__.py,sha256=T68OUB9gGCX0WoisGzsY3Bt2cCFX7ILHKPqi6XKTJM0,113
182
182
  clearskies/mocks/input_output.py,sha256=2wD5GbUyVSkXcBg1GTZ-Oz9VzcYxNHfTlmZAODW-7CI,3898
183
183
  clearskies/mocks/models.py,sha256=DCzsnMddBvPoBA8JwwbSOhzY7enQWrosgeYD4gx2deI,5124
184
- clearskies/model.py,sha256=6H6eGQqMBz22bWu20N4xy7XPa1qYgfvNWik5cXH2DZg,13137
185
- clearskies/models.py,sha256=WJUUPSJ1rhDvAv1DpNDw0igNmhpn7PlZGh0f_g9sl5E,12766
184
+ clearskies/model.py,sha256=N5el03awXEfTBfhqjS3Yuc6ozMjATs7cMLs_IoZNuaE,13511
185
+ clearskies/models.py,sha256=1H5Vohv1U4avN5_YHqULzc7ynZDRytmZ56x775xWTIo,13038
186
186
  clearskies/secrets/__init__.py,sha256=ctTmA_etV9G_5U21APWENI1HvThrBS4DidGWRtEDHQs,1053
187
187
  clearskies/secrets/additional_configs/__init__.py,sha256=cFCrbtKF5nuR061S2y1iKZp349x-y8Srdwe3VZbfSFU,1119
188
188
  clearskies/secrets/additional_configs/mysql_connection_dynamic_producer.py,sha256=fOt2eOrVtQXhnK05XuSfWw9GoX6klxXLisJcFT0ycAE,2562
@@ -205,7 +205,7 @@ clearskies/tests/simple_api/models/__init__.py,sha256=nUA0W6fgXw_Bxa9CudkaDkC80t
205
205
  clearskies/tests/simple_api/models/status.py,sha256=PEhPbaQh5qdUNHp8O0gz91LOLENAEBtqSaHxUPXchaM,699
206
206
  clearskies/tests/simple_api/models/user.py,sha256=5_P4Tp1tTdX7PkMJ__epPM5MA7JAeVYGas69vcWloLc,819
207
207
  clearskies/tests/simple_api/users_api.py,sha256=KYXCgEofDxHeRdQK67txN5oYUPvxxmB8JTku7L-apk4,2344
208
- clear_skies-1.21.0.dist-info/LICENSE,sha256=3Ehd0g3YOpCj8sqj0Xjq5qbOtjjgk9qzhhD9YjRQgOA,1053
209
- clear_skies-1.21.0.dist-info/METADATA,sha256=8c2Jewv9FWljdC_KSg9V1ONMAfdE2EPw5JdGntdVSNQ,1711
210
- clear_skies-1.21.0.dist-info/WHEEL,sha256=d2fvjOD7sXsVzChCqf0Ty0JbHKBaLYwDbGQDwQTnJ50,88
211
- clear_skies-1.21.0.dist-info/RECORD,,
208
+ clear_skies-1.21.2.dist-info/LICENSE,sha256=3Ehd0g3YOpCj8sqj0Xjq5qbOtjjgk9qzhhD9YjRQgOA,1053
209
+ clear_skies-1.21.2.dist-info/METADATA,sha256=lvnx3rHgpMbUzIpeFsfGbjnLa98Bx4mHMMbHmRXfUkk,1817
210
+ clear_skies-1.21.2.dist-info/WHEEL,sha256=d2fvjOD7sXsVzChCqf0Ty0JbHKBaLYwDbGQDwQTnJ50,88
211
+ clear_skies-1.21.2.dist-info/RECORD,,
@@ -119,6 +119,10 @@ class CategoryTree(BelongsTo):
119
119
  "tree_level_column_name",
120
120
  "max_iterations",
121
121
  "parent_models_class",
122
+ "children_column_name",
123
+ "descendents_column_name",
124
+ "ancestors_column_name",
125
+ "load_relatives_strategy",
122
126
  ]
123
127
 
124
128
  def __init__(self, di):
@@ -137,6 +141,11 @@ class CategoryTree(BelongsTo):
137
141
  configuration["tree_models_class"],
138
142
  config_name="tree_models_class",
139
143
  )
144
+ load_relatives_strategy = configuration.get("load_relatives_strategy", None)
145
+ if load_relatives_strategy and load_relatives_strategy not in ["join", "where_in", "individual"]:
146
+ raise ValueError(
147
+ f"Configuration error for category_tree column '{self.name} in model class '{self.model_class.__name__}': load_relatives_strategy must be one of ['join', 'where_in', or 'individual']"
148
+ )
140
149
 
141
150
  def _finalize_configuration(self, configuration):
142
151
  return {
@@ -152,6 +161,10 @@ class CategoryTree(BelongsTo):
152
161
  "tree_is_parent_column_name": configuration.get("tree_is_parent_column_name", "is_parent"),
153
162
  "tree_level_column_name": configuration.get("tree_level_column_name", "level"),
154
163
  "max_iterations": configuration.get("max_iterations", 100),
164
+ "children_column_name": configuration.get("children_column_name", "children"),
165
+ "descendents_column_name": configuration.get("descendents_column_name", "descendents"),
166
+ "ancestors_column_name": configuration.get("ancestors_column_name", "ancestors"),
167
+ "load_relatives_strategy": configuration.get("load_relatives_strategy", "join"),
155
168
  },
156
169
  }
157
170
 
@@ -167,7 +180,7 @@ class CategoryTree(BelongsTo):
167
180
  return data
168
181
 
169
182
  def force_tree_update(self, model):
170
- self.update_tree_table(model, model.id, model.__getattr__(self.name))
183
+ self.update_tree_table(model, model.get(self.id_column_name), model.__getattr__(self.name))
171
184
 
172
185
  def update_tree_table(self, model, child_id, direct_parent_id):
173
186
  tree_models = self.tree_models
@@ -224,3 +237,68 @@ class CategoryTree(BelongsTo):
224
237
  + "You may have accidentally created a circular cateogry tree. If not, and your category tree "
225
238
  + "really _is_ that deep, then adjust the 'max_iterations' configuration for this column accordingly. "
226
239
  )
240
+
241
+ def can_provide(self, column_name):
242
+ return column_name in [
243
+ self.config("model_column_name"),
244
+ self.config("children_column_name"),
245
+ self.config("descendents_column_name"),
246
+ self.config("ancestors_column_name"),
247
+ ]
248
+
249
+ def provide(self, data, column_name):
250
+ if column_name == self.config("model_column_name"):
251
+ return super().provide(data, column_name)
252
+
253
+ if column_name == self.config("children_column_name"):
254
+ return self.relatives(data)
255
+
256
+ if column_name == self.config("descendents_column_name"):
257
+ return self.relatives(data, include_all=True)
258
+
259
+ if column_name == self.config("ancestors_column_name"):
260
+ return self.relatives(data, find_parents=True)
261
+
262
+ def relatives(self, data, include_all=False, find_parents=False):
263
+ id_column_name = self.model_class.id_column_name
264
+ model_id = data[self.model_class.id_column_name]
265
+ model_table_name = self.model_class.table_name()
266
+ tree_table_name = self.config("tree_models_class").table_name()
267
+ parent_id_column_name = self.config("tree_parent_id_column_name")
268
+ child_id_column_name = self.config("tree_child_id_column_name")
269
+ is_parent_column_name = self.config("is_parent_column_name")
270
+ level_column_name = self.config("tree_level_column_name")
271
+
272
+ if find_parents:
273
+ join_on = parent_id_column_name
274
+ search_on = child_id_column_name
275
+ else:
276
+ join_on = child_id_column_name
277
+ search_on = parent_id_column_name
278
+
279
+ # if we can join then use a join.
280
+ if self.config("load_relatives_strategy") == "join":
281
+ relatives = self.parent_models.join(
282
+ f"{tree_table_name} as tree on tree.{join_on}={model_table_name}.{id_column_name}"
283
+ )
284
+ relatives = relatives.where(f"tree.{search_on}={model_id}")
285
+ if not include_all:
286
+ relatives = relatives.where(f"tree.{is_parent_column_name}=1")
287
+ if find_parents:
288
+ relatives = relatives.sort_by(level_column_name, "asc")
289
+ return relatives
290
+
291
+ # joins only work for SQL-like backends. Otherwise, we have to pull out our list of ids
292
+ branches = self.tree_models.where(f"{search_on}={model_id}")
293
+ if not include_all:
294
+ branches = branches.where(f"tree.{is_parent_column_name}=1")
295
+ if find_parents:
296
+ branches = branches.sort_by(level_column_name, "asc")
297
+ ids = [str(branch.get(child_id_column_name)) for branch in branches]
298
+
299
+ # Can we search with a WHERE IN() clause? If the backend supports it, it is probably faster
300
+ if self.config("load_relatives_strategy") == "where_in":
301
+ return self.parent_models.where(f"{id_column_name} IN ('" + "','".join(ids) + "')")
302
+
303
+ # otherwise we have to load each model individually which is SLOW....
304
+ return [self.parent_models.find(f"{id_column_name}={id}") for id in ids]
@@ -3,6 +3,7 @@ from datetime import datetime, timezone
3
3
  import dateparser
4
4
  from ..autodoc.schema import DateTime as AutoDocDateTime
5
5
 
6
+
6
7
  class DateTime(Column):
7
8
  _auto_doc_class = AutoDocDateTime
8
9
  _date_format = "%Y-%m-%d %H:%M:%S"
@@ -138,8 +138,9 @@ class StandardDependencies(DI):
138
138
 
139
139
  def provide_uuid(self):
140
140
  return uuid
141
-
141
+
142
142
  def provide_timezone(self):
143
143
  """Set the default timezone."""
144
144
  import datetime
145
+
145
146
  return datetime.UTC
clearskies/model.py CHANGED
@@ -5,6 +5,11 @@ from .functional import string
5
5
  import re
6
6
  from .models import Models
7
7
 
8
+ try:
9
+ from typing_extensions import Self
10
+ except ModuleNotFoundError:
11
+ from typing import Self
12
+
8
13
 
9
14
  class Model(Models):
10
15
  _configured_columns = None
@@ -14,14 +19,14 @@ class Model(Models):
14
19
  _transformed = None
15
20
  id_column_name = "id"
16
21
 
17
- def __init__(self, backend, columns):
22
+ def __init__(self: Self, backend, columns):
18
23
  super().__init__(backend, columns)
19
24
  self._transformed = {}
20
25
  self._data = {}
21
26
  self._previous_data = None
22
27
  self._touched_columns = None
23
28
 
24
- def model_class(self):
29
+ def model_class(self: Self) -> type[Self]:
25
30
  """
26
31
  Return the model class that this models object will find/return instances of
27
32
 
@@ -30,7 +35,7 @@ class Model(Models):
30
35
  return self.__class__
31
36
 
32
37
  @classmethod
33
- def table_name(cls):
38
+ def table_name(cls: type[Self]) -> str:
34
39
  """Return the name of the table that the model uses for data storage"""
35
40
  singular = string.camel_case_to_snake_case(cls.__name__)
36
41
  if singular[-1] == "y":
@@ -40,16 +45,16 @@ class Model(Models):
40
45
  return f"{singular}s"
41
46
 
42
47
  @abstractmethod
43
- def columns_configuration(self):
48
+ def columns_configuration(self: Self):
44
49
  """Returns an ordered dictionary with the configuration for the columns"""
45
50
  pass
46
51
 
47
- def all_columns(self):
52
+ def all_columns(self: Self):
48
53
  default = OrderedDict([(self.id_column_name, {"class": UUID})])
49
54
  default.update(self.columns_configuration())
50
55
  return default
51
56
 
52
- def columns(self, overrides=None):
57
+ def columns(self: Self, overrides=None):
53
58
  # no caching if we have overrides
54
59
  if overrides is not None:
55
60
  return self._columns.configure(self.all_columns(), self.__class__, overrides=overrides)
@@ -58,13 +63,13 @@ class Model(Models):
58
63
  self._configured_columns = self._columns.configure(self.all_columns(), self.__class__)
59
64
  return self._configured_columns
60
65
 
61
- def supports_n_plus_one(self):
66
+ def supports_n_plus_one(self: Self):
62
67
  return self._backend.supports_n_plus_one
63
68
 
64
- def __getitem__(self, column_name):
69
+ def __getitem__(self: Self, column_name):
65
70
  return self.__getattr__(column_name)
66
71
 
67
- def __getattr__(self, column_name):
72
+ def __getattr__(self: Self, column_name):
68
73
  # this should be adjusted to only return None for empty records if the column name corresponds
69
74
  # to an actual column in the table.
70
75
  if not self.exists:
@@ -72,13 +77,13 @@ class Model(Models):
72
77
 
73
78
  return self.get_transformed_from_data(column_name, self._data)
74
79
 
75
- def get(self, column_name, silent=False):
80
+ def get(self: Self, column_name, silent=False):
76
81
  if not self.exists:
77
82
  return None
78
83
 
79
84
  return self.get_transformed_from_data(column_name, self._data, silent=silent)
80
85
 
81
- def get_transformed_from_data(self, column_name, data, cache=True, check_providers=True, silent=False):
86
+ def get_transformed_from_data(self: Self, column_name, data, cache=True, check_providers=True, silent=False):
82
87
  if cache and column_name in self._transformed:
83
88
  return self._transformed[column_name]
84
89
 
@@ -111,18 +116,18 @@ class Model(Models):
111
116
  return value
112
117
 
113
118
  @property
114
- def exists(self):
119
+ def exists(self: Self) -> bool:
115
120
  return True if (self.id_column_name in self._data and self._data[self.id_column_name]) else False
116
121
 
117
122
  @property
118
- def data(self):
123
+ def data(self: Self):
119
124
  return self._data
120
125
 
121
126
  @data.setter
122
- def data(self, data):
127
+ def data(self: Self, data) -> None:
123
128
  self._data = {} if data is None else data
124
129
 
125
- def save(self, data, columns=None):
130
+ def save(self: Self, data, columns=None) -> bool:
126
131
  """
127
132
  Save data to the database and update the model!
128
133
 
@@ -168,7 +173,7 @@ class Model(Models):
168
173
 
169
174
  return True
170
175
 
171
- def is_changing(self, key, data):
176
+ def is_changing(self: Self, key, data) -> bool:
172
177
  """
173
178
  Returns True/False to denote if the given column is being modified by the active save operation
174
179
 
@@ -184,7 +189,7 @@ class Model(Models):
184
189
 
185
190
  return self.__getattr__(key) != data[key]
186
191
 
187
- def latest(self, key, data):
192
+ def latest(self: Self, key, data):
188
193
  """
189
194
  Returns the 'latest' value for a column during the save operation
190
195
 
@@ -198,7 +203,7 @@ class Model(Models):
198
203
  return data[key]
199
204
  return self.__getattr__(key)
200
205
 
201
- def was_changed(self, key):
206
+ def was_changed(self: Self, key) -> bool:
202
207
  """Returns True/False to denote if a column was changed in the last save"""
203
208
  if self._previous_data is None:
204
209
  raise ValueError("was_changed was called before a save was finished - you must save something first")
@@ -221,10 +226,10 @@ class Model(Models):
221
226
  return old_value != new_value
222
227
  return not columns[key].values_match(old_value, new_value)
223
228
 
224
- def previous_value(self, key):
229
+ def previous_value(self: Self, key):
225
230
  return self.get_transformed_from_data(key, self._previous_data, cache=False, check_providers=False, silent=True)
226
231
 
227
- def delete(self, except_if_not_exists=True):
232
+ def delete(self: Self, except_if_not_exists=True) -> bool:
228
233
  if not self.exists:
229
234
  if except_if_not_exists:
230
235
  raise ValueError("Cannot delete model that already exists")
@@ -240,7 +245,7 @@ class Model(Models):
240
245
  self.post_delete()
241
246
  return True
242
247
 
243
- def columns_pre_save(self, data, columns):
248
+ def columns_pre_save(self: Self, data, columns):
244
249
  """Uses the column information present in the model to make any necessary changes before saving"""
245
250
  for column in columns.values():
246
251
  data = column.pre_save(data, self)
@@ -258,7 +263,7 @@ class Model(Models):
258
263
  """
259
264
  return data
260
265
 
261
- def columns_to_backend(self, data, columns):
266
+ def columns_to_backend(self: Self, data, columns):
262
267
  backend_data = {**data}
263
268
  temporary_data = {}
264
269
  for column in columns.values():
@@ -275,10 +280,10 @@ class Model(Models):
275
280
 
276
281
  return [backend_data, temporary_data]
277
282
 
278
- def to_backend(self, data, columns):
283
+ def to_backend(self: Self, data, columns):
279
284
  return data
280
285
 
281
- def columns_post_save(self, data, id, columns):
286
+ def columns_post_save(self: Self, data, id, columns):
282
287
  """Uses the column information present in the model to make additional changes as needed after saving"""
283
288
  for column in columns.values():
284
289
  data = column.post_save(data, self, id)
@@ -288,12 +293,12 @@ class Model(Models):
288
293
  )
289
294
  return data
290
295
 
291
- def columns_save_finished(self, columns):
296
+ def columns_save_finished(self: Self, columns):
292
297
  """Calls the save_finished method on all of our columns"""
293
298
  for column in columns.values():
294
299
  column.save_finished(self)
295
300
 
296
- def post_save(self, data, id):
301
+ def post_save(self: Self, data, id):
297
302
  """
298
303
  A hook to extend so you can provide additional pre-save logic as needed
299
304
 
@@ -302,7 +307,7 @@ class Model(Models):
302
307
  """
303
308
  pass
304
309
 
305
- def pre_save(self, data):
310
+ def pre_save(self: Self, data):
306
311
  """
307
312
  A hook to extend so you can provide additional pre-save logic as needed
308
313
 
@@ -310,7 +315,7 @@ class Model(Models):
310
315
  """
311
316
  return data
312
317
 
313
- def save_finished(self):
318
+ def save_finished(self: Self):
314
319
  """
315
320
  A hook to extend so you can provide additional logic after a save operation has fully completed
316
321
 
@@ -320,29 +325,29 @@ class Model(Models):
320
325
  """
321
326
  pass
322
327
 
323
- def columns_pre_delete(self, columns):
328
+ def columns_pre_delete(self: Self, columns):
324
329
  """Uses the column information present in the model to make any necessary changes before deleting"""
325
330
  for column in columns.values():
326
331
  column.pre_delete(self)
327
332
 
328
- def pre_delete(self):
333
+ def pre_delete(self: Self):
329
334
  """
330
335
  A hook to extend so you can provide additional pre-delete logic as needed
331
336
  """
332
337
  pass
333
338
 
334
- def columns_post_delete(self, columns):
339
+ def columns_post_delete(self: Self, columns):
335
340
  """Uses the column information present in the model to make any necessary changes after deleting"""
336
341
  for column in columns.values():
337
342
  column.post_delete(self)
338
343
 
339
- def post_delete(self):
344
+ def post_delete(self: Self):
340
345
  """
341
346
  A hook to extend so you can provide additional post-delete logic as needed
342
347
  """
343
348
  pass
344
349
 
345
- def where_for_request(self, models, routing_data, authorization_data, input_output, overrides=None):
350
+ def where_for_request(self: Self, models, routing_data, authorization_data, input_output, overrides=None):
346
351
  """
347
352
  A hook to automatically apply filtering whenever the model makes an appearance in a get/update/list/search handler.
348
353
  """
clearskies/models.py CHANGED
@@ -47,30 +47,30 @@ class Models(ABC, ConditionParser):
47
47
  self.query_select_all = True
48
48
 
49
49
  @abstractmethod
50
- def model_class(self):
50
+ def model_class(self: Self) -> type[Self]:
51
51
  """Return the model class that this models object will find/return instances of"""
52
52
  pass
53
53
 
54
- def clone(self) -> Self:
54
+ def clone(self: Self) -> Self:
55
55
  clone = self.blank()
56
56
  clone.query_configuration = self.query_configuration
57
57
  return clone
58
58
 
59
- def blank(self) -> Self:
59
+ def blank(self: Self) -> Self:
60
60
  return self._build_model()
61
61
 
62
- def get_table_name(self) -> str:
62
+ def get_table_name(self: Self) -> str:
63
63
  if self._table_name is None:
64
64
  self._table_name = self.model_class().table_name()
65
65
  return self._table_name
66
66
 
67
- def get_id_column_name(self) -> str:
67
+ def get_id_column_name(self: Self) -> str:
68
68
  if self._id_column_name is None:
69
69
  self._id_column_name = self.empty_model().id_column_name
70
70
  return self._id_column_name
71
71
 
72
72
  @property
73
- def query_configuration(self) -> Dict[str, Any]:
73
+ def query_configuration(self: Self) -> Dict[str, Any]:
74
74
  return {
75
75
  "wheres": [*self.query_wheres],
76
76
  "sorts": [*self.query_sorts],
@@ -85,7 +85,7 @@ class Models(ABC, ConditionParser):
85
85
  }
86
86
 
87
87
  @query_configuration.setter
88
- def query_configuration(self, configuration: Dict[str, Any]):
88
+ def query_configuration(self: Self, configuration: Dict[str, Any]):
89
89
  self.query_wheres = configuration["wheres"]
90
90
  self.query_sorts = configuration["sorts"]
91
91
  self.query_group_by_column = configuration["group_by_column"]
@@ -97,34 +97,34 @@ class Models(ABC, ConditionParser):
97
97
  self._model_columns = configuration["model_columns"]
98
98
 
99
99
  @property
100
- def model_columns(self):
100
+ def model_columns(self: Self):
101
101
  if self._model_columns is None:
102
102
  self._model_columns = self.empty_model().columns()
103
103
  return self._model_columns
104
104
 
105
- def select(self, selects) -> Self:
105
+ def select(self: Self, selects) -> Self:
106
106
  return self.clone().select_in_place(selects)
107
107
 
108
- def select_in_place(self, selects) -> Self:
108
+ def select_in_place(self: Self, selects) -> Self:
109
109
  self.query_selects.append(selects)
110
110
  self.must_rexecute = True
111
111
  self._next_page_data = None
112
112
  return self
113
113
 
114
- def select_all(self, select_all=True) -> Self:
114
+ def select_all(self: Self, select_all=True) -> Self:
115
115
  return self.clone().select_all_in_place(select_all=select_all)
116
116
 
117
- def select_all_in_place(self, select_all=True) -> Self:
117
+ def select_all_in_place(self: Self, select_all=True) -> Self:
118
118
  self.query_select_all = select_all
119
119
  self.must_rexecute = True
120
120
  self._next_page_data = None
121
121
  return self
122
122
 
123
- def where(self, where: str) -> Self:
123
+ def where(self: Self, where: str) -> Self:
124
124
  """Adds the given condition to the query and returns a new Models object"""
125
125
  return self.clone().where_in_place(where)
126
126
 
127
- def where_in_place(self, where: str) -> Self:
127
+ def where_in_place(self: Self, where: str) -> Self:
128
128
  """Adds the given condition to the query for the current Models object"""
129
129
  condition = self.parse_condition(where)
130
130
  self._validate_column(condition["column"], "filter", table=condition["table"])
@@ -134,17 +134,17 @@ class Models(ABC, ConditionParser):
134
134
  self.must_recount = True
135
135
  return self
136
136
 
137
- def join(self, join: str) -> Self:
137
+ def join(self: Self, join: str) -> Self:
138
138
  return self.clone().join_in_place(join)
139
139
 
140
- def join_in_place(self, join: str) -> Self:
140
+ def join_in_place(self: Self, join: str) -> Self:
141
141
  self.query_joins.append(self.parse_join(join))
142
142
  self.must_rexecute = True
143
143
  self._next_page_data = None
144
144
  self.must_recount = True
145
145
  return self
146
146
 
147
- def is_joined(self, table_name, alias=None):
147
+ def is_joined(self: Self, table_name, alias=None):
148
148
  for join in self.query_joins:
149
149
  if join["table"] != table_name:
150
150
  continue
@@ -155,10 +155,10 @@ class Models(ABC, ConditionParser):
155
155
  return join["alias"] if join["alias"] else join["table"]
156
156
  return False
157
157
 
158
- def group_by(self, group_column: str) -> Self:
158
+ def group_by(self: Self, group_column: str) -> Self:
159
159
  return self.clone().group_by_in_place(group_column)
160
160
 
161
- def group_by_in_place(self, group_column: str) -> Self:
161
+ def group_by_in_place(self: Self, group_column: str) -> Self:
162
162
  self._validate_column(group_column, "group")
163
163
  self.query_group_by_column = group_column
164
164
  self.must_rexecute = True
@@ -167,7 +167,7 @@ class Models(ABC, ConditionParser):
167
167
  return self
168
168
 
169
169
  def sort_by(
170
- self,
170
+ self: Self,
171
171
  primary_column,
172
172
  primary_direction,
173
173
  primary_table=None,
@@ -185,7 +185,7 @@ class Models(ABC, ConditionParser):
185
185
  )
186
186
 
187
187
  def sort_by_in_place(
188
- self,
188
+ self: Self,
189
189
  primary_column,
190
190
  primary_direction,
191
191
  primary_table=None,
@@ -205,7 +205,7 @@ class Models(ABC, ConditionParser):
205
205
  self._next_page_data = None
206
206
  return self
207
207
 
208
- def _normalize_and_validate_sort(self, sort):
208
+ def _normalize_and_validate_sort(self: Self, sort):
209
209
  if "column" not in sort or not sort["column"]:
210
210
  raise ValueError("Missing 'column' for sort")
211
211
  if "direction" not in sort or not sort["direction"]:
@@ -218,7 +218,7 @@ class Models(ABC, ConditionParser):
218
218
  # down the line we may ask the model class what columns we can sort on, but we're good for now
219
219
  return {"column": sort["column"], "direction": sort["direction"], "table": sort.get("table")}
220
220
 
221
- def _validate_column(self, column_name, action, table=None):
221
+ def _validate_column(self: Self, column_name, action, table=None):
222
222
  """
223
223
  Down the line we may use the model configuration to check what columns are valid sort/group/search targets
224
224
  """
@@ -246,19 +246,19 @@ class Models(ABC, ConditionParser):
246
246
  + "to your model definition"
247
247
  )
248
248
 
249
- def limit(self, limit) -> Self:
249
+ def limit(self: Self, limit) -> Self:
250
250
  return self.clone().limit_in_place(limit)
251
251
 
252
- def limit_in_place(self, limit) -> Self:
252
+ def limit_in_place(self: Self, limit) -> Self:
253
253
  self.query_limit = limit
254
254
  self.must_rexecute = True
255
255
  self._next_page_data = None
256
256
  return self
257
257
 
258
- def pagination(self, **kwargs) -> Self:
258
+ def pagination(self: Self, **kwargs) -> Self:
259
259
  return self.clone().pagination_in_place(**kwargs)
260
260
 
261
- def pagination_in_place(self, **kwargs) -> Self:
261
+ def pagination_in_place(self: Self, **kwargs) -> Self:
262
262
  error = self._backend.validate_pagination_kwargs(kwargs, str)
263
263
  if error:
264
264
  raise ValueError(
@@ -270,17 +270,17 @@ class Models(ABC, ConditionParser):
270
270
  self._next_page_data = None
271
271
  return self
272
272
 
273
- def find(self, where: str) -> Self:
273
+ def find(self: Self, where: str) -> Self:
274
274
  """Returns the first model where condition"""
275
275
  return self.clone().where(where).first()
276
276
 
277
- def __len__(self):
277
+ def __len__(self: Self):
278
278
  if self.must_recount:
279
279
  self.count = self._backend.count(self.query_configuration, self.empty_model())
280
280
  self.must_recount = False
281
281
  return self.count
282
282
 
283
- def __iter__(self) -> Iterator[Self]:
283
+ def __iter__(self: Self) -> Iterator[Self]:
284
284
  self._next_page_data = {}
285
285
  raw_rows = self._backend.records(
286
286
  self.query_configuration,
@@ -290,7 +290,7 @@ class Models(ABC, ConditionParser):
290
290
  models = iter([self.model(row) for row in raw_rows])
291
291
  return models
292
292
 
293
- def paginate_all(self) -> List[Self]:
293
+ def paginate_all(self: Self) -> List[Self]:
294
294
  next_models = self.clone()
295
295
  results = list(next_models.__iter__())
296
296
  next_page_data = next_models.next_page_data()
@@ -300,51 +300,51 @@ class Models(ABC, ConditionParser):
300
300
  next_page_data = next_models.next_page_data()
301
301
  return results
302
302
 
303
- def model(self, data) -> Self:
303
+ def model(self: Self, data) -> Self:
304
304
  model = self._build_model()
305
305
  model.data = data
306
306
  return model
307
307
 
308
- def _build_model(self) -> Self:
308
+ def _build_model(self: Self) -> Self:
309
309
  model_class = self.model_class()
310
310
  return model_class(self._backend, self._columns)
311
311
 
312
- def empty_model(self) -> Self:
312
+ def empty_model(self: Self) -> Self:
313
313
  return self.model({})
314
314
 
315
- def create(self, data: Dict[str, Any]) -> Self:
315
+ def create(self: Self, data: Dict[str, Any]) -> Self:
316
316
  empty = self.empty_model()
317
317
  empty.save(data)
318
318
  return empty
319
319
 
320
- def first(self) -> Self:
320
+ def first(self: Self) -> Self:
321
321
  iter = self.__iter__()
322
322
  try:
323
323
  return iter.__next__()
324
324
  except StopIteration:
325
325
  return self.empty_model()
326
326
 
327
- def columns(self, overrides=None):
327
+ def columns(self: Self, overrides=None):
328
328
  model = self.model({})
329
329
  return model.columns(overrides=None)
330
330
 
331
- def raw_columns_configuration(self):
331
+ def raw_columns_configuration(self: Self):
332
332
  return self.model({}).all_columns()
333
333
 
334
- def allowed_pagination_keys(self) -> List[str]:
334
+ def allowed_pagination_keys(self: Self) -> List[str]:
335
335
  return self._backend.allowed_pagination_keys()
336
336
 
337
337
  def validate_pagination_kwargs(self, kwargs: Dict[str, Any], case_mapping: Callable) -> str:
338
338
  return self._backend.validate_pagination_kwargs(kwargs, case_mapping)
339
339
 
340
- def next_page_data(self):
340
+ def next_page_data(self: Self):
341
341
  return self._next_page_data
342
342
 
343
- def documentation_pagination_next_page_response(self, case_mapping: Callable) -> List[Any]:
343
+ def documentation_pagination_next_page_response(self: Self, case_mapping: Callable) -> List[Any]:
344
344
  return self._backend.documentation_pagination_next_page_response(case_mapping)
345
345
 
346
- def documentation_pagination_next_page_example(self, case_mapping: Callable) -> Dict[str, Any]:
346
+ def documentation_pagination_next_page_example(self: Self, case_mapping: Callable) -> Dict[str, Any]:
347
347
  return self._backend.documentation_pagination_next_page_example(case_mapping)
348
348
 
349
- def documentation_pagination_parameters(self, case_mapping: Callable) -> List[Tuple[Any]]:
349
+ def documentation_pagination_parameters(self: Self, case_mapping: Callable) -> List[Tuple[Any]]:
350
350
  return self._backend.documentation_pagination_parameters(case_mapping)