statezero 0.1.0b1__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.
- statezero/__init__.py +0 -0
- statezero/adaptors/__init__.py +0 -0
- statezero/adaptors/django/__init__.py +0 -0
- statezero/adaptors/django/apps.py +97 -0
- statezero/adaptors/django/config.py +99 -0
- statezero/adaptors/django/context_manager.py +12 -0
- statezero/adaptors/django/event_emitters.py +78 -0
- statezero/adaptors/django/exception_handler.py +98 -0
- statezero/adaptors/django/extensions/__init__.py +0 -0
- statezero/adaptors/django/extensions/custom_field_serializers/__init__.py +0 -0
- statezero/adaptors/django/extensions/custom_field_serializers/file_fields.py +141 -0
- statezero/adaptors/django/extensions/custom_field_serializers/money_field.py +75 -0
- statezero/adaptors/django/f_handler.py +312 -0
- statezero/adaptors/django/helpers.py +153 -0
- statezero/adaptors/django/middleware.py +10 -0
- statezero/adaptors/django/migrations/0001_initial.py +33 -0
- statezero/adaptors/django/migrations/0002_delete_modelviewsubscription.py +16 -0
- statezero/adaptors/django/migrations/__init__.py +0 -0
- statezero/adaptors/django/orm.py +915 -0
- statezero/adaptors/django/permissions.py +252 -0
- statezero/adaptors/django/query_optimizer.py +772 -0
- statezero/adaptors/django/schemas.py +324 -0
- statezero/adaptors/django/search_providers/__init__.py +0 -0
- statezero/adaptors/django/search_providers/basic_search.py +24 -0
- statezero/adaptors/django/search_providers/postgres_search.py +51 -0
- statezero/adaptors/django/serializers.py +554 -0
- statezero/adaptors/django/urls.py +14 -0
- statezero/adaptors/django/views.py +336 -0
- statezero/core/__init__.py +34 -0
- statezero/core/ast_parser.py +821 -0
- statezero/core/ast_validator.py +266 -0
- statezero/core/classes.py +167 -0
- statezero/core/config.py +263 -0
- statezero/core/context_storage.py +4 -0
- statezero/core/event_bus.py +175 -0
- statezero/core/event_emitters.py +60 -0
- statezero/core/exceptions.py +106 -0
- statezero/core/interfaces.py +492 -0
- statezero/core/process_request.py +184 -0
- statezero/core/types.py +63 -0
- statezero-0.1.0b1.dist-info/METADATA +252 -0
- statezero-0.1.0b1.dist-info/RECORD +45 -0
- statezero-0.1.0b1.dist-info/WHEEL +5 -0
- statezero-0.1.0b1.dist-info/licenses/license.md +117 -0
- statezero-0.1.0b1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type, Union, Literal, Protocol
|
|
5
|
+
|
|
6
|
+
from statezero.core.classes import ModelSchemaMetadata, SchemaFieldMetadata
|
|
7
|
+
from statezero.core.types import (ActionType, ORMField, ORMModel, ORMQuerySet, RequestType)
|
|
8
|
+
|
|
9
|
+
class AbstractORMProvider(ABC):
|
|
10
|
+
"""
|
|
11
|
+
A merged ORM engine interface that combines both query building (filtering,
|
|
12
|
+
ordering, aggregation, etc.) and ORM provider responsibilities (queryset assembly,
|
|
13
|
+
event signal registration, model graph construction, etc.).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
# === Query Engine Methods ===
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def get_fields(self) -> Set[str]:
|
|
20
|
+
"""
|
|
21
|
+
Get all of the model fields - doesn't apply permissions check.
|
|
22
|
+
"""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def filter_node(self, node: Dict[str, Any]) -> None:
|
|
27
|
+
"""
|
|
28
|
+
Apply filter/and/or/not logic to the current query.
|
|
29
|
+
"""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def search_node(self, search_query: str, search_fields: Set[str]) -> None:
|
|
34
|
+
"""
|
|
35
|
+
Apply search to the current query.
|
|
36
|
+
"""
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def create(self, data: Dict[str, Any]) -> Any:
|
|
41
|
+
"""Create a new record."""
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def update(self, node: Dict[str, Any]) -> int:
|
|
46
|
+
"""
|
|
47
|
+
Update records (by filter or primary key).
|
|
48
|
+
Returns the number of rows updated.
|
|
49
|
+
"""
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
@abstractmethod
|
|
53
|
+
def delete(self, node: Dict[str, Any]) -> int:
|
|
54
|
+
"""
|
|
55
|
+
Delete records (by filter or primary key).
|
|
56
|
+
Returns the number of rows deleted.
|
|
57
|
+
"""
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
def get(self, node: Dict[str, Any]) -> Any:
|
|
62
|
+
"""
|
|
63
|
+
Retrieve a single record. Raises an error if multiple or none are found.
|
|
64
|
+
"""
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
@abstractmethod
|
|
68
|
+
def get_or_create(self, node: Dict[str, Any]) -> Tuple[Any, bool]:
|
|
69
|
+
"""
|
|
70
|
+
Retrieve a record if it exists, otherwise create it.
|
|
71
|
+
Returns a tuple of (instance, created_flag).
|
|
72
|
+
"""
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
@abstractmethod
|
|
76
|
+
def update_or_create(self, node: Dict[str, Any]) -> Tuple[Any, bool]:
|
|
77
|
+
"""
|
|
78
|
+
Update a record if it exists or create it if it doesn't.
|
|
79
|
+
Returns a tuple of (instance, created_flag).
|
|
80
|
+
"""
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
@abstractmethod
|
|
84
|
+
def first(self) -> Any:
|
|
85
|
+
"""Return the first record from the current query."""
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
@abstractmethod
|
|
89
|
+
def last(self) -> Any:
|
|
90
|
+
"""Return the last record from the current query."""
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
@abstractmethod
|
|
94
|
+
def exists(self) -> bool:
|
|
95
|
+
"""Return True if the current query has any results; otherwise False."""
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
@abstractmethod
|
|
99
|
+
def aggregate(self, agg_list: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
100
|
+
"""
|
|
101
|
+
Aggregate the current query based on the provided functions.
|
|
102
|
+
Example:
|
|
103
|
+
[
|
|
104
|
+
{'function': 'count', 'field': 'id', 'alias': 'id_count'},
|
|
105
|
+
{'function': 'sum', 'field': 'price', 'alias': 'price_sum'}
|
|
106
|
+
]
|
|
107
|
+
"""
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
@abstractmethod
|
|
111
|
+
def count(self, field: str) -> int:
|
|
112
|
+
"""Count the number of records for the given field."""
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
@abstractmethod
|
|
116
|
+
def sum(self, field: str) -> Any:
|
|
117
|
+
"""Sum the values of the given field."""
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
@abstractmethod
|
|
121
|
+
def avg(self, field: str) -> Any:
|
|
122
|
+
"""Calculate the average of the given field."""
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
@abstractmethod
|
|
126
|
+
def min(self, field: str) -> Any:
|
|
127
|
+
"""Find the minimum value for the given field."""
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
@abstractmethod
|
|
131
|
+
def max(self, field: str) -> Any:
|
|
132
|
+
"""Find the maximum value for the given field."""
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
@abstractmethod
|
|
136
|
+
def order_by(self, order_list: List[Dict[str, str]]) -> None:
|
|
137
|
+
"""
|
|
138
|
+
Order the query based on a list of fields.
|
|
139
|
+
Each dict should contain 'field' and optionally 'direction' ('asc' or 'desc').
|
|
140
|
+
"""
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
@abstractmethod
|
|
144
|
+
def select_related(self, related_fields: List[str]) -> None:
|
|
145
|
+
"""
|
|
146
|
+
Optimize the query by eager loading the given related fields.
|
|
147
|
+
"""
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
@abstractmethod
|
|
151
|
+
def prefetch_related(self, related_fields: List[str]) -> None:
|
|
152
|
+
"""
|
|
153
|
+
Optimize the query by prefetching the given related fields.
|
|
154
|
+
"""
|
|
155
|
+
pass
|
|
156
|
+
|
|
157
|
+
@abstractmethod
|
|
158
|
+
def fetch_list(self, offset: int, limit: int) -> List[Any]:
|
|
159
|
+
"""
|
|
160
|
+
Return a list of records (as dicts or objects) based on pagination.
|
|
161
|
+
"""
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
# === ORM Provider Methods ===
|
|
165
|
+
|
|
166
|
+
@abstractmethod
|
|
167
|
+
def get_queryset(
|
|
168
|
+
self,
|
|
169
|
+
request: RequestType,
|
|
170
|
+
model: ORMModel, # type:ignore
|
|
171
|
+
initial_ast: Dict[str, Any],
|
|
172
|
+
custom_querysets: Dict[str, Type],
|
|
173
|
+
registered_permissions: List[Type],
|
|
174
|
+
) -> Any:
|
|
175
|
+
"""
|
|
176
|
+
Assemble and return the base QuerySet (or equivalent) for the given model.
|
|
177
|
+
This method considers the request context, initial AST (filters, sorting, etc.),
|
|
178
|
+
custom query sets, and any model-specific permission restrictions.
|
|
179
|
+
"""
|
|
180
|
+
pass
|
|
181
|
+
|
|
182
|
+
@abstractmethod
|
|
183
|
+
def register_event_signals(self, event_emitter: Any) -> None:
|
|
184
|
+
"""
|
|
185
|
+
Wire the ORM provider's signals so that on create, update, or delete events,
|
|
186
|
+
the global event emitter is invoked with the proper event type, instance,
|
|
187
|
+
and global event configuration.
|
|
188
|
+
"""
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
@abstractmethod
|
|
192
|
+
def get_model_by_name(self, model_name: str) -> Type:
|
|
193
|
+
"""
|
|
194
|
+
Retrieve the model class based on a given model name (e.g. "app_label.ModelName").
|
|
195
|
+
"""
|
|
196
|
+
pass
|
|
197
|
+
|
|
198
|
+
@abstractmethod
|
|
199
|
+
def get_model_name(
|
|
200
|
+
self, model: Union[Type[ORMModel], ORMModel]
|
|
201
|
+
) -> str: # type:ignore
|
|
202
|
+
"""
|
|
203
|
+
Retrieve the model name (e.g. "app_label.ModelName") for the given model class OR instance.
|
|
204
|
+
"""
|
|
205
|
+
pass
|
|
206
|
+
|
|
207
|
+
@abstractmethod
|
|
208
|
+
def get_user(self, request: RequestType): # returns User
|
|
209
|
+
"""
|
|
210
|
+
Get the request user.
|
|
211
|
+
"""
|
|
212
|
+
pass
|
|
213
|
+
|
|
214
|
+
@abstractmethod
|
|
215
|
+
def build_model_graph(self, model: ORMModel) -> None: # type:ignore
|
|
216
|
+
"""
|
|
217
|
+
Construct a graph representation of model relationships.
|
|
218
|
+
"""
|
|
219
|
+
pass
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class AbstractCustomQueryset(ABC):
|
|
223
|
+
@abstractmethod
|
|
224
|
+
def get_queryset(self, request: Optional[RequestType] = None) -> Any:
|
|
225
|
+
"""
|
|
226
|
+
Return a custom queryset (e.g. a custom SQLAlchemy Query or Django QuerySet).
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
request: The current request object, which may contain user information
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
A custom queryset
|
|
233
|
+
"""
|
|
234
|
+
pass
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class AbstractDataSerializer(ABC):
|
|
238
|
+
@abstractmethod
|
|
239
|
+
def serialize(
|
|
240
|
+
self,
|
|
241
|
+
data: Any,
|
|
242
|
+
model: ORMModel, # type:ignore
|
|
243
|
+
depth: int,
|
|
244
|
+
fields: Optional[Set[str]] = None,
|
|
245
|
+
allowed_fields: Optional[Dict[str, Set[str]]] = None,
|
|
246
|
+
) -> dict:
|
|
247
|
+
"""
|
|
248
|
+
Serialize the given data (a single instance or a list) for the specified model.
|
|
249
|
+
- `fields`: the set of field names requested by the client.
|
|
250
|
+
- `allowed_fields`: a mapping (by model name) of fields the user is permitted to access.
|
|
251
|
+
|
|
252
|
+
The effective fields are computed as the intersection of requested and allowed (if both are provided).
|
|
253
|
+
"""
|
|
254
|
+
pass
|
|
255
|
+
|
|
256
|
+
@abstractmethod
|
|
257
|
+
def deserialize(
|
|
258
|
+
self,
|
|
259
|
+
model: ORMModel, # type:ignore
|
|
260
|
+
data: dict,
|
|
261
|
+
allowed_fields: Optional[Dict[str, Set[str]]] = None,
|
|
262
|
+
request: Optional[Any] = None,
|
|
263
|
+
) -> dict:
|
|
264
|
+
"""
|
|
265
|
+
Deserialize the input data into validated Python types for the specified model.
|
|
266
|
+
- `allowed_fields`: a mapping (by model name) of fields the user is allowed to edit.
|
|
267
|
+
|
|
268
|
+
Only keys that appear in the allowed set will be processed.
|
|
269
|
+
"""
|
|
270
|
+
pass
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class AbstractSchemaGenerator(ABC):
|
|
274
|
+
@abstractmethod
|
|
275
|
+
def generate_schema(
|
|
276
|
+
self,
|
|
277
|
+
model: ORMModel, # type:ignore
|
|
278
|
+
global_schema_overrides: Dict[ORMField, dict], # type:ignore
|
|
279
|
+
additional_fields: List[ORMField], # type:ignore
|
|
280
|
+
) -> ModelSchemaMetadata:
|
|
281
|
+
"""
|
|
282
|
+
Generate and return a schema for the given model.
|
|
283
|
+
Both global schema overrides and per-model additional fields are applied.
|
|
284
|
+
"""
|
|
285
|
+
pass
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class AbstractSchemaOverride(ABC):
|
|
289
|
+
@abstractmethod
|
|
290
|
+
def get_schema(self) -> Tuple[SchemaFieldMetadata, Dict[str, str], str]:
|
|
291
|
+
"""
|
|
292
|
+
Return the schema for the field type.
|
|
293
|
+
"""
|
|
294
|
+
pass
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
# --- Event Emitter ---
|
|
298
|
+
class AbstractEventEmitter(ABC):
|
|
299
|
+
@abstractmethod
|
|
300
|
+
def emit(
|
|
301
|
+
self, namespace: str, event_type: ActionType, data: Dict[str, Any]
|
|
302
|
+
) -> None:
|
|
303
|
+
"""
|
|
304
|
+
Emit an event to the specified namespace with the given event type and data.
|
|
305
|
+
|
|
306
|
+
Parameters:
|
|
307
|
+
-----------
|
|
308
|
+
namespace: str
|
|
309
|
+
The namespace/channel to emit the event to
|
|
310
|
+
event_type: ActionType
|
|
311
|
+
The type of event being emitted
|
|
312
|
+
data: Dict[str, Any]
|
|
313
|
+
The structured data payload to emit
|
|
314
|
+
"""
|
|
315
|
+
pass
|
|
316
|
+
|
|
317
|
+
@abstractmethod
|
|
318
|
+
def has_permission(self, request: RequestType, namespace: str) -> bool:
|
|
319
|
+
"""
|
|
320
|
+
Check if the given request has permission to access the channel identified by the namespace.
|
|
321
|
+
"""
|
|
322
|
+
pass
|
|
323
|
+
|
|
324
|
+
@abstractmethod
|
|
325
|
+
def authenticate(self, request: RequestType) -> None:
|
|
326
|
+
"""
|
|
327
|
+
Authenticate the request for the event emitter.
|
|
328
|
+
"""
|
|
329
|
+
pass
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
# --- Permissions ---
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
class AbstractPermission(ABC):
|
|
336
|
+
@abstractmethod
|
|
337
|
+
def filter_queryset(
|
|
338
|
+
self, request: RequestType, queryset: ORMQuerySet
|
|
339
|
+
) -> Any: # type:ignore
|
|
340
|
+
"""
|
|
341
|
+
Given the request, queryset, and set of CRUD actions, return a queryset filtered according
|
|
342
|
+
to permission rules.
|
|
343
|
+
"""
|
|
344
|
+
pass
|
|
345
|
+
|
|
346
|
+
@abstractmethod
|
|
347
|
+
def allowed_actions(
|
|
348
|
+
self, request: RequestType, model: ORMModel
|
|
349
|
+
) -> Set[ActionType]: # type:ignore
|
|
350
|
+
"""
|
|
351
|
+
Return the set of CRUD actions the user is permitted to perform on the model.
|
|
352
|
+
"""
|
|
353
|
+
pass
|
|
354
|
+
|
|
355
|
+
@abstractmethod
|
|
356
|
+
def allowed_object_actions(
|
|
357
|
+
self, request: RequestType, obj: Any, model: ORMModel
|
|
358
|
+
) -> Set[ActionType]: # type:ignore
|
|
359
|
+
"""
|
|
360
|
+
Return the set of CRUD actions the user is permitted to perform on the specific object.
|
|
361
|
+
"""
|
|
362
|
+
pass
|
|
363
|
+
|
|
364
|
+
def bulk_operation_allowed(
|
|
365
|
+
self,
|
|
366
|
+
request: RequestType,
|
|
367
|
+
items: ORMQuerySet,
|
|
368
|
+
action_type: ActionType,
|
|
369
|
+
model: type,
|
|
370
|
+
) -> bool:
|
|
371
|
+
"""
|
|
372
|
+
Default bulk permission check that simply loops over 'items'
|
|
373
|
+
and calls 'allowed_object_actions' on each one. If any item
|
|
374
|
+
fails, raise PermissionDenied.
|
|
375
|
+
"""
|
|
376
|
+
for obj in items:
|
|
377
|
+
object_level_perms = self.allowed_object_actions(request, obj, model)
|
|
378
|
+
if action_type not in object_level_perms:
|
|
379
|
+
return False
|
|
380
|
+
return True
|
|
381
|
+
|
|
382
|
+
@abstractmethod
|
|
383
|
+
def visible_fields(
|
|
384
|
+
self, request: RequestType, model: ORMModel
|
|
385
|
+
) -> Union[Set[str], Literal["__all__"]]: # type:ignore
|
|
386
|
+
"""
|
|
387
|
+
Return the set of fields that are visible to the user for the given model and CRUD actions.
|
|
388
|
+
"""
|
|
389
|
+
pass
|
|
390
|
+
|
|
391
|
+
@abstractmethod
|
|
392
|
+
def editable_fields(
|
|
393
|
+
self, request: RequestType, model: ORMModel
|
|
394
|
+
) -> Union[Set[str], Literal["__all__"]]: # type:ignore
|
|
395
|
+
"""
|
|
396
|
+
Return the set of fields that are editable by the user for the given model and CRUD actions.
|
|
397
|
+
"""
|
|
398
|
+
pass
|
|
399
|
+
|
|
400
|
+
@abstractmethod
|
|
401
|
+
def create_fields(
|
|
402
|
+
self, request: RequestType, model: ORMModel
|
|
403
|
+
) -> Union[Set[str], Literal["__all__"]]: # type:ignore
|
|
404
|
+
"""
|
|
405
|
+
Return the set of fields that the user is allowed to specify in their create method
|
|
406
|
+
"""
|
|
407
|
+
pass
|
|
408
|
+
|
|
409
|
+
class AbstractSearchProvider(ABC):
|
|
410
|
+
"""Base class for search providers in StateZero."""
|
|
411
|
+
|
|
412
|
+
@abstractmethod
|
|
413
|
+
def search(self, queryset: ORMQuerySet, query: str, search_fields: Union[Set[str], Literal["__all__"]]) -> ORMQuerySet:
|
|
414
|
+
"""
|
|
415
|
+
Apply search filtering to a queryset.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
queryset: Django queryset
|
|
419
|
+
query: The search query string
|
|
420
|
+
search_fields: Set of field names to search in
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
Filtered queryset with search applied
|
|
424
|
+
"""
|
|
425
|
+
pass
|
|
426
|
+
|
|
427
|
+
class AbstractQueryOptimizer(ABC):
|
|
428
|
+
"""
|
|
429
|
+
Abstract Base Class for query optimizers.
|
|
430
|
+
|
|
431
|
+
Defines the essential interface for optimizing a query object,
|
|
432
|
+
potentially using configuration provided during initialization.
|
|
433
|
+
"""
|
|
434
|
+
|
|
435
|
+
def __init__(
|
|
436
|
+
self,
|
|
437
|
+
depth: Optional[int] = None,
|
|
438
|
+
fields_per_model: Optional[Dict[str, Set[str]]] = None,
|
|
439
|
+
get_model_name_func: Optional[Callable[[Type[ORMModel]], str]] = None
|
|
440
|
+
):
|
|
441
|
+
"""
|
|
442
|
+
Initializes the optimizer with common configuration potentially
|
|
443
|
+
used for generating optimization parameters if not provided directly
|
|
444
|
+
to the optimize method.
|
|
445
|
+
|
|
446
|
+
Args:
|
|
447
|
+
depth (Optional[int]): Default maximum relationship traversal depth
|
|
448
|
+
if generating field paths automatically.
|
|
449
|
+
fields_per_model (Optional[Dict[str, Set[str]]]): Default mapping of
|
|
450
|
+
model names (keys) to sets of required field/relationship names
|
|
451
|
+
(values), used if generating field paths automatically.
|
|
452
|
+
get_model_name_func (Optional[Callable]): Default function to get a
|
|
453
|
+
consistent string name for a model class, used with
|
|
454
|
+
fields_per_model if generating field paths automatically.
|
|
455
|
+
"""
|
|
456
|
+
self.default_depth = depth
|
|
457
|
+
self.default_fields_per_model = fields_per_model
|
|
458
|
+
self.default_get_model_name_func = get_model_name_func
|
|
459
|
+
# Basic validation for depth if provided
|
|
460
|
+
if self.default_depth is not None and self.default_depth < 0:
|
|
461
|
+
raise ValueError("Depth cannot be negative.")
|
|
462
|
+
|
|
463
|
+
@abstractmethod
|
|
464
|
+
def optimize(
|
|
465
|
+
self,
|
|
466
|
+
queryset: Any,
|
|
467
|
+
fields: Optional[List[str]] = None,
|
|
468
|
+
**kwargs: Any
|
|
469
|
+
) -> Any:
|
|
470
|
+
"""
|
|
471
|
+
Optimizes the given query object.
|
|
472
|
+
|
|
473
|
+
Concrete implementations will use the provided queryset and potentially
|
|
474
|
+
the 'fields' list or the configuration from __init__ to apply
|
|
475
|
+
optimizations.
|
|
476
|
+
|
|
477
|
+
Args:
|
|
478
|
+
queryset (Any): The query object to optimize (e.g., a Django QuerySet).
|
|
479
|
+
fields (Optional[List[str]]): An explicit list of field paths to optimize for.
|
|
480
|
+
If provided, this typically overrides any
|
|
481
|
+
automatic path generation based on init config.
|
|
482
|
+
**kwargs: Additional optimization-specific parameters.
|
|
483
|
+
|
|
484
|
+
Returns:
|
|
485
|
+
Any: The optimized query object.
|
|
486
|
+
|
|
487
|
+
Raises:
|
|
488
|
+
NotImplementedError: If the concrete class doesn't implement this.
|
|
489
|
+
ValueError: If required parameters (like 'fields' or init config
|
|
490
|
+
for generation) are missing.
|
|
491
|
+
"""
|
|
492
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any, Dict, Optional, Set, Type
|
|
3
|
+
|
|
4
|
+
from fastapi.encoders import jsonable_encoder
|
|
5
|
+
|
|
6
|
+
from statezero.core import AppConfig, ModelConfig, Registry
|
|
7
|
+
from statezero.core.ast_parser import ASTParser
|
|
8
|
+
from statezero.core.ast_validator import ASTValidator
|
|
9
|
+
from statezero.core.exceptions import PermissionDenied, ValidationError
|
|
10
|
+
from statezero.core.interfaces import (AbstractDataSerializer,
|
|
11
|
+
AbstractORMProvider,
|
|
12
|
+
AbstractSchemaGenerator)
|
|
13
|
+
from statezero.core.types import ActionType
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
logger.setLevel(logging.DEBUG)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _filter_writable_data(
|
|
20
|
+
data: Dict[str, Any],
|
|
21
|
+
req: Any,
|
|
22
|
+
model: Type,
|
|
23
|
+
model_config: ModelConfig,
|
|
24
|
+
orm_provider: AbstractORMProvider,
|
|
25
|
+
create: bool = False,
|
|
26
|
+
) -> Dict[str, Any]:
|
|
27
|
+
"""
|
|
28
|
+
Filter out keys for which the user does not have write permission.
|
|
29
|
+
When `create` is True, use the permission's `create_fields` method;
|
|
30
|
+
otherwise, use `editable_fields`.
|
|
31
|
+
|
|
32
|
+
If the allowed fields set contains "__all__", return the original data.
|
|
33
|
+
"""
|
|
34
|
+
all_fields = orm_provider.get_fields(model)
|
|
35
|
+
allowed_fields: Set[str] = set()
|
|
36
|
+
|
|
37
|
+
for permission_cls in model_config.permissions:
|
|
38
|
+
if create:
|
|
39
|
+
permission_fields = permission_cls().create_fields(req, model)
|
|
40
|
+
else:
|
|
41
|
+
permission_fields = permission_cls().editable_fields(req, model)
|
|
42
|
+
# handle the __all__ shorthand
|
|
43
|
+
if permission_fields == "__all__":
|
|
44
|
+
permission_fields = all_fields
|
|
45
|
+
else:
|
|
46
|
+
permission_fields &= all_fields
|
|
47
|
+
allowed_fields |= permission_fields
|
|
48
|
+
|
|
49
|
+
return {k: v for k, v in data.items() if k in allowed_fields}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class RequestProcessor:
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
config: AppConfig,
|
|
56
|
+
registry: Registry,
|
|
57
|
+
orm_provider: AbstractORMProvider = None,
|
|
58
|
+
data_serializer: AbstractDataSerializer = None,
|
|
59
|
+
schema_generator: AbstractSchemaGenerator = None,
|
|
60
|
+
schema_overrides: Dict = None,
|
|
61
|
+
):
|
|
62
|
+
self.orm_provider = orm_provider or config.orm_provider
|
|
63
|
+
self.data_serializer = data_serializer or config.serializer
|
|
64
|
+
self.schema_generator = schema_generator or config.schema_generator
|
|
65
|
+
self.schema_overrides = schema_overrides or config.schema_overrides
|
|
66
|
+
self.registry = registry
|
|
67
|
+
self.config = config
|
|
68
|
+
|
|
69
|
+
def process_schema(self, req: Any) -> Dict[str, Any]:
|
|
70
|
+
try:
|
|
71
|
+
model_name: str = req.parser_context.get("kwargs", {}).get("model_name")
|
|
72
|
+
model = self.orm_provider.get_model_by_name(model_name)
|
|
73
|
+
config: ModelConfig = self.registry.get_config(model)
|
|
74
|
+
|
|
75
|
+
# In production, check that the user has permission to at least one of the CRUD actions.
|
|
76
|
+
if not self.config.DEBUG:
|
|
77
|
+
allowed_actions: Set[ActionType] = set()
|
|
78
|
+
for permission_cls in config.permissions:
|
|
79
|
+
allowed_actions |= permission_cls().allowed_actions(req, model)
|
|
80
|
+
required_actions = {
|
|
81
|
+
ActionType.CREATE,
|
|
82
|
+
ActionType.READ,
|
|
83
|
+
ActionType.UPDATE,
|
|
84
|
+
ActionType.DELETE,
|
|
85
|
+
}
|
|
86
|
+
if allowed_actions.isdisjoint(required_actions):
|
|
87
|
+
raise PermissionDenied(
|
|
88
|
+
"User does not have any permissions required to access the schema."
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
schema_meta = self.schema_generator.generate_schema(
|
|
92
|
+
model=model,
|
|
93
|
+
global_schema_overrides=self.schema_overrides,
|
|
94
|
+
additional_fields=config.additional_fields,
|
|
95
|
+
)
|
|
96
|
+
schema_dict = schema_meta.model_dump()
|
|
97
|
+
return jsonable_encoder(schema_dict)
|
|
98
|
+
except Exception as e:
|
|
99
|
+
logger.exception("Error in process_schema")
|
|
100
|
+
raise ValidationError(str(e))
|
|
101
|
+
|
|
102
|
+
def process_request(self, req: Any) -> Dict[str, Any]:
|
|
103
|
+
body: Dict[str, Any] = req.data or {}
|
|
104
|
+
ast_body: Dict[str, Any] = body.get("ast", {})
|
|
105
|
+
initial_query_ast: Dict[str, Any] = ast_body.get("initial_query", {})
|
|
106
|
+
final_query_ast: Dict[str, Any] = ast_body.get("query", {})
|
|
107
|
+
|
|
108
|
+
model_name: str = req.parser_context.get("kwargs", {}).get("model_name")
|
|
109
|
+
model = self.orm_provider.get_model_by_name(model_name)
|
|
110
|
+
model_config: ModelConfig = self.registry.get_config(model)
|
|
111
|
+
|
|
112
|
+
base_queryset = self.orm_provider.get_queryset(
|
|
113
|
+
req=req,
|
|
114
|
+
model=model,
|
|
115
|
+
initial_ast=initial_query_ast,
|
|
116
|
+
custom_querysets=model_config.custom_querysets,
|
|
117
|
+
registered_permissions=model_config.permissions,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
for permission_cls in model_config.permissions:
|
|
121
|
+
base_queryset = permission_cls().filter_queryset(req, base_queryset)
|
|
122
|
+
|
|
123
|
+
# ---- PERMISSION CHECKS: Global Level (Write operations remain here) ----
|
|
124
|
+
requested_actions: Set[ActionType] = ASTParser.get_requested_action_types(
|
|
125
|
+
final_query_ast
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
allowed_global_actions: Set[ActionType] = set()
|
|
129
|
+
for permission_cls in model_config.permissions:
|
|
130
|
+
allowed_global_actions |= permission_cls().allowed_actions(req, model)
|
|
131
|
+
if "__all__" not in allowed_global_actions:
|
|
132
|
+
if not requested_actions.issubset(allowed_global_actions):
|
|
133
|
+
missing = requested_actions - allowed_global_actions
|
|
134
|
+
missing_str = ", ".join(action.value for action in missing)
|
|
135
|
+
raise PermissionDenied(
|
|
136
|
+
f"Missing global permissions for actions: {missing_str}"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# For READ operations, delegate field permission checks to ASTValidator.
|
|
140
|
+
serializer_options = ast_body.get("serializerOptions", {})
|
|
141
|
+
|
|
142
|
+
# Invoke the ASTValidator to check read field permissions.
|
|
143
|
+
model_graph = self.orm_provider.build_model_graph(model)
|
|
144
|
+
validator = ASTValidator(
|
|
145
|
+
model_graph=model_graph,
|
|
146
|
+
get_model_name=self.orm_provider.get_model_name,
|
|
147
|
+
registry=self.registry,
|
|
148
|
+
request=req,
|
|
149
|
+
get_model_by_name=self.orm_provider.get_model_by_name,
|
|
150
|
+
)
|
|
151
|
+
validator.validate_fields(final_query_ast, model)
|
|
152
|
+
|
|
153
|
+
# ---- WRITE OPERATIONS: Filter incoming data to include only writable fields. ----
|
|
154
|
+
op = final_query_ast.get("type")
|
|
155
|
+
if op in ["create", "update"]:
|
|
156
|
+
data = final_query_ast.get("data", {})
|
|
157
|
+
# For create operations, pass create=True so that create_fields are used.
|
|
158
|
+
filtered_data = _filter_writable_data(
|
|
159
|
+
data, req, model, model_config, self.orm_provider, create=(op == "create")
|
|
160
|
+
)
|
|
161
|
+
final_query_ast["data"] = filtered_data
|
|
162
|
+
elif op in ["get_or_create", "update_or_create"]:
|
|
163
|
+
if "lookup" in final_query_ast:
|
|
164
|
+
final_query_ast["lookup"] = _filter_writable_data(
|
|
165
|
+
final_query_ast["lookup"], req, model, model_config, self.orm_provider, create=True
|
|
166
|
+
)
|
|
167
|
+
if "defaults" in final_query_ast:
|
|
168
|
+
final_query_ast["defaults"] = _filter_writable_data(
|
|
169
|
+
final_query_ast["defaults"], req, model, model_config, self.orm_provider, create=True
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Create and use the AST parser directly, instead of delegating to ORM provider
|
|
173
|
+
self.orm_provider.set_queryset(base_queryset)
|
|
174
|
+
parser = ASTParser(
|
|
175
|
+
engine=self.orm_provider,
|
|
176
|
+
serializer=self.data_serializer,
|
|
177
|
+
model=model,
|
|
178
|
+
config=self.config,
|
|
179
|
+
registry=self.registry,
|
|
180
|
+
serializer_options=serializer_options or {},
|
|
181
|
+
request=req,
|
|
182
|
+
)
|
|
183
|
+
result: Dict[str, Any] = parser.parse(final_query_ast)
|
|
184
|
+
return result
|