python-saga-orchestrator 0.2.3.dev0__py3-none-any.whl → 0.2.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-saga-orchestrator
3
- Version: 0.2.3.dev0
3
+ Version: 0.2.4
4
4
  Summary: Lightweight embedded saga orchestrator for asyncio Python services
5
5
  Author-email: Maxim Vasilyev <mayxis@inbox.ru>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
- python_saga_orchestrator-0.2.3.dev0.dist-info/licenses/LICENSE,sha256=ESYyLizI0WWtxMeS7rGVcX3ivMezm-HOd5WdeOh-9oU,1056
1
+ python_saga_orchestrator-0.2.4.dist-info/licenses/LICENSE,sha256=ESYyLizI0WWtxMeS7rGVcX3ivMezm-HOd5WdeOh-9oU,1056
2
2
  saga_orchestrator/__init__.py,sha256=FG7zoYhCzpZC_JEsy3ebDraH2ZZ9q3IGIjTGKqBzcF4,2836
3
- saga_orchestrator/_version.py,sha256=b8lE-OZpo51B0ODVlQA6zcNoIpJ5YIH4JrzxWeFFTE0,533
3
+ saga_orchestrator/_version.py,sha256=oIZQE-4i_2bThWVLsbnRskxqsRwrfcev8lVgZK6VSzM,520
4
4
  saga_orchestrator/admin/__init__.py,sha256=TKwKTM7IieI4nlMAbJ0O0OI0KPKfwbclVffNjRtIyAg,80
5
5
  saga_orchestrator/admin/api.py,sha256=u_eLELUlaHpEiuHqweNHzBwSYCtotERAiOxyVtUMe2I,1715
6
6
  saga_orchestrator/core/__init__.py,sha256=EsUqhbO_CgCYZz0yBnf6OUXH3N-_uxMod4A7yGzvbMY,264
@@ -17,11 +17,11 @@ saga_orchestrator/domain/mixins/saga_step_histrory.py,sha256=phB_oue8ksMMtDiNSER
17
17
  saga_orchestrator/domain/mixins/types.py,sha256=FlKee4yNq6y1lXX3W_SeE385udNNg-oZqMxqVXZoEpo,2526
18
18
  saga_orchestrator/domain/models/__init__.py,sha256=vgUSmwVdPhKNijFZsfmZ7mQZD8DJgCV4s4hMVQZNlwE,853
19
19
  saga_orchestrator/domain/models/builder.py,sha256=D3mVYEJT7QJ0e2dNrR2UNHT5xhRK4te6s5WchU57EIA,816
20
- saga_orchestrator/domain/models/context.py,sha256=a0nNYTW0S73Qk5ln85Ji9_siEe38qlb9GPH8XpL0Gi8,3886
20
+ saga_orchestrator/domain/models/context.py,sha256=LF-6sjB70J3adrLQFz_zgEfRaErJA9Ci5TxIfYfkePA,3885
21
21
  saga_orchestrator/domain/models/notify.py,sha256=SuZILSFNAWcUQtjL-I_c4K6VFJXXhD5LLLC2KoMq8dw,837
22
22
  saga_orchestrator/domain/models/retry.py,sha256=UM2ZrSGKDZRiPMj0qOGp36xiTr78CVKsceXFFxtiOug,1362
23
23
  saga_orchestrator/domain/models/saga_snapshot.py,sha256=ysnBVB2rw059pC07Py_xzAA-Bv7d1h6ZZuubM_Fv0_4,867
24
- saga_orchestrator/domain/models/step.py,sha256=-4hWbH2xZpGSI-xFQYHI1SoucXe4Xyq5vQ3t0DH0rQ4,4854
24
+ saga_orchestrator/domain/models/step.py,sha256=GxVQ4Qi0RKTkGt-9P8tIz54WG8VHmEnghiShUT2mW-E,6947
25
25
  saga_orchestrator/domain/models/enums/__init__.py,sha256=IFgCFvhHiQl1MSCN_I_w3O8yR94tZvN5Az8M3V3bXEQ,234
26
26
  saga_orchestrator/domain/models/enums/saga_status.py,sha256=Evz7Uy4-KrqGWFkb6RHhONCWLePd5Gjezw5u-FLmxw0,397
