statezero 0.1.0b20__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.
Files changed (48) hide show
  1. statezero/__init__.py +0 -0
  2. statezero/adaptors/__init__.py +0 -0
  3. statezero/adaptors/django/__init__.py +0 -0
  4. statezero/adaptors/django/actions.py +248 -0
  5. statezero/adaptors/django/apps.py +137 -0
  6. statezero/adaptors/django/config.py +99 -0
  7. statezero/adaptors/django/context_manager.py +12 -0
  8. statezero/adaptors/django/event_emitters.py +78 -0
  9. statezero/adaptors/django/exception_handler.py +98 -0
  10. statezero/adaptors/django/extensions/__init__.py +0 -0
  11. statezero/adaptors/django/extensions/custom_field_serializers/__init__.py +0 -0
  12. statezero/adaptors/django/extensions/custom_field_serializers/file_fields.py +141 -0
  13. statezero/adaptors/django/extensions/custom_field_serializers/money_field.py +83 -0
  14. statezero/adaptors/django/f_handler.py +312 -0
  15. statezero/adaptors/django/helpers.py +153 -0
  16. statezero/adaptors/django/middleware.py +10 -0
  17. statezero/adaptors/django/migrations/0001_initial.py +33 -0
  18. statezero/adaptors/django/migrations/0002_delete_modelviewsubscription.py +16 -0
  19. statezero/adaptors/django/migrations/__init__.py +0 -0
  20. statezero/adaptors/django/orm.py +1073 -0
  21. statezero/adaptors/django/permissions.py +252 -0
  22. statezero/adaptors/django/query_optimizer.py +810 -0
  23. statezero/adaptors/django/schemas.py +360 -0
  24. statezero/adaptors/django/search_providers/__init__.py +0 -0
  25. statezero/adaptors/django/search_providers/basic_search.py +24 -0
  26. statezero/adaptors/django/search_providers/postgres_search.py +51 -0
  27. statezero/adaptors/django/serializers.py +588 -0
  28. statezero/adaptors/django/urls.py +18 -0
  29. statezero/adaptors/django/views.py +588 -0
  30. statezero/core/__init__.py +34 -0
  31. statezero/core/actions.py +92 -0
  32. statezero/core/ast_parser.py +961 -0
  33. statezero/core/ast_validator.py +266 -0
  34. statezero/core/classes.py +224 -0
  35. statezero/core/config.py +267 -0
  36. statezero/core/context_storage.py +4 -0
  37. statezero/core/event_bus.py +175 -0
  38. statezero/core/event_emitters.py +60 -0
  39. statezero/core/exceptions.py +106 -0
  40. statezero/core/hook_checks.py +86 -0
  41. statezero/core/interfaces.py +677 -0
  42. statezero/core/process_request.py +184 -0
  43. statezero/core/types.py +29 -0
  44. statezero-0.1.0b20.dist-info/METADATA +242 -0
  45. statezero-0.1.0b20.dist-info/RECORD +48 -0
  46. statezero-0.1.0b20.dist-info/WHEEL +5 -0
  47. statezero-0.1.0b20.dist-info/licenses/license.md +117 -0
  48. statezero-0.1.0b20.dist-info/top_level.txt +1 -0
