flowforge-sdk 0.1.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.
- flowforge/__init__.py +79 -0
- flowforge/agent.py +86 -0
- flowforge/agent_def.py +76 -0
- flowforge/ai/__init__.py +5 -0
- flowforge/ai/providers.py +203 -0
- flowforge/client.py +380 -0
- flowforge/config.py +199 -0
- flowforge/context.py +154 -0
- flowforge/decorators.py +167 -0
- flowforge/dev/__init__.py +5 -0
- flowforge/dev/server.py +303 -0
- flowforge/exceptions.py +116 -0
- flowforge/execution.py +201 -0
- flowforge/integrations/__init__.py +5 -0
- flowforge/integrations/fastapi.py +171 -0
- flowforge/network.py +142 -0
- flowforge/router.py +155 -0
- flowforge/steps.py +1077 -0
- flowforge/tools.py +282 -0
- flowforge/triggers.py +113 -0
- flowforge/worker.py +144 -0
- flowforge_sdk-0.1.0.dist-info/METADATA +84 -0
- flowforge_sdk-0.1.0.dist-info/RECORD +24 -0
- flowforge_sdk-0.1.0.dist-info/WHEEL +4 -0
flowforge/client.py
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
"""FlowForge client for sending events and managing functions."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any, Callable, Awaitable, TypeVar
|
|
5
|
+
import hashlib
|
|
6
|
+
import hmac
|
|
7
|
+
import json
|
|
8
|
+
import uuid
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from flowforge.context import Context, Event
|
|
13
|
+
from flowforge.triggers import TriggerBuilder
|
|
14
|
+
from flowforge.config import (
|
|
15
|
+
Concurrency,
|
|
16
|
+
RateLimit,
|
|
17
|
+
Throttle,
|
|
18
|
+
Debounce,
|
|
19
|
+
concurrency as make_concurrency,
|
|
20
|
+
rate_limit as make_rate_limit,
|
|
21
|
+
throttle as make_throttle,
|
|
22
|
+
debounce as make_debounce,
|
|
23
|
+
)
|
|
24
|
+
from flowforge.decorators import FlowForgeFunction, function as make_function
|
|
25
|
+
from flowforge.execution import ExecutionEngine, FunctionDefinition
|
|
26
|
+
|
|
27
|
+
T = TypeVar("T")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class FlowForge:
|
|
31
|
+
"""
|
|
32
|
+
FlowForge client for building durable AI workflows.
|
|
33
|
+
|
|
34
|
+
The client provides:
|
|
35
|
+
- Function decorator for defining workflows
|
|
36
|
+
- Event sending for triggering functions
|
|
37
|
+
- Configuration helpers for flow control
|
|
38
|
+
- Framework integrations (FastAPI, Flask, etc.)
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
from flowforge import FlowForge, Context, step
|
|
42
|
+
|
|
43
|
+
flowforge = FlowForge(
|
|
44
|
+
app_id="my-app",
|
|
45
|
+
api_url="http://localhost:8000",
|
|
46
|
+
signing_key="sk_...",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
@flowforge.function(
|
|
50
|
+
id="process-order",
|
|
51
|
+
trigger=flowforge.trigger.event("order/created"),
|
|
52
|
+
)
|
|
53
|
+
async def process_order(ctx: Context) -> dict:
|
|
54
|
+
order = ctx.event.data
|
|
55
|
+
result = await step.run("validate", validate_order, order)
|
|
56
|
+
return {"status": "completed"}
|
|
57
|
+
|
|
58
|
+
# Send an event
|
|
59
|
+
await flowforge.send("order/created", data={"order_id": "123"})
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
app_id: str,
|
|
65
|
+
api_url: str = "http://localhost:8000",
|
|
66
|
+
event_key: str | None = None,
|
|
67
|
+
signing_key: str | None = None,
|
|
68
|
+
) -> None:
|
|
69
|
+
"""
|
|
70
|
+
Initialize the FlowForge client.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
app_id: Unique identifier for your application.
|
|
74
|
+
api_url: URL of the FlowForge API server.
|
|
75
|
+
event_key: API key for sending events.
|
|
76
|
+
signing_key: Key for signing webhook requests.
|
|
77
|
+
"""
|
|
78
|
+
self.app_id = app_id
|
|
79
|
+
self.api_url = api_url.rstrip("/")
|
|
80
|
+
self.event_key = event_key
|
|
81
|
+
self.signing_key = signing_key
|
|
82
|
+
|
|
83
|
+
# Trigger builder
|
|
84
|
+
self.trigger = TriggerBuilder()
|
|
85
|
+
|
|
86
|
+
# Function registry
|
|
87
|
+
self._functions: dict[str, FlowForgeFunction] = {}
|
|
88
|
+
|
|
89
|
+
# Execution engine
|
|
90
|
+
self._engine = ExecutionEngine()
|
|
91
|
+
|
|
92
|
+
# HTTP client
|
|
93
|
+
self._http_client: httpx.AsyncClient | None = None
|
|
94
|
+
|
|
95
|
+
# Configuration helpers
|
|
96
|
+
@staticmethod
|
|
97
|
+
def concurrency(limit: int, key: str | None = None) -> Concurrency:
|
|
98
|
+
"""Create a concurrency configuration."""
|
|
99
|
+
return make_concurrency(limit, key)
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def rate_limit(limit: int, period: str, key: str | None = None) -> RateLimit:
|
|
103
|
+
"""Create a rate limit configuration."""
|
|
104
|
+
return make_rate_limit(limit, period, key)
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
def throttle(
|
|
108
|
+
limit: int, period: str, key: str | None = None, burst: int | None = None
|
|
109
|
+
) -> Throttle:
|
|
110
|
+
"""Create a throttle configuration."""
|
|
111
|
+
return make_throttle(limit, period, key, burst)
|
|
112
|
+
|
|
113
|
+
@staticmethod
|
|
114
|
+
def debounce(period: str, key: str | None = None) -> Debounce:
|
|
115
|
+
"""Create a debounce configuration."""
|
|
116
|
+
return make_debounce(period, key)
|
|
117
|
+
|
|
118
|
+
def function(
|
|
119
|
+
self,
|
|
120
|
+
id: str,
|
|
121
|
+
*,
|
|
122
|
+
trigger: Any = None,
|
|
123
|
+
name: str | None = None,
|
|
124
|
+
retries: int = 3,
|
|
125
|
+
timeout: str = "5m",
|
|
126
|
+
concurrency: Concurrency | None = None,
|
|
127
|
+
rate_limit: RateLimit | None = None,
|
|
128
|
+
throttle: Throttle | None = None,
|
|
129
|
+
debounce: Debounce | None = None,
|
|
130
|
+
cancel_on: list[str] | None = None,
|
|
131
|
+
idempotency_key: str | None = None,
|
|
132
|
+
) -> Callable[[Callable[[Context], Awaitable[T]]], FlowForgeFunction]:
|
|
133
|
+
"""
|
|
134
|
+
Decorator to define a FlowForge function.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
id: Unique identifier for this function.
|
|
138
|
+
trigger: How this function is triggered.
|
|
139
|
+
name: Human-readable name.
|
|
140
|
+
retries: Number of retry attempts.
|
|
141
|
+
timeout: Maximum execution time.
|
|
142
|
+
concurrency: Concurrency configuration.
|
|
143
|
+
rate_limit: Rate limiting configuration.
|
|
144
|
+
throttle: Throttle configuration.
|
|
145
|
+
debounce: Debounce configuration.
|
|
146
|
+
cancel_on: Events that cancel running instances.
|
|
147
|
+
idempotency_key: Expression for deduplication.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Decorator for the function.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
def decorator(fn: Callable[[Context], Awaitable[T]]) -> FlowForgeFunction:
|
|
154
|
+
# Create the wrapped function
|
|
155
|
+
wrapped = make_function(
|
|
156
|
+
id=id,
|
|
157
|
+
trigger=trigger,
|
|
158
|
+
name=name,
|
|
159
|
+
retries=retries,
|
|
160
|
+
timeout=timeout,
|
|
161
|
+
concurrency=concurrency,
|
|
162
|
+
rate_limit=rate_limit,
|
|
163
|
+
throttle=throttle,
|
|
164
|
+
debounce=debounce,
|
|
165
|
+
cancel_on=cancel_on,
|
|
166
|
+
idempotency_key=idempotency_key,
|
|
167
|
+
)(fn)
|
|
168
|
+
|
|
169
|
+
# Register with the client
|
|
170
|
+
self._functions[id] = wrapped
|
|
171
|
+
|
|
172
|
+
# Register with the execution engine
|
|
173
|
+
self._engine.function_registry[id] = FunctionDefinition(
|
|
174
|
+
id=id,
|
|
175
|
+
name=wrapped.name,
|
|
176
|
+
handler=wrapped,
|
|
177
|
+
trigger=trigger,
|
|
178
|
+
config=wrapped.config.to_dict(),
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return wrapped
|
|
182
|
+
|
|
183
|
+
return decorator
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def functions(self) -> list[FlowForgeFunction]:
|
|
187
|
+
"""Get all registered functions."""
|
|
188
|
+
return list(self._functions.values())
|
|
189
|
+
|
|
190
|
+
def get_function(self, function_id: str) -> FlowForgeFunction | None:
|
|
191
|
+
"""Get a function by ID."""
|
|
192
|
+
return self._functions.get(function_id)
|
|
193
|
+
|
|
194
|
+
async def _get_client(self) -> httpx.AsyncClient:
|
|
195
|
+
"""Get or create the HTTP client."""
|
|
196
|
+
if self._http_client is None:
|
|
197
|
+
self._http_client = httpx.AsyncClient(
|
|
198
|
+
base_url=self.api_url,
|
|
199
|
+
timeout=30.0,
|
|
200
|
+
)
|
|
201
|
+
return self._http_client
|
|
202
|
+
|
|
203
|
+
def _sign_request(self, body: bytes) -> str:
|
|
204
|
+
"""Sign a request body with the signing key."""
|
|
205
|
+
if not self.signing_key:
|
|
206
|
+
raise ValueError("Signing key is required for request signing")
|
|
207
|
+
|
|
208
|
+
signature = hmac.new(
|
|
209
|
+
self.signing_key.encode(),
|
|
210
|
+
body,
|
|
211
|
+
hashlib.sha256,
|
|
212
|
+
).hexdigest()
|
|
213
|
+
|
|
214
|
+
return f"sha256={signature}"
|
|
215
|
+
|
|
216
|
+
async def send(
|
|
217
|
+
self,
|
|
218
|
+
name: str,
|
|
219
|
+
data: dict[str, Any],
|
|
220
|
+
id: str | None = None,
|
|
221
|
+
timestamp: datetime | None = None,
|
|
222
|
+
user_id: str | None = None,
|
|
223
|
+
) -> str:
|
|
224
|
+
"""
|
|
225
|
+
Send an event to trigger functions.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
name: Event type name (e.g., "order/created").
|
|
229
|
+
data: Event payload data.
|
|
230
|
+
id: Optional idempotency key (auto-generated if not provided).
|
|
231
|
+
timestamp: Event timestamp (defaults to now).
|
|
232
|
+
user_id: Optional user ID associated with the event.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
The event ID.
|
|
236
|
+
|
|
237
|
+
Example:
|
|
238
|
+
event_id = await flowforge.send(
|
|
239
|
+
"order/created",
|
|
240
|
+
data={"order_id": "123", "total": 99.99},
|
|
241
|
+
)
|
|
242
|
+
"""
|
|
243
|
+
event_id = id or str(uuid.uuid4())
|
|
244
|
+
event_timestamp = timestamp or datetime.utcnow()
|
|
245
|
+
|
|
246
|
+
event = {
|
|
247
|
+
"id": event_id,
|
|
248
|
+
"name": name,
|
|
249
|
+
"data": data,
|
|
250
|
+
"timestamp": event_timestamp.isoformat() + "Z",
|
|
251
|
+
"user_id": user_id,
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
client = await self._get_client()
|
|
255
|
+
|
|
256
|
+
headers = {
|
|
257
|
+
"Content-Type": "application/json",
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if self.event_key:
|
|
261
|
+
headers["X-FlowForge-Event-Key"] = self.event_key
|
|
262
|
+
|
|
263
|
+
body = json.dumps(event).encode()
|
|
264
|
+
|
|
265
|
+
if self.signing_key:
|
|
266
|
+
headers["X-FlowForge-Signature"] = self._sign_request(body)
|
|
267
|
+
|
|
268
|
+
response = await client.post(
|
|
269
|
+
"/api/v1/events",
|
|
270
|
+
content=body,
|
|
271
|
+
headers=headers,
|
|
272
|
+
)
|
|
273
|
+
response.raise_for_status()
|
|
274
|
+
|
|
275
|
+
return event_id
|
|
276
|
+
|
|
277
|
+
async def send_many(self, events: list[dict[str, Any] | Event]) -> list[str]:
|
|
278
|
+
"""
|
|
279
|
+
Send multiple events in a batch.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
events: List of events to send.
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
List of event IDs.
|
|
286
|
+
|
|
287
|
+
Example:
|
|
288
|
+
event_ids = await flowforge.send_many([
|
|
289
|
+
{"name": "user/signup", "data": {"user_id": "1"}},
|
|
290
|
+
{"name": "user/signup", "data": {"user_id": "2"}},
|
|
291
|
+
])
|
|
292
|
+
"""
|
|
293
|
+
event_ids = []
|
|
294
|
+
|
|
295
|
+
for event in events:
|
|
296
|
+
if isinstance(event, Event):
|
|
297
|
+
event_id = await self.send(
|
|
298
|
+
name=event.name,
|
|
299
|
+
data=event.data,
|
|
300
|
+
id=event.id,
|
|
301
|
+
timestamp=event.timestamp,
|
|
302
|
+
user_id=event.user_id,
|
|
303
|
+
)
|
|
304
|
+
else:
|
|
305
|
+
event_id = await self.send(
|
|
306
|
+
name=event["name"],
|
|
307
|
+
data=event.get("data", {}),
|
|
308
|
+
id=event.get("id"),
|
|
309
|
+
timestamp=event.get("timestamp"),
|
|
310
|
+
user_id=event.get("user_id"),
|
|
311
|
+
)
|
|
312
|
+
event_ids.append(event_id)
|
|
313
|
+
|
|
314
|
+
return event_ids
|
|
315
|
+
|
|
316
|
+
def serve(
|
|
317
|
+
self,
|
|
318
|
+
functions: list[FlowForgeFunction] | None = None,
|
|
319
|
+
host: str = "0.0.0.0",
|
|
320
|
+
port: int = 8080,
|
|
321
|
+
) -> None:
|
|
322
|
+
"""
|
|
323
|
+
Start a local development server.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
functions: Functions to serve (uses registered functions if not provided).
|
|
327
|
+
host: Host to bind to.
|
|
328
|
+
port: Port to listen on.
|
|
329
|
+
"""
|
|
330
|
+
from flowforge.dev.server import run_dev_server
|
|
331
|
+
|
|
332
|
+
fns = functions or list(self._functions.values())
|
|
333
|
+
run_dev_server(self, fns, host=host, port=port)
|
|
334
|
+
|
|
335
|
+
def work(
|
|
336
|
+
self,
|
|
337
|
+
functions: list[FlowForgeFunction] | None = None,
|
|
338
|
+
server_url: str | None = None,
|
|
339
|
+
host: str = "0.0.0.0",
|
|
340
|
+
port: int = 8080,
|
|
341
|
+
worker_url: str | None = None,
|
|
342
|
+
) -> None:
|
|
343
|
+
"""
|
|
344
|
+
Start as a worker connected to the central FlowForge server.
|
|
345
|
+
|
|
346
|
+
This mode:
|
|
347
|
+
1. Registers functions with the central server
|
|
348
|
+
2. Exposes an /invoke endpoint for the server to call
|
|
349
|
+
3. Handles function execution
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
functions: Functions to serve (uses registered functions if not provided).
|
|
353
|
+
server_url: URL of the central FlowForge server.
|
|
354
|
+
host: Host to bind to.
|
|
355
|
+
port: Port to listen on.
|
|
356
|
+
worker_url: URL where this worker can be reached by the server.
|
|
357
|
+
"""
|
|
358
|
+
from flowforge.worker import run_worker
|
|
359
|
+
|
|
360
|
+
fns = functions or list(self._functions.values())
|
|
361
|
+
run_worker(
|
|
362
|
+
self,
|
|
363
|
+
fns,
|
|
364
|
+
server_url=server_url,
|
|
365
|
+
host=host,
|
|
366
|
+
port=port,
|
|
367
|
+
worker_url=worker_url,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
async def close(self) -> None:
|
|
371
|
+
"""Close the HTTP client."""
|
|
372
|
+
if self._http_client:
|
|
373
|
+
await self._http_client.aclose()
|
|
374
|
+
self._http_client = None
|
|
375
|
+
|
|
376
|
+
async def __aenter__(self) -> "FlowForge":
|
|
377
|
+
return self
|
|
378
|
+
|
|
379
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
380
|
+
await self.close()
|
flowforge/config.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Configuration classes for FlowForge functions."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class Concurrency:
|
|
9
|
+
"""
|
|
10
|
+
Concurrency configuration for a function.
|
|
11
|
+
|
|
12
|
+
Limits how many instances of a function can run simultaneously.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
limit: int
|
|
16
|
+
"""Maximum number of concurrent executions."""
|
|
17
|
+
|
|
18
|
+
key: str | None = None
|
|
19
|
+
"""
|
|
20
|
+
Optional key expression for per-key concurrency limiting.
|
|
21
|
+
Example: "event.data.user_id" limits concurrency per user.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def to_dict(self) -> dict[str, Any]:
|
|
25
|
+
return {"limit": self.limit, "key": self.key}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class RateLimit:
|
|
30
|
+
"""
|
|
31
|
+
Rate limiting configuration for a function.
|
|
32
|
+
|
|
33
|
+
Limits the rate of function invocations over a time period.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
limit: int
|
|
37
|
+
"""Maximum number of invocations."""
|
|
38
|
+
|
|
39
|
+
period: str
|
|
40
|
+
"""Time period (e.g., "1m", "1h", "1d")."""
|
|
41
|
+
|
|
42
|
+
key: str | None = None
|
|
43
|
+
"""Optional key expression for per-key rate limiting."""
|
|
44
|
+
|
|
45
|
+
def to_dict(self) -> dict[str, Any]:
|
|
46
|
+
return {"limit": self.limit, "period": self.period, "key": self.key}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class Throttle:
|
|
51
|
+
"""
|
|
52
|
+
Throttle configuration for a function.
|
|
53
|
+
|
|
54
|
+
Ensures a minimum time gap between invocations.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
limit: int
|
|
58
|
+
"""Maximum invocations in the period."""
|
|
59
|
+
|
|
60
|
+
period: str
|
|
61
|
+
"""Time period (e.g., "1s", "1m")."""
|
|
62
|
+
|
|
63
|
+
key: str | None = None
|
|
64
|
+
"""Optional key expression for per-key throttling."""
|
|
65
|
+
|
|
66
|
+
burst: int | None = None
|
|
67
|
+
"""Optional burst allowance."""
|
|
68
|
+
|
|
69
|
+
def to_dict(self) -> dict[str, Any]:
|
|
70
|
+
return {
|
|
71
|
+
"limit": self.limit,
|
|
72
|
+
"period": self.period,
|
|
73
|
+
"key": self.key,
|
|
74
|
+
"burst": self.burst,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class Debounce:
|
|
80
|
+
"""
|
|
81
|
+
Debounce configuration for a function.
|
|
82
|
+
|
|
83
|
+
Delays execution until no new events arrive for a period.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
period: str
|
|
87
|
+
"""Time to wait for more events before executing."""
|
|
88
|
+
|
|
89
|
+
key: str | None = None
|
|
90
|
+
"""Optional key expression for per-key debouncing."""
|
|
91
|
+
|
|
92
|
+
def to_dict(self) -> dict[str, Any]:
|
|
93
|
+
return {"period": self.period, "key": self.key}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class Priority:
|
|
98
|
+
"""
|
|
99
|
+
Priority configuration for a function.
|
|
100
|
+
|
|
101
|
+
Controls execution order when jobs are queued.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
run: str | None = None
|
|
105
|
+
"""Expression to determine run priority (e.g., "event.data.priority")."""
|
|
106
|
+
|
|
107
|
+
def to_dict(self) -> dict[str, Any]:
|
|
108
|
+
return {"run": self.run}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass
|
|
112
|
+
class FunctionConfig:
|
|
113
|
+
"""Complete configuration for a FlowForge function."""
|
|
114
|
+
|
|
115
|
+
id: str
|
|
116
|
+
"""Unique identifier for the function."""
|
|
117
|
+
|
|
118
|
+
name: str | None = None
|
|
119
|
+
"""Human-readable name (defaults to function name)."""
|
|
120
|
+
|
|
121
|
+
retries: int = 3
|
|
122
|
+
"""Number of retry attempts on failure."""
|
|
123
|
+
|
|
124
|
+
timeout: str = "5m"
|
|
125
|
+
"""Maximum execution time (e.g., "5m", "1h")."""
|
|
126
|
+
|
|
127
|
+
concurrency: Concurrency | None = None
|
|
128
|
+
"""Concurrency limiting configuration."""
|
|
129
|
+
|
|
130
|
+
rate_limit: RateLimit | None = None
|
|
131
|
+
"""Rate limiting configuration."""
|
|
132
|
+
|
|
133
|
+
throttle: Throttle | None = None
|
|
134
|
+
"""Throttle configuration."""
|
|
135
|
+
|
|
136
|
+
debounce: Debounce | None = None
|
|
137
|
+
"""Debounce configuration."""
|
|
138
|
+
|
|
139
|
+
priority: Priority | None = None
|
|
140
|
+
"""Priority configuration."""
|
|
141
|
+
|
|
142
|
+
cancel_on: list[str] = field(default_factory=list)
|
|
143
|
+
"""Events that cancel running instances."""
|
|
144
|
+
|
|
145
|
+
idempotency_key: str | None = None
|
|
146
|
+
"""Expression for idempotency (e.g., "event.data.order_id")."""
|
|
147
|
+
|
|
148
|
+
def to_dict(self) -> dict[str, Any]:
|
|
149
|
+
config: dict[str, Any] = {
|
|
150
|
+
"id": self.id,
|
|
151
|
+
"name": self.name,
|
|
152
|
+
"retries": self.retries,
|
|
153
|
+
"timeout": self.timeout,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if self.concurrency:
|
|
157
|
+
config["concurrency"] = self.concurrency.to_dict()
|
|
158
|
+
if self.rate_limit:
|
|
159
|
+
config["rate_limit"] = self.rate_limit.to_dict()
|
|
160
|
+
if self.throttle:
|
|
161
|
+
config["throttle"] = self.throttle.to_dict()
|
|
162
|
+
if self.debounce:
|
|
163
|
+
config["debounce"] = self.debounce.to_dict()
|
|
164
|
+
if self.priority:
|
|
165
|
+
config["priority"] = self.priority.to_dict()
|
|
166
|
+
if self.cancel_on:
|
|
167
|
+
config["cancel_on"] = self.cancel_on
|
|
168
|
+
if self.idempotency_key:
|
|
169
|
+
config["idempotency_key"] = self.idempotency_key
|
|
170
|
+
|
|
171
|
+
return config
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# Convenience functions for creating configurations
|
|
175
|
+
def concurrency(limit: int, key: str | None = None) -> Concurrency:
|
|
176
|
+
"""Create a concurrency configuration."""
|
|
177
|
+
return Concurrency(limit=limit, key=key)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def rate_limit(limit: int, period: str, key: str | None = None) -> RateLimit:
|
|
181
|
+
"""Create a rate limit configuration."""
|
|
182
|
+
return RateLimit(limit=limit, period=period, key=key)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def throttle(
|
|
186
|
+
limit: int, period: str, key: str | None = None, burst: int | None = None
|
|
187
|
+
) -> Throttle:
|
|
188
|
+
"""Create a throttle configuration."""
|
|
189
|
+
return Throttle(limit=limit, period=period, key=key, burst=burst)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def debounce(period: str, key: str | None = None) -> Debounce:
|
|
193
|
+
"""Create a debounce configuration."""
|
|
194
|
+
return Debounce(period=period, key=key)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def priority(run: str | None = None) -> Priority:
|
|
198
|
+
"""Create a priority configuration."""
|
|
199
|
+
return Priority(run=run)
|