python-cqrs 4.6.4__tar.gz → 4.7.0__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.
Files changed (84) hide show
  1. {python_cqrs-4.6.4/src/python_cqrs.egg-info → python_cqrs-4.7.0}/PKG-INFO +5 -2
  2. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/README.md +4 -1
  3. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/pyproject.toml +1 -1
  4. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/saga/models.py +7 -6
  5. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/saga/recovery.py +9 -3
  6. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/saga/saga.py +10 -3
  7. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/saga/storage/memory.py +38 -0
  8. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/saga/storage/protocol.py +40 -0
  9. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/saga/storage/sqlalchemy.py +62 -0
  10. {python_cqrs-4.6.4 → python_cqrs-4.7.0/src/python_cqrs.egg-info}/PKG-INFO +5 -2
  11. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/LICENSE +0 -0
  12. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/setup.cfg +0 -0
  13. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/__init__.py +0 -0
  14. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/adapters/__init__.py +0 -0
  15. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/adapters/amqp.py +0 -0
  16. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/adapters/circuit_breaker.py +0 -0
  17. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/adapters/kafka.py +0 -0
  18. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/adapters/protocol.py +0 -0
  19. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/compressors/__init__.py +0 -0
  20. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/compressors/protocol.py +0 -0
  21. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/compressors/zlib.py +0 -0
  22. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/container/__init__.py +0 -0
  23. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/container/dependency_injector.py +0 -0
  24. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/container/di.py +0 -0
  25. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/container/protocol.py +0 -0
  26. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/deserializers/__init__.py +0 -0
  27. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/deserializers/exceptions.py +0 -0
  28. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/deserializers/json.py +0 -0
  29. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/dispatcher/__init__.py +0 -0
  30. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/dispatcher/event.py +0 -0
  31. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/dispatcher/exceptions.py +0 -0
  32. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/dispatcher/models.py +0 -0
  33. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/dispatcher/request.py +0 -0
  34. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/dispatcher/saga.py +0 -0
  35. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/dispatcher/streaming.py +0 -0
  36. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/events/__init__.py +0 -0
  37. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/events/bootstrap.py +0 -0
  38. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/events/event.py +0 -0
  39. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/events/event_emitter.py +0 -0
  40. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/events/event_handler.py +0 -0
  41. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/events/event_processor.py +0 -0
  42. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/events/map.py +0 -0
  43. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/mediator.py +0 -0
  44. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/message_brokers/__init__.py +0 -0
  45. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/message_brokers/amqp.py +0 -0
  46. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/message_brokers/devnull.py +0 -0
  47. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/message_brokers/kafka.py +0 -0
  48. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/message_brokers/protocol.py +0 -0
  49. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/middlewares/__init__.py +0 -0
  50. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/middlewares/base.py +0 -0
  51. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/middlewares/logging.py +0 -0
  52. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/outbox/__init__.py +0 -0
  53. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/outbox/map.py +0 -0
  54. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/outbox/mock.py +0 -0
  55. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/outbox/repository.py +0 -0
  56. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/outbox/sqlalchemy.py +0 -0
  57. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/producer.py +0 -0
  58. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/requests/__init__.py +0 -0
  59. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/requests/bootstrap.py +0 -0
  60. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/requests/cor_request_handler.py +0 -0
  61. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/requests/map.py +0 -0
  62. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/requests/mermaid.py +0 -0
  63. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/requests/request.py +0 -0
  64. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/requests/request_handler.py +0 -0
  65. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/response.py +0 -0
  66. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/saga/__init__.py +0 -0
  67. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/saga/bootstrap.py +0 -0
  68. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/saga/circuit_breaker.py +0 -0
  69. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/saga/compensation.py +0 -0
  70. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/saga/execution.py +0 -0
  71. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/saga/fallback.py +0 -0
  72. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/saga/mermaid.py +0 -0
  73. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/saga/step.py +0 -0
  74. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/saga/storage/__init__.py +0 -0
  75. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/saga/storage/enums.py +0 -0
  76. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/saga/storage/models.py +0 -0
  77. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/saga/validation.py +0 -0
  78. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/serializers/__init__.py +0 -0
  79. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/serializers/default.py +0 -0
  80. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/cqrs/types.py +0 -0
  81. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/python_cqrs.egg-info/SOURCES.txt +0 -0
  82. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/python_cqrs.egg-info/dependency_links.txt +0 -0
  83. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/python_cqrs.egg-info/requires.txt +0 -0
  84. {python_cqrs-4.6.4 → python_cqrs-4.7.0}/src/python_cqrs.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-cqrs
3
- Version: 4.6.4
3
+ Version: 4.7.0
4
4
  Summary: Python CQRS pattern implementation