27
27
  saga_orchestrator/domain/models/enums/saga_step_phase.py,sha256=L73jg8q-IdDXPpMjuMRwhfVZbAMYW_42FkdXNKqwiqI,129
@@ -29,7 +29,7 @@ saga_orchestrator/domain/models/enums/saga_step_status.py,sha256=DZN5qgDorhdFbhI
29
29
  saga_orchestrator/inbox/__init__.py,sha256=uoLytCCT-2-zr369ZpGmQOB_D63fmwjV9T6sCHYGk64,656
30
30
  saga_orchestrator/inbox/contracts.py,sha256=unV7h1Eqil6sZhmpGVE_-9c_voRQhYA3KdE63T79s4U,1768
31
31
  saga_orchestrator/inbox/dispatcher.py,sha256=O4Zzi5fRAZIUQqlk3oJof6rl7_CPzvqSVQ_Un-1WkOY,4323
32
- saga_orchestrator/inbox/models.py,sha256=i6ANKTqDTanmpA2hp5V5pky06u5DoHfo2oCWCsTVQUI,2654
32
+ saga_orchestrator/inbox/models.py,sha256=MBID-3wBepnIetwMMAU22sD9olG_LpNu9K0LyOo_m5c,2632
33
33
  saga_orchestrator/inbox/repository.py,sha256=BdcVlBW9zvM7WrsL2S3Rmihmn-Ri6gr69L8ji7qqip8,5286
34
34
  saga_orchestrator/inbox/retry.py,sha256=jTxg6tZBl7Vllt89Yw-QdOSObYWXrZWjAyW36mCJHdg,590
35
35
  saga_orchestrator/outbox/__init__.py,sha256=g-isr2D737wMV1Mf_vEPFoS34YE6n6l09iKLLAL86uU,917
@@ -37,11 +37,11 @@ saga_orchestrator/outbox/contracts.py,sha256=KhqIDIDvwA26xu4VQJpU6qg2ySmsRAORest
37
37
  saga_orchestrator/outbox/dispatcher.py,sha256=BX9WTuTLx1gfu-5jV8_ejpi9nnEilnKLYY_BljJkxC8,3196
38
38
  saga_orchestrator/outbox/event.py,sha256=Kj-u_JO55jx3YMTpA9arIvmTgKEAxhH2-WQE1eTi4KU,273
39
39
  saga_orchestrator/outbox/factory.py,sha256=_p7XT_ulouBhl9J9fcgWsZkhkjmzkpfIs9m6E55UPdA,1743
40
- saga_orchestrator/outbox/models.py,sha256=ei8Q1b9NnjAWq5qm0vAIeuj22yWVOFx0UsqGszZA0Q0,2355
40
+ saga_orchestrator/outbox/models.py,sha256=BZRAxfKXPun_qpOG3rzG3QI6AxsW0cVrio6ec-yZjFI,2333
41
41
  saga_orchestrator/outbox/repository.py,sha256=A3nCi5UOwQT9mlEY03BrLidyO2sFOQGgYcE4pFeY-pA,4786
42
42
  saga_orchestrator/outbox/retry.py,sha256=9Ygz3I0HK0r9imTYevBgz14TkAZphKiOSf0grJpH99c,599
43
43
  saga_orchestrator/outbox/serialization.py,sha256=CDicJS95CHhLP47XukJAgfm0baZ83daWXQQF3MyczDo,1687
44
- python_saga_orchestrator-0.2.3.dev0.dist-info/METADATA,sha256=TKDQ42mOx7sTrACtJtxs60bcDE0AhWQNH8JZz-6c7Lo,10293
45
- python_saga_orchestrator-0.2.3.dev0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
46
- python_saga_orchestrator-0.2.3.dev0.dist-info/top_level.txt,sha256=XBp_2J8dacJGCoVxIDaUYhSEuOusCN3BD_uhEjBEEBA,18
47
- python_saga_orchestrator-0.2.3.dev0.dist-info/RECORD,,
44
+ python_saga_orchestrator-0.2.4.dist-info/METADATA,sha256=DC0H0aUU0o7iFem4Mu3FZ7erw8WN460xugFT4DmDqUQ,10288
45
+ python_saga_orchestrator-0.2.4.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
46
+ python_saga_orchestrator-0.2.4.dist-info/top_level.txt,sha256=XBp_2J8dacJGCoVxIDaUYhSEuOusCN3BD_uhEjBEEBA,18
47
+ python_saga_orchestrator-0.2.4.dist-info/RECORD,,
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.2.3.dev0'
22
- __version_tuple__ = version_tuple = (0, 2, 3, 'dev0')
21
+ __version__ = version = '0.2.4'
22
+ __version_tuple__ = version_tuple = (0, 2, 4)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -55,7 +55,6 @@ class SagaStepHistoryEntry(BaseModel):
55
55
 
