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.

Files changed (55) hide show
  1. {statezero-0.1.0b4 → statezero-0.1.0b6}/PKG-INFO +2 -2
  2. {statezero-0.1.0b4 → statezero-0.1.0b6}/README.md +1 -1
  3. {statezero-0.1.0b4 → statezero-0.1.0b6}/pyproject.toml +1 -1
  4. statezero-0.1.0b6/statezero/adaptors/django/actions.py +171 -0
  5. statezero-0.1.0b6/statezero/adaptors/django/apps.py +137 -0
  6. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/orm.py +224 -174
  7. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/urls.py +5 -3
  8. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/views.py +133 -31
  9. statezero-0.1.0b6/statezero/core/actions.py +88 -0
  10. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/core/ast_parser.py +315 -175
  11. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/core/interfaces.py +216 -70
  12. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/core/process_request.py +1 -1
  13. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero.egg-info/PKG-INFO +2 -2
  14. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero.egg-info/SOURCES.txt +2 -0
  15. statezero-0.1.0b4/statezero/adaptors/django/apps.py +0 -97
  16. {statezero-0.1.0b4 → statezero-0.1.0b6}/license.md +0 -0
  17. {statezero-0.1.0b4 → statezero-0.1.0b6}/requirements.txt +0 -0
  18. {statezero-0.1.0b4 → statezero-0.1.0b6}/setup.cfg +0 -0
  19. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/__init__.py +0 -0
  20. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/__init__.py +0 -0
  21. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/__init__.py +0 -0
  22. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/config.py +0 -0
  23. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/context_manager.py +0 -0
  24. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/event_emitters.py +0 -0
  25. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/exception_handler.py +0 -0
  26. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/extensions/__init__.py +0 -0
  27. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/extensions/custom_field_serializers/__init__.py +0 -0
  28. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/extensions/custom_field_serializers/file_fields.py +0 -0
  29. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/extensions/custom_field_serializers/money_field.py +0 -0
  30. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/f_handler.py +0 -0
  31. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/helpers.py +0 -0
  32. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/middleware.py +0 -0
  33. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/migrations/0001_initial.py +0 -0
  34. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/migrations/0002_delete_modelviewsubscription.py +0 -0
  35. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/migrations/__init__.py +0 -0
  36. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/permissions.py +0 -0
  37. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/query_optimizer.py +0 -0
  38. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/schemas.py +0 -0
  39. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/search_providers/__init__.py +0 -0
  40. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/search_providers/basic_search.py +0 -0
  41. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/search_providers/postgres_search.py +0 -0
  42. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/adaptors/django/serializers.py +0 -0
  43. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/core/__init__.py +0 -0
  44. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/core/ast_validator.py +0 -0
  45. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/core/classes.py +0 -0
  46. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/core/config.py +0 -0
  47. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/core/context_storage.py +0 -0
  48. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/core/event_bus.py +0 -0
  49. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/core/event_emitters.py +0 -0
  50. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/core/exceptions.py +0 -0
  51. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/core/hook_checks.py +0 -0
  52. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero/core/types.py +0 -0
  53. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero.egg-info/dependency_links.txt +0 -0
  54. {statezero-0.1.0b4 → statezero-0.1.0b6}/statezero.egg-info/requires.txt +0 -0
  55. {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.0b4
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-models
225
+ npx statezero sync
226
226
  ```
227
227
 
228
228
  ## Why Choose StateZero Over...
@@ -192,7 +192,7 @@ npm i @statezero/core
192
192
  ### Generate TypeScript Models
193
193
 
194
194
  ```bash
195
- npx statezero sync-models
195
+ npx statezero sync
196
196
  ```
197
197
 
198
198
  ## 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.0b4"
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)