5
5
  Author-email: Vadim Kozyrevskiy <vadikko2@mail.ru>, Dmitry Kutlubaev <kutlubaev00@mail.ru>
6
6
  Maintainer-email: Vadim Kozyrevskiy <vadikko2@mail.ru>
@@ -97,7 +97,10 @@ Dynamic: license-file
97
97
  </p>
98
98
  </div>
99
99
 
100
-
100
+ > [!WARNING]
101
+ > **Breaking Changes in v5.0.0**
102
+ >
103
+ > Starting with version 5.0.0, Pydantic support will become optional. The default implementations of `Request`, `Response`, `DomainEvent`, and `NotificationEvent` will be migrated to dataclasses-based implementations.
101
104
 
102
105
  ## Overview
103
106
 
@@ -36,7 +36,10 @@
36
36
  </p>
37
37
  </div>
38
38
 
39
-
39
+ > [!WARNING]
40
+ > **Breaking Changes in v5.0.0**
41
+ >
42
+ > Starting with version 5.0.0, Pydantic support will become optional. The default implementations of `Request`, `Response`, `DomainEvent`, and `NotificationEvent` will be migrated to dataclasses-based implementations.
40
43
 
41
44
  ## Overview
42
45
 
@@ -31,7 +31,7 @@ maintainers = [{name = "Vadim Kozyrevskiy", email = "vadikko2@mail.ru"}]
31
31
  name = "python-cqrs"
32
32
  readme = "README.md"
33
33
  requires-python = ">=3.10"
34
- version = "4.6.4"
34
+ version = "4.7.0"
35
35
 
36
36
  [project.optional-dependencies]
37
37
  aiobreaker = ["aiobreaker>=0.3.0"]
@@ -1,5 +1,6 @@
1
1
  import dataclasses
2
2
  import typing
3
+ from dataclass_wizard import asdict, fromdict
3
4
 
4
5
  # Type variable for from_dict classmethod return type
5
6
  _T = typing.TypeVar("_T", bound="SagaContext")
@@ -29,7 +30,7 @@ class SagaContext:
29
30
  Returns:
30
31
  Dictionary representation of the context.
31
32
  """
32
- return dataclasses.asdict(self)
33
+ return asdict(self)
33
34
 
34
35
  @classmethod
35
36
  def from_dict(cls: type[_T], data: dict[str, typing.Any]) -> _T:
@@ -42,11 +43,11 @@ class SagaContext:
42
43
  Returns:
43
44
  Instance of the context class.
44
45
  """
45
- # Get field names from dataclass
46
- field_names = {f.name for f in dataclasses.fields(cls)}
47
- # Filter data to only include known fields
48
- filtered_data = {k: v for k, v in data.items() if k in field_names}
49
- return cls(**filtered_data)
46
+ # # Get field names from dataclass
47
+ # field_names = {f.name for f in dataclasses.fields(cls)}
48
+ # # Filter data to only include known fields
49
+ # filtered_data = {k: v for k, v in data.items() if k in field_names}
50
+ return fromdict(cls, data)
50
51
 
51
52
  def model_dump(self) -> dict[str, typing.Any]:
