avtomatika 1.0b6__py3-none-any.whl → 1.0b8__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,96 @@
1
+ from asyncio import CancelledError, Queue, QueueFull, create_task, sleep
2
+ from contextlib import suppress
3
+ from dataclasses import asdict, dataclass
4
+ from logging import getLogger
5
+ from typing import Any
6
+
7
+ from aiohttp import ClientSession, ClientTimeout
8
+
9
+ logger = getLogger(__name__)
10
+
11
+
12
+ @dataclass
13
+ class WebhookPayload:
14
+ event: str # "job_finished", "job_failed", "job_quarantined"
15
+ job_id: str
16
+ status: str
17
+ result: dict[str, Any] | None = None
18
+ error: str | None = None
19
+
20
+
21
+ class WebhookSender:
22
+ def __init__(self, session: ClientSession):
23
+ self.session = session
24
+ self.timeout = ClientTimeout(total=10)
25
+ self.max_retries = 3
26
+ self._queue: Queue[tuple[str, WebhookPayload]] = Queue(maxsize=1000)
27
+ self._worker_task = None
28
+
29
+ def start(self) -> None:
30
+ if not self._worker_task:
31
+ self._worker_task = create_task(self._worker())
32
+ logger.info("WebhookSender background worker started.")
33
+
34
+ async def stop(self) -> None:
35
+ if self._worker_task:
36
+ self._worker_task.cancel()
37
+ with suppress(CancelledError):
38
+ await self._worker_task
39
+ self._worker_task = None
40
+ logger.info("WebhookSender background worker stopped.")
41
+
42
+ async def send(self, url: str, payload: WebhookPayload) -> None:
43
+ """
44
+ Queues a webhook to be sent. Non-blocking.
45
+ Drops the message if the queue is full to prevent backpressure.
46
+ """
47
+ try:
48
+ self._queue.put_nowait((url, payload))
49
+ except QueueFull:
50
+ logger.error(
51
+ f"Webhook queue is full! Dropping webhook for job {payload.job_id} to {url}. "
52
+ "Consider increasing queue size or checking external service latency."
53
+ )
54
+
55
+ async def _worker(self) -> None:
56
+ while True:
57
+ try:
58
+ url, payload = await self._queue.get()
59
+ try:
60
+ await self._send_single(url, payload)
61
+ except Exception as e:
62
+ logger.exception(f"Unexpected error in webhook worker: {e}")
63
+ finally:
64
+ self._queue.task_done()
65
+ except CancelledError:
66
+ break
67
+
68
+ async def _send_single(self, url: str, payload: WebhookPayload) -> bool:
69
+ """
70
+ Sends a webhook payload to the specified URL with retries.
71
+ Returns True if successful, False otherwise.
72
+ """
73
+ data = asdict(payload)
74
+ for attempt in range(1, self.max_retries + 1):
75
+ try:
76
+ async with self.session.post(url, json=data, timeout=self.timeout) as response:
77
+ if 200 <= response.status < 300:
78
+ logger.info(f"Webhook sent successfully to {url} for job {payload.job_id}")
79
+ return True
80
+ else:
81
+ logger.warning(
82
+ f"Webhook failed for job {payload.job_id} to {url}. "
83
+ f"Status: {response.status}. Attempt {attempt}/{self.max_retries}"
84
+ )
85
+ except Exception as e:
86
+ logger.warning(
87
+ f"Error sending webhook for job {payload.job_id} to {url}: {e}. "
88
+ f"Attempt {attempt}/{self.max_retries}"
89
+ )
90
+
91
+ # Exponential backoff
92
+ if attempt < self.max_retries:
93
+ await sleep(2**attempt)
94
+
95
+ logger.error(f"Failed to send webhook for job {payload.job_id} to {url} after {self.max_retries} attempts.")
96
+ return False
avtomatika/watcher.py CHANGED
@@ -26,48 +26,43 @@ class Watcher:
26
26
  self._running = True
27
27
  while self._running:
28
28
  try:
29
- await sleep(self.watch_interval_seconds)
30
-
31
29
  # Attempt to acquire distributed lock
