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,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
|