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.
- statezero/__init__.py +0 -0
- statezero/adaptors/__init__.py +0 -0
- statezero/adaptors/django/__init__.py +0 -0
- statezero/adaptors/django/apps.py +97 -0
- statezero/adaptors/django/config.py +99 -0
- statezero/adaptors/django/context_manager.py +12 -0
- statezero/adaptors/django/event_emitters.py +78 -0
- statezero/adaptors/django/exception_handler.py +98 -0
- statezero/adaptors/django/extensions/__init__.py +0 -0
- statezero/adaptors/django/extensions/custom_field_serializers/__init__.py +0 -0
- statezero/adaptors/django/extensions/custom_field_serializers/file_fields.py +141 -0
- statezero/adaptors/django/extensions/custom_field_serializers/money_field.py +75 -0
- statezero/adaptors/django/f_handler.py +312 -0
- statezero/adaptors/django/helpers.py +153 -0
- statezero/adaptors/django/middleware.py +10 -0
- statezero/adaptors/django/migrations/0001_initial.py +33 -0
- statezero/adaptors/django/migrations/0002_delete_modelviewsubscription.py +16 -0
- statezero/adaptors/django/migrations/__init__.py +0 -0
- statezero/adaptors/django/orm.py +915 -0
- statezero/adaptors/django/permissions.py +252 -0
- statezero/adaptors/django/query_optimizer.py +772 -0
- statezero/adaptors/django/schemas.py +324 -0
- statezero/adaptors/django/search_providers/__init__.py +0 -0
- statezero/adaptors/django/search_providers/basic_search.py +24 -0
- statezero/adaptors/django/search_providers/postgres_search.py +51 -0
- statezero/adaptors/django/serializers.py +554 -0
- statezero/adaptors/django/urls.py +14 -0
- statezero/adaptors/django/views.py +336 -0
- statezero/core/__init__.py +34 -0
- statezero/core/ast_parser.py +821 -0
- statezero/core/ast_validator.py +266 -0
- statezero/core/classes.py +167 -0
- statezero/core/config.py +263 -0
- statezero/core/context_storage.py +4 -0
- statezero/core/event_bus.py +175 -0
- statezero/core/event_emitters.py +60 -0
- statezero/core/exceptions.py +106 -0
- statezero/core/interfaces.py +492 -0
- statezero/core/process_request.py +184 -0
- statezero/core/types.py +63 -0
- statezero-0.1.0b1.dist-info/METADATA +252 -0
- statezero-0.1.0b1.dist-info/RECORD +45 -0
- statezero-0.1.0b1.dist-info/WHEEL +5 -0
- statezero-0.1.0b1.dist-info/licenses/license.md +117 -0
- statezero-0.1.0b1.dist-info/top_level.txt +1 -0
|
@@ -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
|