32
- # We set TTL slightly longer than the expected execution time,
33
- # but shorter than the interval if possible.
34
- # Actually, a fixed TTL like 60s is fine as long as we release it.
35
- if not await self.storage.acquire_lock("global_watcher_lock", self._instance_id, 60):
36
- logger.debug("Watcher lock held by another instance. Skipping check.")
37
- continue
30
+ # We set TTL slightly longer than the expected execution time (60s)
31
+ if await self.storage.acquire_lock("global_watcher_lock", self._instance_id, 60):
32
+ try:
33
+ logger.debug("Watcher running check for timed out jobs...")
34
+ timed_out_job_ids = await self.storage.get_timed_out_jobs(limit=100)
38
35
 
39
- try:
40
- logger.info("Watcher running check for timed out jobs...")
41
- timed_out_job_ids = await self.storage.get_timed_out_jobs()
36
+ for job_id in timed_out_job_ids:
37
+ logger.warning(f"Job {job_id} timed out. Moving to failed state.")
38
+ try:
39
+ # Get the latest version to avoid overwriting
40
+ job_state = await self.storage.get_job_state(job_id)
41
+ if job_state and job_state["status"] == "waiting_for_worker":
42
+ job_state["status"] = "failed"
43
+ job_state["error_message"] = "Worker task timed out."
44
+ await self.storage.save_job_state(job_id, job_state)
42
45
 
43
- for job_id in timed_out_job_ids:
44
- logger.warning(f"Job {job_id} timed out. Moving to failed state.")
45
- try:
46
- # Get the latest version to avoid overwriting
47
- job_state = await self.storage.get_job_state(job_id)
48
- if job_state and job_state["status"] == "waiting_for_worker":
49
- job_state["status"] = "failed"
50
- job_state["error_message"] = "Worker task timed out."
51
- await self.storage.save_job_state(job_id, job_state)
46
+ # Increment the metric
47
+ from . import metrics
52
48
 
