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.
Files changed (45) 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/apps.py +97 -0
  5. statezero/adaptors/django/config.py +99 -0
  6. statezero/adaptors/django/context_manager.py +12 -0
  7. statezero/adaptors/django/event_emitters.py +78 -0
  8. statezero/adaptors/django/exception_handler.py +98 -0
  9. statezero/adaptors/django/extensions/__init__.py +0 -0
  10. statezero/adaptors/django/extensions/custom_field_serializers/__init__.py +0 -0
  11. statezero/adaptors/django/extensions/custom_field_serializers/file_fields.py +141 -0
  12. statezero/adaptors/django/extensions/custom_field_serializers/money_field.py +75 -0
  13. statezero/adaptors/django/f_handler.py +312 -0
  14. statezero/adaptors/django/helpers.py +153 -0
  15. statezero/adaptors/django/middleware.py +10 -0
  16. statezero/adaptors/django/migrations/0001_initial.py +33 -0
  17. statezero/adaptors/django/migrations/0002_delete_modelviewsubscription.py +16 -0
  18. statezero/adaptors/django/migrations/__init__.py +0 -0
  19. statezero/adaptors/django/orm.py +915 -0
  20. statezero/adaptors/django/permissions.py +252 -0
  21. statezero/adaptors/django/query_optimizer.py +772 -0
  22. statezero/adaptors/django/schemas.py +324 -0
  23. statezero/adaptors/django/search_providers/__init__.py +0 -0
  24. statezero/adaptors/django/search_providers/basic_search.py +24 -0
  25. statezero/adaptors/django/search_providers/postgres_search.py +51 -0
  26. statezero/adaptors/django/serializers.py +554 -0
  27. statezero/adaptors/django/urls.py +14 -0
  28. statezero/adaptors/django/views.py +336 -0
  29. statezero/core/__init__.py +34 -0
  30. statezero/core/ast_parser.py +821 -0
  31. statezero/core/ast_validator.py +266 -0
  32. statezero/core/classes.py +167 -0
  33. statezero/core/config.py +263 -0
  34. statezero/core/context_storage.py +4 -0
  35. statezero/core/event_bus.py +175 -0
  36. statezero/core/event_emitters.py +60 -0
  37. statezero/core/exceptions.py +106 -0
  38. statezero/core/interfaces.py +492 -0
  39. statezero/core/process_request.py +184 -0
  40. statezero/core/types.py +63 -0
  41. statezero-0.1.0b1.dist-info/METADATA +252 -0
  42. statezero-0.1.0b1.dist-info/RECORD +45 -0
  43. statezero-0.1.0b1.dist-info/WHEEL +5 -0
  44. statezero-0.1.0b1.dist-info/licenses/license.md +117 -0
  45. statezero-0.1.0b1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,324 @@
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
+ schema_meta = ModelSchemaMetadata(
131
+ model_name=config.orm_provider.get_model_name(model),
132
+ title=model._meta.verbose_name.title(),
133
+ plural_title=(
134
+ model._meta.verbose_name_plural.title()
135
+ if hasattr(model._meta, "verbose_name_plural")
136
+ else model.__name__ + "s"
137
+ ),
138
+ primary_key_field=model._meta.pk.name,
139
+ filterable_fields=filterable_fields,
140
+ searchable_fields=searchable_fields,
141
+ ordering_fields=ordering_fields,
142
+ properties=properties,
143
+ relationships=relationships,
144
+ default_ordering=default_ordering,
145
+ definitions=merged_definitions,
146
+ class_name=model.__name__,
147
+ date_format=getattr(settings, "REST_FRAMEWORK", {}).get(
148
+ "DATE_FORMAT", "iso-8601"
149
+ ),
150
+ datetime_format=getattr(settings, "REST_FRAMEWORK", {}).get(
151
+ "DATETIME_FORMAT", "iso-8601"
152
+ ),
153
+ time_format=getattr(settings, "REST_FRAMEWORK", {}).get(
154
+ "TIME_FORMAT", "iso-8601"
155
+ ),
156
+ )
157
+ return schema_meta
158
+
159
+ def get_pk_schema(self, field: models.Field) -> SchemaFieldMetadata:
160
+ title = self.get_field_title(field)
161
+ description = (
162
+ str(field.help_text)
163
+ if hasattr(field, "help_text") and field.help_text
164
+ else None
165
+ )
166
+
167
+ if isinstance(field, models.AutoField):
168
+ return SchemaFieldMetadata(
169
+ type=FieldType.INTEGER,
170
+ title=title,
171
+ required=True,
172
+ nullable=False,
173
+ format=FieldFormat.ID,
174
+ description=description,
175
+ )
176
+ elif isinstance(field, models.UUIDField):
177
+ return SchemaFieldMetadata(
178
+ type=FieldType.STRING,
179
+ title=title,
180
+ required=True,
181
+ nullable=False,
182
+ format=FieldFormat.UUID,
183
+ description=description,
184
+ )
185
+ elif isinstance(field, models.CharField):
186
+ return SchemaFieldMetadata(
187
+ type=FieldType.STRING,
188
+ title=title,
189
+ required=True,
190
+ nullable=False,
191
+ format=FieldFormat.ID,
192
+ max_length=field.max_length,
193
+ description=description,
194
+ )
195
+ else:
196
+ return SchemaFieldMetadata(
197
+ type=FieldType.OBJECT,
198
+ title=title,
199
+ required=True,
200
+ nullable=False,
201
+ format=FieldFormat.ID,
202
+ description=description,
203
+ )
204
+
205
+ def get_field_metadata(
206
+ self, field: models.Field, global_schema_overrides: Dict[ORMField, dict]
207
+ ) -> SchemaFieldMetadata: # type:ignore
208
+
209
+ # Check for a custom schema override for this field type.
210
+ override: AbstractSchemaOverride = global_schema_overrides.get(field.__class__)
211
+ if override:
212
+ schema, definition, key = override.get_schema(field)
213
+ if definition and key:
214
+ self.definitions[key] = definition
215
+ return schema
216
+
217
+ # Process normally
218
+ title = self.get_field_title(field)
219
+ required = self.is_field_required(field)
220
+ nullable = field.null
221
+ choices = (
222
+ {str(choice[0]): choice[1] for choice in field.choices}
223
+ if field.choices
224
+ else None
225
+ )
226
+ max_length = getattr(field, "max_length", None)
227
+ max_digits = getattr(field, "max_digits", None)
228
+ decimal_places = getattr(field, "decimal_places", None)
229
+ description = (
230
+ str(field.help_text)
231
+ if hasattr(field, "help_text") and field.help_text
232
+ else None
233
+ )
234
+
235
+ if isinstance(field, models.TextField):
236
+ field_type = FieldType.STRING
237
+ field_format = FieldFormat.TEXT
238
+ elif isinstance(field, models.CharField):
239
+ field_type = FieldType.STRING
240
+ field_format = None
241
+ elif isinstance(field, models.IntegerField):
242
+ field_type = FieldType.INTEGER
243
+ field_format = None
244
+ elif isinstance(field, models.BooleanField):
245
+ field_type = FieldType.BOOLEAN
246
+ field_format = None
247
+ elif isinstance(field, models.DateTimeField):
248
+ field_type = FieldType.STRING
249
+ field_format = FieldFormat.DATETIME
250
+ elif isinstance(field, models.DateField):
251
+ field_type = FieldType.STRING
252
+ field_format = FieldFormat.DATE
253
+ elif isinstance(field, (models.ForeignKey, models.OneToOneField)):
254
+ field_type = self.get_pk_type(field)
255
+ field_format = self.get_relation_type(field)
256
+ elif isinstance(field, models.ManyToManyField):
257
+ field_type = FieldType.ARRAY
258
+ field_format = FieldFormat.MANY_TO_MANY
259
+ elif isinstance(field, models.DecimalField):
260
+ field_type = FieldType.NUMBER
261
+ field_format = FieldFormat.DECIMAL
262
+ elif isinstance(field, models.FileField):
263
+ field_type = FieldType.STRING
264
+ field_format = FieldFormat.FILE_PATH
265
+ elif isinstance(field, models.ImageField):
266
+ field_type = FieldType.STRING
267
+ field_format = FieldFormat.IMAGE_PATH
268
+ elif isinstance(field, models.JSONField):
269
+ field_type = FieldType.OBJECT
270
+ field_format = FieldFormat.JSON
271
+ else:
272
+ field_type = FieldType.OBJECT
273
+ field_format = None
274
+
275
+ # Handle callable defaults: ignore if callable or NOT_PROVIDED.
276
+ default = field.default
277
+
278
+ if default == models.fields.NOT_PROVIDED:
279
+ default = None
280
+ elif callable(default):
281
+ default = default()
282
+
283
+ return SchemaFieldMetadata(
284
+ type=field_type,
285
+ title=title,
286
+ required=required,
287
+ nullable=nullable,
288
+ format=field_format,
289
+ max_length=max_length,
290
+ choices=choices,
291
+ default=default,
292
+ validators=[],
293
+ max_digits=max_digits,
294
+ decimal_places=decimal_places,
295
+ description=description,
296
+ )
297
+
298
+ def get_field_title(self, field: models.Field) -> str:
299
+ if field.verbose_name and not hasattr(field, "title"):
300
+ return field.verbose_name.capitalize()
301
+ return field.title or field.name.replace("_", " ").title()
302
+
303
+ @staticmethod
304
+ def is_field_required(field: models.Field) -> bool:
305
+ return (
306
+ not field.blank
307
+ and not field.null
308
+ and field.default == models.fields.NOT_PROVIDED
309
+ )
310
+
311
+ def get_relation_type(self, field: models.Field) -> Optional[FieldFormat]:
312
+ if isinstance(field, models.ForeignKey):
313
+ return FieldFormat.FOREIGN_KEY
314
+ elif isinstance(field, models.OneToOneField):
315
+ return FieldFormat.ONE_TO_ONE
316
+ elif isinstance(field, models.ManyToManyField):
317
+ return FieldFormat.MANY_TO_MANY
318
+ return None
319
+
320
+ def get_pk_type(self, field: models.Field) -> FieldType:
321
+ target_field = field.target_field if hasattr(field, "target_field") else field
322
+ if isinstance(target_field, (models.UUIDField, models.CharField)):
323
+ return FieldType.STRING
324
+ return FieldType.INTEGER
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