clear-skies 2.0.1__py3-none-any.whl → 2.0.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.
- {clear_skies-2.0.1.dist-info → clear_skies-2.0.2.dist-info}/METADATA +1 -1
- {clear_skies-2.0.1.dist-info → clear_skies-2.0.2.dist-info}/RECORD +65 -65
- clearskies/__init__.py +5 -8
- clearskies/authentication/jwks.py +2 -2
- clearskies/authentication/secret_bearer.py +2 -2
- clearskies/backends/api_backend.py +2 -2
- clearskies/backends/secrets_backend.py +0 -1
- clearskies/column.py +6 -2
- clearskies/columns/audit.py +3 -2
- clearskies/columns/belongs_to_id.py +2 -2
- clearskies/columns/belongs_to_model.py +6 -2
- clearskies/columns/belongs_to_self.py +2 -2
- clearskies/columns/boolean.py +6 -2
- clearskies/columns/category_tree.py +2 -2
- clearskies/columns/category_tree_children.py +2 -2
- clearskies/columns/created.py +3 -2
- clearskies/columns/created_by_authorization_data.py +2 -2
- clearskies/columns/created_by_header.py +2 -2
- clearskies/columns/created_by_ip.py +2 -2
- clearskies/columns/created_by_routing_data.py +3 -2
- clearskies/columns/created_by_user_agent.py +2 -2
- clearskies/columns/date.py +6 -2
- clearskies/columns/datetime.py +6 -2
- clearskies/columns/float.py +6 -2
- clearskies/columns/has_many.py +2 -2
- clearskies/columns/has_many_self.py +2 -2
- clearskies/columns/integer.py +6 -2
- clearskies/columns/json.py +6 -2
- clearskies/columns/many_to_many_ids.py +6 -2
- clearskies/columns/many_to_many_ids_with_data.py +6 -2
- clearskies/columns/many_to_many_models.py +6 -2
- clearskies/columns/many_to_many_pivots.py +3 -2
- clearskies/columns/phone.py +3 -2
- clearskies/columns/select.py +3 -2
- clearskies/columns/string.py +4 -0
- clearskies/columns/timestamp.py +6 -2
- clearskies/columns/updated.py +2 -2
- clearskies/columns/uuid.py +2 -2
- clearskies/configs/__init__.py +1 -1
- clearskies/{parameters_to_properties.py → decorators.py} +2 -0
- clearskies/di/injectable_properties.py +2 -2
- clearskies/endpoint.py +2 -2
- clearskies/endpoint_group.py +1 -1
- clearskies/endpoints/callable.py +1 -1
- clearskies/endpoints/create.py +1 -1
- clearskies/endpoints/delete.py +1 -1
- clearskies/endpoints/get.py +1 -1
- clearskies/endpoints/health_check.py +1 -1
- clearskies/endpoints/list.py +1 -1
- clearskies/endpoints/restful_api.py +2 -2
- clearskies/endpoints/simple_search.py +1 -1
- clearskies/endpoints/update.py +1 -1
- clearskies/model.py +691 -37
- clearskies/security_headers/cache_control.py +2 -2
- clearskies/security_headers/cors.py +2 -2
- clearskies/security_headers/csp.py +2 -2
- clearskies/security_headers/hsts.py +2 -2
- clearskies/validators/after_column.py +2 -2
- clearskies/validators/in_the_future.py +1 -1
- clearskies/validators/in_the_past.py +1 -1
- clearskies/validators/required.py +0 -1
- clearskies/validators/timedelta.py +2 -2
- clearskies/validators/unique.py +0 -1
- {clear_skies-2.0.1.dist-info → clear_skies-2.0.2.dist-info}/LICENSE +0 -0
- {clear_skies-2.0.1.dist-info → clear_skies-2.0.2.dist-info}/WHEEL +0 -0
clearskies/model.py
CHANGED
|
@@ -263,29 +263,187 @@ class Model(Schema, InjectableProperties):
|
|
|
263
263
|
|
|
264
264
|
def save(self: Self, data: dict[str, Any] | None = None, columns: dict[str, Column] = {}, no_data=False) -> bool:
|
|
265
265
|
"""
|
|
266
|
-
Save data to the database and update the
|
|
267
|
-
|
|
268
|
-
|
|
266
|
+
Save data to the database and create/update the underlying record.
|
|
267
|
+
|
|
268
|
+
### Lifecycle of a Save
|
|
269
|
+
|
|
270
|
+
Before discussing the mechanics of how to save a model, it helps to understand the full lifecycle of a save
|
|
271
|
+
operation. Of course you can ignore this lifecycle and simply use the save process to send data to a
|
|
272
|
+
backend, but then you miss out on one of the key advantages of clearskies - supporting a state machine
|
|
273
|
+
flow for defining your applications. The save process is controlled not just by the model but also by
|
|
274
|
+
the columns, with equivalent hooks for both. This creates a lot of flexibility for how to control and
|
|
275
|
+
organize an application. The overall save process looks like this:
|
|
276
|
+
|
|
277
|
+
1. The `pre_save` hook in each column is called (including the `on_change_pre_save` actions attached to the columns)
|
|
278
|
+
2. The `pre_save` hook for the model is called
|
|
279
|
+
3. The `to_backend` hook for each column is called and temporary data is removed from the save dictionary
|
|
280
|
+
4. The `to_backend` hook for the model is called
|
|
281
|
+
5. The data is persisted to the backend via a create or update call as appropriate
|
|
282
|
+
6. The `post_save` hook in each column is called (including the `on_change_post_save` actions attached to the columns)
|
|
283
|
+
7. The `post_save` hook in the model is called
|
|
284
|
+
8. Any data returned by the backend during the create/update operation is saved to the model along with the temporary data
|
|
285
|
+
9. The `save_finished` hook in each column is called (including the `on_change_save_finished` actions attached to the columns)
|
|
286
|
+
10. The `save_finished` hook in the model is called
|
|
287
|
+
|
|
288
|
+
Note that pre/post/finished hooks for all columns are called - not just the ones with data in the save.
|
|
289
|
+
Thus, any column attached to a model can always influence the save process.
|
|
290
|
+
|
|
291
|
+
From this we can see how to use these hooks. In particular:
|
|
292
|
+
|
|
293
|
+
1. The `pre_save` hook is used to modify the data before it is persisted to the backend. This means that changes
|
|
294
|
+
can be made to the data dictionary in the `pre_save` step and there will still only be a single save operation
|
|
295
|
+
with the backend. For columns, the `on_change_pre_save` methods *MUST* be stateless - they can return data to
|
|
296
|
+
change the save but should not make any changes themselves. This is because they may be called more than once
|
|
297
|
+
in a given save operation.
|
|
298
|
+
2. `to_backend` is used to modify data on its way to the backend. Consider dates: in python these are typically represented
|
|
299
|
+
by datetime objects but, to persist this to (for instance) an SQL database, it usually has to be converted to a string
|
|
300
|
+
format first. That happens in the `to_backend` method of the datetime column.
|
|
301
|
+
3. The `post_save` hook is called after the backend is updated. Therefore, if you are using auto-incrementing ids,
|
|
302
|
+
the id will only be available in ths hook. For consistency with this, clearskies doesn't directly provide the record id
|
|
303
|
+
until the `post_save` hook. If you need to make more data changes in this hook, an additional operation will
|
|
304
|
+
be required. Since the backend has already been updated, this hook does not require a return value (and anything
|
|
305
|
+
returned will be ignored).
|
|
306
|
+
4. The save finished hook happens after the save is fully completed. The backend is updated and the model has been
|
|
307
|
+
updated and the model state reflects the new backend state.
|
|
308
|
+
|
|
309
|
+
The following table summarizes some key details of these hooks:
|
|
310
|
+
|
|
311
|
+
| Name | Stateful | Return Value | Id Present | Backend Updated | Model Updated |
|
|
312
|
+
|-----------------|----------|----------------|------------|-----------------|---------------|
|
|
313
|
+
| `pre_save` | No | dict[str, Any] | No | No | No |
|
|
314
|
+
| `post_save` | Yes | None | Yes | Yes | No |
|
|
315
|
+
| `save_finished` | Yes | None | Yes | Yes | Yes |
|
|
316
|
+
|
|
317
|
+
### How to Create/Update a Model
|
|
269
318
|
|
|
270
319
|
There are two supported flows. One is to pass in a dictionary of data to save:
|
|
271
320
|
|
|
272
321
|
```python
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
322
|
+
import clearskies
|
|
323
|
+
|
|
324
|
+
class User(clearskies.Model):
|
|
325
|
+
id_column_name = "id"
|
|
326
|
+
backend = clearskies.backends.MemoryBackend()
|
|
327
|
+
|
|
328
|
+
id = clearskies.columns.Uuid()
|
|
329
|
+
name = clearskies.columns.String()
|
|
330
|
+
|
|
331
|
+
def my_application(user):
|
|
332
|
+
user.save({
|
|
333
|
+
"name": "Awesome Person",
|
|
334
|
+
})
|
|
335
|
+
return {"id": user.id, "name": user.name}
|
|
336
|
+
|
|
337
|
+
cli = clearskies.contexts.Cli(
|
|
338
|
+
my_application,
|
|
339
|
+
classes=[User],
|
|
340
|
+
)
|
|
341
|
+
cli()
|
|
277
342
|
```
|
|
278
343
|
|
|
279
344
|
And the other is to set new values on the columns attributes and then call save without data:
|
|
280
345
|
|
|
281
346
|
```python
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
347
|
+
import clearskies
|
|
348
|
+
|
|
349
|
+
class User(clearskies.Model):
|
|
350
|
+
id_column_name = "id"
|
|
351
|
+
backend = clearskies.backends.MemoryBackend()
|
|
352
|
+
|
|
353
|
+
id = clearskies.columns.Uuid()
|
|
354
|
+
name = clearskies.columns.String()
|
|
355
|
+
|
|
356
|
+
def my_application(user):
|
|
357
|
+
user.name = "Awesome Person"
|
|
358
|
+
user.save()
|
|
359
|
+
return {"id": user.id, "name": user.name}
|
|
360
|
+
|
|
361
|
+
cli = clearskies.contexts.Cli(
|
|
362
|
+
my_application,
|
|
363
|
+
classes=[User],
|
|
364
|
+
)
|
|
365
|
+
cli()
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
The primray difference is that setting attributes provides strict type checking capabilities, while passing a
|
|
369
|
+
dictionary can be done in one line. Note that you cannot combine these methods: if you set a value on a
|
|
370
|
+
column attribute and also pass in a dictionary of data to the save, then an exception will be raised.
|
|
371
|
+
In either case the save operation acts in place on the model object. The return value is always True - in
|
|
372
|
+
the event of an error an exception will be raised.
|
|
373
|
+
|
|
374
|
+
If a record already exists in the model being saved, then an update operation will be executed. Otherwise,
|
|
375
|
+
a new record will be inserted. To understand the difference yourself, you can convert a model to a boolean
|
|
376
|
+
value - it will return True if a record has been loaded and false otherwise. You can see that with this
|
|
377
|
+
example, where all the `if` statements will evaluate to `True`:
|
|
378
|
+
|
|
379
|
+
```
|
|
380
|
+
import clearskies
|
|
381
|
+
|
|
382
|
+
class User(clearskies.Model):
|
|
383
|
+
id_column_name = "id"
|
|
384
|
+
backend = clearskies.backends.MemoryBackend()
|
|
385
|
+
|
|
386
|
+
id = clearskies.columns.Uuid()
|
|
387
|
+
name = clearskies.columns.String()
|
|
388
|
+
|
|
389
|
+
def my_application(user):
|
|
390
|
+
|
|
391
|
+
if not user:
|
|
392
|
+
print("We will execute a create operation")
|
|
393
|
+
|
|
394
|
+
user.save({"name": "Test One"})
|
|
395
|
+
new_id = user.id
|
|
396
|
+
|
|
397
|
+
if user:
|
|
398
|
+
print("We will execute an update operation")
|
|
399
|
+
|
|
400
|
+
user.save({"name": "Test Two"})
|
|
401
|
+
|
|
402
|
+
final_id = user.id
|
|
403
|
+
|
|
404
|
+
if new_id == final_id:
|
|
405
|
+
print("The id did not chnage because the second save performed an update")
|
|
406
|
+
|
|
407
|
+
return {"id": user.id, "name": user.name}
|
|
408
|
+
|
|
409
|
+
cli = clearskies.contexts.Cli(
|
|
410
|
+
my_application,
|
|
411
|
+
classes=[User],
|
|
412
|
+
)
|
|
413
|
+
cli()
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
occassionaly, you may want to execute a save operation without actually providing any data. This may happen,
|
|
417
|
+
for instance, if you want to create a record in the database that will be filled in later, and so just need
|
|
418
|
+
an auto-generated id. By default if you call save without setting attributes on the model and without
|
|
419
|
+
providing data to the `save` call, this will raise an exception, but you can make this happen with the
|
|
420
|
+
`no_data` kwarg:
|
|
421
|
+
|
|
285
422
|
```
|
|
423
|
+
import clearskies
|
|
424
|
+
|
|
425
|
+
class User(clearskies.Model):
|
|
426
|
+
id_column_name = "id"
|
|
427
|
+
backend = clearskies.backends.MemoryBackend()
|
|
428
|
+
|
|
429
|
+
id = clearskies.columns.Uuid()
|
|
430
|
+
name = clearskies.columns.String()
|
|
431
|
+
|
|
432
|
+
def my_application(user):
|
|
433
|
+
# create a record with just an id
|
|
434
|
+
user.save(no_data=True)
|
|
435
|
+
|
|
436
|
+
# and now we can set the name
|
|
437
|
+
user.save({"name": "Test"})
|
|
438
|
+
|
|
439
|
+
return {"id": user.id, "name": user.name}
|
|
286
440
|
|
|
287
|
-
|
|
288
|
-
|
|
441
|
+
cli = clearskies.contexts.Cli(
|
|
442
|
+
my_application,
|
|
443
|
+
classes=[User],
|
|
444
|
+
)
|
|
445
|
+
cli()
|
|
446
|
+
```
|
|
289
447
|
"""
|
|
290
448
|
self.no_queries()
|
|
291
449
|
if not data and not self._next_data and not no_data:
|
|
@@ -340,7 +498,76 @@ class Model(Schema, InjectableProperties):
|
|
|
340
498
|
"""
|
|
341
499
|
Return True/False to denote if the given column is being modified by the active save operation.
|
|
342
500
|
|
|
343
|
-
|
|
501
|
+
A column is considered to be changing if:
|
|
502
|
+
|
|
503
|
+
- During a create operation
|
|
504
|
+
- It is present in the data array, even if a null value
|
|
505
|
+
- During an update operation
|
|
506
|
+
- It is present in the data array and the value is changing
|
|
507
|
+
|
|
508
|
+
Note whether or not the value is changing is typically evaluated with a simple `=` comparison,
|
|
509
|
+
but columns can optionally implement their own custom logic.
|
|
510
|
+
|
|
511
|
+
Pass in the name of the column to check and the data dictionary from the save in progress. This only
|
|
512
|
+
returns meaningful results during a save, which typically happens in the pre-save/post-save hooks
|
|
513
|
+
(either on the model class itself or in a column). Here's an examle that extends the `pre_save` hook
|
|
514
|
+
on the model to demonstrate how `is_changing` works:
|
|
515
|
+
|
|
516
|
+
```
|
|
517
|
+
from typing import Any, Self
|
|
518
|
+
import clearskies
|
|
519
|
+
|
|
520
|
+
class User(clearskies.Model):
|
|
521
|
+
id_column_name = "id"
|
|
522
|
+
backend = clearskies.backends.MemoryBackend()
|
|
523
|
+
|
|
524
|
+
id = clearskies.columns.Uuid()
|
|
525
|
+
name = clearskies.columns.String()
|
|
526
|
+
age = clearskies.columns.Integer()
|
|
527
|
+
|
|
528
|
+
def pre_save(self: Self, data: dict[str, Any]) -> dict[str, Any]:
|
|
529
|
+
if self.is_changing("name", data) and self.is_changing("age", data):
|
|
530
|
+
print("My name and age have changed!")
|
|
531
|
+
elif self.is_changing("name", data):
|
|
532
|
+
print("Only my name is changing")
|
|
533
|
+
elif self.is_changing("age", data):
|
|
534
|
+
print("Only my age is changing")
|
|
535
|
+
else:
|
|
536
|
+
print("Nothing changed")
|
|
537
|
+
return data
|
|
538
|
+
|
|
539
|
+
def my_application(users):
|
|
540
|
+
jane = users.create({"name": "Jane"})
|
|
541
|
+
jane.save({"age": 22})
|
|
542
|
+
jane.save({"name": "Anon", "age": 23})
|
|
543
|
+
jane.save({"name": "Anon", "age": 23})
|
|
544
|
+
|
|
545
|
+
return {"id": jane.id, "name": jane.name}
|
|
546
|
+
|
|
547
|
+
cli = clearskies.contexts.Cli(
|
|
548
|
+
my_application,
|
|
549
|
+
classes=[User],
|
|
550
|
+
)
|
|
551
|
+
cli()
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
If you run the above example it will print out:
|
|
555
|
+
|
|
556
|
+
```
|
|
557
|
+
Only my name is changing
|
|
558
|
+
Only my age is changing
|
|
559
|
+
My name and age have changed
|
|
560
|
+
Nothing changed
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
The first message is printed out when the record is created - during a create operation, any column that
|
|
564
|
+
is being set to a non-null value is considered to be changing. We then set the age, and since it changes
|
|
565
|
+
from a null value (we didn't originally set an age with the create operation, so the age was null) to a
|
|
566
|
+
non-null value, `is_changed` returns True. We perform another update operation and set both
|
|
567
|
+
name and age to new values, so both change. Finally we repeat the same save operation. This will result
|
|
568
|
+
in another update operation on the backend, but `is_changed` reflects the fact that the values haven't
|
|
569
|
+
actually changed from their previous values.
|
|
570
|
+
|
|
344
571
|
"""
|
|
345
572
|
self.no_queries()
|
|
346
573
|
has_old_value = key in self._data
|
|
@@ -348,20 +575,73 @@ class Model(Schema, InjectableProperties):
|
|
|
348
575
|
|
|
349
576
|
if not has_new_value:
|
|
350
577
|
return False
|
|
578
|
+
|
|
351
579
|
if not has_old_value:
|
|
352
580
|
return True
|
|
353
581
|
|
|
354
|
-
|
|
582
|
+
columns = self.get_columns()
|
|
583
|
+
new_value = data[key]
|
|
584
|
+
old_value = self._data[key]
|
|
585
|
+
if key not in columns:
|
|
586
|
+
return old_value != new_value
|
|
587
|
+
return not columns[key].values_match(old_value, new_value)
|
|
355
588
|
|
|
356
589
|
def latest(self: Self, key: str, data: dict[str, Any]) -> Any:
|
|
357
590
|
"""
|
|
358
591
|
Return the 'latest' value for a column during the save operation.
|
|
359
592
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
593
|
+
During the pre_save and post_save hooks, the model is not yet updated with the latest data.
|
|
594
|
+
In these hooks, it's common to want the "latest" data for the model - e.g. either the column value
|
|
595
|
+
from the model or from the data dictionary (if the column is being updated in the save). This happens
|
|
596
|
+
via slightly verbose lines like: `data.get(column_name, getattr(self, column_name))`. The `latest`
|
|
597
|
+
method is just a substitue for this:
|
|
598
|
+
|
|
599
|
+
```
|
|
600
|
+
from typing import Any, Self
|
|
601
|
+
import clearskies
|
|
602
|
+
|
|
603
|
+
class User(clearskies.Model):
|
|
604
|
+
id_column_name = "id"
|
|
605
|
+
backend = clearskies.backends.MemoryBackend()
|
|
606
|
+
|
|
607
|
+
id = clearskies.columns.Uuid()
|
|
608
|
+
name = clearskies.columns.String()
|
|
609
|
+
age = clearskies.columns.Integer()
|
|
610
|
+
|
|
611
|
+
def pre_save(self: Self, data: dict[str, Any]) -> dict[str, Any]:
|
|
612
|
+
if not self:
|
|
613
|
+
print("Create operation in progress!")
|
|
614
|
+
else:
|
|
615
|
+
print("Update operation in progress!")
|
|
616
|
+
|
|
617
|
+
print("Latest name: " + str(self.latest("name", data)))
|
|
618
|
+
print("Latest age: " + str(self.latest("age", data)))
|
|
619
|
+
return data
|
|
620
|
+
|
|
621
|
+
def my_application(users):
|
|
622
|
+
jane = users.create({"name": "Jane"})
|
|
623
|
+
jane.save({"age": 25})
|
|
624
|
+
return {"id": jane.id, "name": jane.name}
|
|
625
|
+
|
|
626
|
+
cli = clearskies.contexts.Cli(
|
|
627
|
+
my_application,
|
|
628
|
+
classes=[User],
|
|
629
|
+
)
|
|
630
|
+
cli()
|
|
631
|
+
```
|
|
632
|
+
The above example will print:
|
|
633
|
+
|
|
634
|
+
```
|
|
635
|
+
Create operation in progress!
|
|
636
|
+
Latest name: Jane
|
|
637
|
+
Latest age: None
|
|
638
|
+
Update operation in progress!
|
|
639
|
+
Latest name: Jane
|
|
640
|
+
Latest age: 25
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
e.g. `latest` returns the value in the data array (if present), the value for the column in the model, or None.
|
|
363
644
|
|
|
364
|
-
Pass in the name of the column to check and the data dictionary from the save in progress
|
|
365
645
|
"""
|
|
366
646
|
self.no_queries()
|
|
367
647
|
if key in data:
|
|
@@ -369,7 +649,41 @@ class Model(Schema, InjectableProperties):
|
|
|
369
649
|
return getattr(self, key)
|
|
370
650
|
|
|
371
651
|
def was_changed(self: Self, key: str) -> bool:
|
|
372
|
-
"""
|
|
652
|
+
"""
|
|
653
|
+
Return True/False to denote if a column was changed in the last save.
|
|
654
|
+
|
|
655
|
+
To emphasize, the difference between this and `is_changing` is that `is_changing` is available during
|
|
656
|
+
the save prcess while `was_changed` is available after the save has finished. Otherwise, the logic for
|
|
657
|
+
deciding if a column has changed is identical as for `is_changing`.
|
|
658
|
+
|
|
659
|
+
```
|
|
660
|
+
import clearskies
|
|
661
|
+
|
|
662
|
+
class User(clearskies.Model):
|
|
663
|
+
id_column_name = "id"
|
|
664
|
+
backend = clearskies.backends.MemoryBackend()
|
|
665
|
+
|
|
666
|
+
id = clearskies.columns.Uuid()
|
|
667
|
+
name = clearskies.columns.String()
|
|
668
|
+
age = clearskies.columns.Integer()
|
|
669
|
+
|
|
670
|
+
def my_application(users):
|
|
671
|
+
jane = users.create({"name": "Jane"})
|
|
672
|
+
return {
|
|
673
|
+
"name_changed": jane.was_changed("name"),
|
|
674
|
+
"age_changed": jane.was_changed("age"),
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
cli = clearskies.contexts.Cli(
|
|
678
|
+
my_application,
|
|
679
|
+
classes=[User],
|
|
680
|
+
)
|
|
681
|
+
cli()
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
In the above example the name is changed while the age is not.
|
|
685
|
+
|
|
686
|
+
"""
|
|
373
687
|
self.no_queries()
|
|
374
688
|
if self._previous_data is None:
|
|
375
689
|
raise ValueError("was_changed was called before a save was finished - you must save something first")
|
|
@@ -392,13 +706,86 @@ class Model(Schema, InjectableProperties):
|
|
|
392
706
|
return old_value != new_value
|
|
393
707
|
return not columns[key].values_match(old_value, new_value)
|
|
394
708
|
|
|
395
|
-
def previous_value(self: Self, key: str):
|
|
396
|
-
"""
|
|
709
|
+
def previous_value(self: Self, key: str, silent=False):
|
|
710
|
+
"""
|
|
711
|
+
Return the value of a column from before the most recent save.
|
|
712
|
+
|
|
713
|
+
```
|
|
714
|
+
import clearskies
|
|
715
|
+
|
|
716
|
+
class User(clearskies.Model):
|
|
717
|
+
id_column_name = "id"
|
|
718
|
+
backend = clearskies.backends.MemoryBackend()
|
|
719
|
+
|
|
720
|
+
id = clearskies.columns.Uuid()
|
|
721
|
+
name = clearskies.columns.String()
|
|
722
|
+
|
|
723
|
+
def my_application(users):
|
|
724
|
+
jane = users.create({"name": "Jane"})
|
|
725
|
+
jane.save({"name": "Jane Doe"})
|
|
726
|
+
return {"name": jane.name, "previous_name": jane.previous_value("name")}
|
|
727
|
+
|
|
728
|
+
cli = clearskies.contexts.Cli(
|
|
729
|
+
my_application,
|
|
730
|
+
classes=[User],
|
|
731
|
+
)
|
|
732
|
+
cli()
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
The above example returns `{"name": "Jane Doe", "previous_name": "Jane"}`
|
|
736
|
+
|
|
737
|
+
If you request a key that is neither a column nor was present in the previous data array,
|
|
738
|
+
then you'll receive a key error. You can suppress this by setting `silent=True` in your call to
|
|
739
|
+
previous_value.
|
|
740
|
+
"""
|
|
397
741
|
self.no_queries()
|
|
398
|
-
|
|
742
|
+
if key not in self.get_columns() and key not in self._previous_data:
|
|
743
|
+
raise KeyError(f"Unknown previous data key: {key}")
|
|
744
|
+
if key not in self.get_columns():
|
|
745
|
+
return self._previous_data.get(key)
|
|
746
|
+
return getattr(self.__class__, key).from_backend(self._previous_data.get(key))
|
|
399
747
|
|
|
400
748
|
def delete(self: Self, except_if_not_exists=True) -> bool:
|
|
401
|
-
"""
|
|
749
|
+
"""
|
|
750
|
+
Delete a record.
|
|
751
|
+
|
|
752
|
+
If you try to delete a record that doesn't exist, an exception will be thrown unless you set
|
|
753
|
+
`except_if_not_exists=False`. After the record is deleted from the backend, the model instance
|
|
754
|
+
is left unchanged and can be used to fetch the data previously stored. In the following example
|
|
755
|
+
both statements will be printed and the id and name in the "Alice" record will be returned,
|
|
756
|
+
even though the record no longer exists:
|
|
757
|
+
|
|
758
|
+
```
|
|
759
|
+
import clearskies
|
|
760
|
+
|
|
761
|
+
class User(clearskies.Model):
|
|
762
|
+
id_column_name = "id"
|
|
763
|
+
backend = clearskies.backends.MemoryBackend()
|
|
764
|
+
|
|
765
|
+
id = clearskies.columns.Uuid()
|
|
766
|
+
name = clearskies.columns.String()
|
|
767
|
+
|
|
768
|
+
def my_application(users):
|
|
769
|
+
alice = users.create({"name": "Alice"})
|
|
770
|
+
|
|
771
|
+
if users.find("name=Alice"):
|
|
772
|
+
print("Alice exists")
|
|
773
|
+
|
|
774
|
+
alice.delete()
|
|
775
|
+
|
|
776
|
+
if not users.find("name=Alice"):
|
|
777
|
+
print("No more Alice")
|
|
778
|
+
|
|
779
|
+
return {"id": alice.id, "name": alice.name}
|
|
780
|
+
|
|
781
|
+
cli = clearskies.contexts.Cli(
|
|
782
|
+
my_application,
|
|
783
|
+
classes=[User],
|
|
784
|
+
)
|
|
785
|
+
cli()
|
|
786
|
+
|
|
787
|
+
```
|
|
788
|
+
"""
|
|
402
789
|
self.no_queries()
|
|
403
790
|
if not self:
|
|
404
791
|
if except_if_not_exists:
|
|
@@ -466,26 +853,132 @@ class Model(Schema, InjectableProperties):
|
|
|
466
853
|
for column in columns.values():
|
|
467
854
|
column.save_finished(self)
|
|
468
855
|
|
|
469
|
-
def
|
|
856
|
+
def pre_save(self: Self, data: dict[str, Any]) -> dict[str, Any]:
|
|
470
857
|
"""
|
|
471
|
-
|
|
858
|
+
A hook to add additional logic in the pre-save step of the save process.
|
|
859
|
+
|
|
860
|
+
The pre/post/finished steps of the model are directly analogous to the pre/post/finished steps for the columns.
|
|
861
|
+
|
|
862
|
+
pre-save is inteneded to be a stateless hook (e.g. you should not make changes to the backend) where you can
|
|
863
|
+
adjust the data being saved to the model. It is called before any data is persisted to the backend and
|
|
864
|
+
must return a dictionary of data that will be added to the save, potentially over-writing the save data.
|
|
865
|
+
Since pre-save happens before communicating with the backend, the record itself will not yet exist in the
|
|
866
|
+
event of a create operation, and so the id will not be-present for auto-incrementing ids. As a result, the
|
|
867
|
+
record id is not provided during the pre-save hook. See the breakdown of the save lifecycle in the `save`
|
|
868
|
+
documentation above for more details.
|
|
869
|
+
|
|
870
|
+
An here's an example of using it to set some additional data during a save:
|
|
871
|
+
|
|
872
|
+
```
|
|
873
|
+
from typing import Any, Self
|
|
874
|
+
import clearskies
|
|
875
|
+
|
|
876
|
+
class User(clearskies.Model):
|
|
877
|
+
id_column_name = "id"
|
|
878
|
+
backend = clearskies.backends.MemoryBackend()
|
|
879
|
+
|
|
880
|
+
id = clearskies.columns.Uuid()
|
|
881
|
+
name = clearskies.columns.String()
|
|
882
|
+
is_anonymous = clearskies.columns.Boolean()
|
|
883
|
+
|
|
884
|
+
def pre_save(self: Self, data: dict[str, Any]) -> dict[str, Any]:
|
|
885
|
+
additional_data = {}
|
|
886
|
+
|
|
887
|
+
if self.is_changing("name", data):
|
|
888
|
+
additional_data["is_anonymous"] = not bool(data["name"])
|
|
889
|
+
|
|
890
|
+
return additional_data
|
|
891
|
+
|
|
892
|
+
def my_application(users):
|
|
893
|
+
jane = users.create({"name": "Jane"})
|
|
894
|
+
is_anonymous_after_create = jane.is_anonymous
|
|
895
|
+
|
|
896
|
+
jane.save({"name":""})
|
|
897
|
+
is_anonymous_after_first_update = jane.is_anonymous
|
|
898
|
+
|
|
899
|
+
jane.save({"name": "Jane Doe"})
|
|
900
|
+
is_anonymous_after_last_update = jane.is_anonymous
|
|
901
|
+
|
|
902
|
+
return {
|
|
903
|
+
"is_anonymous_after_create": is_anonymous_after_create,
|
|
904
|
+
"is_anonymous_after_first_update": is_anonymous_after_first_update,
|
|
905
|
+
"is_anonymous_after_last_update": is_anonymous_after_last_update,
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
cli = clearskies.contexts.Cli(
|
|
909
|
+
my_application,
|
|
910
|
+
classes=[User],
|
|
911
|
+
)
|
|
912
|
+
cli()
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
In our pre-save hook we set the `is_anonymous` field to either True or False depending on whether or
|
|
916
|
+
not there is a value in the incoming `name` column. As a result, after the original create operation
|
|
917
|
+
(when the `name` is `"Jane"`, `is_anonymous` is False. We then update the name and set it to an empty
|
|
918
|
+
string, and `is_anonymous` becomes True. We then update one last time to set a name again and
|
|
919
|
+
`is_anonymous` becomes False.
|
|
472
920
|
|
|
473
|
-
It is passed in the data being saved as well as the id. It should take action as needed and then return
|
|
474
|
-
either the original data array or an adjusted one if appropriate.
|
|
475
921
|
"""
|
|
476
|
-
|
|
922
|
+
return data
|
|
477
923
|
|
|
478
|
-
def
|
|
924
|
+
def post_save(self: Self, data: dict[str, Any], id: str | int) -> None:
|
|
479
925
|
"""
|
|
480
|
-
|
|
926
|
+
A hook to add additional logic in the post-save step of the save process.
|
|
927
|
+
|
|
928
|
+
It is passed in the data being saved as well as the id of the record. Keep in mind that the post save
|
|
929
|
+
hook happens after the backend has been updated (but before the model is updated) so if you need to make
|
|
930
|
+
any changes to the backend you must execute another save operation. Since the backend is already updated,
|
|
931
|
+
the return value from this function is ignored (it should return None):
|
|
932
|
+
|
|
933
|
+
```
|
|
934
|
+
from typing import Any, Self
|
|
935
|
+
import clearskies
|
|
936
|
+
|
|
937
|
+
class History(clearskies.Model):
|
|
938
|
+
id_column_name = "id"
|
|
939
|
+
backend = clearskies.backends.MemoryBackend()
|
|
940
|
+
|
|
941
|
+
id = clearskies.columns.Uuid()
|
|
942
|
+
message = clearskies.columns.String()
|
|
943
|
+
created_at = clearskies.columns.Created(date_format="%Y-%m-%d %H:%M:%S.%f")
|
|
944
|
+
|
|
945
|
+
class User(clearskies.Model):
|
|
946
|
+
id_column_name = "id"
|
|
947
|
+
backend = clearskies.backends.MemoryBackend()
|
|
948
|
+
histories = clearskies.di.inject.ByClass(History)
|
|
949
|
+
|
|
950
|
+
id = clearskies.columns.Uuid()
|
|
951
|
+
age = clearskies.columns.Integer()
|
|
952
|
+
name = clearskies.columns.String()
|
|
953
|
+
|
|
954
|
+
def post_save(self: Self, data: dict[str, Any], id: str | int) -> None:
|
|
955
|
+
if not self.is_changing("age", data):
|
|
956
|
+
return
|
|
957
|
+
|
|
958
|
+
name = self.latest("name", data)
|
|
959
|
+
age = self.latest("age", data)
|
|
960
|
+
self.histories.create({"message": f"My name is {name} and I am {age} years old"})
|
|
961
|
+
|
|
962
|
+
def my_application(users, histories):
|
|
963
|
+
jane = users.create({"name": "Jane"})
|
|
964
|
+
jane.save({"age": 25})
|
|
965
|
+
jane.save({"age": 26})
|
|
966
|
+
jane.save({"age": 30})
|
|
481
967
|
|
|
482
|
-
|
|
968
|
+
return [history.message for history in histories.sort_by("created_at", "ASC")]
|
|
969
|
+
|
|
970
|
+
cli = clearskies.contexts.Cli(
|
|
971
|
+
my_application,
|
|
972
|
+
classes=[User, History],
|
|
973
|
+
)
|
|
974
|
+
cli()
|
|
975
|
+
```
|
|
483
976
|
"""
|
|
484
|
-
|
|
977
|
+
pass
|
|
485
978
|
|
|
486
979
|
def save_finished(self: Self) -> None:
|
|
487
980
|
"""
|
|
488
|
-
|
|
981
|
+
A hook to add additional logicin the save_finished step of the save process.
|
|
489
982
|
|
|
490
983
|
It has no retrun value and is passed no data. By the time this fires the model has already been
|
|
491
984
|
updated with the new data. You can decide on the necessary actions using the `was_changed` and
|
|
@@ -528,7 +1021,63 @@ class Model(Schema, InjectableProperties):
|
|
|
528
1021
|
### From here down is functionality related to list/search ###
|
|
529
1022
|
##############################################################
|
|
530
1023
|
def has_query(self) -> bool:
|
|
531
|
-
"""
|
|
1024
|
+
"""
|
|
1025
|
+
Whether or not this model instance represents a query.
|
|
1026
|
+
|
|
1027
|
+
The model class is used for both querying records and modifying individual records. As a result, each model class instance
|
|
1028
|
+
keeps track of whether it is being used to query things, or whether it represents an individual record. This distinction
|
|
1029
|
+
is not usually very important to the developer (because there's no good reason to use one model for both), but it may
|
|
1030
|
+
occassionaly be useful to tell how a given model is being used. Clearskies itself does use this to ensure that you
|
|
1031
|
+
can't accidentally use a single model instance for both purposes, mostly because when this happens it's usually a sign
|
|
1032
|
+
of a bug.
|
|
1033
|
+
|
|
1034
|
+
```
|
|
1035
|
+
import clearskies
|
|
1036
|
+
|
|
1037
|
+
class User(clearskies.Model):
|
|
1038
|
+
id_column_name = "id"
|
|
1039
|
+
backend = clearskies.backends.MemoryBackend()
|
|
1040
|
+
|
|
1041
|
+
id = clearskies.columns.Uuid()
|
|
1042
|
+
name = clearskies.columns.String()
|
|
1043
|
+
|
|
1044
|
+
def my_application(users):
|
|
1045
|
+
jane = users.create({"name": "Jane"})
|
|
1046
|
+
jane_instance_has_query = jane.has_query()
|
|
1047
|
+
|
|
1048
|
+
some_search = users.where("name=Jane")
|
|
1049
|
+
some_search_has_query = some_search.has_query()
|
|
1050
|
+
|
|
1051
|
+
invalid_request_error = ""
|
|
1052
|
+
try:
|
|
1053
|
+
some_search.save({"not": "valid"})
|
|
1054
|
+
except ValueError as e:
|
|
1055
|
+
invalid_request_error = str(e)
|
|
1056
|
+
|
|
1057
|
+
return {
|
|
1058
|
+
"jane_instance_has_query": jane_instance_has_query,
|
|
1059
|
+
"some_search_has_query": some_search_has_query,
|
|
1060
|
+
"invalid_request_error": invalid_request_error,
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
cli = clearskies.contexts.Cli(
|
|
1064
|
+
my_application,
|
|
1065
|
+
classes=[User],
|
|
1066
|
+
)
|
|
1067
|
+
cli()
|
|
1068
|
+
```
|
|
1069
|
+
|
|
1070
|
+
Which if you run will return:
|
|
1071
|
+
|
|
1072
|
+
```
|
|
1073
|
+
{
|
|
1074
|
+
"jane_instance_has_query": false,
|
|
1075
|
+
"some_search_has_query": true,
|
|
1076
|
+
"invalid_request_error": "You attempted to save/read record data for a model being used to make a query. This is not allowed, as it is typically a sign of a bug in your application code."
|
|
1077
|
+
}
|
|
1078
|
+
```
|
|
1079
|
+
|
|
1080
|
+
"""
|
|
532
1081
|
return bool(self._query)
|
|
533
1082
|
|
|
534
1083
|
def get_query(self) -> Query:
|
|
@@ -777,7 +1326,11 @@ class Model(Schema, InjectableProperties):
|
|
|
777
1326
|
return False
|
|
778
1327
|
|
|
779
1328
|
def group_by(self: Self, group_by_column_name: str) -> Self:
|
|
780
|
-
"""
|
|
1329
|
+
"""
|
|
1330
|
+
Add a group by clause to the query.
|
|
1331
|
+
|
|
1332
|
+
You just provide the name of the column to group by. Of course, not all backends support a group by clause.
|
|
1333
|
+
"""
|
|
781
1334
|
self.no_single_model()
|
|
782
1335
|
return self.with_query(self.get_query().set_group_by(group_by_column_name))
|
|
783
1336
|
|
|
@@ -1051,7 +1604,39 @@ class Model(Schema, InjectableProperties):
|
|
|
1051
1604
|
Create a new model object and populates it with the data in `data`.
|
|
1052
1605
|
|
|
1053
1606
|
NOTE: the difference between this and `model.create` is that model.create() actually saves a record in the backend,
|
|
1054
|
-
while this method just creates a model object populated with the given data.
|
|
1607
|
+
while this method just creates a model object populated with the given data. This can be helpful if you have record
|
|
1608
|
+
data loaded up in some alternate way and want to wrap a model around it. Calling the `model` method does not result
|
|
1609
|
+
in any interactions with the backend.
|
|
1610
|
+
|
|
1611
|
+
In the following example we create a record in the backend and then make a new model instance using `model`, which
|
|
1612
|
+
we then use to udpate the record. The returned name will be `Jane Doe`.
|
|
1613
|
+
|
|
1614
|
+
```
|
|
1615
|
+
import clearskies
|
|
1616
|
+
|
|
1617
|
+
class User(clearskies.Model):
|
|
1618
|
+
id_column_name = "id"
|
|
1619
|
+
backend = clearskies.backends.MemoryBackend()
|
|
1620
|
+
|
|
1621
|
+
id = clearskies.columns.Uuid()
|
|
1622
|
+
name = clearskies.columns.String()
|
|
1623
|
+
|
|
1624
|
+
def my_application(users):
|
|
1625
|
+
jane = users.create({"name": "Jane"})
|
|
1626
|
+
|
|
1627
|
+
# This effectively makes a new model instance that points to the jane record in the backend
|
|
1628
|
+
another_jane_object = users.model({"id": jane.id, "name": jane.name})
|
|
1629
|
+
# and we can perform an update operation like usual
|
|
1630
|
+
another_jane_object.save({"name": "Jane Doe"})
|
|
1631
|
+
|
|
1632
|
+
return {"id": another_jane_object.id, "name": another_jane_object.name}
|
|
1633
|
+
|
|
1634
|
+
cli = clearskies.contexts.Cli(
|
|
1635
|
+
my_application,
|
|
1636
|
+
classes=[User],
|
|
1637
|
+
)
|
|
1638
|
+
cli()
|
|
1639
|
+
```
|
|
1055
1640
|
"""
|
|
1056
1641
|
model = self._di.build(self.__class__, cache=False)
|
|
1057
1642
|
model.set_raw_data(data)
|
|
@@ -1060,6 +1645,40 @@ class Model(Schema, InjectableProperties):
|
|
|
1060
1645
|
def empty(self: Self) -> Self:
|
|
1061
1646
|
"""
|
|
1062
1647
|
An alias for self.model({})
|
|
1648
|
+
|
|
1649
|
+
This just provides you a fresh, empty model instance that you can use for populating with data or creating
|
|
1650
|
+
a new record. Here's a simple exmaple. Both print statements will be printed and it will return the id
|
|
1651
|
+
for the Alice record, and then null for `blank_id`:
|
|
1652
|
+
|
|
1653
|
+
```
|
|
1654
|
+
import clearskies
|
|
1655
|
+
|
|
1656
|
+
class User(clearskies.Model):
|
|
1657
|
+
id_column_name = "id"
|
|
1658
|
+
backend = clearskies.backends.MemoryBackend()
|
|
1659
|
+
|
|
1660
|
+
id = clearskies.columns.Uuid()
|
|
1661
|
+
name = clearskies.columns.String()
|
|
1662
|
+
|
|
1663
|
+
def my_application(users):
|
|
1664
|
+
alice = users.create({"name": "Alice"})
|
|
1665
|
+
|
|
1666
|
+
if users.find("name=Alice"):
|
|
1667
|
+
print("Alice exists")
|
|
1668
|
+
|
|
1669
|
+
blank = alice.empty()
|
|
1670
|
+
|
|
1671
|
+
if not blank:
|
|
1672
|
+
print("Fresh instance, ready to go")
|
|
1673
|
+
|
|
1674
|
+
return {"alice_id": alice.id, "blank_id": blank.id}
|
|
1675
|
+
|
|
1676
|
+
cli = clearskies.contexts.Cli(
|
|
1677
|
+
my_application,
|
|
1678
|
+
classes=[User],
|
|
1679
|
+
)
|
|
1680
|
+
cli()
|
|
1681
|
+
```
|
|
1063
1682
|
"""
|
|
1064
1683
|
return self.model({})
|
|
1065
1684
|
|
|
@@ -1067,7 +1686,42 @@ class Model(Schema, InjectableProperties):
|
|
|
1067
1686
|
"""
|
|
1068
1687
|
Create a new record in the backend using the information in `data`.
|
|
1069
1688
|
|
|
1070
|
-
|
|
1689
|
+
The `save` method always operates changes the model directly rather than creating a new model instance.
|
|
1690
|
+
Often, when creating a new record, you will need to both create a new (empty) model instance and save
|
|
1691
|
+
data to it. You can do this via `model.empty().save({"data": "here"})`, and this method provides a simple,
|
|
1692
|
+
unambiguous shortcut to do exactly that. So, you pass your save data to the `create` method and you will get
|
|
1693
|
+
back a new model:
|
|
1694
|
+
|
|
1695
|
+
```
|
|
1696
|
+
import clearskies
|
|
1697
|
+
|
|
1698
|
+
class User(clearskies.Model):
|
|
1699
|
+
id_column_name = "id"
|
|
1700
|
+
backend = clearskies.backends.MemoryBackend()
|
|
1701
|
+
|
|
1702
|
+
id = clearskies.columns.Uuid()
|
|
1703
|
+
name = clearskies.columns.String()
|
|
1704
|
+
|
|
1705
|
+
def my_application(user):
|
|
1706
|
+
# let's create a new record
|
|
1707
|
+
user.save({"name": "Alice"})
|
|
1708
|
+
|
|
1709
|
+
# and now use `create` to both create a new record and get a new model instance
|
|
1710
|
+
bob = user.create({"name": "Bob"})
|
|
1711
|
+
|
|
1712
|
+
return {
|
|
1713
|
+
"Alice": user.name,
|
|
1714
|
+
"Bob": bob.name,
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
cli = clearskies.contexts.Cli(
|
|
1718
|
+
my_application,
|
|
1719
|
+
classes=[User],
|
|
1720
|
+
)
|
|
1721
|
+
cli()
|
|
1722
|
+
```
|
|
1723
|
+
|
|
1724
|
+
Like with `save`, you can set `no_data=True` to create a record without specifying any model data.
|
|
1071
1725
|
"""
|
|
1072
1726
|
empty = self.model()
|
|
1073
1727
|
empty.save(data, columns=columns, no_data=no_data)
|