easy-faststream 0.2.0__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.
@@ -0,0 +1,17 @@
1
+ from easy_faststream.app import StreamApp
2
+ from easy_faststream.config import StreamSettings
3
+ from easy_faststream.context import MessageContext
4
+ from easy_faststream.exceptions import NonRetryableError, RetryableError
5
+ from easy_faststream.retry import RetryPolicy
6
+
7
+ __all__ = [
8
+ "MessageContext",
9
+ "NonRetryableError",
10
+ "RetryPolicy",
11
+ "RetryableError",
12
+ "StreamApp",
13
+ "StreamSettings",
14
+ ]
15
+
16
+ __version__ = "0.2.0"
17
+
easy_faststream/app.py ADDED
@@ -0,0 +1,467 @@
1
+ import inspect
2
+ import json
3
+ import logging
4
+ from datetime import UTC, datetime
5
+ from typing import Any
6
+
7
+ from faststream import AckPolicy, FastStream
8
+ from faststream.rabbit import (
9
+ ExchangeType,
10
+ RabbitBroker,
11
+ RabbitExchange,
12
+ RabbitMessage,
13
+ RabbitQueue,
14
+ )
15
+ from pydantic import ValidationError
16
+
17
+ from easy_faststream.config import StreamSettings
18
+ from easy_faststream.context import MessageContext
19
+ from easy_faststream.exceptions import NonRetryableError
20
+ from easy_faststream.retry import RetryPolicy
21
+ from easy_faststream.topology import (
22
+ RetryDestination,
23
+ build_retry_destinations,
24
+ declare_retry_topology,
25
+ )
26
+ from easy_faststream.types import ConsumerHandler, SchemaT
27
+
28
+ logger = logging.getLogger("easy_faststream")
29
+
30
+
31
+ class StreamApp:
32
+ RETRY_COUNT_HEADER = "x-easy-retry-count"
33
+ RETRY_HISTORY_HEADER = "x-easy-retry-history"
34
+
35
+ def __init__(self, settings: StreamSettings | None = None) -> None:
36
+ self.settings = settings or StreamSettings()
37
+
38
+ self.broker = RabbitBroker(
39
+ self.settings.rabbitmq_url,
40
+ app_id=self.settings.app_name,
41
+ graceful_timeout=self.settings.graceful_timeout,
42
+ )
43
+ self.app = FastStream(self.broker)
44
+
45
+ @classmethod
46
+ def from_env(cls) -> "StreamApp":
47
+ return cls(StreamSettings())
48
+
49
+ def consumer(
50
+ self,
51
+ *,
52
+ event: str,
53
+ schema: type[SchemaT],
54
+ queue: str | None = None,
55
+ exchange: str | None = None,
56
+ routing_key: str | None = None,
57
+ retries: int | None = None,
58
+ retry_delay: float | None = None,
59
+ retry_backoff: float | None = None,
60
+ ):
61
+ queue_name = queue or event
62
+ exchange_name = exchange or self.settings.default_exchange
63
+ route = routing_key or event
64
+
65
+ retry_policy = RetryPolicy(
66
+ max_retries=(
67
+ self.settings.max_retries
68
+ if retries is None
69
+ else retries
70
+ ),
71
+ delay_seconds=(
72
+ self.settings.retry_delay_seconds
73
+ if retry_delay is None
74
+ else retry_delay
75
+ ),
76
+ backoff=(
77
+ self.settings.retry_backoff
78
+ if retry_backoff is None
79
+ else retry_backoff
80
+ ),
81
+ )
82
+
83
+ retry_destinations = build_retry_destinations(
84
+ source_queue=queue_name,
85
+ source_exchange=exchange_name,
86
+ source_routing_key=route,
87
+ retry_exchange=self.settings.retry_exchange,
88
+ queue_suffix=self.settings.retry_queue_suffix,
89
+ policy=retry_policy,
90
+ )
91
+ retry_map = {
92
+ destination.retry_number: destination
93
+ for destination in retry_destinations
94
+ }
95
+
96
+ source_exchange = RabbitExchange(
97
+ exchange_name,
98
+ type=ExchangeType.TOPIC,
99
+ durable=True,
100
+ )
101
+ source_queue = RabbitQueue(
102
+ queue_name,
103
+ durable=True,
104
+ routing_key=route,
105
+ )
106
+
107
+ @self.app.after_startup
108
+ async def setup_retry_topology() -> None:
109
+ await declare_retry_topology(
110
+ broker=self.broker,
111
+ retry_exchange_name=self.settings.retry_exchange,
112
+ destinations=retry_destinations,
113
+ )
114
+
115
+ logger.info(
116
+ "retry_topology_declared",
117
+ extra={
118
+ "event": event,
119
+ "queue": queue_name,
120
+ "retry_queues": len(retry_destinations),
121
+ },
122
+ )
123
+
124
+ def decorator(handler: ConsumerHandler[SchemaT]):
125
+ parameter_count = len(inspect.signature(handler).parameters)
126
+
127
+ if parameter_count not in (1, 2):
128
+ raise TypeError(
129
+ f"{handler.__name__} must accept "
130
+ "(message) or (message, context)"
131
+ )
132
+
133
+ @self.broker.subscriber(
134
+ source_queue,
135
+ source_exchange,
136
+ ack_policy=AckPolicy.MANUAL,
137
+ )
138
+ async def wrapped(
139
+ raw_message: Any,
140
+ message: RabbitMessage,
141
+ ) -> None:
142
+ context = self._make_context(
143
+ message=message,
144
+ queue=queue_name,
145
+ exchange=exchange_name,
146
+ routing_key=route,
147
+ )
148
+
149
+ retry_count = self._read_retry_count(
150
+ context.headers
151
+ )
152
+ retry_history = self._read_retry_history(
153
+ context.headers
154
+ )
155
+
156
+ try:
157
+ payload = schema.model_validate(raw_message)
158
+ except ValidationError as error:
159
+ await self._dead_letter_or_nack(
160
+ raw_message=raw_message,
161
+ message=message,
162
+ context=context,
163
+ error=error,
164
+ retry_count=retry_count,
165
+ retry_history=retry_history,
166
+ error_kind="validation",
167
+ )
168
+ return
169
+
170
+ try:
171
+ if parameter_count == 1:
172
+ await handler(payload)
173
+ else:
174
+ await handler(payload, context)
175
+
176
+ except NonRetryableError as error:
177
+ await self._dead_letter_or_nack(
178
+ raw_message=raw_message,
179
+ message=message,
180
+ context=context,
181
+ error=error,
182
+ retry_count=retry_count,
183
+ retry_history=retry_history,
184
+ error_kind="non_retryable",
185
+ )
186
+ return
187
+
188
+ except Exception as error:
189
+ if retry_count < retry_policy.max_retries:
190
+ next_retry = retry_count + 1
191
+ destination = retry_map[next_retry]
192
+
193
+ retry_history.append(
194
+ self._retry_record(
195
+ retry_number=next_retry,
196
+ destination=destination,
197
+ error=error,
198
+ )
199
+ )
200
+
201
+ await self._schedule_retry_or_nack(
202
+ raw_message=raw_message,
203
+ message=message,
204
+ context=context,
205
+ error=error,
206
+ destination=destination,
207
+ retry_history=retry_history,
208
+ )
209
+ return
210
+
211
+ await self._dead_letter_or_nack(
212
+ raw_message=raw_message,
213
+ message=message,
214
+ context=context,
215
+ error=error,
216
+ retry_count=retry_count,
217
+ retry_history=retry_history,
218
+ error_kind="processing",
219
+ )
220
+ return
221
+
222
+ await message.ack()
223
+
224
+ logger.info(
225
+ "message_processed",
226
+ extra={
227
+ "event": event,
228
+ "message_id": context.message_id,
229
+ "retry_count": retry_count,
230
+ },
231
+ )
232
+
233
+ return handler
234
+
235
+ return decorator
236
+
237
+ async def _schedule_retry_or_nack(
238
+ self,
239
+ *,
240
+ raw_message: Any,
241
+ message: RabbitMessage,
242
+ context: MessageContext,
243
+ error: Exception,
244
+ destination: RetryDestination,
245
+ retry_history: list[dict[str, Any]],
246
+ ) -> None:
247
+ headers = dict(context.headers)
248
+ headers[self.RETRY_COUNT_HEADER] = (
249
+ destination.retry_number
250
+ )
251
+ headers[self.RETRY_HISTORY_HEADER] = json.dumps(
252
+ retry_history,
253
+ ensure_ascii=False,
254
+ default=str,
255
+ )
256
+
257
+ retry_exchange = RabbitExchange(
258
+ self.settings.retry_exchange,
259
+ type=ExchangeType.TOPIC,
260
+ durable=True,
261
+ )
262
+
263
+ try:
264
+ await self.broker.publish(
265
+ raw_message,
266
+ exchange=retry_exchange,
267
+ routing_key=destination.routing_key,
268
+ message_id=context.message_id,
269
+ correlation_id=context.correlation_id,
270
+ headers=headers,
271
+ persist=True,
272
+ )
273
+ except Exception:
274
+ logger.exception(
275
+ "retry_publish_failed",
276
+ extra={
277
+ "message_id": context.message_id,
278
+ "retry_number": destination.retry_number,
279
+ },
280
+ )
281
+ await message.nack(requeue=True)
282
+ return
283
+
284
+ await message.ack()
285
+
286
+ logger.warning(
287
+ "message_scheduled_for_retry",
288
+ extra={
289
+ "message_id": context.message_id,
290
+ "retry_number": destination.retry_number,
291
+ "delay_seconds": destination.delay_seconds,
292
+ "error_type": type(error).__name__,
293
+ },
294
+ )
295
+
296
+ async def _dead_letter_or_nack(
297
+ self,
298
+ *,
299
+ raw_message: Any,
300
+ message: RabbitMessage,
301
+ context: MessageContext,
302
+ error: Exception,
303
+ retry_count: int,
304
+ retry_history: list[dict[str, Any]],
305
+ error_kind: str,
306
+ ) -> None:
307
+ envelope = {
308
+ "original_payload": self._json_safe(raw_message),
309
+ "failure": {
310
+ "kind": error_kind,
311
+ "error_type": type(error).__name__,
312
+ "error_message": str(error),
313
+ "retry_count": retry_count,
314
+ "retry_history": retry_history,
315
+ "failed_at": datetime.now(UTC).isoformat(),
316
+ },
317
+ "source": {
318
+ "queue": context.queue,
319
+ "exchange": context.exchange,
320
+ "routing_key": context.routing_key,
321
+ "message_id": context.message_id,
322
+ "correlation_id": context.correlation_id,
323
+ "headers": self._json_safe(context.headers),
324
+ },
325
+ }
326
+
327
+ dlq_name = (
328
+ f"{context.queue}"
329
+ f"{self.settings.dlq_queue_suffix}"
330
+ )
331
+ dlq_exchange = RabbitExchange(
332
+ self.settings.dlq_exchange,
333
+ type=ExchangeType.TOPIC,
334
+ durable=True,
335
+ )
336
+ dlq_queue = RabbitQueue(
337
+ dlq_name,
338
+ durable=True,
339
+ routing_key=context.routing_key,
340
+ )
341
+
342
+ try:
343
+ declared_exchange = (
344
+ await self.broker.declare_exchange(
345
+ dlq_exchange
346
+ )
347
+ )
348
+ declared_queue = await self.broker.declare_queue(
349
+ dlq_queue
350
+ )
351
+ await declared_queue.bind(
352
+ declared_exchange,
353
+ routing_key=context.routing_key,
354
+ )
355
+
356
+ await self.broker.publish(
357
+ envelope,
358
+ exchange=dlq_exchange,
359
+ routing_key=context.routing_key,
360
+ message_id=context.message_id,
361
+ correlation_id=context.correlation_id,
362
+ headers={
363
+ "x-original-queue": context.queue,
364
+ "x-retry-count": retry_count,
365
+ "x-error-type": type(error).__name__,
366
+ },
367
+ persist=True,
368
+ )
369
+ except Exception:
370
+ logger.exception(
371
+ "dead_letter_publish_failed",
372
+ extra={
373
+ "message_id": context.message_id,
374
+ "queue": context.queue,
375
+ },
376
+ )
377
+ await message.nack(requeue=True)
378
+ return
379
+
380
+ await message.ack()
381
+
382
+ logger.error(
383
+ "message_dead_lettered",
384
+ extra={
385
+ "message_id": context.message_id,
386
+ "queue": dlq_name,
387
+ "retry_count": retry_count,
388
+ "error_type": type(error).__name__,
389
+ },
390
+ )
391
+
392
+ @classmethod
393
+ def _read_retry_count(
394
+ cls,
395
+ headers: dict[str, Any],
396
+ ) -> int:
397
+ value = headers.get(cls.RETRY_COUNT_HEADER, 0)
398
+
399
+ try:
400
+ return max(0, int(value))
401
+ except (TypeError, ValueError):
402
+ return 0
403
+
404
+ @classmethod
405
+ def _read_retry_history(
406
+ cls,
407
+ headers: dict[str, Any],
408
+ ) -> list[dict[str, Any]]:
409
+ value = headers.get(cls.RETRY_HISTORY_HEADER)
410
+
411
+ if value is None:
412
+ return []
413
+
414
+ if isinstance(value, bytes):
415
+ value = value.decode("utf-8")
416
+
417
+ if not isinstance(value, str):
418
+ return []
419
+
420
+ try:
421
+ history = json.loads(value)
422
+ except (TypeError, json.JSONDecodeError):
423
+ return []
424
+
425
+ return history if isinstance(history, list) else []
426
+
427
+ @staticmethod
428
+ def _retry_record(
429
+ *,
430
+ retry_number: int,
431
+ destination: RetryDestination,
432
+ error: Exception,
433
+ ) -> dict[str, Any]:
434
+ return {
435
+ "retry_number": retry_number,
436
+ "error_type": type(error).__name__,
437
+ "error_message": str(error),
438
+ "delay_seconds": destination.delay_seconds,
439
+ "failed_at": datetime.now(UTC).isoformat(),
440
+ }
441
+
442
+ @staticmethod
443
+ def _json_safe(value: Any) -> Any:
444
+ return json.loads(
445
+ json.dumps(
446
+ value,
447
+ ensure_ascii=False,
448
+ default=str,
449
+ )
450
+ )
451
+
452
+ @staticmethod
453
+ def _make_context(
454
+ *,
455
+ message: RabbitMessage,
456
+ queue: str,
457
+ exchange: str,
458
+ routing_key: str,
459
+ ) -> MessageContext:
460
+ return MessageContext(
461
+ message_id=message.message_id,
462
+ correlation_id=message.correlation_id,
463
+ headers=dict(message.headers or {}),
464
+ queue=queue,
465
+ exchange=exchange,
466
+ routing_key=routing_key,
467
+ )
@@ -0,0 +1,26 @@
1
+ from pydantic import Field
2
+ from pydantic_settings import BaseSettings, SettingsConfigDict
3
+
4
+
5
+ class StreamSettings(BaseSettings):
6
+ model_config = SettingsConfigDict(
7
+ env_prefix="EASY_STREAM_",
8
+ env_file=".env",
9
+ extra="ignore",
10
+ )
11
+
12
+ rabbitmq_url: str = "amqp://guest:guest@localhost:5672/"
13
+ app_name: str = "easy-faststream-app"
14
+
15
+ default_exchange: str = "easy.events"
16
+ retry_exchange: str = "easy.events.retry"
17
+ dlq_exchange: str = "easy.events.dead"
18
+
19
+ retry_queue_suffix: str = ".retry"
20
+ dlq_queue_suffix: str = ".dead"
21
+
22
+ max_retries: int = Field(default=3, ge=0)
23
+ retry_delay_seconds: float = Field(default=1.0, ge=0)
24
+ retry_backoff: float = Field(default=2.0, ge=1)
25
+
26
+ graceful_timeout: float = Field(default=30.0, gt=0)
@@ -0,0 +1,13 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Any
3
+
4
+
5
+ @dataclass(frozen=True, slots=True)
6
+ class MessageContext:
7
+ message_id: str | None
8
+ correlation_id: str | None
9
+ headers: dict[str, Any] = field(default_factory=dict)
10
+ queue: str = ""
11
+ exchange: str = ""
12
+ routing_key: str = ""
13
+
@@ -0,0 +1,11 @@
1
+ class EasyFastStreamError(Exception):
2
+ """Base exception for easy-faststream."""
3
+
4
+
5
+ class RetryableError(EasyFastStreamError):
6
+ """Raise when processing may succeed on a later attempt."""
7
+
8
+
9
+ class NonRetryableError(EasyFastStreamError):
10
+ """Raise when retrying cannot resolve the failure."""
11
+
@@ -0,0 +1,22 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass(frozen=True, slots=True)
5
+ class RetryPolicy:
6
+ max_retries: int = 3
7
+ delay_seconds: float = 1.0
8
+ backoff: float = 2.0
9
+
10
+ def __post_init__(self) -> None:
11
+ if self.max_retries < 0:
12
+ raise ValueError("max_retries must be at least 0")
13
+ if self.delay_seconds < 0:
14
+ raise ValueError("delay_seconds must be at least 0")
15
+ if self.backoff < 1:
16
+ raise ValueError("backoff must be at least 1")
17
+
18
+ def delay_for(self, retry_number: int) -> float:
19
+ if retry_number < 1:
20
+ raise ValueError("retry_number must be at least 1")
21
+ return self.delay_seconds * (self.backoff ** (retry_number - 1))
22
+
@@ -0,0 +1,83 @@
1
+ from dataclasses import dataclass
2
+
3
+ from faststream.rabbit import (
4
+ ExchangeType,
5
+ RabbitBroker,
6
+ RabbitExchange,
7
+ RabbitQueue,
8
+ )
9
+
10
+ from easy_faststream.retry import RetryPolicy
11
+
12
+
13
+ @dataclass(frozen=True, slots=True)
14
+ class RetryDestination:
15
+ retry_number: int
16
+ delay_seconds: float
17
+ routing_key: str
18
+ queue: RabbitQueue
19
+
20
+
21
+ def build_retry_destinations(
22
+ *,
23
+ source_queue: str,
24
+ source_exchange: str,
25
+ source_routing_key: str,
26
+ retry_exchange: str,
27
+ queue_suffix: str,
28
+ policy: RetryPolicy,
29
+ ) -> list[RetryDestination]:
30
+ destinations: list[RetryDestination] = []
31
+
32
+ for retry_number in range(1, policy.max_retries + 1):
33
+ delay_seconds = policy.delay_for(retry_number)
34
+ retry_routing_key = (
35
+ f"{source_queue}{queue_suffix}.{retry_number}"
36
+ )
37
+
38
+ retry_queue = RabbitQueue(
39
+ name=retry_routing_key,
40
+ durable=True,
41
+ routing_key=retry_routing_key,
42
+ arguments={
43
+ "x-message-ttl": int(delay_seconds * 1000),
44
+ "x-dead-letter-exchange": source_exchange,
45
+ "x-dead-letter-routing-key": source_routing_key,
46
+ },
47
+ )
48
+
49
+ destinations.append(
50
+ RetryDestination(
51
+ retry_number=retry_number,
52
+ delay_seconds=delay_seconds,
53
+ routing_key=retry_routing_key,
54
+ queue=retry_queue,
55
+ )
56
+ )
57
+
58
+ return destinations
59
+
60
+
61
+ async def declare_retry_topology(
62
+ *,
63
+ broker: RabbitBroker,
64
+ retry_exchange_name: str,
65
+ destinations: list[RetryDestination],
66
+ ) -> RabbitExchange:
67
+ retry_exchange = RabbitExchange(
68
+ retry_exchange_name,
69
+ type=ExchangeType.TOPIC,
70
+ durable=True,
71
+ )
72
+
73
+ declared_exchange = await broker.declare_exchange(retry_exchange)
74
+
75
+ for destination in destinations:
76
+ declared_queue = await broker.declare_queue(destination.queue)
77
+
78
+ await declared_queue.bind(
79
+ declared_exchange,
80
+ routing_key=destination.routing_key,
81
+ )
82
+
83
+ return retry_exchange
@@ -0,0 +1,14 @@
1
+ from collections.abc import Awaitable, Callable
2
+ from typing import TypeAlias, TypeVar
3
+
4
+ from pydantic import BaseModel
5
+
6
+ from easy_faststream.context import MessageContext
7
+
8
+ SchemaT = TypeVar("SchemaT", bound=BaseModel)
9
+
10
+ ConsumerHandler: TypeAlias = (
11
+ Callable[[SchemaT], Awaitable[None]]
12
+ | Callable[[SchemaT, MessageContext], Awaitable[None]]
13
+ )
14
+
@@ -0,0 +1,84 @@
1
+ Metadata-Version: 2.4
2
+ Name: easy-faststream
3
+ Version: 0.2.0
4
+ Summary: A schema-first FastStream framework with automatic retries and dead-letter handling.
5
+ Author: Ek
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.11
9
+ Requires-Dist: faststream[cli,rabbit]<0.8,>=0.7.1
10
+ Requires-Dist: pydantic-settings<3,>=2.2
11
+ Requires-Dist: pydantic<3,>=2.7
12
+ Provides-Extra: dev
13
+ Requires-Dist: build>=1.2; extra == 'dev'
14
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
15
+ Requires-Dist: pytest>=8.0; extra == 'dev'
16
+ Requires-Dist: ruff>=0.6; extra == 'dev'
17
+ Description-Content-Type: text/markdown
18
+
19
+ # easy-faststream
20
+
21
+ `easy-faststream` is a schema-first wrapper around FastStream for RabbitMQ.
22
+ Application developers define a Pydantic schema and a consumer function; the
23
+ framework handles validation, retry, acknowledgements, and dead-letter routing.
24
+
25
+ ## Example
26
+
27
+ ```python
28
+ from datetime import datetime
29
+ from uuid import UUID
30
+
31
+ from pydantic import BaseModel
32
+
33
+ from easy_faststream import MessageContext, StreamApp
34
+
35
+
36
+ class TripRequested(BaseModel):
37
+ order_id: UUID
38
+ passenger_id: UUID | None = None
39
+ service_type: str
40
+ event_at: datetime
41
+
42
+
43
+ app = StreamApp.from_env()
44
+
45
+
46
+ @app.consumer(
47
+ event="passapp.trip.requested",
48
+ schema=TripRequested,
49
+ queue="p_q.passapp.trip.requested",
50
+ exchange="ex.passapp.event.trip",
51
+ )
52
+ async def consume_trip(
53
+ event: TripRequested,
54
+ context: MessageContext,
55
+ ) -> None:
56
+ print(event.order_id, context.message_id)
57
+ ```
58
+
59
+ Run the application through FastStream:
60
+
61
+ ```bash
62
+ faststream run app:app.app
63
+ ```
64
+
65
+ ## Environment variables
66
+
67
+ ```env
68
+ EASY_STREAM_RABBITMQ_URL=amqp://guest:guest@localhost:5672/
69
+ EASY_STREAM_APP_NAME=trip-consumer
70
+ EASY_STREAM_DEFAULT_EXCHANGE=easy.events
71
+ EASY_STREAM_DLQ_EXCHANGE=easy.events.dead
72
+ EASY_STREAM_MAX_RETRIES=3
73
+ EASY_STREAM_RETRY_DELAY_SECONDS=1
74
+ EASY_STREAM_RETRY_BACKOFF=2
75
+ ```
76
+
77
+ ## Failure behavior
78
+
79
+ - Invalid schema: publish the original payload and validation details to the DLQ.
80
+ - Processing error: retry according to the configured policy, then publish to DLQ.
81
+ - `NonRetryableError`: skip retries and publish directly to DLQ.
82
+ - DLQ publish error: NACK and requeue the original RabbitMQ message.
83
+ - Success or successful DLQ publish: ACK the original message.
84
+
@@ -0,0 +1,12 @@
1
+ easy_faststream/__init__.py,sha256=tHd5_43iPm-fNjYQhFDY2tDM9psFuWKh0_zZ1DnEOQg,428
2
+ easy_faststream/app.py,sha256=jdA-jYOzYt24UxTH_74RUfD8KtDTCFN7QLFrCcOojiE,14350
3
+ easy_faststream/config.py,sha256=-_X51pZc3JesFere4Jsco_11UR2u0pILS-LziD421_U,783
4
+ easy_faststream/context.py,sha256=XjMau6aMZ3d2jD6TB3OZcY58J-BN_qI1jZgX5siSEQk,310
5
+ easy_faststream/exceptions.py,sha256=g4ggg1gHBKDHxq9JDcA9_8b18WdvIzTwkUq3RCSMOGs,300
6
+ easy_faststream/retry.py,sha256=_TgGZYtsWc_c0pHDY-lb78gO8h3Dq_wgcu8jVShG0j0,715
7
+ easy_faststream/topology.py,sha256=vcBJddeCmzmZip0CMdrUFV3FOxdQtMy4KkHGbY9fH1w,2176
8
+ easy_faststream/types.py,sha256=16zoRKgQUOxJzAOiCDll6M1hq_2XW6bt9pGBrC4RzBw,352
9
+ easy_faststream-0.2.0.dist-info/METADATA,sha256=pYrgCxecweJLGs-MwPjSBX5bsF3fgUZXMn0f5GCPbw0,2243
10
+ easy_faststream-0.2.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
11
+ easy_faststream-0.2.0.dist-info/licenses/LICENSE,sha256=fjl_453_2LoQPw6Pzk0ih3asEJ25c5nf_JPu6ie56oA,1078
12
+ easy_faststream-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.