statezero 0.1.0b4__tar.gz → 0.1.0b6__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.0b4 → statezero-0.1.0b6}/PKG-INFO +2 -2
- {statezero-0.1.0b4 → statezero-0.1.0b6}/README.md +1 -1
- {statezero-0.1.0b4 → statezero-0.1.0b6}/pyproject.toml +1 -1
- statezero-0.1.0b6/statezero/adaptors/django/actions.py +171 -0
- statezero-0.1.0b6/statezero/adaptors/django/apps.py +137 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/orm.py +224 -174
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/urls.py +5 -3
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/views.py +133 -31
- statezero-0.1.0b6/statezero/core/actions.py +88 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/core/ast_parser.py +315 -175
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/core/interfaces.py +216 -70
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/core/process_request.py +1 -1
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero.egg-info/PKG-INFO +2 -2
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero.egg-info/SOURCES.txt +2 -0
- statezero-0.1.0b4/statezero/adaptors/django/apps.py +0 -97
- {statezero-0.1.0b4 → statezero-0.1.0b6}/license.md +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/requirements.txt +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/setup.cfg +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/__init__.py +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/__init__.py +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/__init__.py +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/config.py +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/context_manager.py +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/event_emitters.py +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/exception_handler.py +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/extensions/__init__.py +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/extensions/custom_field_serializers/__init__.py +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/extensions/custom_field_serializers/file_fields.py +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/extensions/custom_field_serializers/money_field.py +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/f_handler.py +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/helpers.py +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/middleware.py +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/migrations/0001_initial.py +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/migrations/0002_delete_modelviewsubscription.py +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/migrations/__init__.py +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/permissions.py +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/query_optimizer.py +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/schemas.py +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/search_providers/__init__.py +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/search_providers/basic_search.py +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/search_providers/postgres_search.py +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/serializers.py +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/core/__init__.py +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/core/ast_validator.py +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/core/classes.py +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/core/config.py +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/core/context_storage.py +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/core/event_bus.py +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/core/event_emitters.py +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/core/exceptions.py +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/core/hook_checks.py +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/core/types.py +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero.egg-info/dependency_links.txt +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero.egg-info/requires.txt +0 -0
- {statezero-0.1.0b4 → statezero-0.1.0b6}/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.0b6
|
|
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
|
|
@@ -222,7 +222,7 @@ npm i @statezero/core
|
|
|
222
222
|
### Generate TypeScript Models
|
|
223
223
|
|
|
224
224
|
```bash
|
|
225
|
-
npx statezero sync
|
|
225
|
+
npx statezero sync
|
|
226
226
|
```
|
|
227
227
|
|
|
228
228
|
## Why Choose StateZero Over...
|
|
@@ -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.0b6"
|
|
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" }
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from django.apps import apps
|
|
3
|
+
from rest_framework.response import Response
|
|
4
|
+
from rest_framework import fields
|
|
5
|
+
from statezero.core.actions import action_registry
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DjangoActionSchemaGenerator:
|
|
9
|
+
"""Django-specific action schema generator that matches StateZero model schema format"""
|
|
10
|
+
|
|
11
|
+
@staticmethod
|
|
12
|
+
def generate_actions_schema():
|
|
13
|
+
"""Generate schema for all registered actions matching StateZero model schema format"""
|
|
14
|
+
actions_schema = {}
|
|
15
|
+
all_app_configs = list(apps.get_app_configs())
|
|
16
|
+
|
|
17
|
+
for action_name, action_config in action_registry.get_actions().items():
|
|
18
|
+
func = action_config.get("function")
|
|
19
|
+
if not func:
|
|
20
|
+
raise ValueError(
|
|
21
|
+
f"Action '{action_name}' is missing a function and cannot be processed."
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
func_path = os.path.abspath(func.__code__.co_filename)
|
|
25
|
+
found_app = None
|
|
26
|
+
|
|
27
|
+
for app_config in all_app_configs:
|
|
28
|
+
app_path = os.path.abspath(app_config.path)
|
|
29
|
+
if func_path.startswith(app_path + os.sep):
|
|
30
|
+
if not found_app or len(app_path) > len(
|
|
31
|
+
os.path.abspath(found_app.path)
|
|
32
|
+
):
|
|
33
|
+
found_app = app_config
|
|
34
|
+
|
|
35
|
+
if not found_app:
|
|
36
|
+
raise LookupError(
|
|
37
|
+
f"Action '{action_name}' from file '{func_path}' does not belong to any "
|
|
38
|
+
f"installed Django app. Please ensure the parent app is in INSTALLED_APPS."
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
app_name = found_app.label
|
|
42
|
+
docstring = action_config.get("docstring")
|
|
43
|
+
|
|
44
|
+
schema_info = {
|
|
45
|
+
"action_name": action_name,
|
|
46
|
+
"app": app_name,
|
|
47
|
+
"title": action_name.replace("_", " ").title(),
|
|
48
|
+
"docstring": docstring,
|
|
49
|
+
"class_name": "".join(
|
|
50
|
+
word.capitalize() for word in action_name.split("_")
|
|
51
|
+
),
|
|
52
|
+
"input_properties": DjangoActionSchemaGenerator._get_serializer_schema(
|
|
53
|
+
action_config["serializer"]
|
|
54
|
+
),
|
|
55
|
+
"response_properties": DjangoActionSchemaGenerator._get_serializer_schema(
|
|
56
|
+
action_config["response_serializer"]
|
|
57
|
+
),
|
|
58
|
+
"permissions": [
|
|
59
|
+
perm.__name__ for perm in action_config.get("permissions", [])
|
|
60
|
+
],
|
|
61
|
+
}
|
|
62
|
+
actions_schema[action_name] = schema_info
|
|
63
|
+
|
|
64
|
+
return Response({"actions": actions_schema, "count": len(actions_schema)})
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def _get_serializer_schema(serializer_class):
|
|
68
|
+
if not serializer_class:
|
|
69
|
+
return {}
|
|
70
|
+
try:
|
|
71
|
+
serializer_instance = serializer_class()
|
|
72
|
+
properties = {}
|
|
73
|
+
for field_name, field in serializer_instance.fields.items():
|
|
74
|
+
field_info = {
|
|
75
|
+
"type": DjangoActionSchemaGenerator._get_field_type(field),
|
|
76
|
+
"title": getattr(field, "label")
|
|
77
|
+
or field_name.replace("_", " ").title(),
|
|
78
|
+
"required": field.required,
|
|
79
|
+
"description": getattr(field, "help_text", None),
|
|
80
|
+
"nullable": getattr(field, "allow_null", False),
|
|
81
|
+
"format": DjangoActionSchemaGenerator._get_field_format(field),
|
|
82
|
+
"max_length": getattr(field, "max_length", None),
|
|
83
|
+
"choices": DjangoActionSchemaGenerator._get_field_choices(field),
|
|
84
|
+
"default": DjangoActionSchemaGenerator._get_field_default(field),
|
|
85
|
+
"validators": [],
|
|
86
|
+
"max_digits": getattr(field, "max_digits", None),
|
|
87
|
+
"decimal_places": getattr(field, "decimal_places", None),
|
|
88
|
+
"read_only": field.read_only,
|
|
89
|
+
"ref": None,
|
|
90
|
+
}
|
|
91
|
+
if hasattr(field, "max_value") and field.max_value is not None:
|
|
92
|
+
field_info["max_value"] = field.max_value
|
|
93
|
+
if hasattr(field, "min_value") and field.min_value is not None:
|
|
94
|
+
field_info["min_value"] = field.min_value
|
|
95
|
+
if hasattr(field, "min_length") and field.min_length is not None:
|
|
96
|
+
field_info["min_length"] = field.min_length
|
|
97
|
+
properties[field_name] = field_info
|
|
98
|
+
return properties
|
|
99
|
+
except Exception as e:
|
|
100
|
+
return {"error": f"Could not inspect serializer: {str(e)}"}
|
|
101
|
+
|
|
102
|
+
@staticmethod
|
|
103
|
+
def _get_field_type(field):
|
|
104
|
+
type_mapping = {
|
|
105
|
+
fields.BooleanField: "boolean",
|
|
106
|
+
fields.CharField: "string",
|
|
107
|
+
fields.EmailField: "string",
|
|
108
|
+
fields.URLField: "string",
|
|
109
|
+
fields.UUIDField: "string",
|
|
110
|
+
fields.IntegerField: "integer",
|
|
111
|
+
fields.FloatField: "number",
|
|
112
|
+
fields.DecimalField: "string",
|
|
113
|
+
fields.DateField: "string",
|
|
114
|
+
fields.DateTimeField: "string",
|
|
115
|
+
fields.TimeField: "string",
|
|
116
|
+
fields.JSONField: "object",
|
|
117
|
+
fields.DictField: "object",
|
|
118
|
+
fields.ListField: "array",
|
|
119
|
+
}
|
|
120
|
+
return type_mapping.get(type(field), "string")
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def _get_field_format(field):
|
|
124
|
+
format_mapping = {
|
|
125
|
+
fields.EmailField: "email",
|
|
126
|
+
fields.URLField: "uri",
|
|
127
|
+
fields.UUIDField: "uuid",
|
|
128
|
+
fields.DateField: "date",
|
|
129
|
+
fields.DateTimeField: "date-time",
|
|
130
|
+
fields.TimeField: "time",
|
|
131
|
+
}
|
|
132
|
+
return format_mapping.get(type(field))
|
|
133
|
+
|
|
134
|
+
@staticmethod
|
|
135
|
+
def _get_field_choices(field):
|
|
136
|
+
if hasattr(field, "choices") and field.choices:
|
|
137
|
+
choices = field.choices
|
|
138
|
+
|
|
139
|
+
# Handle dict format: {'low': 'Low', 'high': 'High'}
|
|
140
|
+
if isinstance(choices, dict):
|
|
141
|
+
return list(choices.keys())
|
|
142
|
+
|
|
143
|
+
# Handle list/tuple format: [('low', 'Low'), ('high', 'High')]
|
|
144
|
+
elif isinstance(choices, (list, tuple)):
|
|
145
|
+
try:
|
|
146
|
+
return [choice[0] for choice in choices]
|
|
147
|
+
except (IndexError, TypeError) as e:
|
|
148
|
+
raise ValueError(
|
|
149
|
+
f"Invalid choice format for field '{field}'. Expected list of tuples "
|
|
150
|
+
f"like [('value', 'display')], but got: {choices}. Error: {e}"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Handle unexpected format
|
|
154
|
+
else:
|
|
155
|
+
raise ValueError(
|
|
156
|
+
f"Unsupported choice format for field '{field}'. Expected dict or list of tuples, "
|
|
157
|
+
f"but got {type(choices)}: {choices}"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
@staticmethod
|
|
163
|
+
def _get_field_default(field):
|
|
164
|
+
if hasattr(field, "default"):
|
|
165
|
+
default = field.default
|
|
166
|
+
if default is fields.empty:
|
|
167
|
+
return None
|
|
168
|
+
if callable(default):
|
|
169
|
+
return None
|
|
170
|
+
return default
|
|
171
|
+
return None
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from django.apps import AppConfig as DjangoAppConfig
|
|
6
|
+
from django.apps import apps
|
|
7
|
+
from django.conf import settings
|
|
8
|
+
|
|
9
|
+
from statezero.adaptors.django.config import config, registry
|
|
10
|
+
|
|
11
|
+
# Attempt to import Rich for nicer console output.
|
|
12
|
+
try:
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.panel import Panel
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
except ImportError:
|
|
18
|
+
console = None
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class StateZeroDjangoConfig(DjangoAppConfig):
|
|
24
|
+
name = "statezero.adaptors.django"
|
|
25
|
+
verbose_name = "StateZero Django Integration"
|
|
26
|
+
label = "statezero"
|
|
27
|
+
|
|
28
|
+
def ready(self):
|
|
29
|
+
# Import crud modules which register models in the registry.
|
|
30
|
+
if hasattr(settings, "CONFIG_FILE_PREFIX"):
|
|
31
|
+
config_file_prefix: str = settings.CONFIG_FILE_PREFIX
|
|
32
|
+
config_file_prefix = config_file_prefix.replace(".py", "")
|
|
33
|
+
if (not isinstance(config_file_prefix, str)) or (
|
|
34
|
+
len(config_file_prefix) < 1
|
|
35
|
+
):
|
|
36
|
+
raise ValueError(
|
|
37
|
+
f"If provided, CONFIG_FILE_PREFIX must be a string with at least one character. In your settings.py it is set to {settings.CONFIG_FILE_PREFIX}. Either delete the setting completely or use a valid file name like 'crud'"
|
|
38
|
+
)
|
|
39
|
+
else:
|
|
40
|
+
config_file_prefix = "crud"
|
|
41
|
+
for app_config_instance in apps.get_app_configs():
|
|
42
|
+
module_name = f"{app_config_instance.name}.{config_file_prefix}"
|
|
43
|
+
try:
|
|
44
|
+
importlib.import_module(module_name)
|
|
45
|
+
logger.debug(
|
|
46
|
+
f"Imported {config_file_prefix} module from {app_config_instance.name}"
|
|
47
|
+
)
|
|
48
|
+
except ModuleNotFoundError:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
# Import actions modules which register actions in the action registry.
|
|
52
|
+
from statezero.core.actions import action_registry
|
|
53
|
+
|
|
54
|
+
for app_config_instance in apps.get_app_configs():
|
|
55
|
+
actions_module_name = f"{app_config_instance.name}.actions"
|
|
56
|
+
try:
|
|
57
|
+
importlib.import_module(actions_module_name)
|
|
58
|
+
logger.debug(f"Imported actions module from {app_config_instance.name}")
|
|
59
|
+
except ModuleNotFoundError:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
# Once all the apps are imported, initialize StateZero and provide the registry to the event bus.
|
|
63
|
+
config.initialize()
|
|
64
|
+
config.validate_exposed_models(
|
|
65
|
+
registry
|
|
66
|
+
) # Raises an exception if a non StateZero model is implicitly exposed
|
|
67
|
+
config.event_bus.set_registry(registry)
|
|
68
|
+
|
|
69
|
+
# Print the list of published models and actions to confirm StateZero is running.
|
|
70
|
+
try:
|
|
71
|
+
published_models = []
|
|
72
|
+
for model in registry._models_config.keys():
|
|
73
|
+
# Use the ORM provider's get_model_name to get the namespaced model name.
|
|
74
|
+
model_name = model.__name__
|
|
75
|
+
published_models.append(model_name)
|
|
76
|
+
|
|
77
|
+
# Get registered actions
|
|
78
|
+
registered_actions = list(action_registry.get_actions().keys())
|
|
79
|
+
|
|
80
|
+
# Build base message for models
|
|
81
|
+
if published_models:
|
|
82
|
+
models_message = (
|
|
83
|
+
"[bold green]StateZero is exposing models:[/bold green] [bold yellow]"
|
|
84
|
+
+ ", ".join(published_models)
|
|
85
|
+
+ "[/bold yellow]"
|
|
86
|
+
)
|
|
87
|
+
else:
|
|
88
|
+
models_message = "[bold yellow]StateZero is running but no models are registered.[/bold yellow]"
|
|
89
|
+
|
|
90
|
+
# Build message for actions (limit to first 10 to avoid cluttering console)
|
|
91
|
+
if registered_actions:
|
|
92
|
+
displayed_actions = registered_actions[:10]
|
|
93
|
+
actions_message = (
|
|
94
|
+
"\n[bold green]StateZero is exposing actions:[/bold green] [bold cyan]"
|
|
95
|
+
+ ", ".join(displayed_actions)
|
|
96
|
+
)
|
|
97
|
+
if len(registered_actions) > 10:
|
|
98
|
+
actions_message += (
|
|
99
|
+
f" [dim](and {len(registered_actions) - 10} more)[/dim]"
|
|
100
|
+
)
|
|
101
|
+
actions_message += "[/bold cyan]"
|
|
102
|
+
else:
|
|
103
|
+
actions_message = (
|
|
104
|
+
"\n[bold yellow]No actions are registered.[/bold yellow]"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
base_message = models_message + actions_message
|
|
108
|
+
|
|
109
|
+
# Append the npm command instruction only in debug mode.
|
|
110
|
+
if (published_models or registered_actions) and settings.DEBUG:
|
|
111
|
+
npm_message = (
|
|
112
|
+
"\n[bold blue]Next step:[/bold blue] Run [italic]npm run sync[/italic] in your frontend project directory "
|
|
113
|
+
"to generate or update the client-side code corresponding to these models and actions. "
|
|
114
|
+
"Note: This command should only be executed in a development environment."
|
|
115
|
+
)
|
|
116
|
+
message = base_message + npm_message
|
|
117
|
+
else:
|
|
118
|
+
message = base_message
|
|
119
|
+
|
|
120
|
+
# Use Rich Panel for a boxed display if Rich is available.
|
|
121
|
+
if console:
|
|
122
|
+
final_message = Panel(message, expand=False)
|
|
123
|
+
console.print(final_message)
|
|
124
|
+
else:
|
|
125
|
+
# Fallback to simple demarcation lines if Rich isn't available.
|
|
126
|
+
demarcation = "\n" + "-" * 50 + "\n"
|
|
127
|
+
final_message = demarcation + message + demarcation
|
|
128
|
+
logger.info(final_message)
|
|
129
|
+
except Exception as e:
|
|
130
|
+
error_message = f"[bold red]Error retrieving published models and actions: {e}[/bold red]"
|
|
131
|
+
if console:
|
|
132
|
+
final_message = Panel(error_message, expand=False)
|
|
133
|
+
console.print(final_message)
|
|
134
|
+
else:
|
|
135
|
+
demarcation = "\n" + "-" * 50 + "\n"
|
|
136
|
+
final_message = demarcation + error_message + demarcation
|
|
137
|
+
logger.info(final_message)
|