52
53
  """
@@ -29,6 +29,11 @@ async def recover_saga(
29
29
  Already completed steps will be skipped.
30
30
  If the saga was in a compensating state, compensation will resume.
31
31
 
32
+ On recovery failure (exception during resume), the storage's
33
+ increment_recovery_attempts is called automatically so the saga can be
34
+ retried or excluded by get_sagas_for_recovery(max_recovery_attempts=...).
35
+ Callers do not need to call increment_recovery_attempts themselves.
36
+
32
37
  Args:
33
38
  saga: The saga orchestrator instance.
34
39
  saga_id: The ID of the saga to recover.
@@ -109,11 +114,12 @@ async def recover_saga(
109
114
  )
110
115
  # Re-raise to allow callers to handle this case
111
116
  raise
112
- # For other RuntimeErrors, log and re-raise
117
+ # For other RuntimeErrors, recovery failed: increment attempts and re-raise
113
118
  logger.error(f"Saga {saga_id} recovery ended with error: {e}")
119
+ await storage.increment_recovery_attempts(saga_id, new_status=SagaStatus.FAILED)
114
120
  raise
115
121
  except Exception as e:
116
122
  logger.error(f"Saga {saga_id} recovery ended with error: {e}")
117
- # The transaction handles exception and runs compensation, so the saga state
118
- # should be updated to FAILED (or COMPENSATED) in storage.
123
+ # Recovery failed: increment attempts so saga can be retried or excluded later
124
+ await storage.increment_recovery_attempts(saga_id, new_status=SagaStatus.FAILED)
119
125
  raise
@@ -142,9 +142,14 @@ class SagaTransaction(typing.Generic[ContextT]):
142
142
  exc_val: BaseException | None,
143
143
  exc_tb: types.TracebackType | None,
144
144
  ) -> bool:
145
- # If an exception occurred, compensate all completed steps
146
- # Only compensate if not already compensated in __aiter__
147
- if exc_val is not None and not self._compensated:
145
+ # If an exception occurred, compensate all completed steps.
146
+ # Do not compensate on GeneratorExit: consumer stopped iteration intentionally
147
+ # (e.g. to resume later), which is not a failure.
148
+ if (
149
+ exc_val is not None
150
+ and exc_type is not GeneratorExit
151
+ and not self._compensated
152
+ ):
148
153
  self._error = exc_val
149
154
  await self._compensate()
150
155
  return False # Don't suppress the exception
@@ -302,6 +307,8 @@ class SagaTransaction(typing.Generic[ContextT]):
302
307
 
303
308
  yield step_result
304
309
 
310
+ # Update context one final time before marking as completed
311
+ await self._state_manager.update_context(self._context)
305
312
  await self._state_manager.update_status(SagaStatus.COMPLETED)
306
313
 
307
314
  except Exception as e:
@@ -37,6 +37,7 @@ class MemorySagaStorage(ISagaStorage):
37
37
  "created_at": now,
38
38
  "updated_at": now,
39
39
  "version": 1,
40
+ "recovery_attempts": 0,
40
41
  }
41
42
  self._logs[saga_id] = []
42
43
 
@@ -115,3 +116,40 @@ class MemorySagaStorage(ISagaStorage):
115
116
  return []
116
117
  # Sort by timestamp
117
118
  return sorted(self._logs[saga_id], key=lambda x: x.timestamp)
119
+
120
+ async def get_sagas_for_recovery(
121
+ self,
122
+ limit: int,
123
+ max_recovery_attempts: int = 5,
124
+ stale_after_seconds: int | None = None,
125
+ ) -> list[uuid.UUID]:
126
+ recoverable = (SagaStatus.RUNNING, SagaStatus.COMPENSATING, SagaStatus.FAILED)
127
+ now = datetime.datetime.now(datetime.timezone.utc)
128
+ threshold = (
129
+ (now - datetime.timedelta(seconds=stale_after_seconds))
130
+ if stale_after_seconds is not None
131
+ else None
132
+ )
133
+ candidates = [
134
+ sid
135
+ for sid, data in self._sagas.items()
136
+ if data["status"] in recoverable
137
+ and data.get("recovery_attempts", 0) < max_recovery_attempts
138
+ and (threshold is None or data["updated_at"] < threshold)
139
+ ]
140
+ candidates.sort(key=lambda sid: self._sagas[sid]["updated_at"])
141
+ return candidates[:limit]
142
+
143
+ async def increment_recovery_attempts(
144
+ self,
145
+ saga_id: uuid.UUID,
146
+ new_status: SagaStatus | None = None,
147
+ ) -> None:
148
+ if saga_id not in self._sagas:
149
+ raise ValueError(f"Saga {saga_id} not found")
150
+ data = self._sagas[saga_id]
151
+ data["recovery_attempts"] = data.get("recovery_attempts", 0) + 1
152
+ data["updated_at"] = datetime.datetime.now(datetime.timezone.utc)
153
+ data["version"] += 1
154
+ if new_status is not None:
155
+ data["status"] = new_status
@@ -70,3 +70,43 @@ class ISagaStorage(abc.ABC):
70
70
  saga_id: uuid.UUID,
71
71
  ) -> list[SagaLogEntry]:
72
72
  """Get step execution history."""
73
+
74
+ @abc.abstractmethod
75
+ async def get_sagas_for_recovery(
76
+ self,
77
+ limit: int,
78
+ max_recovery_attempts: int = 5,
79
+ stale_after_seconds: int | None = None,
80
+ ) -> list[uuid.UUID]:
81
+ """Return saga IDs that need recovery.
82
+
83
+ Args:
84
+ limit: Maximum number of saga IDs to return.
85
+ max_recovery_attempts: Only include sagas with recovery_attempts
86
+ strictly less than this value. Default 5.
87
+ stale_after_seconds: If set, only include sagas whose updated_at
88
+ is older than (now_utc - stale_after_seconds). Use this to
89
+ avoid picking sagas that are currently being executed (recently
90
+ updated). None means no staleness filter (backward compatible).
91
+
92
+ Returns:
93
+ List of saga IDs (RUNNING, COMPENSATING, or FAILED), ordered by
94
+ updated_at ascending, with recovery_attempts < max_recovery_attempts,
95
+ and optionally updated_at older than the staleness threshold.
96
+ """
97
+
98
+ @abc.abstractmethod
99
+ async def increment_recovery_attempts(
100
+ self,
101
+ saga_id: uuid.UUID,
102
+ new_status: SagaStatus | None = None,
103
+ ) -> None:
104
+ """Atomically increment recovery attempts after a failed recovery.
105
+
106
+ Updates recovery_attempts += 1, updated_at = now(), and optionally
107
+ status. Also increments version for optimistic locking.
108
+
109
+ Args:
110
+ saga_id: The saga to update.
111
+ new_status: If provided, set saga status to this value (e.g. FAILED).
112
+ """
@@ -78,6 +78,13 @@ class SagaExecutionModel(Base):
78
78
  onupdate=func.now(),
79
79
  comment="Last update timestamp",
80
80
  )
81
+ recovery_attempts = sqlalchemy.Column(
82
+ sqlalchemy.Integer,
83
+ nullable=False,
84
+ default=0,
85
+ server_default=sqlalchemy.text("0"),
86
+ comment="Number of recovery attempts",
87
+ )
81
88
 
82
89
 
83
90
  class SagaLogModel(Base):
@@ -143,6 +150,7 @@ class SqlAlchemySagaStorage(ISagaStorage):
143
150
  status=SagaStatus.PENDING,
144
151
  context=context,
145
152
  version=1,
153
+ recovery_attempts=0,
146
154
  )
147
155
  session.add(execution)
148
156
  await session.commit()
@@ -303,3 +311,57 @@ class SqlAlchemySagaStorage(ISagaStorage):
303
311
  )
304
312
  for row in rows
305
313
  ]
314
+
315
+ async def get_sagas_for_recovery(
316
+ self,
317
+ limit: int,
318
+ max_recovery_attempts: int = 5,
319
+ stale_after_seconds: int | None = None,
320
+ ) -> list[uuid.UUID]:
321
+ recoverable = (
322
+ SagaStatus.RUNNING,
323
+ SagaStatus.COMPENSATING,
324
+ SagaStatus.FAILED,
325
+ )
326
+ async with self.session_factory() as session:
327
+ stmt = (
328
+ sqlalchemy.select(SagaExecutionModel.id)
329
+ .where(SagaExecutionModel.status.in_(recoverable))
330
+ .where(SagaExecutionModel.recovery_attempts < max_recovery_attempts)
331
+ )
332
+ if stale_after_seconds is not None:
333
+ threshold = datetime.datetime.now(
334
+ datetime.timezone.utc,
335
+ ) - datetime.timedelta(
336
+ seconds=stale_after_seconds,
337
+ )
338
+ stmt = stmt.where(SagaExecutionModel.updated_at < threshold)
339
+ stmt = stmt.order_by(SagaExecutionModel.updated_at.asc()).limit(limit)
340
+ result = await session.execute(stmt)
341
+ rows = result.scalars().all()
342
+ return [typing.cast(uuid.UUID, row) for row in rows]
343
+
344
+ async def increment_recovery_attempts(
345
+ self,
346
+ saga_id: uuid.UUID,
347
+ new_status: SagaStatus | None = None,
348
+ ) -> None:
349
+ async with self.session_factory() as session:
350
+ try:
351
+ values: dict[str, typing.Any] = {
352
+ "recovery_attempts": SagaExecutionModel.recovery_attempts + 1,
353
+ "version": SagaExecutionModel.version + 1,
354
+ }
355
+ if new_status is not None:
356
+ values["status"] = new_status
357
+ result = await session.execute(
358
+ sqlalchemy.update(SagaExecutionModel)
359
+ .where(SagaExecutionModel.id == saga_id)
360
+ .values(**values),
361
+ )
362
+ if result.rowcount == 0: # type: ignore[attr-defined]
363
+ raise ValueError(f"Saga {saga_id} not found")
364
+ await session.commit()
365
+ except SQLAlchemyError:
366
+ await session.rollback()
367
+ raise
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-cqrs
3
- Version: 4.6.4
3
+ Version: 4.7.0
4
4
  Summary: Python CQRS pattern implementation
5
5
  Author-email: Vadim Kozyrevskiy <vadikko2@mail.ru>, Dmitry Kutlubaev <kutlubaev00@mail.ru>
6
6
  Maintainer-email: Vadim Kozyrevskiy <vadikko2@mail.ru>
@@ -97,7 +97,10 @@ Dynamic: license-file
97
97
  </p>
98
98
  </div>
99
99
 
100
-
100
+ > [!WARNING]
101
+ > **Breaking Changes in v5.0.0**
102
+ >
103
+ > Starting with version 5.0.0, Pydantic support will become optional. The default implementations of `Request`, `Response`, `DomainEvent`, and `NotificationEvent` will be migrated to dataclasses-based implementations.
101
104
 
102
105
  ## Overview
103
106
 
File without changes
File without changes