56
56
 
57
57
  class SagaContext(BaseModel):
58
-
59
58
  model_config = ConfigDict(from_attributes=True, validate_assignment=True)
60
59
 
61
60
  # -- Core data --
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import contextlib
4
3
  import inspect
5
4
  from collections.abc import Callable
6
5
  from dataclasses import dataclass
@@ -8,6 +7,7 @@ from datetime import timedelta
8
7
  from typing import (
9
8
  Any,
10
9
  Generic,
10
+ Mapping,
11
11
  TypeAlias,
12
12
  TypeVar,
13
13
  get_args,
@@ -60,16 +60,16 @@ class InputContext:
60
60
  """
61
61
  Возвращает тип ('event_type') последнего полученного события.
62
62
  """
63
- with contextlib.suppress(KeyError, TypeError):
64
- return self.context["latest_event_meta"]["event_type"]
65
- return None
63
+ if not isinstance(self.context.latest_event_meta, Mapping):
64
+ return None
65
+ return self.context.latest_event_meta.get("event_type")
66
66
 
67
67
  @property
68
68
  def latest_event_payload(self) -> Any | None:
69
69
  """
70
70
  Возвращает "сырую" полезную нагрузку (payload) последнего полученного события.
71
71
  """
72
- return self.context.get("latest_event")
72
+ return self.context.latest_event
73
73
 
74
74
 
75
75
  RootInputMap: TypeAlias = Callable[[InputContext], InputModelT | dict[str, Any]]
@@ -102,16 +102,49 @@ class BaseStep(Generic[InputModelT, OutputModelT]):
102
102
 
103
103
  def __init_subclass__(cls) -> None:
104
104
  super().__init_subclass__()
105
- if cls is BaseStep:
105
+ if cls is BaseStep or inspect.isabstract(cls):
106
106
  return
107
+ generic_base = None
108
+ for base in getattr(cls, "__orig_bases__", []):
109
+ origin = get_origin(base)
110
+ if origin and issubclass(origin, BaseStep):
111
+ generic_base = base
112
+ break
113
+
114
+ if not generic_base:
115
+ raise TypeValidationError(
116
+ f"Could not find generic parameters for {cls.__name__}. "
117
+ f"Ensure it inherits from a parameterized BaseStep, e.g., BaseStep[MyInput, MyOutput]"
118
+ )
119
+
120
+ concrete_args = get_args(generic_base)
121
+ if not concrete_args or any(isinstance(arg, TypeVar) for arg in concrete_args):
122
+ raise TypeValidationError(f"Step '{cls.__name__}' inherits from a generic Step "
123
+ "but was not parameterized with concrete Input/Output models.")
124
+
125
+ concrete_input_model, concrete_output_model = concrete_args
126
+ try:
127
+ hints = get_type_hints(cls.execute, globalns=inspect.getmodule(cls).__dict__, include_extras=True)
128
+ except (AttributeError, NameError) as e:
129
+ raise TypeValidationError(
130
+ f"Could not resolve type hints for '{cls.__name__}.execute'. "
131
+ f"Ensure all types are correctly imported. Original error: {e}"
132
+ )
133
+
107
134
 
108
- hints = get_type_hints(cls.execute)
109
135
  if "inp" not in hints or "return" not in hints:
110
136
  raise TypeValidationError(
111
137
  f"Step '{cls.__name__}' must type annotate execute(inp) and return type"
112
138
  )
113
- input_model = hints["inp"]
114
- output_model = cls._resolve_output_model(hints["return"])
139
+
140
+ input_annotation = hints["inp"]
141
+ if isinstance(input_annotation, TypeVar):
142
+ input_model = concrete_input_model
143
+ else:
144
+ input_model = input_annotation
145
+
146
+ return_annotation = hints["return"]
147
+ output_model = cls._resolve_output_model(return_annotation, concrete_output_model)
115
148
  if not (inspect.isclass(input_model) and issubclass(input_model, BaseModel)):
116
149
  raise TypeValidationError(
117
150
  f"Step '{cls.__name__}' input must inherit from pydantic BaseModel"
@@ -120,22 +153,36 @@ class BaseStep(Generic[InputModelT, OutputModelT]):
120
153
  cls.output_model = output_model
121
154
 
122
155
  @staticmethod
123
- def _resolve_output_model(annotation: Any) -> type[BaseModel]:
156
+ def _resolve_output_model(annotation: Any, concrete_model: type[BaseModel] | None = None) -> type[BaseModel]:
157
+ def find_model_in_args(args_tuple: tuple) -> list[type[BaseModel]]:
158
+ candidates = []
159
+ for arg in args_tuple:
160
+ if isinstance(arg, TypeVar) and concrete_model:
161
+ candidates.append(concrete_model)
162
+ elif inspect.isclass(arg) and issubclass(arg, BaseModel):
163
+ candidates.append(arg)
164
+ return list(dict.fromkeys(candidates))
165
+
124
166
  if inspect.isclass(annotation) and issubclass(annotation, BaseModel):
125
167
  return annotation
126
168
 
127
169
  origin = get_origin(annotation)
128
170
  if origin is None:
171
+ if isinstance(annotation, TypeVar) and concrete_model:
172
+ return concrete_model
129
173
  raise TypeValidationError("Step execute return type must include BaseModel")
130
- args = tuple(arg for arg in get_args(annotation) if arg is not type(None))
131
- model_candidates = [
132
- arg for arg in args if inspect.isclass(arg) and issubclass(arg, BaseModel)
133
- ]
174
+
175
+ args = get_args(annotation)
176
+ model_candidates = find_model_in_args(args)
134
177
  await_candidates = [arg for arg in args if arg is StepAwaitEvent]
178
+
135
179
  if len(model_candidates) == 1 and len(await_candidates) <= 1:
136
180
  return model_candidates[0]
181
+
137
182
  raise TypeValidationError(
138
- "Step execute return type must be BaseModel or BaseModel | StepAwaitEvent"
183
+ f"Step execute return type must be BaseModel or BaseModel | StepAwaitEvent. "
184
+ f"Found models: {[m.__name__ for m in model_candidates]}, "
185
+ f"Found await events: {len(await_candidates)}."
139
186
  )
140
187
 
141
188
  async def execute(self, inp: InputModelT) -> OutputModelT | StepAwaitEvent:
@@ -5,9 +5,8 @@ from datetime import datetime
5
5
  from enum import Enum
6
6
  from typing import Any
7
7
 
8
- from sqlalchemy import JSON, DateTime
8
+ from sqlalchemy import JSON, DateTime, Integer, String, Text, UniqueConstraint, func
9
9
  from sqlalchemy import Enum as SqlEnum
10
- from sqlalchemy import Integer, String, Text, UniqueConstraint, func
11
10
  from sqlalchemy.dialects.postgresql import JSONB, UUID
12
11
  from sqlalchemy.ext.mutable import MutableDict
13
12
  from sqlalchemy.orm import Mapped, declarative_mixin, mapped_column
@@ -5,9 +5,8 @@ from datetime import datetime
5
5
  from enum import Enum
6
6
  from typing import Any
7
7
 
8
- from sqlalchemy import JSON, DateTime
8
+ from sqlalchemy import JSON, DateTime, Integer, String, Text, func
9
9
  from sqlalchemy import Enum as SqlEnum
10
- from sqlalchemy import Integer, String, Text, func
11
10
  from sqlalchemy.dialects.postgresql import JSONB, UUID
12
11
  from sqlalchemy.ext.mutable import MutableDict
13
12
  from sqlalchemy.orm import Mapped, declarative_mixin, mapped_column