statezero 0.1.0b62__py3-none-any.whl → 0.1.0b64__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.
@@ -7,9 +7,10 @@ class OperationIDMiddleware(MiddlewareMixin):
7
7
  def process_request(self, request):
8
8
  # The header in Django is available via request.META (HTTP headers are prefixed with HTTP_)
9
9
  op_id = request.META.get("HTTP_X_OPERATION_ID")
10
- current_operation_id.set(op_id)
10
+ # Always reset to avoid leaking IDs across requests in reused threads.
11
+ current_operation_id.set(op_id if op_id else None)
11
12
 
12
13
  # Also read canonical_id from headers if provided by client
13
14
  canonical_id = request.META.get("HTTP_X_CANONICAL_ID")
14
- if canonical_id:
15
- current_canonical_id.set(canonical_id)
15
+ # Always reset; only set when explicitly provided.
16
+ current_canonical_id.set(canonical_id if canonical_id else None)
@@ -3,7 +3,7 @@ from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type, Union
3
3
 
4
4
  import networkx as nx
5
5
  from django.apps import apps
6
- from django.db import models
6
+ from django.db import models, transaction
7
7
  from django.db.models import Avg, Count, Max, Min, Q, Sum, QuerySet
8
8
  from django.db.models.signals import post_delete, post_save, pre_delete, pre_save
9
9
  from django.dispatch import receiver
@@ -977,7 +977,8 @@ class DjangoORMAdapter(AbstractORMProvider):
977
977
  def post_save_receiver(sender, instance, created, **kwargs):
978
978
  action = ActionType.CREATE if created else ActionType.UPDATE
979
979
  try:
980
- event_bus.emit_event(action, instance)
980
+ # Emit after commit so clients don't re-fetch stale rows.
981
+ transaction.on_commit(lambda: event_bus.emit_event(action, instance))
981
982
  except Exception as e:
982
983
  logger.exception(
983
984
  "Error emitting event %s for instance %s: %s", action, instance, e
@@ -994,7 +995,10 @@ class DjangoORMAdapter(AbstractORMProvider):
994
995
 
995
996
  def post_delete_receiver(sender, instance, **kwargs):
996
997
  try:
997
- event_bus.emit_event(ActionType.DELETE, instance)
998
+ # Emit after commit so clients don't re-fetch stale rows.
999
+ transaction.on_commit(
1000
+ lambda: event_bus.emit_event(ActionType.DELETE, instance)
1001
+ )
998
1002
  except Exception as e:
999
1003
  logger.exception(
1000
1004
  "Error emitting DELETE event for instance %s: %s", instance, e
statezero/core/classes.py CHANGED
@@ -4,7 +4,7 @@ from typing import Any, Dict, List, Literal, Optional, Set, Type, Union, Annotat
4
4
 
5
5
  import jsonschema
6
6
  from fastapi.encoders import jsonable_encoder
7
- from pydantic import BaseModel, Field, field_validator
7
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
8
8
  from pydantic.dataclasses import dataclass
9
9
 
10
10
  from statezero.core.types import ORMField
@@ -83,7 +83,7 @@ class FieldFormat(str, Enum):
83
83
  MONEY = "money"
84
84
 
85
85
 
86
- @dataclass
86
+ @dataclass(config=ConfigDict(arbitrary_types_allowed=True))
87
87
  class AdditionalField:
88
88
  """
89
89
  Represents configuration for an additional computed field in the schema.
@@ -95,7 +95,7 @@ class AdditionalField:
95
95
  """
96
96
 
97
97
  name: str # The property/method name to pull from
98
- field: Type[ORMField] # The instantiated serializer field (e.g. CharField(max_length=255)) #type:ignore
98
+ field: ORMField # The instantiated serializer field (e.g. CharField(max_length=255)) #type:ignore
99
99
  title: Optional[str] # Optional display name override
100
100
 
101
101
  class SchemaFieldMetadata(BaseModel):
@@ -259,7 +259,7 @@ class Display:
259
259
  label: Label to show above the display
260
260
  extra: Additional custom metadata passed to the component
261
261
  """
262
- context_path: str
262
+ context_path: Optional[str] = None
263
263
  display_component: str = "text"
264
264
  label: Optional[str] = None
265
265
  extra: Optional[Dict[str, Any]] = None
@@ -1,5 +1,4 @@
1
1
  import contextvars
2
- from uuid import uuid4
3
2
  from typing import Any, Optional
4
3
 
5
4
  # This context variable holds the current operation id (from client headers).
@@ -7,18 +6,3 @@ current_operation_id = contextvars.ContextVar("current_operation_id", default=No
7
6
 
8
7
  # This context variable holds the canonical id (server-generated for cache sharing).
9
8
  current_canonical_id = contextvars.ContextVar("current_canonical_id", default=None)
10
-
11
-
12
- def get_or_create_canonical_id():
13
- """
14
- Get the current canonical_id, or generate a new one if it doesn't exist.
15
- Canonical IDs are used for cross-client cache sharing.
16
-
17
- Returns:
18
- str: The canonical ID for this request context
19
- """
20
- canonical_id = current_canonical_id.get()
21
- if canonical_id is None:
22
- canonical_id = str(uuid4())
23
- current_canonical_id.set(canonical_id)
24
- return canonical_id
@@ -2,6 +2,8 @@ from statezero.core.context_storage import current_operation_id, current_canonic
2
2
  import logging
3
3
  from typing import Any, List, Type, Union
4
4
  from fastapi.encoders import jsonable_encoder
5
+ from django.utils import timezone
6
+ from uuid import uuid4
5
7
 
6
8
  from statezero.core.interfaces import AbstractEventEmitter, AbstractORMProvider
7
9
  from statezero.core.types import ActionType, ORMModel, ORMQuerySet
@@ -72,11 +74,14 @@ class EventBus:
72
74
  pk_field_name = instance._meta.pk.name
73
75
  pk_value = instance.pk
74
76
 
77
+ now = timezone.now()
78
+ event_canonical_id = str(uuid4())
75
79
  data = {
76
80
  "event": action_type.value,
77
81
  "model": model_name,
78
82
  "operation_id": current_operation_id.get(),
79
- "canonical_id": current_canonical_id.get(),
83
+ "canonical_id": event_canonical_id,
84
+ "server_ts_ms": int(now.timestamp() * 1000),
80
85
  "instances": [pk_value],
81
86
  "pk_field_name": pk_field_name,
82
87
  }
@@ -153,11 +158,14 @@ class EventBus:
153
158
  pk_field_name = first_instance._meta.pk.name
154
159
  pks = [instance.pk for instance in instances]
155
160
 
161
+ now = timezone.now()
162
+ event_canonical_id = str(uuid4())
156
163
  data = {
157
164
  "event": action_type.value,
158
165
  "model": model_name,
159
166
  "operation_id": current_operation_id.get(),
160
- "canonical_id": current_canonical_id.get(),
167
+ "canonical_id": event_canonical_id,
168
+ "server_ts_ms": int(now.timestamp() * 1000),
161
169
  "instances": pks,
162
170
  "pk_field_name": pk_field_name,
163
171
  }
@@ -130,6 +130,12 @@ def get_cached_query_result(queryset, operation_context: Optional[str] = None) -
130
130
  # Someone else is processing, wait for their result
131
131
  logger.debug(f"Query being processed by another request, waiting... | txn {txn_id[:8]}...")
132
132
 
133
+ # TODO: FIX DESIGN (needs to be done, I didn't have time yet): if the leader
134
+ # query exceeds the wait timeout, followers will stop waiting and execute the
135
+ # same query themselves (automatic fan-out). Replace with hard single-flight
136
+ # (no fan-out) and propagate the leader's error/timeout to all followers
137
+ # (cache error responses).
138
+
133
139
  # Calculate wait timeout based on Django's query timeout setting
134
140
  # Wait for query_timeout + 1 second to allow the first request to complete
135
141
  from django.conf import settings
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: statezero
3
- Version: 0.1.0b62
3
+ Version: 0.1.0b64
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
@@ -11,8 +11,8 @@ statezero/adaptors/django/event_emitters.py,sha256=RXwTGNEP7CMB_F4AbyksjGxB5yZyn
11
11
  statezero/adaptors/django/exception_handler.py,sha256=fjNYT3XuhqnToP38YtYix6kpDuXAQ8Da_vGoSdjeD28,3863
12
12
  statezero/adaptors/django/f_handler.py,sha256=r-4qwpWZi2sDgj0REpdGT1owemsr5iMJnDGzAxWJvZI,11994
13
13
  statezero/adaptors/django/helpers.py,sha256=7bwsQtr_zoO1UuJpyQ8dLbOHkR25Jh5TA7dekYKQOVo,5822
14
- statezero/adaptors/django/middleware.py,sha256=6asxJs_PwDXDT_jM6xwmMPTJAAnu1uRXyYLnkjG6FMA,631
15
- statezero/adaptors/django/orm.py,sha256=-k3mSuwtIhcw_TI_qQbfcJyWFxjpfNqs21uBmHVp4Kw,44120
14
+ statezero/adaptors/django/middleware.py,sha256=2utpDUVQmAk-KBEP9Zt638IO2H6QN0U7sQ-n9LwP1VU,786
15
+ statezero/adaptors/django/orm.py,sha256=7BKVjCHkHLUHprLF-RN7Hlre2sTDxepqc8rPd_g6tgM,44381
16
16
  statezero/adaptors/django/permissions.py,sha256=bwiTRYn0EWcvdd2htw_Gk-_gf-NUGsdrfhWsEg5BVT8,11163
17
17
  statezero/adaptors/django/query_optimizer.py,sha256=k9_trhL1tYtUKVUDXsqQXYn88k3gVmLkqqrtw_QiPxo,40653
18
18
  statezero/adaptors/django/schemas.py,sha256=SBYPKre7o6RANCXhzDgyZOvhChIO32W609ATVm0NzxM,16625
@@ -39,19 +39,19 @@ statezero/core/__init__.py,sha256=IaNGa9WGZ-2OopG8UTqmijDZcwpC8tIGrfkl_cvCjBk,10
39
39
  statezero/core/actions.py,sha256=MO1NN5Vtc-nu1e43x32hHrEdPRXWTYRvbC5sKPZczXQ,5385
40
40
  statezero/core/ast_parser.py,sha256=oKDchCrv2FDN2UsnC3mfI-6D3HiqXtlID5JLock6Aow,45382
41
41
  statezero/core/ast_validator.py,sha256=130IMTemJnIWd3PQHTLZYLfMFBLMmkTm37ckwHpt-QY,12214
42
- statezero/core/classes.py,sha256=wEdGyA6jsXk4chWiBH1ySxSS0PRaE3SEuKvexTDcj3s,13200
42
+ statezero/core/classes.py,sha256=UZugF4ZAqewyOtr_WMu0N9pa0U6bwTfEwYEML8Fq6qs,13272
43
43
  statezero/core/config.py,sha256=LdFlGutkJF-rsIViJwbRHVOFKWzt-33Q0Inhcba8PnY,14875
44
- statezero/core/context_storage.py,sha256=nyQX8dGavLeaFosApupLqsMmy1269Ederwz8PjRRjvE,846
45
- statezero/core/event_bus.py,sha256=jqmzTTHJCqBoQ0OnDYWt5fbpNt_z1hgZu_2TdWZ2CP4,8303
44
+ statezero/core/context_storage.py,sha256=19yPpy_m0HciYfWUkYUkXJAONPX01iGvQfX8E3IAAMQ,385
45
+ statezero/core/event_bus.py,sha256=8BCwM66xbVmXDmgbrO2uAodVxYbWgXwS0xt4qVhHXNE,8624
46
46
  statezero/core/event_emitters.py,sha256=nGvfQyubJKqW0iLfJHRfn0Me3TszNj9B7PueKEdqM5Q,1986
47
47
  statezero/core/exceptions.py,sha256=sVl4brAdoK3Y09C_DDa1iNw0eR_oCVwN68j5SIZ1XD4,3224
48
48
  statezero/core/hook_checks.py,sha256=jeD1mjqPnE-sf5lg2eH9L1iY0leQEpGME2Qo0_6IlH4,3340
49
49
  statezero/core/interfaces.py,sha256=0nTZGhzznzSwSgCVXEYYorj-i3nW-u7FFsoN_d8rSw4,22132
50
50
  statezero/core/process_request.py,sha256=s1yys2ms856KJxV-cIfnVIdMkrOW9nLfzyLor64ogME,13334
51
- statezero/core/query_cache.py,sha256=zLRbvWw4H30Wn0JSPlPyNc1FVqhzFeu4n3dhCWILj4U,9155
51
+ statezero/core/query_cache.py,sha256=haEBdd4nCm8h8ugj8KmCHUbyiHApU4rv-9PncKJJCys,9528
52
52
  statezero/core/telemetry.py,sha256=EV2yLV6WAS-MTYCQSRQadiMgOD_ViJ_qUspgvbD0GqA,7757
53
53
  statezero/core/types.py,sha256=An57YP1sdd7u6eppXeKMoSudEn_6-Pb6UoC3IdR5E8w,916
54
- statezero-0.1.0b62.dist-info/METADATA,sha256=rZluIIOllIt8i0mCvkDxp2Hd9LHn90CcwCDk13kZ-d8,9872
55
- statezero-0.1.0b62.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
56
- statezero-0.1.0b62.dist-info/top_level.txt,sha256=UAuZYPKczradU1kcMQxsGjUzEW0qdgsqzhXyscrcLpw,10
57
- statezero-0.1.0b62.dist-info/RECORD,,
54
+ statezero-0.1.0b64.dist-info/METADATA,sha256=w9RB4CSkF-uQ5WZSRrYb2D_kHFwtXu-V216Pe229kAM,9872
55
+ statezero-0.1.0b64.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
56
+ statezero-0.1.0b64.dist-info/top_level.txt,sha256=UAuZYPKczradU1kcMQxsGjUzEW0qdgsqzhXyscrcLpw,10
57
+ statezero-0.1.0b64.dist-info/RECORD,,