@@ -0,0 +1,360 @@
1
+ from typing import Any, Dict, List, Literal, Optional, Set, Type, Union
2
+
3
+ from django.apps import apps
4
+ from django.db import models
5
+ from djmoney.models.fields import MoneyField
6
+ from django.conf import settings
7
+
8
+ from statezero.adaptors.django.config import config, registry
9
+ from statezero.core.classes import (
10
+ FieldFormat,
11
+ FieldType,
12
+ ModelSchemaMetadata,
13
+ SchemaFieldMetadata,
14
+ )
15
+
16
+ from statezero.core.interfaces import AbstractSchemaGenerator, AbstractSchemaOverride
17
+ from statezero.core.types import ORMField
18
+
19
+
20
+ class DjangoSchemaGenerator(AbstractSchemaGenerator):
21
+ def __init__(self):
22
+ # Initialize definitions as an empty dictionary.
23
+ self.definitions: Dict[str, Dict[str, Any]] = {}
24
+
25
+ def generate_schema(
26
+ self,
27
+ model: Type,
28
+ global_schema_overrides: Dict[ORMField, dict], # type:ignore
29
+ additional_fields: List[Any],
30
+ definitions: Dict[str, Dict[str, Any]] = {},
31
+ allowed_fields: Optional[Set[str]] = None, # Pre-computed allowed fields.
32
+ ) -> ModelSchemaMetadata:
33
+ properties: Dict[str, SchemaFieldMetadata] = {}
34
+ relationships: Dict[str, Dict[str, Any]] = {}
35
+
36
+ # Get model config from registry
37
+ model_config = registry.get_config(model)
38
+
39
+ # Process all concrete fields and many-to-many fields
40
+ all_fields = list(model._meta.fields) + list(model._meta.many_to_many)
41
+ all_field_names: Set[str] = set()
42
+ db_field_names: Set[str] = set()
43
+
44
+ if model_config.fields != "__all__":
45
+ all_fields = [
46
+ field for field in all_fields if field.name in model_config.fields
47
+ ]
48
+
49
+ for field in all_fields:
50
+ # Skip auto-created reverse relations.
51
+ if getattr(field, "auto_created", False) and not field.concrete:
52
+ continue
53
+
54
+ all_field_names.add(field.name)
55
+ db_field_names.add(field.name)
56
+
57
+ # If allowed_fields is provided and is not the magic "__all__", skip fields not in the allowed set.
58
+ if (
59
+ allowed_fields is not None
60
+ and allowed_fields != "__all__"
61
+ and field.name not in allowed_fields
62
+ ):
63
+ continue
64
+
65
+ if field == model._meta.pk:
66
+ schema_field = self.get_pk_schema(field)
67
+ else:
68
+ schema_field = self.get_field_metadata(field, global_schema_overrides)
69
+
70
+ properties[field.name] = schema_field
71
+
72
+ # If the field represents a relation, record that.
73
+ if field.is_relation:
74
+ relationships[field.name] = {
75
+ "type": self.get_relation_type(field),
76
+ "model": config.orm_provider.get_model_name(field.related_model),
77
+ "class_name": field.related_model.__name__,
78
+ "primary_key_field": field.related_model._meta.pk.name,
79
+ }
80
+
81
+ # Process any additional fields from the registry
82
+ add_fields = model_config.additional_fields or []
83
+ for field in add_fields:
84
+ # If allowed_fields is provided and is not "__all__", skip additional fields not allowed.
85
+ if (
86
+ allowed_fields is not None
87
+ and allowed_fields != "__all__"
88
+ and field.name not in allowed_fields
89
+ ):
90
+ continue
91
+
92
+ # Ensure the underlying model field has the expected properties.
93
+ field.field.name = field.name
94
+ field.field.title = field.title
95
+
96
+ schema_field = self.get_field_metadata(field.field, global_schema_overrides)
97
+ # Override title if provided (in case the model field didn't have one)
98
+ schema_field.title = field.title or schema_field.title
99
+ schema_field.read_only = True # Always mark additional fields as read-only
100
+ properties[field.name] = schema_field
101
+ all_field_names.add(field.name)
102
+
103
+ # Handle "__all__" notation and set up field sets.
104
+ filterable_fields = (
105
+ db_field_names
106
+ if model_config.filterable_fields == "__all__"
107
+ else model_config.filterable_fields or set()
108
+ )
109
+
110
+ searchable_fields = (
111
+ db_field_names
112
+ if model_config.searchable_fields == "__all__"
113
+ else model_config.searchable_fields or set()
114
+ )
115
+
116
+ ordering_fields = (
117
+ db_field_names
118
+ if model_config.ordering_fields == "__all__"
119
+ else model_config.ordering_fields or set()
120
+ )
121
+
122
+ # Merge passed definitions with those collected during field processing.
123
+ merged_definitions = {**definitions, **self.definitions}
124
+
125
+ # Extract default ordering from model's Meta
126
+ default_ordering = None
127
+ if hasattr(model._meta, "ordering") and model._meta.ordering:
128
+ default_ordering = list(model._meta.ordering)
129
+
130
+ # Serialize display metadata if present
131
+ display_data = None
132
+ if model_config.display:
133
+ display_data = self._serialize_display_metadata(model_config.display)
134
+
135
+ schema_meta = ModelSchemaMetadata(
136
+ model_name=config.orm_provider.get_model_name(model),
137
+ title=model._meta.verbose_name.title(),
138
+ plural_title=(
139
+ model._meta.verbose_name_plural.title()
140
+ if hasattr(model._meta, "verbose_name_plural")
141
+ else model.__name__ + "s"
142
+ ),
143
+ primary_key_field=model._meta.pk.name,
144
+ filterable_fields=filterable_fields,
145
+ searchable_fields=searchable_fields,
146
+ ordering_fields=ordering_fields,
147
+ properties=properties,
148
+ relationships=relationships,
149
+ default_ordering=default_ordering,
150
+ definitions=merged_definitions,
151
+ class_name=model.__name__,
152
+ date_format=getattr(settings, "REST_FRAMEWORK", {}).get(
153
+ "DATE_FORMAT", "iso-8601"
154
+ ),
155
+ datetime_format=getattr(settings, "REST_FRAMEWORK", {}).get(
156
+ "DATETIME_FORMAT", "iso-8601"
157
+ ),
158
+ time_format=getattr(settings, "REST_FRAMEWORK", {}).get(
159
+ "TIME_FORMAT", "iso-8601"
160
+ ),
161
+ display=display_data,
162
+ )
163
+ return schema_meta
164
+
165
+ def get_pk_schema(self, field: models.Field) -> SchemaFieldMetadata:
166
+ title = self.get_field_title(field)
167
+ description = (
168
+ str(field.help_text)
169
+ if hasattr(field, "help_text") and field.help_text
170
+ else None
171
+ )
172
+
173
+ if isinstance(field, models.AutoField):
174
+ return SchemaFieldMetadata(
175
+ type=FieldType.INTEGER,
176
+ title=title,
177
+ required=True,
178
+ nullable=False,
179
+ format=FieldFormat.ID,
180
+ description=description,
181
+ read_only=True,
182
+ )
183
+ elif isinstance(field, models.UUIDField):
184
+ return SchemaFieldMetadata(
185
+ type=FieldType.STRING,
186
+ title=title,
187
+ required=True,
188
+ nullable=False,
189
+ format=FieldFormat.UUID,
190
+ description=description,
191
+ read_only=True,
192
+ )
193
+ elif isinstance(field, models.CharField):
194
+ return SchemaFieldMetadata(
195
+ type=FieldType.STRING,
196
+ title=title,
197
+ required=True,
198
+ nullable=False,
199
+ format=FieldFormat.ID,
200
+ max_length=field.max_length,
201
+ description=description,
202
+ read_only=True,
203
+ )
204
+ else:
205
+ return SchemaFieldMetadata(
206
+ type=FieldType.OBJECT,
207
+ title=title,
208
+ required=True,
209
+ nullable=False,
210
+ format=FieldFormat.ID,
211
+ description=description,
212
+ read_only=True,
213
+ )
214
+
215
+ def get_field_metadata(
216
+ self, field: models.Field, global_schema_overrides: Dict[ORMField, dict]
217
+ ) -> SchemaFieldMetadata: # type:ignore
218
+
219
+ # Check for a custom schema override for this field type.
220
+ override: AbstractSchemaOverride = global_schema_overrides.get(field.__class__)
221
+ if override:
222
+ schema, definition, key = override.get_schema(field)
223
+ if definition and key:
224
+ self.definitions[key] = definition
225
+ return schema
226
+
227
+ # Process normally
228
+ title = self.get_field_title(field)
229
+ required = self.is_field_required(field)
230
+ nullable = field.null
231
+ choices = (
232
+ {str(choice[0]): choice[1] for choice in field.choices}
233
+ if field.choices
234
+ else None
235
+ )
236
+ max_length = getattr(field, "max_length", None)
237
+ max_digits = getattr(field, "max_digits", None)
238
+ decimal_places = getattr(field, "decimal_places", None)
239
+ description = (
240
+ str(field.help_text)
241
+ if hasattr(field, "help_text") and field.help_text
242
+ else None
243
+ )
244
+
245
+ if isinstance(field, models.TextField):
246
+ field_type = FieldType.STRING
247
+ field_format = FieldFormat.TEXT
248
+ elif isinstance(field, models.CharField):
249
+ field_type = FieldType.STRING
250
+ field_format = None
251
+ elif isinstance(field, models.IntegerField):
252
+ field_type = FieldType.INTEGER
253
+ field_format = None
254
+ elif isinstance(field, models.BooleanField):
255
+ field_type = FieldType.BOOLEAN
256
+ field_format = None
257
+ elif isinstance(field, models.DateTimeField):
258
+ field_type = FieldType.STRING
259
+ field_format = FieldFormat.DATETIME
260
+ elif isinstance(field, models.DateField):
261
+ field_type = FieldType.STRING
262
+ field_format = FieldFormat.DATE
263
+ elif isinstance(field, models.TimeField):
264
+ field_type = FieldType.STRING
265
+ field_format = FieldFormat.TIME
266
+ elif isinstance(field, (models.ForeignKey, models.OneToOneField)):
267
+ field_type = self.get_pk_type(field)
268
+ field_format = self.get_relation_type(field)
269
+ elif isinstance(field, models.ManyToManyField):
270
+ field_type = FieldType.ARRAY
271
+ field_format = FieldFormat.MANY_TO_MANY
272
+ elif isinstance(field, models.DecimalField):
273
+ field_type = FieldType.NUMBER
274
+ field_format = FieldFormat.DECIMAL
275
+ elif isinstance(field, models.FileField):
276
+ field_type = FieldType.STRING
277
+ field_format = FieldFormat.FILE_PATH
278
+ elif isinstance(field, models.ImageField):
279
+ field_type = FieldType.STRING
280
+ field_format = FieldFormat.IMAGE_PATH
281
+ elif isinstance(field, models.JSONField):
282
+ field_type = FieldType.OBJECT
283
+ field_format = FieldFormat.JSON
284
+ else:
285
+ field_type = FieldType.OBJECT
286
+ field_format = None
287
+
288
+ # Handle callable defaults: ignore if callable or NOT_PROVIDED.
289
+ default = field.default
290
+
291
+ if default == models.fields.NOT_PROVIDED:
292
+ default = None
293
+ elif callable(default):
294
+ default = default()
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
+
302
+ return SchemaFieldMetadata(
303
+ type=field_type,
304
+ title=title,
305
+ required=required,
306
+ nullable=nullable,
307
+ format=field_format,
308
+ max_length=max_length,
309
+ choices=choices,
310
+ default=default,
311
+ validators=[],
312
+ max_digits=max_digits,
313
+ decimal_places=decimal_places,
314
+ description=description,
315
+ read_only=read_only,
316
+ )
317
+
318
+ def get_field_title(self, field: models.Field) -> str:
319
+ if field.verbose_name and not hasattr(field, "title"):
320
+ return field.verbose_name.capitalize()
321
+ return field.title or field.name.replace("_", " ").title()
322
+
323
+ @staticmethod
324
+ def is_field_required(field: models.Field) -> bool:
325
+ return (
326
+ not field.null
327
+ and field.default == models.fields.NOT_PROVIDED
328
+ )
329
+
330
+ def get_relation_type(self, field: models.Field) -> Optional[FieldFormat]:
331
+ if isinstance(field, models.ForeignKey):
332
+ return FieldFormat.FOREIGN_KEY
333
+ elif isinstance(field, models.OneToOneField):
334
+ return FieldFormat.ONE_TO_ONE
335
+ elif isinstance(field, models.ManyToManyField):
336
+ return FieldFormat.MANY_TO_MANY
337
+ return None
338
+
339
+ def get_pk_type(self, field: models.Field) -> FieldType:
340
+ target_field = field.target_field if hasattr(field, "target_field") else field
341
+ if isinstance(target_field, (models.UUIDField, models.CharField)):
342
+ return FieldType.STRING
343
+ return FieldType.INTEGER
344
+
345
+ @staticmethod
346
+ def _serialize_display_metadata(display) -> Dict[str, Any]:
347
+ """Convert DisplayMetadata dataclass to dict for JSON serialization"""
348
+ from dataclasses import asdict, is_dataclass
349
+
350
+ if display is None:
351
+ return None
352
+
353
+ if is_dataclass(display):
354
+ return asdict(display)
355
+
356
+ # If it's already a dict, return as-is
357
+ if isinstance(display, dict):
358
+ return display
359
+
360
+ return None
File without changes
@@ -0,0 +1,24 @@
1
+ from typing import Set
2
+ from django.db.models import QuerySet, Q
3
+ from statezero.core.interfaces import AbstractSearchProvider
4
+
5
+ class BasicSearchProvider(AbstractSearchProvider):
6
+ """Simple search provider using basic Django field lookups."""
7
+
8
+ def search(self, queryset: QuerySet, query: str, search_fields: Set[str]) -> QuerySet:
9
+ """Apply search using basic field lookups."""
10
+ if not search_fields or not query or not query.strip():
11
+ return queryset
12
+
13
+ # Split the query into individual terms.
14
+ terms = [term.strip() for term in query.split() if term.strip()]
15
+ if not terms:
16
+ return queryset
17
+
18
+ # Build Q objects to OR across fields and terms.
19
+ q_objects = Q()
20
+ for field in search_fields:
21
+ for term in terms:
22
+ q_objects |= Q(**{f"{field}__icontains": term})
23
+
24
+ return queryset.filter(q_objects)
@@ -0,0 +1,51 @@
1
+ from typing import Set
2
+ import logging
3
+ from django.db.models import QuerySet
4
+ from django.contrib.postgres.search import SearchVector, SearchQuery, SearchRank
5
+ from django.db import connection
6
+
7
+ from statezero.core.interfaces import AbstractSearchProvider
8
+ from statezero.adaptors.django.config import registry
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ class PostgreSQLSearchProvider(AbstractSearchProvider):
13
+ """
14
+ PostgreSQL-specific search provider using full-text search capabilities.
15
+ Uses a precomputed 'pg_search_vector' column if available and if the provided
16
+ search_fields exactly match the expected model configuration (pulled from the registry);
17
+ otherwise, builds the search vector dynamically from the given fields.
18
+ """
19
+ def search(self, queryset: QuerySet, query: str, search_fields: Set[str]) -> QuerySet:
20
+ if not query or not query.strip() or not search_fields:
21
+ return queryset
22
+
23
+ # Pull expected_search_fields from the model configuration via the registry.
24
+ model_config = registry.get_config(queryset.model)
25
+ expected_search_fields = set(getattr(model_config, "searchable_fields", []))
26
+
27
+ search_query = SearchQuery(query, search_type='websearch')
28
+
29
+ use_precomputed = False
30
+ if self._has_search_column(queryset):
31
+ # Only use the precomputed column if the provided search_fields match the expected ones.
32
+ if expected_search_fields and search_fields == expected_search_fields:
33
+ use_precomputed = True
34
+
35
+ if use_precomputed:
36
+ return queryset.annotate(
37
+ rank=SearchRank('pg_search_vector', search_query)
38
+ ).filter(pg_search_vector=search_query).order_by('-rank')
39
+
40
+ # Fallback: build the search vector dynamically using the provided fields.
41
+ search_vector = SearchVector(*search_fields)
42
+ return queryset.annotate(
43
+ pg_search_vector=search_vector,
44
+ rank=SearchRank(search_vector, search_query)
45
+ ).filter(pg_search_vector=search_query).order_by('-rank')
46
+
47
+ def _has_search_column(self, queryset: QuerySet) -> bool:
48
+ table_name = queryset.model._meta.db_table
49
+ with connection.cursor() as cursor:
50
+ columns = [col.name for col in connection.introspection.get_table_description(cursor, table_name)]
51
+ return 'pg_search_vector' in columns