statezero 0.1.0b12__tar.gz → 0.1.0b22__tar.gz
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 statezero might be problematic. Click here for more details.
- {statezero-0.1.0b12 → statezero-0.1.0b22}/PKG-INFO +1 -1
- {statezero-0.1.0b12 → statezero-0.1.0b22}/pyproject.toml +1 -1
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/adaptors/django/actions.py +8 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/adaptors/django/extensions/custom_field_serializers/money_field.py +8 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/adaptors/django/orm.py +29 -5
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/adaptors/django/permissions.py +6 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/adaptors/django/query_optimizer.py +40 -2
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/adaptors/django/schemas.py +15 -2
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/adaptors/django/serializers.py +84 -36
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/adaptors/django/urls.py +2 -1
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/adaptors/django/views.py +101 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/core/ast_parser.py +43 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/core/classes.py +1 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/core/config.py +0 -37
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/core/interfaces.py +29 -4
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/core/process_request.py +0 -1
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/core/types.py +1 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero.egg-info/PKG-INFO +1 -1
- {statezero-0.1.0b12 → statezero-0.1.0b22}/README.md +0 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/license.md +0 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/requirements.txt +0 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/setup.cfg +0 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/__init__.py +0 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/adaptors/__init__.py +0 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/adaptors/django/__init__.py +0 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/adaptors/django/apps.py +0 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/adaptors/django/config.py +0 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/adaptors/django/context_manager.py +0 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/adaptors/django/event_emitters.py +0 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/adaptors/django/exception_handler.py +0 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/adaptors/django/extensions/__init__.py +0 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/adaptors/django/extensions/custom_field_serializers/__init__.py +0 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/adaptors/django/extensions/custom_field_serializers/file_fields.py +0 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/adaptors/django/f_handler.py +0 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/adaptors/django/helpers.py +0 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/adaptors/django/middleware.py +0 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/adaptors/django/migrations/0001_initial.py +0 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/adaptors/django/migrations/0002_delete_modelviewsubscription.py +0 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/adaptors/django/migrations/__init__.py +0 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/adaptors/django/search_providers/__init__.py +0 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/adaptors/django/search_providers/basic_search.py +0 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/adaptors/django/search_providers/postgres_search.py +0 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/core/__init__.py +0 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/core/actions.py +0 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/core/ast_validator.py +0 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/core/context_storage.py +0 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/core/event_bus.py +0 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/core/event_emitters.py +0 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/core/exceptions.py +0 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/core/hook_checks.py +0 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero.egg-info/SOURCES.txt +0 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero.egg-info/dependency_links.txt +0 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero.egg-info/requires.txt +0 -0
- {statezero-0.1.0b12 → statezero-0.1.0b22}/statezero.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: statezero
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.0b22
|
|
4
4
|
Summary: Connect your Python backend to a modern JavaScript SPA frontend with 90% less complexity.
|
|
5
5
|
Author-email: Robert <robert.herring@statezero.dev>
|
|
6
6
|
Project-URL: homepage, https://www.statezero.dev
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "statezero"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.0b22"
|
|
8
8
|
description = "Connect your Python backend to a modern JavaScript SPA frontend with 90% less complexity."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -125,6 +125,14 @@ class DjangoActionSchemaGenerator:
|
|
|
125
125
|
return "string"
|
|
126
126
|
return "integer"
|
|
127
127
|
|
|
128
|
+
# Handle nested serializers (many=True creates a ListSerializer)
|
|
129
|
+
if isinstance(field, serializers.ListSerializer):
|
|
130
|
+
return "array"
|
|
131
|
+
|
|
132
|
+
# Handle nested serializers (single nested serializer)
|
|
133
|
+
if isinstance(field, serializers.Serializer):
|
|
134
|
+
return "object"
|
|
135
|
+
|
|
128
136
|
type_mapping = {
|
|
129
137
|
fields.BooleanField: "boolean",
|
|
130
138
|
fields.CharField: "string",
|
|
@@ -15,6 +15,14 @@ class MoneyFieldSerializer(serializers.Field):
|
|
|
15
15
|
self.decimal_places = kwargs.pop("decimal_places", 2)
|
|
16
16
|
super().__init__(**kwargs)
|
|
17
17
|
|
|
18
|
+
@classmethod
|
|
19
|
+
def get_prefetch_db_fields(cls, field_name: str):
|
|
20
|
+
"""
|
|
21
|
+
Return all database fields required for this field to serialize.
|
|
22
|
+
MoneyField creates two database columns: field_name and field_name_currency.
|
|
23
|
+
"""
|
|
24
|
+
return [field_name, f"{field_name}_currency"]
|
|
25
|
+
|
|
18
26
|
def to_representation(self, value):
|
|
19
27
|
djmoney_field = MoneyField(
|
|
20
28
|
max_digits=self.max_digits, decimal_places=self.decimal_places
|
|
@@ -258,6 +258,26 @@ class DjangoORMAdapter(AbstractORMProvider):
|
|
|
258
258
|
fields_map=fields_map,
|
|
259
259
|
)
|
|
260
260
|
|
|
261
|
+
def bulk_create(
|
|
262
|
+
self,
|
|
263
|
+
model: Type[models.Model],
|
|
264
|
+
data_list: List[Dict[str, Any]],
|
|
265
|
+
serializer,
|
|
266
|
+
req,
|
|
267
|
+
fields_map,
|
|
268
|
+
) -> List[models.Model]:
|
|
269
|
+
"""Create multiple model instances using Django's bulk_create."""
|
|
270
|
+
# Create instances without saving to DB yet
|
|
271
|
+
instances = [model(**data) for data in data_list]
|
|
272
|
+
|
|
273
|
+
# Use Django's bulk_create for efficiency
|
|
274
|
+
created_instances = model.objects.bulk_create(instances)
|
|
275
|
+
|
|
276
|
+
# Emit bulk create event for cache invalidation and frontend notification
|
|
277
|
+
config.event_bus.emit_bulk_event(ActionType.BULK_CREATE, created_instances)
|
|
278
|
+
|
|
279
|
+
return created_instances
|
|
280
|
+
|
|
261
281
|
def update_instance(
|
|
262
282
|
self,
|
|
263
283
|
model: Type[models.Model],
|
|
@@ -750,19 +770,15 @@ class DjangoORMAdapter(AbstractORMProvider):
|
|
|
750
770
|
req: RequestType,
|
|
751
771
|
model: Type,
|
|
752
772
|
initial_ast: Dict[str, Any],
|
|
753
|
-
custom_querysets: Dict[str, Type[AbstractCustomQueryset]],
|
|
754
773
|
registered_permissions: List[Type[AbstractPermission]],
|
|
755
774
|
) -> Any:
|
|
756
775
|
"""Assemble and return the base QuerySet for the given model."""
|
|
757
|
-
custom_name = initial_ast.get("custom_queryset")
|
|
758
|
-
if custom_name and custom_name in custom_querysets:
|
|
759
|
-
custom_queryset_class = custom_querysets[custom_name]
|
|
760
|
-
return custom_queryset_class().get_queryset(req)
|
|
761
776
|
return model.objects.all()
|
|
762
777
|
|
|
763
778
|
def get_fields(self, model: models.Model) -> Set[str]:
|
|
764
779
|
"""
|
|
765
780
|
Return a set of the model fields.
|
|
781
|
+
Includes both database fields and additional_fields (computed fields).
|
|
766
782
|
"""
|
|
767
783
|
model_config = registry.get_config(model)
|
|
768
784
|
if model_config.fields and "__all__" != model_config.fields:
|
|
@@ -775,6 +791,14 @@ class DjangoORMAdapter(AbstractORMProvider):
|
|
|
775
791
|
resolved_fields = resolved_fields.union(additional_fields)
|
|
776
792
|
return resolved_fields
|
|
777
793
|
|
|
794
|
+
def get_db_fields(self, model: models.Model) -> Set[str]:
|
|
795
|
+
"""
|
|
796
|
+
Return only actual database fields for the model.
|
|
797
|
+
Excludes read-only additional_fields (computed fields).
|
|
798
|
+
Used for deserialization - hooks can write to any DB field.
|
|
799
|
+
"""
|
|
800
|
+
return set(field.name for field in model._meta.get_fields())
|
|
801
|
+
|
|
778
802
|
def build_model_graph(
|
|
779
803
|
self, model: Type[models.Model], model_graph: nx.DiGraph = None
|
|
780
804
|
) -> nx.DiGraph:
|
|
@@ -25,6 +25,7 @@ class AllowAllPermission(AbstractPermission):
|
|
|
25
25
|
ActionType.DELETE,
|
|
26
26
|
ActionType.READ,
|
|
27
27
|
ActionType.UPDATE,
|
|
28
|
+
ActionType.BULK_CREATE,
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
def allowed_object_actions(self, request, obj, model: Type[ORMModel]) -> Set[ActionType]: # type: ignore
|
|
@@ -33,6 +34,7 @@ class AllowAllPermission(AbstractPermission):
|
|
|
33
34
|
ActionType.DELETE,
|
|
34
35
|
ActionType.READ,
|
|
35
36
|
ActionType.UPDATE,
|
|
37
|
+
ActionType.BULK_CREATE,
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
def _get_user_fields(self) -> Set[str]:
|
|
@@ -74,6 +76,7 @@ class IsAuthenticatedPermission(AbstractPermission):
|
|
|
74
76
|
ActionType.DELETE,
|
|
75
77
|
ActionType.READ,
|
|
76
78
|
ActionType.UPDATE,
|
|
79
|
+
ActionType.BULK_CREATE,
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
def allowed_object_actions(
|
|
@@ -86,6 +89,7 @@ class IsAuthenticatedPermission(AbstractPermission):
|
|
|
86
89
|
ActionType.DELETE,
|
|
87
90
|
ActionType.READ,
|
|
88
91
|
ActionType.UPDATE,
|
|
92
|
+
ActionType.BULK_CREATE,
|
|
89
93
|
}
|
|
90
94
|
|
|
91
95
|
def _get_user_fields(self) -> Set[str]:
|
|
@@ -134,6 +138,7 @@ class IsStaffPermission(AbstractPermission):
|
|
|
134
138
|
ActionType.DELETE,
|
|
135
139
|
ActionType.READ,
|
|
136
140
|
ActionType.UPDATE,
|
|
141
|
+
ActionType.BULK_CREATE,
|
|
137
142
|
}
|
|
138
143
|
|
|
139
144
|
def allowed_object_actions(
|
|
@@ -146,6 +151,7 @@ class IsStaffPermission(AbstractPermission):
|
|
|
146
151
|
ActionType.DELETE,
|
|
147
152
|
ActionType.READ,
|
|
148
153
|
ActionType.UPDATE,
|
|
154
|
+
ActionType.BULK_CREATE,
|
|
149
155
|
}
|
|
150
156
|
|
|
151
157
|
def _get_user_fields(self) -> Set[str]:
|
|
@@ -477,7 +477,34 @@ def optimize_query(queryset, fields=None, fields_map=None, depth=0, use_only=Tru
|
|
|
477
477
|
related_fields_to_fetch = set()
|
|
478
478
|
|
|
479
479
|
if fields_map and related_model_name in fields_map:
|
|
480
|
-
|
|
480
|
+
# Process each field, checking for custom serializers
|
|
481
|
+
from statezero.adaptors.django.serializers import get_custom_serializer
|
|
482
|
+
related_meta = _get_model_meta(related_model)
|
|
483
|
+
for field_name in fields_map[related_model_name]:
|
|
484
|
+
try:
|
|
485
|
+
field_obj = related_meta.get_field(field_name)
|
|
486
|
+
if not field_obj.is_relation:
|
|
487
|
+
# Check if this field has a custom serializer with explicit DB field requirements
|
|
488
|
+
custom_serializer = get_custom_serializer(field_obj.__class__)
|
|
489
|
+
if custom_serializer and hasattr(custom_serializer, 'get_prefetch_db_fields'):
|
|
490
|
+
# Use the explicit list from the custom serializer
|
|
491
|
+
db_fields = custom_serializer.get_prefetch_db_fields(field_name)
|
|
492
|
+
for db_field in db_fields:
|
|
493
|
+
related_fields_to_fetch.add(db_field)
|
|
494
|
+
logger.debug(f"Using custom DB fields {db_fields} for field '{field_name}' in {related_model_name}")
|
|
495
|
+
else:
|
|
496
|
+
# No custom serializer, just add the field itself
|
|
497
|
+
related_fields_to_fetch.add(field_name)
|
|
498
|
+
else:
|
|
499
|
+
# Relation field, add as-is
|
|
500
|
+
related_fields_to_fetch.add(field_name)
|
|
501
|
+
except FieldDoesNotExist:
|
|
502
|
+
# Field doesn't exist, add it anyway (might be computed)
|
|
503
|
+
related_fields_to_fetch.add(field_name)
|
|
504
|
+
except Exception as e:
|
|
505
|
+
logger.error(f"Error checking custom serializer for field '{field_name}' in {related_model_name}: {e}")
|
|
506
|
+
# On error, add the field anyway to be safe
|
|
507
|
+
related_fields_to_fetch.add(field_name)
|
|
481
508
|
else:
|
|
482
509
|
# If no field restrictions are provided, get all fields
|
|
483
510
|
all_fields = [f.name for f in related_model._meta.get_fields() if f.concrete]
|
|
@@ -531,7 +558,18 @@ def optimize_query(queryset, fields=None, fields_map=None, depth=0, use_only=Tru
|
|
|
531
558
|
try:
|
|
532
559
|
field_obj = root_meta.get_field(field_name)
|
|
533
560
|
if not field_obj.is_relation:
|
|
534
|
-
|
|
561
|
+
# Check if this field has a custom serializer with explicit DB field requirements
|
|
562
|
+
from statezero.adaptors.django.serializers import get_custom_serializer
|
|
563
|
+
custom_serializer = get_custom_serializer(field_obj.__class__)
|
|
564
|
+
if custom_serializer and hasattr(custom_serializer, 'get_prefetch_db_fields'):
|
|
565
|
+
# Use the explicit list from the custom serializer
|
|
566
|
+
db_fields = custom_serializer.get_prefetch_db_fields(field_name)
|
|
567
|
+
for db_field in db_fields:
|
|
568
|
+
root_fields_to_fetch.add(db_field)
|
|
569
|
+
logger.debug(f"Using custom DB fields {db_fields} for field '{field_name}'")
|
|
570
|
+
else:
|
|
571
|
+
# No custom serializer, just add the field itself
|
|
572
|
+
root_fields_to_fetch.add(field_name)
|
|
535
573
|
elif isinstance(field_obj, (ForeignKey, OneToOneField)):
|
|
536
574
|
# If FK/O2O itself is requested directly, include its id field
|
|
537
575
|
root_fields_to_fetch.add(field_obj.attname)
|
|
@@ -178,6 +178,7 @@ class DjangoSchemaGenerator(AbstractSchemaGenerator):
|
|
|
178
178
|
nullable=False,
|
|
179
179
|
format=FieldFormat.ID,
|
|
180
180
|
description=description,
|
|
181
|
+
read_only=True,
|
|
181
182
|
)
|
|
182
183
|
elif isinstance(field, models.UUIDField):
|
|
183
184
|
return SchemaFieldMetadata(
|
|
@@ -187,6 +188,7 @@ class DjangoSchemaGenerator(AbstractSchemaGenerator):
|
|
|
187
188
|
nullable=False,
|
|
188
189
|
format=FieldFormat.UUID,
|
|
189
190
|
description=description,
|
|
191
|
+
read_only=True,
|
|
190
192
|
)
|
|
191
193
|
elif isinstance(field, models.CharField):
|
|
192
194
|
return SchemaFieldMetadata(
|
|
@@ -197,6 +199,7 @@ class DjangoSchemaGenerator(AbstractSchemaGenerator):
|
|
|
197
199
|
format=FieldFormat.ID,
|
|
198
200
|
max_length=field.max_length,
|
|
199
201
|
description=description,
|
|
202
|
+
read_only=True,
|
|
200
203
|
)
|
|
201
204
|
else:
|
|
202
205
|
return SchemaFieldMetadata(
|
|
@@ -206,6 +209,7 @@ class DjangoSchemaGenerator(AbstractSchemaGenerator):
|
|
|
206
209
|
nullable=False,
|
|
207
210
|
format=FieldFormat.ID,
|
|
208
211
|
description=description,
|
|
212
|
+
read_only=True,
|
|
209
213
|
)
|
|
210
214
|
|
|
211
215
|
def get_field_metadata(
|
|
@@ -256,6 +260,9 @@ class DjangoSchemaGenerator(AbstractSchemaGenerator):
|
|
|
256
260
|
elif isinstance(field, models.DateField):
|
|
257
261
|
field_type = FieldType.STRING
|
|
258
262
|
field_format = FieldFormat.DATE
|
|
263
|
+
elif isinstance(field, models.TimeField):
|
|
264
|
+
field_type = FieldType.STRING
|
|
265
|
+
field_format = FieldFormat.TIME
|
|
259
266
|
elif isinstance(field, (models.ForeignKey, models.OneToOneField)):
|
|
260
267
|
field_type = self.get_pk_type(field)
|
|
261
268
|
field_format = self.get_relation_type(field)
|
|
@@ -286,6 +293,12 @@ class DjangoSchemaGenerator(AbstractSchemaGenerator):
|
|
|
286
293
|
elif callable(default):
|
|
287
294
|
default = default()
|
|
288
295
|
|
|
296
|
+
# Check if field should be read-only (auto_now or auto_now_add)
|
|
297
|
+
read_only = False
|
|
298
|
+
if isinstance(field, (models.DateTimeField, models.DateField, models.TimeField)):
|
|
299
|
+
if getattr(field, "auto_now", False) or getattr(field, "auto_now_add", False):
|
|
300
|
+
read_only = True
|
|
301
|
+
|
|
289
302
|
return SchemaFieldMetadata(
|
|
290
303
|
type=field_type,
|
|
291
304
|
title=title,
|
|
@@ -299,6 +312,7 @@ class DjangoSchemaGenerator(AbstractSchemaGenerator):
|
|
|
299
312
|
max_digits=max_digits,
|
|
300
313
|
decimal_places=decimal_places,
|
|
301
314
|
description=description,
|
|
315
|
+
read_only=read_only,
|
|
302
316
|
)
|
|
303
317
|
|
|
304
318
|
def get_field_title(self, field: models.Field) -> str:
|
|
@@ -309,8 +323,7 @@ class DjangoSchemaGenerator(AbstractSchemaGenerator):
|
|
|
309
323
|
@staticmethod
|
|
310
324
|
def is_field_required(field: models.Field) -> bool:
|
|
311
325
|
return (
|
|
312
|
-
not field.
|
|
313
|
-
and not field.null
|
|
326
|
+
not field.null
|
|
314
327
|
and field.default == models.fields.NOT_PROVIDED
|
|
315
328
|
)
|
|
316
329
|
|
|
@@ -6,7 +6,8 @@ from rest_framework import serializers
|
|
|
6
6
|
import contextvars
|
|
7
7
|
from contextlib import contextmanager
|
|
8
8
|
import logging
|
|
9
|
-
from cytoolz import pluck
|
|
9
|
+
from cytoolz import pluck, keyfilter
|
|
10
|
+
from cytoolz.functoolz import thread_first
|
|
10
11
|
from zen_queries import queries_disabled
|
|
11
12
|
|
|
12
13
|
from statezero.adaptors.django.config import config, registry
|
|
@@ -476,41 +477,78 @@ class DRFDynamicSerializer(AbstractDataSerializer):
|
|
|
476
477
|
def deserialize(
|
|
477
478
|
self,
|
|
478
479
|
model: Type[models.Model],
|
|
479
|
-
data: Dict[str, Any],
|
|
480
|
+
data: Union[Dict[str, Any], List[Dict[str, Any]]],
|
|
480
481
|
fields_map: Optional[Dict[str, Set[str]]],
|
|
481
482
|
partial: bool = False,
|
|
482
483
|
request: Optional[RequestType] = None,
|
|
483
|
-
|
|
484
|
+
many: bool = False,
|
|
485
|
+
) -> Union[Dict[str, Any], List[Dict[str, Any]]]:
|
|
484
486
|
# Serious security issue if fields_map is None
|
|
485
487
|
assert fields_map is not None, "fields_map is required and cannot be None"
|
|
486
488
|
|
|
487
|
-
#
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
serializer_class = DynamicModelSerializer.for_model(model)
|
|
491
|
-
available_fields = set(serializer_class().fields.keys())
|
|
489
|
+
# Get model name and allowed fields from fields_map
|
|
490
|
+
model_name = config.orm_provider.get_model_name(model)
|
|
491
|
+
allowed_fields = fields_map.get(model_name, set())
|
|
492
492
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
493
|
+
# Filter user input to only allowed fields (security boundary)
|
|
494
|
+
if many:
|
|
495
|
+
data = [dict(keyfilter(lambda k: k in allowed_fields, item)) for item in data]
|
|
496
|
+
else:
|
|
497
|
+
data = dict(keyfilter(lambda k: k in allowed_fields, data))
|
|
498
|
+
|
|
499
|
+
try:
|
|
500
|
+
model_config = registry.get_config(model)
|
|
501
|
+
except ValueError:
|
|
502
|
+
# No model config available
|
|
503
|
+
model_config = None
|
|
504
|
+
|
|
505
|
+
# Run pre-hooks on filtered data (hooks can add any DB fields)
|
|
506
|
+
if model_config and model_config.pre_hooks:
|
|
507
|
+
# Wrap hooks with validation in DEBUG mode
|
|
508
|
+
if settings.DEBUG:
|
|
509
|
+
# Get all DB fields for validation
|
|
510
|
+
all_db_fields = config.orm_provider.get_db_fields(model)
|
|
511
|
+
|
|
512
|
+
def make_validated_pre_hook(hook):
|
|
513
|
+
def validated_hook(data, request=None):
|
|
514
|
+
original_data = data
|
|
515
|
+
result = hook(data, request=request)
|
|
516
|
+
return _check_pre_hook_result(
|
|
517
|
+
original_data=original_data,
|
|
518
|
+
result_data=result,
|
|
519
|
+
model=model,
|
|
520
|
+
serializer_fields=all_db_fields
|
|
521
|
+
)
|
|
522
|
+
return validated_hook
|
|
523
|
+
hook_funcs = [(make_validated_pre_hook(hook), request) for hook in model_config.pre_hooks]
|
|
524
|
+
else:
|
|
525
|
+
hook_funcs = [(hook, request) for hook in model_config.pre_hooks]
|
|
526
|
+
|
|
527
|
+
if many:
|
|
528
|
+
data = [thread_first(item, *hook_funcs) for item in data]
|
|
529
|
+
else:
|
|
530
|
+
data = thread_first(data, *hook_funcs)
|
|
531
|
+
|
|
532
|
+
# Expand fields_map to include fields that hooks may have added
|
|
533
|
+
# For partial updates, only include allowed_fields + any fields in the data
|
|
534
|
+
# This prevents validation errors on required fields that were filtered out
|
|
535
|
+
if partial:
|
|
536
|
+
# For partial updates: only include fields that are either allowed or in the data
|
|
537
|
+
expanded_fields = allowed_fields | set(data.keys())
|
|
538
|
+
else:
|
|
539
|
+
# For creates: include all DB fields to allow hooks to add any field
|
|
540
|
+
expanded_fields = config.orm_provider.get_db_fields(model)
|
|
541
|
+
expanded_fields_map = {model_name: expanded_fields}
|
|
542
|
+
|
|
543
|
+
# Use the context manager with expanded fields map
|
|
544
|
+
with fields_map_context(expanded_fields_map):
|
|
545
|
+
# Create serializer class with all DB fields available
|
|
546
|
+
serializer_class = DynamicModelSerializer.for_model(model)
|
|
510
547
|
|
|
511
548
|
# Create serializer
|
|
512
549
|
serializer = serializer_class(
|
|
513
|
-
data=data,
|
|
550
|
+
data=data,
|
|
551
|
+
many=many,
|
|
514
552
|
partial=partial,
|
|
515
553
|
request=request
|
|
516
554
|
)
|
|
@@ -518,16 +556,26 @@ class DRFDynamicSerializer(AbstractDataSerializer):
|
|
|
518
556
|
validated_data = serializer.validated_data
|
|
519
557
|
|
|
520
558
|
if model_config and model_config.post_hooks:
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
original_data=
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
559
|
+
# Wrap hooks with validation in DEBUG mode
|
|
560
|
+
if settings.DEBUG:
|
|
561
|
+
def make_validated_hook(hook):
|
|
562
|
+
def validated_hook(data, request=None):
|
|
563
|
+
original_data = data
|
|
564
|
+
result = hook(data, request=request)
|
|
565
|
+
return _check_post_hook_result(
|
|
566
|
+
original_data=original_data,
|
|
567
|
+
result_data=result,
|
|
568
|
+
model=model
|
|
569
|
+
)
|
|
570
|
+
return validated_hook
|
|
571
|
+
hook_funcs = [(make_validated_hook(hook), request) for hook in model_config.post_hooks]
|
|
572
|
+
else:
|
|
573
|
+
hook_funcs = [(hook, request) for hook in model_config.post_hooks]
|
|
574
|
+
|
|
575
|
+
if many:
|
|
576
|
+
validated_data = [thread_first(item, *hook_funcs) for item in validated_data]
|
|
577
|
+
else:
|
|
578
|
+
validated_data = thread_first(validated_data, *hook_funcs)
|
|
531
579
|
|
|
532
580
|
return validated_data
|
|
533
581
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from django.urls import path
|
|
2
2
|
|
|
3
|
-
from .views import EventsAuthView, ModelListView, ModelView, SchemaView, FileUploadView, FastUploadView, ActionSchemaView, ActionView, ValidateView
|
|
3
|
+
from .views import EventsAuthView, ModelListView, ModelView, SchemaView, FileUploadView, FastUploadView, ActionSchemaView, ActionView, ValidateView, FieldPermissionsView
|
|
4
4
|
|
|
5
5
|
app_name = "statezero"
|
|
6
6
|
|
|
@@ -12,6 +12,7 @@ urlpatterns = [
|
|
|
12
12
|
path("actions/<str:action_name>/", ActionView.as_view(), name="action"),
|
|
13
13
|
path("actions-schema/", ActionSchemaView.as_view(), name="actions_schema"),
|
|
14
14
|
path("<str:model_name>/validate/", ValidateView.as_view(), name="validate"),
|
|
15
|
+
path("<str:model_name>/field-permissions/", FieldPermissionsView.as_view(), name="field_permissions"),
|
|
15
16
|
path("<str:model_name>/get-schema/", SchemaView.as_view(), name="schema_view"),
|
|
16
17
|
path("<str:model_name>/", ModelView.as_view(), name="model_view"),
|
|
17
18
|
]
|
|
@@ -485,3 +485,104 @@ class ValidateView(APIView):
|
|
|
485
485
|
except Exception as original_exception:
|
|
486
486
|
# Let StateZero's exception handler deal with ValidationError, PermissionDenied, etc.
|
|
487
487
|
return explicit_exception_handler(original_exception)
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
class FieldPermissionsView(APIView):
|
|
491
|
+
"""
|
|
492
|
+
Returns user-specific field permissions for a given model.
|
|
493
|
+
Used by frontend forms to determine which fields to show/enable at runtime.
|
|
494
|
+
"""
|
|
495
|
+
|
|
496
|
+
permission_classes = [permission_class]
|
|
497
|
+
|
|
498
|
+
def get(self, request, model_name):
|
|
499
|
+
"""Get field permissions for the current user."""
|
|
500
|
+
try:
|
|
501
|
+
# Create processor following the same pattern as other views
|
|
502
|
+
processor = RequestProcessor(config=config, registry=registry)
|
|
503
|
+
|
|
504
|
+
# Get model using the processor's ORM provider
|
|
505
|
+
try:
|
|
506
|
+
model = processor.orm_provider.get_model_by_name(model_name)
|
|
507
|
+
except (LookupError, ValueError):
|
|
508
|
+
return Response({"error": f"Model {model_name} not found"}, status=404)
|
|
509
|
+
|
|
510
|
+
if not model:
|
|
511
|
+
return Response({"error": f"Model {model_name} not found"}, status=404)
|
|
512
|
+
|
|
513
|
+
try:
|
|
514
|
+
model_config = processor.registry.get_config(model)
|
|
515
|
+
except ValueError:
|
|
516
|
+
return Response(
|
|
517
|
+
{"error": f"Model {model_name} not registered"}, status=404
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
# Compute field permissions using the same logic as ASTParser._get_operation_fields
|
|
521
|
+
all_fields = processor.orm_provider.get_fields(model)
|
|
522
|
+
|
|
523
|
+
visible_fields = self._compute_operation_fields(
|
|
524
|
+
model, model_config, all_fields, request, "read"
|
|
525
|
+
)
|
|
526
|
+
creatable_fields = self._compute_operation_fields(
|
|
527
|
+
model, model_config, all_fields, request, "create"
|
|
528
|
+
)
|
|
529
|
+
editable_fields = self._compute_operation_fields(
|
|
530
|
+
model, model_config, all_fields, request, "update"
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
return Response(
|
|
534
|
+
{
|
|
535
|
+
"visible_fields": list(visible_fields),
|
|
536
|
+
"creatable_fields": list(creatable_fields),
|
|
537
|
+
"editable_fields": list(editable_fields),
|
|
538
|
+
},
|
|
539
|
+
status=200,
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
except Exception as original_exception:
|
|
543
|
+
# Let StateZero's exception handler deal with errors
|
|
544
|
+
return explicit_exception_handler(original_exception)
|
|
545
|
+
|
|
546
|
+
def _compute_operation_fields(self, model, model_config, all_fields, request, operation_type):
|
|
547
|
+
"""
|
|
548
|
+
Compute allowed fields for a specific operation type.
|
|
549
|
+
Replicates the logic from ASTParser._get_operation_fields.
|
|
550
|
+
"""
|
|
551
|
+
from typing import Union, Set, Literal
|
|
552
|
+
|
|
553
|
+
allowed_fields = set()
|
|
554
|
+
|
|
555
|
+
for permission_cls in model_config.permissions:
|
|
556
|
+
permission = permission_cls()
|
|
557
|
+
|
|
558
|
+
# Get the appropriate field set based on operation
|
|
559
|
+
if operation_type == "read":
|
|
560
|
+
fields = permission.visible_fields(request, model)
|
|
561
|
+
elif operation_type == "create":
|
|
562
|
+
fields = permission.create_fields(request, model)
|
|
563
|
+
elif operation_type == "update":
|
|
564
|
+
fields = permission.editable_fields(request, model)
|
|
565
|
+
else:
|
|
566
|
+
fields = set()
|
|
567
|
+
|
|
568
|
+
# If any permission allows all fields
|
|
569
|
+
if fields == "__all__":
|
|
570
|
+
# For read operations, default "__all__" to frontend_fields
|
|
571
|
+
if operation_type == "read":
|
|
572
|
+
# If frontend_fields is also "__all__", then return all fields
|
|
573
|
+
if model_config.fields == "__all__":
|
|
574
|
+
return all_fields
|
|
575
|
+
# Otherwise, use frontend_fields as the default for "__all__"
|
|
576
|
+
else:
|
|
577
|
+
fields = model_config.fields
|
|
578
|
+
fields &= all_fields # Ensure fields actually exist
|
|
579
|
+
allowed_fields |= fields
|
|
580
|
+
else:
|
|
581
|
+
# For create/update operations, "__all__" means truly all fields
|
|
582
|
+
return all_fields
|
|
583
|
+
else:
|
|
584
|
+
# Add allowed fields from this permission
|
|
585
|
+
fields &= all_fields # Ensure fields actually exist
|
|
586
|
+
allowed_fields |= fields
|
|
587
|
+
|
|
588
|
+
return allowed_fields
|
|
@@ -5,6 +5,7 @@ import networkx as nx
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
from statezero.core.config import AppConfig, Registry
|
|
8
|
+
from statezero.core.exceptions import PermissionDenied
|
|
8
9
|
from statezero.core.interfaces import (
|
|
9
10
|
AbstractDataSerializer,
|
|
10
11
|
AbstractPermission,
|
|
@@ -86,6 +87,7 @@ class ASTParser:
|
|
|
86
87
|
# Lookup table mapping AST op types to handler methods.
|
|
87
88
|
self.handlers: Dict[str, Callable[[Dict[str, Any]], Dict[str, Any]]] = {
|
|
88
89
|
"create": self._handle_create,
|
|
90
|
+
"bulk_create": self._handle_bulk_create,
|
|
89
91
|
"update": self._handle_update,
|
|
90
92
|
"delete": self._handle_delete,
|
|
91
93
|
"get": self._handle_get,
|
|
@@ -525,6 +527,46 @@ class ASTParser:
|
|
|
525
527
|
"metadata": {"created": True, "response_type": ResponseType.INSTANCE.value},
|
|
526
528
|
}
|
|
527
529
|
|
|
530
|
+
def _handle_bulk_create(self, ast: Dict[str, Any]) -> Dict[str, Any]:
|
|
531
|
+
""" Handle bulk create operation."""
|
|
532
|
+
data_list = ast.get("data", [])
|
|
533
|
+
|
|
534
|
+
# Check model-level CREATE permission
|
|
535
|
+
if not self._has_operation_permission(self.model, operation_type="create"):
|
|
536
|
+
raise PermissionDenied("Create not allowed")
|
|
537
|
+
|
|
538
|
+
# Validate all data items using many=True
|
|
539
|
+
validated_data_list = self.serializer.deserialize(
|
|
540
|
+
model=self.model,
|
|
541
|
+
data=data_list,
|
|
542
|
+
partial=False,
|
|
543
|
+
request=self.request,
|
|
544
|
+
fields_map=self.create_fields_map,
|
|
545
|
+
many=True,
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
# Bulk create all records
|
|
549
|
+
records = self.engine.bulk_create(
|
|
550
|
+
self.model,
|
|
551
|
+
validated_data_list,
|
|
552
|
+
self.serializer,
|
|
553
|
+
self.request,
|
|
554
|
+
self.create_fields_map,
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
# Serialize the created records
|
|
558
|
+
serialized = self.serializer.serialize(
|
|
559
|
+
records,
|
|
560
|
+
self.model,
|
|
561
|
+
many=True,
|
|
562
|
+
depth=self.depth,
|
|
563
|
+
fields_map=self.read_fields_map,
|
|
564
|
+
)
|
|
565
|
+
return {
|
|
566
|
+
"data": serialized,
|
|
567
|
+
"metadata": {"created": True, "response_type": ResponseType.QUERYSET.value},
|
|
568
|
+
}
|
|
569
|
+
|
|
528
570
|
def _handle_update(self, ast: Dict[str, Any]) -> Dict[str, Any]:
|
|
529
571
|
""" Pass current queryset to update method."""
|
|
530
572
|
data = ast.get("data", {})
|
|
@@ -942,6 +984,7 @@ class ASTParser:
|
|
|
942
984
|
all_ops = ASTParser._extract_all_operations(ast)
|
|
943
985
|
OPERATION_MAPPING = {
|
|
944
986
|
"create": ActionType.CREATE,
|
|
987
|
+
"bulk_create": ActionType.BULK_CREATE,
|
|
945
988
|
"update": ActionType.UPDATE,
|
|
946
989
|
"update_or_create": ActionType.UPDATE,
|
|
947
990
|
"delete": ActionType.DELETE,
|
|
@@ -144,8 +144,6 @@ class ModelConfig:
|
|
|
144
144
|
-----------
|
|
145
145
|
model: Type
|
|
146
146
|
The model class to register
|
|
147
|
-
custom_querysets: Dict[str, Type[AbstractCustomQueryset]], optional
|
|
148
|
-
Custom queryset methods for this model
|
|
149
147
|
permissions: List[Type[AbstractPermission]], optional
|
|
150
148
|
Permission classes that control access to this model
|
|
151
149
|
pre_hooks: List[Callable], optional
|
|
@@ -171,8 +169,6 @@ class ModelConfig:
|
|
|
171
169
|
def __init__(
|
|
172
170
|
self,
|
|
173
171
|
model: Type,
|
|
174
|
-
custom_querysets: Optional[Dict[str, Type[AbstractCustomQueryset]]] = None,
|
|
175
|
-
custom_querysets_user_scoped: Optional[Dict[str, bool]] = None,
|
|
176
172
|
permissions: Optional[List[Type[AbstractPermission]]] = None,
|
|
177
173
|
pre_hooks: Optional[List] = None,
|
|
178
174
|
post_hooks: Optional[List] = None,
|
|
@@ -185,8 +181,6 @@ class ModelConfig:
|
|
|
185
181
|
DEBUG: bool = False,
|
|
186
182
|
):
|
|
187
183
|
self.model = model
|
|
188
|
-
self._custom_querysets = custom_querysets or {}
|
|
189
|
-
self._custom_querysets_user_scoped = custom_querysets_user_scoped or {}
|
|
190
184
|
self._permissions = permissions or []
|
|
191
185
|
self.pre_hooks = pre_hooks or []
|
|
192
186
|
self.post_hooks = post_hooks or []
|
|
@@ -214,37 +208,6 @@ class ModelConfig:
|
|
|
214
208
|
resolved.append(perm)
|
|
215
209
|
return resolved
|
|
216
210
|
|
|
217
|
-
@property
|
|
218
|
-
def custom_querysets(self):
|
|
219
|
-
"""Resolve queryset class strings to actual classes on each access"""
|
|
220
|
-
resolved = {}
|
|
221
|
-
for key, queryset in self._custom_querysets.items():
|
|
222
|
-
if isinstance(queryset, str):
|
|
223
|
-
from django.utils.module_loading import import_string
|
|
224
|
-
try:
|
|
225
|
-
qs_class = import_string(queryset)
|
|
226
|
-
resolved[key] = qs_class
|
|
227
|
-
except ImportError:
|
|
228
|
-
raise ImportError(f"Could not import queryset class: {queryset}")
|
|
229
|
-
else:
|
|
230
|
-
resolved[key] = queryset
|
|
231
|
-
return resolved
|
|
232
|
-
|
|
233
|
-
@property
|
|
234
|
-
def custom_querysets_user_scoped(self):
|
|
235
|
-
"""Resolve queryset class strings to actual classes on each access"""
|
|
236
|
-
resolved = {}
|
|
237
|
-
for key, queryset in self._custom_querysets_user_scoped.items():
|
|
238
|
-
if isinstance(queryset, str):
|
|
239
|
-
from django.utils.module_loading import import_string
|
|
240
|
-
try:
|
|
241
|
-
qs_class = import_string(queryset)
|
|
242
|
-
resolved[key] = qs_class
|
|
243
|
-
except ImportError:
|
|
244
|
-
raise ImportError(f"Could not import queryset class: {queryset}")
|
|
245
|
-
else:
|
|
246
|
-
resolved[key] = queryset
|
|
247
|
-
return resolved
|
|
248
211
|
|
|
249
212
|
class Registry:
|
|
250
213
|
"""
|
|
@@ -70,6 +70,16 @@ class AbstractORMProvider(ABC):
|
|
|
70
70
|
def get_fields(self, model: ORMModel) -> Set[str]:
|
|
71
71
|
"""
|
|
72
72
|
Get all of the model fields - doesn't apply permissions check.
|
|
73
|
+
Includes both database fields and additional_fields (computed fields).
|
|
74
|
+
"""
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
@abstractmethod
|
|
78
|
+
def get_db_fields(self, model: ORMModel) -> Set[str]:
|
|
79
|
+
"""
|
|
80
|
+
Get only the actual database fields for a model.
|
|
81
|
+
Excludes read-only additional_fields (computed fields).
|
|
82
|
+
Used for deserialization - hooks can write to any DB field.
|
|
73
83
|
"""
|
|
74
84
|
pass
|
|
75
85
|
|
|
@@ -207,6 +217,20 @@ class AbstractORMProvider(ABC):
|
|
|
207
217
|
"""Create a new record using the model class."""
|
|
208
218
|
pass
|
|
209
219
|
|
|
220
|
+
@abstractmethod
|
|
221
|
+
def bulk_create(
|
|
222
|
+
self,
|
|
223
|
+
model: Type[ORMModel],
|
|
224
|
+
data_list: List[Dict[str, Any]],
|
|
225
|
+
*args,
|
|
226
|
+
**kwargs
|
|
227
|
+
) -> List[Any]:
|
|
228
|
+
"""
|
|
229
|
+
Create multiple records using the model class.
|
|
230
|
+
Returns a list of created instances.
|
|
231
|
+
"""
|
|
232
|
+
pass
|
|
233
|
+
|
|
210
234
|
@abstractmethod
|
|
211
235
|
def update(
|
|
212
236
|
self,
|
|
@@ -315,13 +339,12 @@ class AbstractORMProvider(ABC):
|
|
|
315
339
|
request: RequestType,
|
|
316
340
|
model: ORMModel, # type:ignore
|
|
317
341
|
initial_ast: Dict[str, Any],
|
|
318
|
-
custom_querysets: Dict[str, Type],
|
|
319
342
|
registered_permissions: List[Type],
|
|
320
343
|
) -> Any:
|
|
321
344
|
"""
|
|
322
345
|
Assemble and return the base QuerySet (or equivalent) for the given model.
|
|
323
346
|
This method considers the request context, initial AST (filters, sorting, etc.),
|
|
324
|
-
|
|
347
|
+
and any model-specific permission restrictions.
|
|
325
348
|
"""
|
|
326
349
|
pass
|
|
327
350
|
|
|
@@ -406,13 +429,15 @@ class AbstractDataSerializer(ABC):
|
|
|
406
429
|
def deserialize(
|
|
407
430
|
self,
|
|
408
431
|
model: ORMModel, # type:ignore
|
|
409
|
-
data: dict,
|
|
432
|
+
data: Union[dict, List[dict]],
|
|
410
433
|
allowed_fields: Optional[Dict[str, Set[str]]] = None,
|
|
411
434
|
request: Optional[Any] = None,
|
|
412
|
-
|
|
435
|
+
many: bool = False,
|
|
436
|
+
) -> Union[dict, List[dict]]:
|
|
413
437
|
"""
|
|
414
438
|
Deserialize the input data into validated Python types for the specified model.
|
|
415
439
|
- `allowed_fields`: a mapping (by model name) of fields the user is allowed to edit.
|
|
440
|
+
- `many`: if True, expects data to be a list of dicts and returns a list of validated dicts.
|
|
416
441
|
|
|
417
442
|
Only keys that appear in the allowed set will be processed.
|
|
418
443
|
"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: statezero
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.0b22
|
|
4
4
|
Summary: Connect your Python backend to a modern JavaScript SPA frontend with 90% less complexity.
|
|
5
5
|
Author-email: Robert <robert.herring@statezero.dev>
|
|
6
6
|
Project-URL: homepage, https://www.statezero.dev
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/adaptors/django/migrations/0001_initial.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/adaptors/django/search_providers/__init__.py
RENAMED
|
File without changes
|
{statezero-0.1.0b12 → statezero-0.1.0b22}/statezero/adaptors/django/search_providers/basic_search.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|