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.
Files changed (45) hide show
  1. statezero/__init__.py +0 -0
  2. statezero/adaptors/__init__.py +0 -0
  3. statezero/adaptors/django/__init__.py +0 -0
  4. statezero/adaptors/django/apps.py +97 -0
  5. statezero/adaptors/django/config.py +99 -0
  6. statezero/adaptors/django/context_manager.py +12 -0
  7. statezero/adaptors/django/event_emitters.py +78 -0
  8. statezero/adaptors/django/exception_handler.py +98 -0
  9. statezero/adaptors/django/extensions/__init__.py +0 -0
  10. statezero/adaptors/django/extensions/custom_field_serializers/__init__.py +0 -0
  11. statezero/adaptors/django/extensions/custom_field_serializers/file_fields.py +141 -0
  12. statezero/adaptors/django/extensions/custom_field_serializers/money_field.py +75 -0
  13. statezero/adaptors/django/f_handler.py +312 -0
  14. statezero/adaptors/django/helpers.py +153 -0
  15. statezero/adaptors/django/middleware.py +10 -0
  16. statezero/adaptors/django/migrations/0001_initial.py +33 -0
  17. statezero/adaptors/django/migrations/0002_delete_modelviewsubscription.py +16 -0
  18. statezero/adaptors/django/migrations/__init__.py +0 -0
  19. statezero/adaptors/django/orm.py +915 -0
  20. statezero/adaptors/django/permissions.py +252 -0
  21. statezero/adaptors/django/query_optimizer.py +772 -0
  22. statezero/adaptors/django/schemas.py +324 -0
  23. statezero/adaptors/django/search_providers/__init__.py +0 -0
  24. statezero/adaptors/django/search_providers/basic_search.py +24 -0
  25. statezero/adaptors/django/search_providers/postgres_search.py +51 -0
  26. statezero/adaptors/django/serializers.py +554 -0
  27. statezero/adaptors/django/urls.py +14 -0
  28. statezero/adaptors/django/views.py +336 -0
  29. statezero/core/__init__.py +34 -0
  30. statezero/core/ast_parser.py +821 -0
  31. statezero/core/ast_validator.py +266 -0
  32. statezero/core/classes.py +167 -0
  33. statezero/core/config.py +263 -0
  34. statezero/core/context_storage.py +4 -0
  35. statezero/core/event_bus.py +175 -0
  36. statezero/core/event_emitters.py +60 -0
  37. statezero/core/exceptions.py +106 -0
  38. statezero/core/interfaces.py +492 -0
  39. statezero/core/process_request.py +184 -0
  40. statezero/core/types.py +63 -0
  41. statezero-0.1.0b1.dist-info/METADATA +252 -0
  42. statezero-0.1.0b1.dist-info/RECORD +45 -0
  43. statezero-0.1.0b1.dist-info/WHEEL +5 -0
  44. statezero-0.1.0b1.dist-info/licenses/license.md +117 -0
  45. statezero-0.1.0b1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,4 @@
