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.
@@ -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, 'CONFIG_FILE_PREFIX'):
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('.py', '')
33
- if (not isinstance(config_file_prefix, str)) or (len(config_file_prefix) < 1):
34
- raise ValueError(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'")
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(f"Imported {config_file_prefix} module from {app_config_instance.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}")
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(registry) # Raises an exception if a non StateZero model is implicitly exposed
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 (from registry) to confirm StateZero is running.
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
- base_message = (
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
- base_message = "[bold yellow]StateZero is running but no models are registered.[/bold yellow]"
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-models[/italic] in your frontend project directory "
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)