53
- # Increment the metric
54
- from . import metrics
55
-
56
- metrics.jobs_failed_total.inc(
57
- {
58
- metrics.LABEL_BLUEPRINT: job_state.get(
59
- "blueprint_name",
60
- "unknown",
61
- ),
62
- },
49
+ metrics.jobs_failed_total.inc(
50
+ {
51
+ metrics.LABEL_BLUEPRINT: job_state.get(
52
+ "blueprint_name",
53
+ "unknown",
54
+ ),
55
+ },
56
+ )
57
+ except Exception:
58
+ logger.exception(
59
+ f"Failed to update state for timed out job {job_id}",
63
60
  )
64
- except Exception:
65
- logger.exception(
66
- f"Failed to update state for timed out job {job_id}",
67
- )
68
- finally:
69
- # Always release the lock so we (or others) can run next time
70
- await self.storage.release_lock("global_watcher_lock", self._instance_id)
61
+ finally:
62
+ # Always release the lock so we (or others) can run next time
63
+ await self.storage.release_lock("global_watcher_lock", self._instance_id)
64
+ else:
65
+ logger.debug("Watcher lock held by another instance. Skipping check.")
71
66
 
72
67
  except CancelledError:
73
68
  logger.info("Watcher received cancellation request.")
@@ -75,7 +70,8 @@ class Watcher:
75
70
  except Exception:
76
71
  logger.exception("Error in Watcher main loop.")
77
72
 
78
- logger.info("Watcher stopped.")
73
+ # Sleep at the end of iteration
74
+ await sleep(self.watch_interval_seconds)
79
75
 
80
76
  def stop(self):
81
77
  """Stops the watcher."""
avtomatika/ws_manager.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from asyncio import Lock
2
2
  from logging import getLogger
3
+ from typing import Any
3
4
 
4
5
  from aiohttp import web
5
6
 
@@ -9,11 +10,11 @@ logger = getLogger(__name__)
9
10
  class WebSocketManager:
10
11
  """Manages active WebSocket connections from workers."""
11
12
 
12
- def __init__(self):
13
+ def __init__(self) -> None:
13
14
  self._connections: dict[str, web.WebSocketResponse] = {}
14
15
  self._lock = Lock()
15
16
 
16
- async def register(self, worker_id: str, ws: web.WebSocketResponse):
17
+ async def register(self, worker_id: str, ws: web.WebSocketResponse) -> None:
17
18
  """Registers a new WebSocket connection for a worker."""
18
19
  async with self._lock:
19
20
  if worker_id in self._connections:
@@ -22,14 +23,14 @@ class WebSocketManager:
22
23
  self._connections[worker_id] = ws
23
24
  logger.info(f"WebSocket connection registered for worker {worker_id}.")
24
25
 
25
- async def unregister(self, worker_id: str):
26
+ async def unregister(self, worker_id: str) -> None:
26
27
  """Unregisters a WebSocket connection."""
27
28
  async with self._lock:
28
29
  if worker_id in self._connections:
29
30
  del self._connections[worker_id]
30
31
  logger.info(f"WebSocket connection for worker {worker_id} unregistered.")
31
32
 
32
- async def send_command(self, worker_id: str, command: dict):
33
+ async def send_command(self, worker_id: str, command: dict[str, Any]) -> bool:
33
34
  """Sends a JSON command to a specific worker."""
34
35
  async with self._lock:
35
36
  connection = self._connections.get(worker_id)
@@ -46,7 +47,7 @@ class WebSocketManager:
46
47
  return False
47
48
 
48
49
  @staticmethod
49
- async def handle_message(worker_id: str, message: dict):
50
+ async def handle_message(worker_id: str, message: dict[str, Any]) -> None:
50
51
  """Handles an incoming message from a worker."""
51
52
  event_type = message.get("event")
52
53
  if event_type == "progress_update":
@@ -59,7 +60,7 @@ class WebSocketManager:
59
60
  else:
60
61
  logger.debug(f"Received unhandled event from worker {worker_id}: {event_type}")
61
62
 
62
- async def close_all(self):
63
+ async def close_all(self) -> None:
63
64
  """Closes all active WebSocket connections."""
64
65
  async with self._lock:
65
66
  logger.info(f"Closing {len(self._connections)} active WebSocket connections...")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: avtomatika
3
- Version: 1.0b6
3
+ Version: 1.0b8
4
4
  Summary: A state-machine based orchestrator for long-running AI and other jobs.
5
5
  Project-URL: Homepage, https://github.com/avtomatika-ai/avtomatika
6
6
  Project-URL: Bug Tracker, https://github.com/avtomatika-ai/avtomatika/issues
@@ -12,7 +12,6 @@ Requires-Python: >=3.11
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
14
  Requires-Dist: aiohttp~=3.12
15
- Requires-Dist: aiocache~=0.12
16
15
  Requires-Dist: python-json-logger~=4.0
17
16
  Requires-Dist: graphviz~=0.21
18
17
  Requires-Dist: zstandard~=0.24
@@ -21,6 +20,9 @@ Requires-Dist: msgpack~=1.1
21
20
  Requires-Dist: orjson~=3.11
22
21
  Provides-Extra: redis
23
22
  Requires-Dist: redis~=7.1; extra == "redis"
23
+ Provides-Extra: s3
24
+ Requires-Dist: obstore>=0.2; extra == "s3"
25
+ Requires-Dist: aiofiles~=23.2; extra == "s3"
24
26
  Provides-Extra: history
25
27
  Requires-Dist: aiosqlite~=0.22; extra == "history"
26
28
  Requires-Dist: asyncpg~=0.30; extra == "history"
@@ -38,10 +40,13 @@ Requires-Dist: pytest-mock~=3.14; extra == "test"
38
40
  Requires-Dist: aioresponses~=0.7; extra == "test"
39
41
  Requires-Dist: backports.zstd~=1.2; extra == "test"
40
42
  Requires-Dist: opentelemetry-instrumentation-aiohttp-client; extra == "test"
43
+ Requires-Dist: obstore>=0.2; extra == "test"
44
+ Requires-Dist: aiofiles~=23.2; extra == "test"
41
45
  Provides-Extra: all
42
46
  Requires-Dist: avtomatika[redis]; extra == "all"
43
47
  Requires-Dist: avtomatika[history]; extra == "all"
44
48
  Requires-Dist: avtomatika[telemetry]; extra == "all"
49
+ Requires-Dist: avtomatika[s3]; extra == "all"
45
50
  Dynamic: license-file
46
51
 
47
52
  # Avtomatika Orchestrator
@@ -61,6 +66,8 @@ This document serves as a comprehensive guide for developers looking to build pi
61
66
  - [Parallel Execution and Aggregation (Fan-out/Fan-in)](#parallel-execution-and-aggregation-fan-outfan-in)
62
67
  - [Dependency Injection (DataStore)](#dependency-injection-datastore)
63
68
  - [Native Scheduler](#native-scheduler)
69
+ - [S3 Payload Offloading](#s3-payload-offloading)
70
+ - [Webhook Notifications](#webhook-notifications)
64
71
  - [Production Configuration](#production-configuration)
65
72
  - [Fault Tolerance](#fault-tolerance)
66
73
  - [Storage Backend](#storage-backend)
@@ -107,6 +114,11 @@ Avtomatika is part of a larger ecosystem:
107
114
  pip install "avtomatika[telemetry]"
108
115
  ```
109
116
 
117
+ * **Install with S3 support (Payload Offloading):**
118
+ ```bash
119
+ pip install "avtomatika[s3]"
120
+ ```
121
+
110
122
  * **Install all dependencies, including for testing:**
111
123
  ```bash
112
124
  pip install "avtomatika[all,test]"
@@ -157,7 +169,13 @@ async def end_handler(context):
157
169
  engine = OrchestratorEngine(storage, config)
158
170
  engine.register_blueprint(my_blueprint)
159
171
 
160
- # 4. Define the main entrypoint to run the server
172
+ # 4. Accessing Components (Optional)
173
+ # You can access the internal aiohttp app and core components using AppKeys
174
+ # from avtomatika.app_keys import ENGINE_KEY, DISPATCHER_KEY
175
+ # app = engine.app
176
+ # dispatcher = app[DISPATCHER_KEY]
177
+
178
+ # 5. Define the main entrypoint to run the server
161
179
  async def main():
162
180
  await engine.start()
163
181
 
@@ -244,6 +262,19 @@ async def publish_handler_old_style(context):
244
262
  print(f"Job {context.job_id}: Publishing video at {output_path} ({duration}s).")
245
263
  context.actions.transition_to("complete")
246
264
  ```
265
+ ## Key Concepts: JobContext and Actions
266
+
267
+ ### High Performance Architecture
268
+
269
+ Avtomatika is engineered for high-load environments with thousands of concurrent workers.
270
+
271
+ * **O(1) Dispatcher**: Uses advanced Redis Set intersections to find suitable workers instantly, regardless of the cluster size. No O(N) scanning.
272
+ * **Non-Blocking I/O**:
273
+ * **Webhooks**: Sent via a bounded background queue to prevent backpressure.
274
+ * **History Logging**: Writes to SQL databases are buffered and asynchronous, ensuring the main execution loop never blocks.
275
+ * **Redis Streams**: Uses blocking reads to eliminate busy-waiting and reduce CPU usage.
276
+ * **Memory Safety**: S3 file transfers use streaming to handle multi-gigabyte files with constant, low RAM usage.
277
+
247
278
  ## Blueprint Cookbook: Key Features
248
279
 
249
280
  ### 1. Conditional Transitions (`.when()`)
@@ -355,6 +386,63 @@ blueprint = "backup_flow"
355
386
  daily_at = "02:00"
356
387
  ```
357
388
 
389
+ ### 6. Webhook Notifications
390
+
391
+ The orchestrator can send asynchronous notifications to an external system when a job completes, fails, or is quarantined. This eliminates the need for clients to constantly poll the API for status updates.
392
+
393
+ ### 7. S3 Payload Offloading
394
+
395
+ Orchestrator provides first-class support for handling large files via S3-compatible storage, powered by the high-performance `obstore` library (Rust bindings).
396
+
397
+ * **Memory Safe (Streaming)**: Uses streaming for uploads and downloads, allowing processing of files larger than available RAM without OOM errors.
398
+ * **Managed Mode**: The Orchestrator manages file lifecycle (automatic cleanup of S3 objects and local temporary files on job completion).
399
+ * **Dependency Injection**: Use the `task_files` argument in your handlers to easily read/write data.
400
+ * **Directory Support**: Supports recursive download and upload of entire directories.
401
+
402
+ ```python
403
+ @bp.handler_for("process_data")
404
+ async def process_data(task_files, actions):
405
+ # Streaming download of a large file
406
+ local_path = await task_files.download("large_dataset.csv")
407
+
408
+ # ... process data ...
409
+
410
+ # Upload results
411
+ await task_files.write_json("results.json", {"status": "done"})
412
+
413
+ actions.transition_to("finished")
414
+ ```
415
+
416
+ ## Production Configuration
417
+ * **Events:**
418
+ * `job_finished`: The job reached a final success state.
419
+ * `job_failed`: The job failed (e.g., due to an error or invalid input).
420
+ * `job_quarantined`: The job was moved to quarantine after repeated failures.
421
+
422
+ **Example Request:**
423
+ ```json
424
+ POST /api/v1/jobs/my_flow
425
+ {
426
+ "initial_data": {
427
+ "video_url": "..."
428
+ },
429
+ "webhook_url": "https://my-app.com/webhooks/avtomatika"
430
+ }
431
+ ```
432
+
433
+ **Example Webhook Payload:**
434
+ ```json
435
+ {
436
+ "event": "job_finished",
437
+ "job_id": "123e4567-e89b-12d3-a456-426614174000",
438
+ "status": "finished",
439
+ "result": {
440
+ "output_path": "/videos/result.mp4"
441
+ },
442
+ "error": null
443
+ }
444
+ ```
445
+
358
446
  ## Production Configuration
359
447
 
360
448
  The orchestrator's behavior can be configured through environment variables. Additionally, any configuration parameter loaded from environment variables can be programmatically overridden in your application code after the `Config` object has been initialized. This provides flexibility for different deployment and testing scenarios.
@@ -0,0 +1,46 @@
1
+ avtomatika/__init__.py,sha256=D5r3L-H06uxsY_wgfh7u9YR29QvZMer1BlvzjW9Umfo,701
2
+ avtomatika/api.html,sha256=RLx-D1uFCSAXIf_2WgFlSTWrWPcmonNYM-9oNanKXBg,32835
3
+ avtomatika/app_keys.py,sha256=XL9Q1Ef5JYa_JG8LM3jzb3cOQjnKaM6ypwqXWnUKeDU,1212
4
+ avtomatika/blueprint.py,sha256=AJ8hZkzvqkdu9aPmlJeFzuSj57L6Xm41KS9kBMGB4Vg,11827
5
+ avtomatika/client_config_loader.py,sha256=zVVHZlxSqZUaNpZ4zoU0T1CFYXdxy-3vKSmPcaFuHSY,2772
6
+ avtomatika/compression.py,sha256=bhA1kw4YrCR3I3kdquZSY0fAzCrRrjtz55uepzLUDKI,2498
7
+ avtomatika/config.py,sha256=zqaZrt44Zd_ppvWPDkSth1nJydhAsK0jQLDo0QIW77A,3103
8
+ avtomatika/constants.py,sha256=WL58Nh-EY6baM9Ur_tR9merwPRGb41_klUG2V-yUUaA,963
9
+ avtomatika/context.py,sha256=T6Ux4Fb1DwWRGTpMNeukM51MQDQbGk2HS6Cwpc0dc1s,4248
10
+ avtomatika/data_types.py,sha256=nm9pLhLLx8fngjvAE8d3nQZkD8Y3Q28RHch9kjYAG2Y,1447
11
+ avtomatika/datastore.py,sha256=gJjhZ5kxjF8pmbbPQb_qu3HPUpfy2c6T75KZ-smb_zg,545
12
+ avtomatika/dispatcher.py,sha256=CKqniknJZe7gjY7QmGlODJc5X8B6qWjtr2UxRivUbuY,8603
13
+ avtomatika/engine.py,sha256=VUw5s65Q403a1JgkRtF1KeSiVx1W6MFmukxZpYN-f00,16175
14
+ avtomatika/executor.py,sha256=K7RGGxWz_4n6q0m8tW0oNtUJm90b-4O2U9lTB_yLX-k,24461
15
+ avtomatika/health_checker.py,sha256=jXYSH4BPeZ4LCxSZV4uXM4BZhGJYgpoAOWQXE8yojLo,2078
16
+ avtomatika/logging_config.py,sha256=Zb6f9Nri9WVWhlpuBg6Lpi5SWRLGIUmS8Dc3xD1Gg0g,2993
17
+ avtomatika/metrics.py,sha256=tiksK1fFSOMlz8zFu6GT19JTduvxMTNlLu0QFrTHoQI,1866
18
+ avtomatika/py.typed,sha256=CT_L7gw2MLcQY-X0vs-xB5Vr0wzvGo7GuQYPI_qwJE8,65
19
+ avtomatika/quota.py,sha256=DNcaL6k0J1REeP8sVqbY9FprY_3BSr2SxM2Vf4mEqdw,1612
20
+ avtomatika/ratelimit.py,sha256=hFGW5oN9G6_W_jnHmopXW8bRjjzlvanY19MLghsNLE8,1306
21
+ avtomatika/reputation.py,sha256=pK-x9FrPN2Oc2gtPa1AZJHlhvkd7xlRe4orxM2auJJc,3979
22
+ avtomatika/s3.py,sha256=d-zyqdS0dV6K_92BbOH4Lo0Um0AvrMxfSFoD5QFPBSo,11941
23
+ avtomatika/scheduler.py,sha256=F5Kv5Rx34nDd0mE5jxjwpjRg8duDZBEr91N5Y6CNR24,4231
24
+ avtomatika/scheduler_config_loader.py,sha256=F6mLM8yPRgG4bMHV_WnXX7UOrXD8fCXJT30bbEuQ2mk,1311
25
+ avtomatika/security.py,sha256=kkU68YmLWq1ClMUdEW98pS9WsEwHinHoZcdMoPm63Uk,4417
26
+ avtomatika/telemetry.py,sha256=ZBt1_xJ36PzDSz-zdCXeNp58NiezUgbqvMctTG25PT0,2352
27
+ avtomatika/watcher.py,sha256=-oqyUKq4WYtFYe0PjfDkAjjTUxpyN79JTnOzwlg-Z84,3467
28
+ avtomatika/worker_config_loader.py,sha256=n0j8gfuJDacWONr8744RsHTCWpc_1ZTRMC-rJZh6P6A,2249
29
+ avtomatika/ws_manager.py,sha256=fjIGWZF_L5j5QEpTdNT2Qz4N5ut9pjKlgEKI0sY2GOc,3148
30
+ avtomatika/api/handlers.py,sha256=MO6QkbT001jj4latUXGT0hGOmQf_6TkRkmwx19OcXeQ,22176
31
+ avtomatika/api/routes.py,sha256=vSwj2jJlmftZrjrctt-mNYLF23CfCtlUfaMoZzNOqCk,4895
32
+ avtomatika/history/base.py,sha256=RsCvCkHK1teHjXSk9ZHVEtpQlIjz8kWsfKYHVnapf6c,3848
33
+ avtomatika/history/noop.py,sha256=hLzt0RblsrKUtoyQNauOni6jCi-IYCWEPsiR0vh7tho,1226
34
+ avtomatika/history/postgres.py,sha256=T0XpDurnh48pPI-2JhB285GdNIexNkCSu8ExhLJzcxc,9538
35
+ avtomatika/history/sqlite.py,sha256=txWax9RVzBQzIZuU-SjHnEXEzBmGzIjqzoVsK2oyiAQ,9252
36
+ avtomatika/storage/__init__.py,sha256=mGRj_40dWZ7R7uYbqC6gCsUWCKHAbZz4ZVIhYg5dT_E,262
37
+ avtomatika/storage/base.py,sha256=l6ppbvbIshb5cD0yByDAeCSePBWp05iuaovbi4gaWww,12939
38
+ avtomatika/storage/memory.py,sha256=cZRVHqXRn3Gm06jJirBOk2IgB-ocqaJQSmzUEsCmK-M,14009
39
+ avtomatika/storage/redis.py,sha256=WrUbDq1wC9fpFjMM0IqkbYAvmwm9-s3DmEFo8sO18FA,18664
40
+ avtomatika/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
+ avtomatika/utils/webhook_sender.py,sha256=4Ud2Tz18yV4kzfGQyDO2by8s2EIEwHkv1vtIUis9kVQ,3606
42
+ avtomatika-1.0b8.dist-info/licenses/LICENSE,sha256=tqCjw9Y1vbU-hLcWi__7wQstLbt2T1XWPdbQYqCxuWY,1072
43
+ avtomatika-1.0b8.dist-info/METADATA,sha256=AV7TG3pNVxvHyj7eyS5OTo7BXrBigb4mFCfJvtZqzTM,26382
44
+ avtomatika-1.0b8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
45
+ avtomatika-1.0b8.dist-info/top_level.txt,sha256=gLDWhA_wxHj0I6fG5X8vw9fE0HSN4hTE2dEJzeVS2x8,11
46
+ avtomatika-1.0b8.dist-info/RECORD,,
@@ -1,40 +0,0 @@
1
- avtomatika/__init__.py,sha256=D5r3L-H06uxsY_wgfh7u9YR29QvZMer1BlvzjW9Umfo,701
2
- avtomatika/api.html,sha256=RLx-D1uFCSAXIf_2WgFlSTWrWPcmonNYM-9oNanKXBg,32835
3
- avtomatika/blueprint.py,sha256=jU1Un9yiIUbZ9I7k20XyAhnMbXyrnkC0AakMX3GbK6k,9207
4
- avtomatika/client_config_loader.py,sha256=zVVHZlxSqZUaNpZ4zoU0T1CFYXdxy-3vKSmPcaFuHSY,2772
5
- avtomatika/compression.py,sha256=bhA1kw4YrCR3I3kdquZSY0fAzCrRrjtz55uepzLUDKI,2498
6
- avtomatika/config.py,sha256=Tc-vpaQS11i_JTa1pQjGuQD3R5Kj9sIf6gGJGjItBBo,2487
7
- avtomatika/constants.py,sha256=WL58Nh-EY6baM9Ur_tR9merwPRGb41_klUG2V-yUUaA,963
8
- avtomatika/context.py,sha256=MT_RMMxSCAVEhlo5HUQdQ1uaR86D9d4s_3nrd6O5xAg,4241
9
- avtomatika/data_types.py,sha256=VWWbTcJaQt43JaD09qZL1spwNdzCR9vPXVthlM133gM,1362
10
- avtomatika/datastore.py,sha256=gJjhZ5kxjF8pmbbPQb_qu3HPUpfy2c6T75KZ-smb_zg,545
11
- avtomatika/dispatcher.py,sha256=Tsye9zOcLN2c7O0AalbX9Il5l3XdT64UDV7iQKiLIJE,9627
12
- avtomatika/engine.py,sha256=fSCalpR3-j-ATmDE5XMYjz1PSubVcrbeMFWGX_NRI8g,41883
13
- avtomatika/executor.py,sha256=-SKqLs-DVtMw5P7bZInK57f0M4zRFoVFr1fU5MUrtiY,22543
14
- avtomatika/health_checker.py,sha256=WXwvRJ-3cZC2Udc_ogsyIQp7VzcvJjq_IaqzkTdE0TE,1265
15
- avtomatika/logging_config.py,sha256=Zb6f9Nri9WVWhlpuBg6Lpi5SWRLGIUmS8Dc3xD1Gg0g,2993
16
- avtomatika/metrics.py,sha256=7XDhr_xMJ9JpElpZmBG7R0ml7AMdAp9UYp_W-i7tyLg,1858
17
- avtomatika/py.typed,sha256=CT_L7gw2MLcQY-X0vs-xB5Vr0wzvGo7GuQYPI_qwJE8,65
18
- avtomatika/quota.py,sha256=DNcaL6k0J1REeP8sVqbY9FprY_3BSr2SxM2Vf4mEqdw,1612
19
- avtomatika/ratelimit.py,sha256=hFGW5oN9G6_W_jnHmopXW8bRjjzlvanY19MLghsNLE8,1306
20
- avtomatika/reputation.py,sha256=IHcaIAILWZftPPmXj5En28OSDNK7U8ivQ-w30zIF8fk,3748
21
- avtomatika/scheduler.py,sha256=D-qpoGSA_Jqq1R7GlJNma9tssCujNwkOnWcRbbCHTnk,4138
22
- avtomatika/scheduler_config_loader.py,sha256=F6mLM8yPRgG4bMHV_WnXX7UOrXD8fCXJT30bbEuQ2mk,1311
23
- avtomatika/security.py,sha256=kkU68YmLWq1ClMUdEW98pS9WsEwHinHoZcdMoPm63Uk,4417
24
- avtomatika/telemetry.py,sha256=ZBt1_xJ36PzDSz-zdCXeNp58NiezUgbqvMctTG25PT0,2352
25
- avtomatika/watcher.py,sha256=IHaqSqp3XSGXjRY-LEeTG9BJpq2nqJSnmjY_Vdvk3jo,3493
26
- avtomatika/worker_config_loader.py,sha256=n0j8gfuJDacWONr8744RsHTCWpc_1ZTRMC-rJZh6P6A,2249
27
- avtomatika/ws_manager.py,sha256=pi5xe0ivsCjRZw08ri5N-gAChMH2I2YPLpl3E2tP89k,3057
28
- avtomatika/history/base.py,sha256=Gfw0Gb4Mt9wQrMlYLugZwey_6-cDej5OUctiMTCWg7Q,1668
29
- avtomatika/history/noop.py,sha256=ETVtPiTfkaMpzhGD8c0_4Iu6pWD89dnPrrRrSIjmc8s,970
30
- avtomatika/history/postgres.py,sha256=vtW4LMW7Vli5MjcGYY3ez667-C8Cq3I7kIHrcEgSYps,9409
31
- avtomatika/history/sqlite.py,sha256=Blc9ckvzoDaMRStXyfJOzMAdU_t2JcwtQtVdPgnr6s0,9131
32
- avtomatika/storage/__init__.py,sha256=mGRj_40dWZ7R7uYbqC6gCsUWCKHAbZz4ZVIhYg5dT_E,262
33
- avtomatika/storage/base.py,sha256=hW7XFhe6CQDP69q5NPSkUzEInIFxDR1-AyRPZNPEDEc,11424
34
- avtomatika/storage/memory.py,sha256=Px_ISqZz5Nhl5wLvGpMqY21-eKaLfkzgof7jquCmHaM,12234
35
- avtomatika/storage/redis.py,sha256=opOhqBL_uCsNXcMD_W_tJU-8wzDUSjBJWEsXrwP2_YM,21035
36
- avtomatika-1.0b6.dist-info/licenses/LICENSE,sha256=tqCjw9Y1vbU-hLcWi__7wQstLbt2T1XWPdbQYqCxuWY,1072
37
- avtomatika-1.0b6.dist-info/METADATA,sha256=kBfLgOgvd1e06JZ_l6TJwsyAHi81jLFnqZ03QoZbISA,22986
38
- avtomatika-1.0b6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
39
- avtomatika-1.0b6.dist-info/top_level.txt,sha256=gLDWhA_wxHj0I6fG5X8vw9fE0HSN4hTE2dEJzeVS2x8,11
40
- avtomatika-1.0b6.dist-info/RECORD,,