1
+ import contextvars
2
+
3
+ # This context variable holds the current operation id.
4
+ current_operation_id = contextvars.ContextVar("current_operation_id", default=None)
@@ -0,0 +1,175 @@
1
+ from statezero.core.context_storage import current_operation_id
2
+ import logging
3
+ from typing import Any, Type, Union, List
4
+ from fastapi.encoders import jsonable_encoder
5
+
6
+ from statezero.core.interfaces import AbstractEventEmitter, AbstractORMProvider
7
+ from statezero.core.types import ActionType, ORMModel, ORMQuerySet
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class EventBus:
13
+ def __init__(
14
+ self,
15
+ broadcast_emitter: AbstractEventEmitter,
16
+ orm_provider: AbstractORMProvider = None,
17
+ ) -> None:
18
+ """
19
+ Initialize the EventBus with a broadcast emitter.
20
+
21
+ Parameters:
22
+ -----------
23
+ broadcast_emitter: AbstractEventEmitter
24
+ Emitter responsible for broadcasting events to clients
25
+ orm_provider : AbstractORMProvider
26
+ The orm provider to be used to get the default namespace for events
27
+ """
28
+ self.broadcast_emitter: AbstractEventEmitter = broadcast_emitter
29
+ self.orm_provider = orm_provider
30
+
31
+ def set_registry(self, registry):
32
+ """Set the model registry after initialization if needed."""
33
+ from statezero.core.config import Registry
34
+
35
+ self.registry: Registry = registry
36
+
37
+ def emit_event(self, action_type: ActionType, instance: Any) -> None:
38
+ """
39
+ Emit an event for a model instance to appropriate namespaces.
40
+
41
+ Parameters:
42
+ -----------
43
+ action_type: ActionType
44
+ The type of event (CREATE, UPDATE, DELETE)
45
+ instance: Any
46
+ The model instance that triggered the event
47
+ """
48
+ # Unused actions, no need to broadcast
49
+ if action_type in (ActionType.PRE_DELETE, ActionType.PRE_UPDATE):
50
+ return
51
+
52
+ if not self.broadcast_emitter or not self.orm_provider:
53
+ return
54
+
55
+ try:
56
+ # Get model class and registry config
57
+ model_class = instance.__class__
58
+ model_config = None
59
+
60
+ # Use the registry to get model config
61
+ if self.registry:
62
+ try:
63
+ model_config = self.registry.get_config(model_class)
64
+ except ValueError:
65
+ pass
66
+
67
+ default_namespace = self.orm_provider.get_model_name(model_class)
68
+ namespaces = [default_namespace]
69
+
70
+ # Create payload data from instance
71
+ model_name = self.orm_provider.get_model_name(instance)
72
+ pk_field_name = instance._meta.pk.name
73
+ pk_value = instance.pk
74
+
75
+ data = {
76
+ "event": action_type.value,
77
+ "model": model_name,
78
+ "operation_id": current_operation_id.get(),
79
+ "instances": [pk_value],
80
+ "pk_field_name": pk_field_name,
81
+ }
82
+
83
+ for namespace in namespaces:
84
+ try:
85
+ # Emit data to this namespace
86
+ self.broadcast_emitter.emit(
87
+ namespace, action_type, jsonable_encoder(data)
88
+ )
89
+ except Exception as e:
90
+ logger.exception(
91
+ "Error emitting to namespace %s for event %s: %s",
92
+ namespace,
93
+ action_type,
94
+ e,
95
+ )
96
+ except Exception as e:
97
+ logger.exception(
98
+ "Error in broadcast emitter dispatching event %s for instance %s: %s",
99
+ action_type,
100
+ instance,
101
+ e,
102
+ )
103
+
104
+ def emit_bulk_event(
105
+ self, action_type: ActionType, instances: Union[List[Any], ORMQuerySet]
106
+ ) -> None:
107
+ """
108
+ Emit a bulk event for multiple instances.
109
+
110
+ Parameters:
111
+ -----------
112
+ action_type: ActionType
113
+ The type of bulk event (e.g., BULK_UPDATE, BULK_DELETE)
114
+ instances: Union[List[Any], ORMQuerySet]
115
+ The instances affected by the bulk operation (can be a list or queryset)
116
+ """
117
+ # Convert QuerySet to list if needed
118
+ if hasattr(instances, "all") and callable(getattr(instances, "all")):
119
+ instances = list(instances)
120
+
121
+ if not instances:
122
+ return
123
+
124
+ # Get the model class from the first instance
125
+ first_instance = instances[0]
126
+ model_class = first_instance.__class__
127
+
128
+ if not self.broadcast_emitter or not self.orm_provider:
129
+ return
130
+
131
+ try:
132
+ # Get model config
133
+ model_config = None
134
+ if hasattr(self, "registry"):
135
+ try:
136
+ model_config = self.registry.get_config(model_class)
137
+ except (ValueError, AttributeError):
138
+ pass
139
+
140
+ default_namespace = self.orm_provider.get_model_name(model_class)
141
+
142
+ # Create payload data from instances
143
+ model_name = self.orm_provider.get_model_name(first_instance)
144
+ pk_field_name = first_instance._meta.pk.name
145
+ pks = [instance.pk for instance in instances]
146
+
147
+ data = {
148
+ "event": action_type.value,
149
+ "model": model_name,
150
+ "operation_id": current_operation_id.get(),
151
+ "instances": pks,
152
+ "pk_field_name": pk_field_name,
153
+ }
154
+
155
+ # Create a dictionary to group instances by namespace
156
+ namespaces = ["global", default_namespace]
157
+
158
+ for namespace in namespaces:
159
+ try:
160
+ # Emit data to this namespace
161
+ self.broadcast_emitter.emit(
162
+ namespace, action_type, jsonable_encoder(data)
163
+ )
164
+ except Exception as e:
165
+ logger.exception(
166
+ "Error emitting bulk event to namespace %s: %s",
167
+ namespace,
168
+ e,
169
+ )
170
+ except Exception as e:
171
+ logger.exception(
172
+ "Error in broadcast emitter dispatching bulk event %s: %s",
173
+ action_type,
174
+ e,
175
+ )
@@ -0,0 +1,60 @@
1
+ import json
2
+ import logging
3
+ from typing import Callable, Type, Dict, List, Any, Optional
4
+
5
+ from statezero.core.context_storage import current_operation_id
6
+ from statezero.core.interfaces import AbstractEventEmitter
7
+ from statezero.core.types import ActionType, ORMModel, RequestType
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class ConsoleEventEmitter(AbstractEventEmitter):
13
+ def __init__(self) -> None:
14
+ pass
15
+
16
+ def has_permission(self, request: RequestType, namespace: str) -> bool:
17
+ return True
18
+
19
+ def emit(
20
+ self,
21
+ namespace: str,
22
+ event_type: ActionType,
23
+ data: Dict[str, Any]
24
+ ) -> None:
25
+ logger.info(f"Event emitted to namespace '{namespace}': {json.dumps(data)}")
26
+
27
+ def authenticate(self, request: RequestType) -> None:
28
+ channel = request.data.get("channel_name")
29
+ socket_id = request.data.get("socket_id")
30
+ logger.debug(f"Console authentication for channel '{channel}' and socket_id '{socket_id}'")
31
+
32
+
33
+ class PusherEventEmitter(AbstractEventEmitter):
34
+ def __init__(
35
+ self,
36
+ pusher_client,
37
+ ) -> None:
38
+ self.pusher_client = pusher_client
39
+
40
+ def has_permission(self, request: RequestType, namespace: str) -> bool:
41
+ return True
42
+
43
+ def emit(
44
+ self,
45
+ namespace: str,
46
+ event_type: ActionType,
47
+ data: Dict[str, Any]
48
+ ) -> None:
49
+ channel = f"private-{namespace}"
50
+
51
+ try:
52
+ self.pusher_client.trigger(channel, event_type.value, data)
53
+ except Exception as e:
54
+ logger.error(f"Error emitting event '{event_type.value}' on channel '{channel}': {e}")
55
+
56
+ def authenticate(self, request: RequestType) -> dict:
57
+ channel = request.data.get("channel_name")
58
+ socket_id = request.data.get("socket_id")
59
+ logger.debug(f"Pusher authentication for channel '{channel}' and socket_id '{socket_id}'")
60
+ return self.pusher_client.authenticate(channel=channel, socket_id=socket_id)
@@ -0,0 +1,106 @@
1
+ from dataclasses import dataclass
2
+ from typing import Dict, List, Optional, Union
3
+
4
+
5
+ @dataclass
6
+ class ErrorDetail:
7
+ message: str
8
+ code: str
9
+
10
+ def __str__(self) -> str:
11
+ return self.message
12
+
13
+ def __repr__(self) -> str:
14
+ return f"ErrorDetail(message={self.message!r}, code={self.code!r})"
15
+
16
+
17
+ class StateZeroError(Exception):
18
+ """Base exception for all StateZero errors."""
19
+
20
+ status_code: int = 500
21
+ default_detail: Union[str, Dict, List] = "A server error occurred."
22
+ default_code: str = "error"
23
+
24
+ def __init__(
25
+ self,
26
+ detail: Optional[Union[str, Dict, List]] = None,
27
+ code: Optional[str] = None,
28
+ ):
29
+ detail = detail if detail is not None else self.default_detail
30
+ self.detail = self._normalize_detail(detail, code or self.default_code)
31
+ super().__init__(str(self.detail))
32
+
33
+ def _normalize_detail(
34
+ self, detail: Union[str, Dict, List], code: Optional[str]
35
+ ) -> Union[ErrorDetail, Dict, List]:
36
+ """Convert details to ErrorDetail objects recursively."""
37
+ if isinstance(detail, str):
38
+ return ErrorDetail(detail, code or self.default_code)
39
+ elif isinstance(detail, dict):
40
+ return {
41
+ key: self._normalize_detail(value, code)
42
+ for key, value in detail.items()
43
+ }
44
+ elif isinstance(detail, list):
45
+ return [self._normalize_detail(item, code) for item in detail]
46
+ return detail
47
+
48
+
49
+ class ValidationError(StateZeroError):
50
+ """Error raised for invalid input. Corresponds to HTTP 400."""
51
+
52
+ status_code = 400
53
+ default_detail = "Invalid input."
54
+ default_code = "validation_error"
55
+
56
+ def __init__(self, detail: Optional[Union[Dict, List]] = None):
57
+ super().__init__(detail, self.default_code)
58
+
59
+
60
+ class NotFound(StateZeroError):
61
+ """Error raised when an object is not found. Corresponds to HTTP 404."""
62
+
63
+ status_code = 404
64
+ default_detail = "Not found."
65
+ default_code = "not_found"
66
+
67
+ def __init__(self, detail: Optional[str] = None):
68
+ super().__init__(detail, self.default_code)
69
+
70
+
71
+ class PermissionDenied(StateZeroError):
72
+ """Error raised for permission issues. Corresponds to HTTP 403."""
73
+
74
+ status_code = 403
75
+ default_detail = "Permission denied."
76
+ default_code = "permission_denied"
77
+
78
+ def __init__(self, detail: Optional[str] = None):
79
+ super().__init__(detail, self.default_code)
80
+
81
+
82
+ class MultipleObjectsReturned(StateZeroError):
83
+ """Error raised when multiple objects are returned but only one was expected."""
84
+
85
+ status_code = 400
86
+ default_detail = "Multiple objects returned."
87
+ default_code = "multiple_objects_returned"
88
+
89
+ def __init__(self, detail: Optional[str] = None):
90
+ super().__init__(detail, self.default_code)
91
+
92
+
93
+ class ASTValidationError(StateZeroError):
94
+ """Error raised for invalid query syntax (AST issues)."""
95
+
96
+ status_code = 400
97
+ default_detail = "Query syntax error."
98
+ default_code = "ast_validation_error"
99
+
100
+ def __init__(self, detail: Optional[Union[Dict, List, str]] = None):
101
+ super().__init__(detail, self.default_code)
102
+
103
+
104
+ class ConfigError(Exception):
105
+ """Error raised for configuration issues."""
106
+ pass