clear-skies 2.0.0__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.0.dist-info → clear_skies-2.0.2.dist-info}/METADATA +2 -2
- {clear_skies-2.0.0.dist-info → clear_skies-2.0.2.dist-info}/RECORD +78 -77
- clearskies/__init__.py +5 -8
- clearskies/authentication/authentication.py +4 -0
- clearskies/authentication/authorization.py +4 -0
- clearskies/authentication/jwks.py +8 -3
- clearskies/authentication/secret_bearer.py +3 -3
- clearskies/backends/api_backend.py +2 -2
- clearskies/backends/backend.py +13 -0
- clearskies/backends/secrets_backend.py +0 -1
- clearskies/column.py +9 -8
- 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 +4 -1
- clearskies/configs/config.py +4 -1
- clearskies/configs/endpoint_list.py +28 -0
- clearskies/contexts/cli.py +4 -0
- clearskies/contexts/context.py +13 -0
- clearskies/contexts/wsgi.py +4 -0
- clearskies/contexts/wsgi_ref.py +4 -0
- clearskies/{parameters_to_properties.py → decorators.py} +2 -0
- clearskies/di/di.py +2 -2
- clearskies/di/injectable_properties.py +2 -2
- clearskies/endpoint.py +7 -6
- clearskies/endpoint_group.py +14 -1
- clearskies/endpoints/callable.py +5 -4
- 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 +1214 -64
- clearskies/query/query.py +4 -4
- clearskies/security_header.py +7 -0
- 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/validator.py +12 -0
- 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.0.dist-info → clear_skies-2.0.2.dist-info}/LICENSE +0 -0
- {clear_skies-2.0.0.dist-info → clear_skies-2.0.2.dist-info}/WHEEL +0 -0
clearskies/model.py
CHANGED
|
@@ -19,13 +19,166 @@ class Model(Schema, InjectableProperties):
|
|
|
19
19
|
"""
|
|
20
20
|
A clearskies model.
|
|
21
21
|
|
|
22
|
-
To be useable, a model class needs
|
|
22
|
+
To be useable, a model class needs four things:
|
|
23
23
|
|
|
24
|
-
1.
|
|
25
|
-
2.
|
|
26
|
-
3. A
|
|
27
|
-
4.
|
|
24
|
+
1. The name of the id column
|
|
25
|
+
2. A backend
|
|
26
|
+
3. A destination name (equivalent to a table name for SQL backends)
|
|
27
|
+
4. Columns
|
|
28
28
|
|
|
29
|
+
In more detail:
|
|
30
|
+
|
|
31
|
+
### Id Column Name
|
|
32
|
+
|
|
33
|
+
clearskies assumes that all models have a column that uniquely identifies each record. This id column is
|
|
34
|
+
provided where appropriate in the lifecycle of the model save process to help connect and find related records.
|
|
35
|
+
It's defined as a simple class attribute called `id_column_name`. There **MUST** be a column with the same name
|
|
36
|
+
in the column definitions. A simple approach to take is to use the Uuid column as an id column. This will
|
|
37
|
+
automatically provide a random UUID when the record is first created. If you are using auto-incrementing integers,
|
|
38
|
+
you can simply use an `Int` column type and define the column as auto-incrementing in your database.
|
|
39
|
+
|
|
40
|
+
### Backend
|
|
41
|
+
|
|
42
|
+
Every model needs a backend, which is an object that extends clearskies.Backend and is attached to the
|
|
43
|
+
`backend` attribute of the model class. clearskies comes with a variety of backends in the `clearskies.backends`
|
|
44
|
+
module that you can use, and you can also define your own or import more from additional packages.
|
|
45
|
+
|
|
46
|
+
### Destination Name
|
|
47
|
+
|
|
48
|
+
The destination name is the equivalent of a table name in other frameworks, but the name is more generic to
|
|
49
|
+
reflect the fact that clearskies is intended to work with a variety of backends - not just SQL databases.
|
|
50
|
+
The exact meaning of the destination name depends on the backend: for a cursor backend it is in fact used
|
|
51
|
+
as the table name when fetching/storing records. For the API backend it is frequently appended to a base
|
|
52
|
+
URL to reach the corect endpoint.
|
|
53
|
+
|
|
54
|
+
This is provided by a class function call `destination_name`. The base model class declares a generic method
|
|
55
|
+
for this which takes the class name, converts it from title case to snake case, and makes it plural. Hence,
|
|
56
|
+
a model class called `User` will have a default destination name of `users` and a model class of `OrderProduct`
|
|
57
|
+
will have a default destination name of `order_products`. Of course, this system isn't pefect: your backend
|
|
58
|
+
may have a different convention or you may have one of the many words in the english language that are
|
|
59
|
+
exceptions to the grammatical rules of making words plural. In this case you can simply extend the method
|
|
60
|
+
and change it according to your needs, e.g.:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
from typing import Self
|
|
64
|
+
import clearskies
|
|
65
|
+
|
|
66
|
+
class Fish(clearskies.Model):
|
|
67
|
+
@classmethod
|
|
68
|
+
def destination_name(cls: type[Self]) -> str:
|
|
69
|
+
return "fish"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Columns
|
|
73
|
+
|
|
74
|
+
Finally, columns are defined by attaching attributes to your model class that extend clearskies.Column. A variety
|
|
75
|
+
are provided by default in the clearskies.columns module, and you can always create more or import them from
|
|
76
|
+
other packages.
|
|
77
|
+
|
|
78
|
+
### Fetching From the Di Container
|
|
79
|
+
|
|
80
|
+
In order to use a model in your application you need to retrieve it from the dependency injection system. Like
|
|
81
|
+
everything, you can do this by either the name or with type hinting. Models do have a special rule for
|
|
82
|
+
injection-via-name: like all classes their dependency injection name is made by converting the class name from
|
|
83
|
+
title case to snake case, but they are also available via the pluralized name. Here's a quick example of all
|
|
84
|
+
three approaches for dependency injection:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
import clearskies
|
|
88
|
+
|
|
89
|
+
class User(clearskies.Model):
|
|
90
|
+
id_column_name = "id"
|
|
91
|
+
backend = clearskies.backends.MemoryBackend()
|
|
92
|
+
|
|
93
|
+
id = clearskies.columns.Uuid()
|
|
94
|
+
name = clearskies.columns.String()
|
|
95
|
+
|
|
96
|
+
def my_application(user, users, by_type_hint: User):
|
|
97
|
+
return {
|
|
98
|
+
"all_are_user_models": isinstance(user, User) and isinstance(users, User) and isinstance(by_type_hint, User)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
cli = clearskies.contexts.Cli(my_application, classes=[User])
|
|
102
|
+
cli()
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Note that the `User` model class was provided in the `classes` list sent to the context: that's important as it
|
|
106
|
+
informs the dependency injection system that this is a class we want to provide. It's common (but not required)
|
|
107
|
+
to put all models for a clearskies application in their own separate python module and then provide those to
|
|
108
|
+
the depedency injection system via the `modules` argument to the context. So you may have a directory structure
|
|
109
|
+
like this:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
├── app/
|
|
113
|
+
│ └── models/
|
|
114
|
+
│ ├── __init__.py
|
|
115
|
+
│ ├── category.py
|
|
116
|
+
│ ├── order.py
|
|
117
|
+
│ ├── product.py
|
|
118
|
+
│ ├── status.py
|
|
119
|
+
│ └── user.py
|
|
120
|
+
└── api.py
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Where `__init__.py` imports all the models:
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
from app.models.category import Category
|
|
127
|
+
from app.models.order import Order
|
|
128
|
+
from app.models.proudct import Product
|
|
129
|
+
from app.models.status import Status
|
|
130
|
+
from app.models.user import User
|
|
131
|
+
|
|
132
|
+
__all__ = ["Category", "Order", "Product", "Status", "User"]
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Then in your main application you can just import the whole `models` module into your context:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
import app.models
|
|
139
|
+
|
|
140
|
+
cli = clearskies.contexts.cli(SomeApplication, modules=[app.models])
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Adding Dependencies
|
|
144
|
+
|
|
145
|
+
The base model class extends `clearskies.di.InjectableProperties` which means that you can inject dependencies into your model
|
|
146
|
+
using the `di.inject` classes. Here's an example that demonstrates dependency injection for models:
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
import datetime
|
|
150
|
+
import clearskies
|
|
151
|
+
|
|
152
|
+
class SomeClass:
|
|
153
|
+
# Since this will be built by the DI system directly, we can declare dependencies in the __init__
|
|
154
|
+
def __init__(self, some_date):
|
|
155
|
+
self.some_date = some_date
|
|
156
|
+
|
|
157
|
+
class User(clearskies.Model):
|
|
158
|
+
id_column_name = "id"
|
|
159
|
+
backend = clearskies.backends.MemoryBackend()
|
|
160
|
+
|
|
161
|
+
utcnow = clearskies.di.inject.Utcnow()
|
|
162
|
+
some_class = clearskies.di.inject.ByClass(SomeClass)
|
|
163
|
+
|
|
164
|
+
id = clearskies.columns.Uuid()
|
|
165
|
+
name = clearskies.columns.String()
|
|
166
|
+
|
|
167
|
+
def some_date_in_the_past(self):
|
|
168
|
+
return self.some_class.some_date < self.utcnow
|
|
169
|
+
|
|
170
|
+
def my_application(user):
|
|
171
|
+
return user.some_date_in_the_past()
|
|
172
|
+
|
|
173
|
+
cli = clearskies.contexts.Cli(
|
|
174
|
+
my_application,
|
|
175
|
+
classes=[User],
|
|
176
|
+
bindings={
|
|
177
|
+
"some_date": datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=1),
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
cli()
|
|
181
|
+
```
|
|
29
182
|
"""
|
|
30
183
|
|
|
31
184
|
_previous_data: dict[str, Any] = {}
|
|
@@ -110,29 +263,187 @@ class Model(Schema, InjectableProperties):
|
|
|
110
263
|
|
|
111
264
|
def save(self: Self, data: dict[str, Any] | None = None, columns: dict[str, Column] = {}, no_data=False) -> bool:
|
|
112
265
|
"""
|
|
113
|
-
Save data to the database and update the
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
116
318
|
|
|
117
319
|
There are two supported flows. One is to pass in a dictionary of data to save:
|
|
118
320
|
|
|
119
321
|
```python
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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()
|
|
124
342
|
```
|
|
125
343
|
|
|
126
344
|
And the other is to set new values on the columns attributes and then call save without data:
|
|
127
345
|
|
|
128
346
|
```python
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
+
|
|
132
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")
|
|
133
393
|
|
|
134
|
-
|
|
135
|
-
|
|
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
|
+
|
|
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}
|
|
440
|
+
|
|
441
|
+
cli = clearskies.contexts.Cli(
|
|
442
|
+
my_application,
|
|
443
|
+
classes=[User],
|
|
444
|
+
)
|
|
445
|
+
cli()
|
|
446
|
+
```
|
|
136
447
|
"""
|
|
137
448
|
self.no_queries()
|
|
138
449
|
if not data and not self._next_data and not no_data:
|
|
@@ -187,7 +498,76 @@ class Model(Schema, InjectableProperties):
|
|
|
187
498
|
"""
|
|
188
499
|
Return True/False to denote if the given column is being modified by the active save operation.
|
|
189
500
|
|
|
190
|
-
|
|
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
|
+
|
|
191
571
|
"""
|
|
192
572
|
self.no_queries()
|
|
193
573
|
has_old_value = key in self._data
|
|
@@ -195,20 +575,73 @@ class Model(Schema, InjectableProperties):
|
|
|
195
575
|
|
|
196
576
|
if not has_new_value:
|
|
197
577
|
return False
|
|
578
|
+
|
|
198
579
|
if not has_old_value:
|
|
199
580
|
return True
|
|
200
581
|
|
|
201
|
-
|
|
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)
|
|
202
588
|
|
|
203
589
|
def latest(self: Self, key: str, data: dict[str, Any]) -> Any:
|
|
204
590
|
"""
|
|
205
591
|
Return the 'latest' value for a column during the save operation.
|
|
206
592
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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.
|
|
210
644
|
|
|
211
|
-
Pass in the name of the column to check and the data dictionary from the save in progress
|
|
212
645
|
"""
|
|
213
646
|
self.no_queries()
|
|
214
647
|
if key in data:
|
|
@@ -216,7 +649,41 @@ class Model(Schema, InjectableProperties):
|
|
|
216
649
|
return getattr(self, key)
|
|
217
650
|
|
|
218
651
|
def was_changed(self: Self, key: str) -> bool:
|
|
219
|
-
"""
|
|
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
|
+
"""
|
|
220
687
|
self.no_queries()
|
|
221
688
|
if self._previous_data is None:
|
|
222
689
|
raise ValueError("was_changed was called before a save was finished - you must save something first")
|
|
@@ -239,11 +706,86 @@ class Model(Schema, InjectableProperties):
|
|
|
239
706
|
return old_value != new_value
|
|
240
707
|
return not columns[key].values_match(old_value, new_value)
|
|
241
708
|
|
|
242
|
-
def previous_value(self: Self, key: str):
|
|
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
|
+
"""
|
|
243
741
|
self.no_queries()
|
|
244
|
-
|
|
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))
|
|
245
747
|
|
|
246
748
|
def delete(self: Self, except_if_not_exists=True) -> bool:
|
|
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
|
+
"""
|
|
247
789
|
self.no_queries()
|
|
248
790
|
if not self:
|
|
249
791
|
if except_if_not_exists:
|
|
@@ -311,26 +853,132 @@ class Model(Schema, InjectableProperties):
|
|
|
311
853
|
for column in columns.values():
|
|
312
854
|
column.save_finished(self)
|
|
313
855
|
|
|
314
|
-
def
|
|
856
|
+
def pre_save(self: Self, data: dict[str, Any]) -> dict[str, Any]:
|
|
315
857
|
"""
|
|
316
|
-
|
|
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.
|
|
317
920
|
|
|
318
|
-
It is passed in the data being saved as well as the id. It should take action as needed and then return
|
|
319
|
-
either the original data array or an adjusted one if appropriate.
|
|
320
921
|
"""
|
|
321
|
-
|
|
922
|
+
return data
|
|
322
923
|
|
|
323
|
-
def
|
|
924
|
+
def post_save(self: Self, data: dict[str, Any], id: str | int) -> None:
|
|
324
925
|
"""
|
|
325
|
-
|
|
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")
|
|
326
944
|
|
|
327
|
-
|
|
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})
|
|
967
|
+
|
|
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
|
+
```
|
|
328
976
|
"""
|
|
329
|
-
|
|
977
|
+
pass
|
|
330
978
|
|
|
331
979
|
def save_finished(self: Self) -> None:
|
|
332
980
|
"""
|
|
333
|
-
|
|
981
|
+
A hook to add additional logicin the save_finished step of the save process.
|
|
334
982
|
|
|
335
983
|
It has no retrun value and is passed no data. By the time this fires the model has already been
|
|
336
984
|
updated with the new data. You can decide on the necessary actions using the `was_changed` and
|
|
@@ -373,7 +1021,63 @@ class Model(Schema, InjectableProperties):
|
|
|
373
1021
|
### From here down is functionality related to list/search ###
|
|
374
1022
|
##############################################################
|
|
375
1023
|
def has_query(self) -> bool:
|
|
376
|
-
"""
|
|
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
|
+
"""
|
|
377
1081
|
return bool(self._query)
|
|
378
1082
|
|
|
379
1083
|
def get_query(self) -> Query:
|
|
@@ -454,30 +1158,154 @@ class Model(Schema, InjectableProperties):
|
|
|
454
1158
|
|
|
455
1159
|
def where(self: Self, where: str | Condition) -> Self:
|
|
456
1160
|
"""
|
|
457
|
-
Add
|
|
1161
|
+
Add a condition to a query.
|
|
1162
|
+
|
|
1163
|
+
The `where` method (in combination with the `find` method) is typically the starting point for query records in
|
|
1164
|
+
a model. You don't *have* to add a condition to a model in order to fetch records, but of course it's a very
|
|
1165
|
+
common use case. Conditions in clearskies can be built from the columns or can be constructed as SQL-like
|
|
1166
|
+
string conditions, e.g. `model.where("name=Bob")` or `model.where(model.name.equals("Bob"))`. The latter
|
|
1167
|
+
provides strict type-checking, while the former does not. Either way they have the same result. The list of
|
|
1168
|
+
supported operators for a given column can be seen by checking the `_allowed_search_operators` attribute of the
|
|
1169
|
+
column class. Most columns accept all allowed operators, which are:
|
|
1170
|
+
|
|
1171
|
+
- "<=>"
|
|
1172
|
+
- "!="
|
|
1173
|
+
- "<="
|
|
1174
|
+
- ">="
|
|
1175
|
+
- ">"
|
|
1176
|
+
- "<"
|
|
1177
|
+
- "="
|
|
1178
|
+
- "in"
|
|
1179
|
+
- "is not null"
|
|
1180
|
+
- "is null"
|
|
1181
|
+
- "like"
|
|
1182
|
+
|
|
1183
|
+
When working with string conditions, it is safe to inject user input into the condition. The allowed
|
|
1184
|
+
format for conditions is very simple: `f"{column_name}\\s?{operator}\\s?{value}"`. This makes it possible to
|
|
1185
|
+
unambiguously separate all three pieces from eachother. It's not possible to inject malicious payloads into either
|
|
1186
|
+
the column names or operators because both are checked against a strict allow list (e.g. the columns declared in the
|
|
1187
|
+
model or the list of allowed operators above). The value is then extracted from the leftovers, and this is
|
|
1188
|
+
provided to the backend separately so it can use it appropriately (e.g. using prepared statements for the cursor
|
|
1189
|
+
backend). Of course, you generally shouldn't have to inject user input into conditions very often because, most
|
|
1190
|
+
often, the various list/search endpoints do this for you, but if you have to do it there are no security
|
|
1191
|
+
concerns.
|
|
1192
|
+
|
|
1193
|
+
You can include a table name before the column name, with the two separated by a period. As always, if you do this,
|
|
1194
|
+
ensure that you include a supporting join statement (via the `join` method - see it for examples).
|
|
1195
|
+
|
|
1196
|
+
When you call the `where` method it returns a new model object with it's query configured to include the additional
|
|
1197
|
+
condition. The original model object remains unchanged. Multiple conditions are always joined with AND. There is
|
|
1198
|
+
no explicit option for OR. The closest is using an IN condition.
|
|
1199
|
+
|
|
1200
|
+
To access the results you have to iterate over the resulting model. If you are only expecting one result
|
|
1201
|
+
and want to work directly with it, then you can use `model.find(condition)` or `model.where(condition).first()`.
|
|
1202
|
+
|
|
1203
|
+
Example:
|
|
1204
|
+
```python
|
|
1205
|
+
import clearskies
|
|
458
1206
|
|
|
459
|
-
|
|
1207
|
+
class Order(clearskies.Model):
|
|
1208
|
+
id_column_name = "id"
|
|
1209
|
+
backend = clearskies.backends.MemoryBackend()
|
|
460
1210
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
1211
|
+
id = clearskies.columns.Uuid()
|
|
1212
|
+
user_id = clearskies.columns.String()
|
|
1213
|
+
status = clearskies.columns.Select(["Pending", "In Progress"])
|
|
1214
|
+
total = clearskies.columns.Float()
|
|
464
1215
|
|
|
465
|
-
|
|
466
|
-
|
|
1216
|
+
def my_application(orders):
|
|
1217
|
+
orders.create({"user_id": "Bob", "status": "Pending", "total": 25})
|
|
1218
|
+
orders.create({"user_id": "Alice", "status": "In Progress", "total": 15})
|
|
1219
|
+
orders.create({"user_id": "Jane", "status": "Pending", "total": 30})
|
|
467
1220
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
1221
|
+
return [order.user_id for order in orders.where("status=Pending").where(Order.total.greater_than(25))]
|
|
1222
|
+
|
|
1223
|
+
cli = clearskies.contexts.Cli(
|
|
1224
|
+
my_application,
|
|
1225
|
+
classes=[Order],
|
|
1226
|
+
)
|
|
1227
|
+
cli()
|
|
474
1228
|
```
|
|
1229
|
+
|
|
1230
|
+
Which, if ran, returns: `["Jane"]`
|
|
1231
|
+
|
|
475
1232
|
"""
|
|
476
1233
|
self.no_single_model()
|
|
477
1234
|
return self.with_query(self.get_query().add_where(where if isinstance(where, Condition) else Condition(where)))
|
|
478
1235
|
|
|
479
1236
|
def join(self: Self, join: str) -> Self:
|
|
480
|
-
"""
|
|
1237
|
+
"""
|
|
1238
|
+
Add a join clause to the query.
|
|
1239
|
+
|
|
1240
|
+
As with the `where` method, this expects a string which is parsed accordingly. The syntax is not as flexible as
|
|
1241
|
+
SQL and expects a format of:
|
|
1242
|
+
|
|
1243
|
+
```
|
|
1244
|
+
[left|right|inner]? join [right_table_name] ON [right_table_name].[right_column_name]=[left_table_name].[left_column_name].
|
|
1245
|
+
```
|
|
1246
|
+
|
|
1247
|
+
This is case insensitive. Aliases are allowed. If you don't specify a join type it defaults to inner.
|
|
1248
|
+
Here are two examples of valid join statements:
|
|
1249
|
+
|
|
1250
|
+
- `join orders on orders.user_id=users.id`
|
|
1251
|
+
- `left join user_orders as orders on orders.id=users.id`
|
|
1252
|
+
|
|
1253
|
+
Note that joins are not strictly limited to SQL-like backends, but of course no all backends will support joining.
|
|
1254
|
+
|
|
1255
|
+
A basic example:
|
|
1256
|
+
|
|
1257
|
+
```
|
|
1258
|
+
import clearskies
|
|
1259
|
+
|
|
1260
|
+
class User(clearskies.Model):
|
|
1261
|
+
id_column_name = "id"
|
|
1262
|
+
backend = clearskies.backends.MemoryBackend()
|
|
1263
|
+
|
|
1264
|
+
id = clearskies.columns.Uuid()
|
|
1265
|
+
name = clearskies.columns.String()
|
|
1266
|
+
|
|
1267
|
+
class Order(clearskies.Model):
|
|
1268
|
+
id_column_name = "id"
|
|
1269
|
+
backend = clearskies.backends.MemoryBackend()
|
|
1270
|
+
|
|
1271
|
+
id = clearskies.columns.Uuid()
|
|
1272
|
+
user_id = clearskies.columns.BelongsToId(User, readable_parent_columns=["id", "name"])
|
|
1273
|
+
user = clearskies.columns.BelongsToModel("user_id")
|
|
1274
|
+
status = clearskies.columns.Select(["Pending", "In Progress"])
|
|
1275
|
+
total = clearskies.columns.Float()
|
|
1276
|
+
|
|
1277
|
+
def my_application(users, orders):
|
|
1278
|
+
jane = users.create({"name": "Jane"})
|
|
1279
|
+
another_jane = users.create({"name": "Jane"})
|
|
1280
|
+
bob = users.create({"name": "Bob"})
|
|
1281
|
+
|
|
1282
|
+
# Jane's orders
|
|
1283
|
+
orders.create({"user_id": jane.id, "status": "Pending", "total": 25})
|
|
1284
|
+
orders.create({"user_id": jane.id, "status": "Pending", "total": 30})
|
|
1285
|
+
orders.create({"user_id": jane.id, "status": "In Progress", "total": 35})
|
|
1286
|
+
|
|
1287
|
+
# Another Jane's orders
|
|
1288
|
+
orders.create({"user_id": another_jane.id, "status": "Pending", "total": 15})
|
|
1289
|
+
|
|
1290
|
+
# Bob's orders
|
|
1291
|
+
orders.create({"user_id": bob.id, "status": "Pending", "total": 28})
|
|
1292
|
+
orders.create({"user_id": bob.id, "status": "In Progress", "total": 35})
|
|
1293
|
+
|
|
1294
|
+
# return all orders for anyone named Jane that have a status of Pending
|
|
1295
|
+
return orders.join("join users on users.id=orders.user_id").where("users.name=Jane").sort_by("total", "asc").where("status=Pending")
|
|
1296
|
+
|
|
1297
|
+
cli = clearskies.contexts.Cli(
|
|
1298
|
+
clearskies.endpoints.Callable(
|
|
1299
|
+
my_application,
|
|
1300
|
+
model_class=Order,
|
|
1301
|
+
readable_column_names=["user", "total"],
|
|
1302
|
+
),
|
|
1303
|
+
classes=[Order, User],
|
|
1304
|
+
)
|
|
1305
|
+
cli()
|
|
1306
|
+
|
|
1307
|
+
```
|
|
1308
|
+
"""
|
|
481
1309
|
self.no_single_model()
|
|
482
1310
|
return self.with_query(self.get_query().add_join(Join(join)))
|
|
483
1311
|
|
|
@@ -498,6 +1326,11 @@ class Model(Schema, InjectableProperties):
|
|
|
498
1326
|
return False
|
|
499
1327
|
|
|
500
1328
|
def group_by(self: Self, group_by_column_name: str) -> Self:
|
|
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
|
+
"""
|
|
501
1334
|
self.no_single_model()
|
|
502
1335
|
return self.with_query(self.get_query().set_group_by(group_by_column_name))
|
|
503
1336
|
|
|
@@ -510,6 +1343,41 @@ class Model(Schema, InjectableProperties):
|
|
|
510
1343
|
secondary_direction: str = "",
|
|
511
1344
|
secondary_table_name: str = "",
|
|
512
1345
|
) -> Self:
|
|
1346
|
+
"""
|
|
1347
|
+
Add a sort by clause to the query. You can sort by up to two columns at once.
|
|
1348
|
+
|
|
1349
|
+
Example:
|
|
1350
|
+
```
|
|
1351
|
+
import clearskies
|
|
1352
|
+
|
|
1353
|
+
class Order(clearskies.Model):
|
|
1354
|
+
id_column_name = "id"
|
|
1355
|
+
backend = clearskies.backends.MemoryBackend()
|
|
1356
|
+
|
|
1357
|
+
id = clearskies.columns.Uuid()
|
|
1358
|
+
user_id = clearskies.columns.String()
|
|
1359
|
+
status = clearskies.columns.Select(["Pending", "In Progress"])
|
|
1360
|
+
total = clearskies.columns.Float()
|
|
1361
|
+
|
|
1362
|
+
def my_application(orders):
|
|
1363
|
+
orders.create({"user_id": "Bob", "status": "Pending", "total": 25})
|
|
1364
|
+
orders.create({"user_id": "Alice", "status": "In Progress", "total": 15})
|
|
1365
|
+
orders.create({"user_id": "Alice", "status": "Pending", "total": 30})
|
|
1366
|
+
orders.create({"user_id": "Bob", "status": "Pending", "total": 26})
|
|
1367
|
+
|
|
1368
|
+
return orders.sort_by("user_id", "asc", secondary_column_name="total", secondary_direction="desc")
|
|
1369
|
+
|
|
1370
|
+
cli = clearskies.contexts.Cli(
|
|
1371
|
+
clearskies.endpoints.Callable(
|
|
1372
|
+
my_application,
|
|
1373
|
+
model_class=Order,
|
|
1374
|
+
readable_column_names=["user_id", "total"],
|
|
1375
|
+
),
|
|
1376
|
+
classes=[Order],
|
|
1377
|
+
)
|
|
1378
|
+
cli()
|
|
1379
|
+
```
|
|
1380
|
+
"""
|
|
513
1381
|
self.no_single_model()
|
|
514
1382
|
sort = Sort(primary_table_name, primary_column_name, primary_direction)
|
|
515
1383
|
secondary_sort = None
|
|
@@ -518,10 +1386,96 @@ class Model(Schema, InjectableProperties):
|
|
|
518
1386
|
return self.with_query(self.get_query().set_sort(sort, secondary_sort))
|
|
519
1387
|
|
|
520
1388
|
def limit(self: Self, limit: int) -> Self:
|
|
1389
|
+
"""
|
|
1390
|
+
Set the number of records to return.
|
|
1391
|
+
|
|
1392
|
+
```
|
|
1393
|
+
import clearskies
|
|
1394
|
+
|
|
1395
|
+
class Order(clearskies.Model):
|
|
1396
|
+
id_column_name = "id"
|
|
1397
|
+
backend = clearskies.backends.MemoryBackend()
|
|
1398
|
+
|
|
1399
|
+
id = clearskies.columns.Uuid()
|
|
1400
|
+
user_id = clearskies.columns.String()
|
|
1401
|
+
status = clearskies.columns.Select(["Pending", "In Progress"])
|
|
1402
|
+
total = clearskies.columns.Float()
|
|
1403
|
+
|
|
1404
|
+
def my_application(orders):
|
|
1405
|
+
orders.create({"user_id": "Bob", "status": "Pending", "total": 25})
|
|
1406
|
+
orders.create({"user_id": "Alice", "status": "In Progress", "total": 15})
|
|
1407
|
+
orders.create({"user_id": "Alice", "status": "Pending", "total": 30})
|
|
1408
|
+
orders.create({"user_id": "Bob", "status": "Pending", "total": 26})
|
|
1409
|
+
|
|
1410
|
+
return orders.limit(2)
|
|
1411
|
+
|
|
1412
|
+
cli = clearskies.contexts.Cli(
|
|
1413
|
+
clearskies.endpoints.Callable(
|
|
1414
|
+
my_application,
|
|
1415
|
+
model_class=Order,
|
|
1416
|
+
readable_column_names=["user_id", "total"],
|
|
1417
|
+
),
|
|
1418
|
+
classes=[Order],
|
|
1419
|
+
)
|
|
1420
|
+
cli()
|
|
1421
|
+
```
|
|
1422
|
+
"""
|
|
521
1423
|
self.no_single_model()
|
|
522
1424
|
return self.with_query(self.get_query().set_limit(limit))
|
|
523
1425
|
|
|
524
1426
|
def pagination(self: Self, **pagination_data) -> Self:
|
|
1427
|
+
"""
|
|
1428
|
+
Set the pagination parameter(s) for the query.
|
|
1429
|
+
|
|
1430
|
+
The exact details of how pagination work depend on the backend. For instance, the cursor and memory backend
|
|
1431
|
+
expect to be given a `start` parameter, while an API backend will vary with the API, and the dynamodb backend
|
|
1432
|
+
expects a kwarg called `cursor`. As a result, it's necessary to check the backend documentation to understand
|
|
1433
|
+
how to properly set pagination. The endpoints automatically account for this because backends are required
|
|
1434
|
+
to declare pagination details via the `allowed_pagination_keys` method. If you attempt to set invalid
|
|
1435
|
+
pagination data via this method, clearskies will raise a ValueError.
|
|
1436
|
+
|
|
1437
|
+
Example:
|
|
1438
|
+
```
|
|
1439
|
+
import clearskies
|
|
1440
|
+
|
|
1441
|
+
class Order(clearskies.Model):
|
|
1442
|
+
id_column_name = "id"
|
|
1443
|
+
backend = clearskies.backends.MemoryBackend()
|
|
1444
|
+
|
|
1445
|
+
id = clearskies.columns.Uuid()
|
|
1446
|
+
user_id = clearskies.columns.String()
|
|
1447
|
+
status = clearskies.columns.Select(["Pending", "In Progress"])
|
|
1448
|
+
total = clearskies.columns.Float()
|
|
1449
|
+
|
|
1450
|
+
def my_application(orders):
|
|
1451
|
+
orders.create({"user_id": "Bob", "status": "Pending", "total": 25})
|
|
1452
|
+
orders.create({"user_id": "Alice", "status": "In Progress", "total": 15})
|
|
1453
|
+
orders.create({"user_id": "Alice", "status": "Pending", "total": 30})
|
|
1454
|
+
orders.create({"user_id": "Bob", "status": "Pending", "total": 26})
|
|
1455
|
+
|
|
1456
|
+
return orders.sort_by("total", "asc").pagination(start=2)
|
|
1457
|
+
|
|
1458
|
+
cli = clearskies.contexts.Cli(
|
|
1459
|
+
clearskies.endpoints.Callable(
|
|
1460
|
+
my_application,
|
|
1461
|
+
model_class=Order,
|
|
1462
|
+
readable_column_names=["user_id", "total"],
|
|
1463
|
+
),
|
|
1464
|
+
classes=[Order],
|
|
1465
|
+
)
|
|
1466
|
+
cli()
|
|
1467
|
+
```
|
|
1468
|
+
|
|
1469
|
+
However, if the return line in `my_application` is switched for either of these:
|
|
1470
|
+
|
|
1471
|
+
```
|
|
1472
|
+
return orders.sort_by("total", "asc").pagination(start="asdf")
|
|
1473
|
+
return orders.sort_by("total", "asc").pagination(something_else=5)
|
|
1474
|
+
```
|
|
1475
|
+
|
|
1476
|
+
Will result in an exception that explains exactly what is wrong.
|
|
1477
|
+
|
|
1478
|
+
"""
|
|
525
1479
|
self.no_single_model()
|
|
526
1480
|
error = self.backend.validate_pagination_data(pagination_data, str)
|
|
527
1481
|
if error:
|
|
@@ -538,8 +1492,36 @@ class Model(Schema, InjectableProperties):
|
|
|
538
1492
|
This is just shorthand for `models.where("column=value").find()`. Example:
|
|
539
1493
|
|
|
540
1494
|
```python
|
|
541
|
-
|
|
542
|
-
|
|
1495
|
+
import clearskies
|
|
1496
|
+
|
|
1497
|
+
class Order(clearskies.Model):
|
|
1498
|
+
id_column_name = "id"
|
|
1499
|
+
backend = clearskies.backends.MemoryBackend()
|
|
1500
|
+
|
|
1501
|
+
id = clearskies.columns.Uuid()
|
|
1502
|
+
user_id = clearskies.columns.String()
|
|
1503
|
+
status = clearskies.columns.Select(["Pending", "In Progress"])
|
|
1504
|
+
total = clearskies.columns.Float()
|
|
1505
|
+
|
|
1506
|
+
def my_application(orders):
|
|
1507
|
+
orders.create({"user_id": "Bob", "status": "Pending", "total": 25})
|
|
1508
|
+
orders.create({"user_id": "Alice", "status": "In Progress", "total": 15})
|
|
1509
|
+
orders.create({"user_id": "Jane", "status": "Pending", "total": 30})
|
|
1510
|
+
|
|
1511
|
+
jane = orders.find("user_id=Jane")
|
|
1512
|
+
jane.total = 35
|
|
1513
|
+
jane.save()
|
|
1514
|
+
|
|
1515
|
+
return {
|
|
1516
|
+
"user_id": jane.user_id,
|
|
1517
|
+
"total": jane.total,
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
cli = clearskies.contexts.Cli(
|
|
1521
|
+
my_application,
|
|
1522
|
+
classes=[Order],
|
|
1523
|
+
)
|
|
1524
|
+
cli()
|
|
543
1525
|
```
|
|
544
1526
|
"""
|
|
545
1527
|
self.no_single_model()
|
|
@@ -564,13 +1546,48 @@ class Model(Schema, InjectableProperties):
|
|
|
564
1546
|
"""
|
|
565
1547
|
Loop through all available pages of results and returns a list of all models that match the query.
|
|
566
1548
|
|
|
567
|
-
|
|
568
|
-
|
|
1549
|
+
If you don't set a limit on a query, some backends will return all records but some backends have a
|
|
1550
|
+
default maximum number of results that they will return. In the latter case, you can use `paginate_all`
|
|
1551
|
+
to fetch all records by instructing clearskies to iterate over all pages. This is possible because backends
|
|
1552
|
+
are required to define how pagination works in a way that clearskies can automatically understand and
|
|
1553
|
+
use. To demonstrate this, the following example sets a limit of 1 which stops the memory backend
|
|
1554
|
+
from returning everything, and then uses `paginate_all` to fetch all records. The memory backend
|
|
1555
|
+
doesn't have a default limit, so in practice the `paginate_all` is unnecessary here, but this is done
|
|
1556
|
+
for demonstration purposes.
|
|
569
1557
|
|
|
570
|
-
```python
|
|
571
|
-
for model in models.where("column=value").paginate_all():
|
|
572
|
-
print(model.id)
|
|
573
1558
|
```
|
|
1559
|
+
import clearskies
|
|
1560
|
+
|
|
1561
|
+
class Order(clearskies.Model):
|
|
1562
|
+
id_column_name = "id"
|
|
1563
|
+
backend = clearskies.backends.MemoryBackend()
|
|
1564
|
+
|
|
1565
|
+
id = clearskies.columns.Uuid()
|
|
1566
|
+
user_id = clearskies.columns.String()
|
|
1567
|
+
status = clearskies.columns.Select(["Pending", "In Progress"])
|
|
1568
|
+
total = clearskies.columns.Float()
|
|
1569
|
+
|
|
1570
|
+
def my_application(orders):
|
|
1571
|
+
orders.create({"user_id": "Bob", "status": "Pending", "total": 25})
|
|
1572
|
+
orders.create({"user_id": "Alice", "status": "In Progress", "total": 15})
|
|
1573
|
+
orders.create({"user_id": "Alice", "status": "Pending", "total": 30})
|
|
1574
|
+
orders.create({"user_id": "Bob", "status": "Pending", "total": 26})
|
|
1575
|
+
|
|
1576
|
+
return orders.limit(1).paginate_all()
|
|
1577
|
+
|
|
1578
|
+
cli = clearskies.contexts.Cli(
|
|
1579
|
+
clearskies.endpoints.Callable(
|
|
1580
|
+
my_application,
|
|
1581
|
+
model_class=Order,
|
|
1582
|
+
readable_column_names=["user_id", "total"],
|
|
1583
|
+
),
|
|
1584
|
+
classes=[Order],
|
|
1585
|
+
)
|
|
1586
|
+
cli()
|
|
1587
|
+
```
|
|
1588
|
+
|
|
1589
|
+
NOTE: this loads up all records in memory before returning (e.g. it isn't using generators yet), so
|
|
1590
|
+
expect delays for large record sets.
|
|
574
1591
|
"""
|
|
575
1592
|
self.no_single_model()
|
|
576
1593
|
next_models = self.with_query(self.get_query())
|
|
@@ -587,7 +1604,39 @@ class Model(Schema, InjectableProperties):
|
|
|
587
1604
|
Create a new model object and populates it with the data in `data`.
|
|
588
1605
|
|
|
589
1606
|
NOTE: the difference between this and `model.create` is that model.create() actually saves a record in the backend,
|
|
590
|
-
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
|
+
```
|
|
591
1640
|
"""
|
|
592
1641
|
model = self._di.build(self.__class__, cache=False)
|
|
593
1642
|
model.set_raw_data(data)
|
|
@@ -596,6 +1645,40 @@ class Model(Schema, InjectableProperties):
|
|
|
596
1645
|
def empty(self: Self) -> Self:
|
|
597
1646
|
"""
|
|
598
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
|
+
```
|
|
599
1682
|
"""
|
|
600
1683
|
return self.model({})
|
|
601
1684
|
|
|
@@ -603,7 +1686,42 @@ class Model(Schema, InjectableProperties):
|
|
|
603
1686
|
"""
|
|
604
1687
|
Create a new record in the backend using the information in `data`.
|
|
605
1688
|
|
|
606
|
-
|
|
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.
|
|
607
1725
|
"""
|
|
608
1726
|
empty = self.model()
|
|
609
1727
|
empty.save(data, columns=columns, no_data=no_data)
|
|
@@ -611,11 +1729,43 @@ class Model(Schema, InjectableProperties):
|
|
|
611
1729
|
|
|
612
1730
|
def first(self: Self) -> Self:
|
|
613
1731
|
"""
|
|
614
|
-
Return the first model
|
|
1732
|
+
Return the first model for a given query.
|
|
1733
|
+
|
|
1734
|
+
The `where` method returns an object meant to be iterated over. If you are expecting your query to return a single
|
|
1735
|
+
record, then you can use first to turn that directly into the matching model so you don't have to iterate over it:
|
|
1736
|
+
|
|
1737
|
+
```
|
|
1738
|
+
import clearskies
|
|
1739
|
+
|
|
1740
|
+
class Order(clearskies.Model):
|
|
1741
|
+
id_column_name = "id"
|
|
1742
|
+
backend = clearskies.backends.MemoryBackend()
|
|
1743
|
+
|
|
1744
|
+
id = clearskies.columns.Uuid()
|
|
1745
|
+
user_id = clearskies.columns.String()
|
|
1746
|
+
status = clearskies.columns.Select(["Pending", "In Progress"])
|
|
1747
|
+
total = clearskies.columns.Float()
|
|
1748
|
+
|
|
1749
|
+
def my_application(orders):
|
|
1750
|
+
orders.create({"user_id": "Bob", "status": "Pending", "total": 25})
|
|
1751
|
+
orders.create({"user_id": "Alice", "status": "In Progress", "total": 15})
|
|
1752
|
+
orders.create({"user_id": "Jane", "status": "Pending", "total": 30})
|
|
1753
|
+
|
|
1754
|
+
jane = orders.where("status=Pending").where(Order.total.greater_than(25)).first()
|
|
1755
|
+
jane.total = 35
|
|
1756
|
+
jane.save()
|
|
1757
|
+
|
|
1758
|
+
return {
|
|
1759
|
+
"user_id": jane.user_id,
|
|
1760
|
+
"total": jane.total,
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
cli = clearskies.contexts.Cli(
|
|
1764
|
+
my_application,
|
|
1765
|
+
classes=[Order],
|
|
1766
|
+
)
|
|
1767
|
+
cli()
|
|
615
1768
|
|
|
616
|
-
```python
|
|
617
|
-
model = models.where("column=value").sort_by("age", "DESC").first()
|
|
618
|
-
print(model.id)
|
|
619
1769
|
```
|
|
620
1770
|
"""
|
|
621
1771
|
self.no_single_model()
|