statezero 0.1.0b4__py3-none-any.whl → 0.1.0b6__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/adaptors/django/actions.py +171 -0
- statezero/adaptors/django/apps.py +57 -17
- statezero/adaptors/django/orm.py +224 -174
- statezero/adaptors/django/urls.py +5 -3
- statezero/adaptors/django/views.py +133 -31
- statezero/core/actions.py +88 -0
- statezero/core/ast_parser.py +315 -175
- statezero/core/interfaces.py +216 -70
- statezero/core/process_request.py +1 -1
- {statezero-0.1.0b4.dist-info → statezero-0.1.0b6.dist-info}/METADATA +2 -2
- {statezero-0.1.0b4.dist-info → statezero-0.1.0b6.dist-info}/RECORD +14 -12
- {statezero-0.1.0b4.dist-info → statezero-0.1.0b6.dist-info}/WHEEL +0 -0
- {statezero-0.1.0b4.dist-info → statezero-0.1.0b6.dist-info}/licenses/license.md +0 -0
- {statezero-0.1.0b4.dist-info → statezero-0.1.0b6.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
@@ -27,27 +27,46 @@ class StateZeroDjangoConfig(DjangoAppConfig):
|
|
|
27
27
|
|
|
28
28
|
def ready(self):
|
|
29
29
|
# Import crud modules which register models in the registry.
|
|
30
|
-
if hasattr(settings,
|
|
30
|
+
if hasattr(settings, "CONFIG_FILE_PREFIX"):
|
|
31
31
|
config_file_prefix: str = settings.CONFIG_FILE_PREFIX
|
|
32
|
-
config_file_prefix = config_file_prefix.replace(
|
|
33
|
-
if (not isinstance(config_file_prefix, str)) or (
|
|
34
|
-
|
|
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
|
+
)
|
|
35
39
|
else:
|
|
36
40
|
config_file_prefix = "crud"
|
|
37
41
|
for app_config_instance in apps.get_app_configs():
|
|
38
42
|
module_name = f"{app_config_instance.name}.{config_file_prefix}"
|
|
39
43
|
try:
|
|
40
44
|
importlib.import_module(module_name)
|
|
41
|
-
logger.debug(
|
|
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}")
|
|
42
59
|
except ModuleNotFoundError:
|
|
43
60
|
pass
|
|
44
61
|
|
|
45
62
|
# Once all the apps are imported, initialize StateZero and provide the registry to the event bus.
|
|
46
63
|
config.initialize()
|
|
47
|
-
config.validate_exposed_models(
|
|
64
|
+
config.validate_exposed_models(
|
|
65
|
+
registry
|
|
66
|
+
) # Raises an exception if a non StateZero model is implicitly exposed
|
|
48
67
|
config.event_bus.set_registry(registry)
|
|
49
68
|
|
|
50
|
-
# Print the list of published models
|
|
69
|
+
# Print the list of published models and actions to confirm StateZero is running.
|
|
51
70
|
try:
|
|
52
71
|
published_models = []
|
|
53
72
|
for model in registry._models_config.keys():
|
|
@@ -55,20 +74,43 @@ class StateZeroDjangoConfig(DjangoAppConfig):
|
|
|
55
74
|
model_name = model.__name__
|
|
56
75
|
published_models.append(model_name)
|
|
57
76
|
|
|
77
|
+
# Get registered actions
|
|
78
|
+
registered_actions = list(action_registry.get_actions().keys())
|
|
79
|
+
|
|
80
|
+
# Build base message for models
|
|
58
81
|
if published_models:
|
|
59
|
-
|
|
82
|
+
models_message = (
|
|
60
83
|
"[bold green]StateZero is exposing models:[/bold green] [bold yellow]"
|
|
61
84
|
+ ", ".join(published_models)
|
|
62
|
-
+ "[/bold yellow]"
|
|
85
|
+
+ "[/bold yellow]"
|
|
63
86
|
)
|
|
64
87
|
else:
|
|
65
|
-
|
|
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
|
|
66
108
|
|
|
67
109
|
# Append the npm command instruction only in debug mode.
|
|
68
|
-
if published_models and settings.DEBUG:
|
|
110
|
+
if (published_models or registered_actions) and settings.DEBUG:
|
|
69
111
|
npm_message = (
|
|
70
|
-
"\n[bold blue]Next step:[/bold blue] Run [italic]npm run sync
|
|
71
|
-
"to generate or update the client-side code corresponding to these models. "
|
|
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. "
|
|
72
114
|
"Note: This command should only be executed in a development environment."
|
|
73
115
|
)
|
|
74
116
|
message = base_message + npm_message
|
|
@@ -85,13 +127,11 @@ class StateZeroDjangoConfig(DjangoAppConfig):
|
|
|
85
127
|
final_message = demarcation + message + demarcation
|
|
86
128
|
logger.info(final_message)
|
|
87
129
|
except Exception as e:
|
|
88
|
-
error_message =
|
|
89
|
-
f"[bold red]Error retrieving published models: {e}[/bold red]"
|
|
90
|
-
)
|
|
130
|
+
error_message = f"[bold red]Error retrieving published models and actions: {e}[/bold red]"
|
|
91
131
|
if console:
|
|
92
132
|
final_message = Panel(error_message, expand=False)
|
|
93
133
|
console.print(final_message)
|
|
94
134
|
else:
|
|
95
135
|
demarcation = "\n" + "-" * 50 + "\n"
|
|
96
136
|
final_message = demarcation + error_message + demarcation
|
|
97
|
-
logger.info(final_message)
|
|
137
|
+
logger.info(final_message)
|