avtomatika-worker 1.0b3__py3-none-any.whl → 1.0b4__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,16 +1,21 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: avtomatika-worker
3
- Version: 1.0b3
3
+ Version: 1.0b4
4
4
  Summary: Worker SDK for the Avtomatika orchestrator.
5
+ Author-email: Dmitrii Gagarin <madgagarin@gmail.com>
5
6
  Project-URL: Homepage, https://github.com/avtomatika-ai/avtomatika-worker
6
7
  Project-URL: Bug Tracker, https://github.com/avtomatika-ai/avtomatika-worker/issues
8
+ Keywords: worker,sdk,orchestrator,distributed,task-queue,rxon,hln
7
9
  Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
8
11
  Classifier: Programming Language :: Python :: 3
9
12
  Classifier: License :: OSI Approved :: MIT License
10
13
  Classifier: Operating System :: OS Independent
14
+ Classifier: Typing :: Typed
11
15
  Requires-Python: >=3.11
12
16
  Description-Content-Type: text/markdown
13
17
  License-File: LICENSE
18
+ Requires-Dist: rxon==1.0b2
14
19
  Requires-Dist: aiohttp~=3.13.2
15
20
  Requires-Dist: python-json-logger~=4.0.0
16
21
  Requires-Dist: obstore>=0.1
@@ -28,7 +33,11 @@ Dynamic: license-file
28
33
 
29
34
  # Avtomatika Worker SDK
30
35
 
31
- This is the official SDK for creating workers compatible with the **[Avtomatika Orchestrator](https://github.com/avtomatika-ai/avtomatika)**. It implements the **[RCA Protocol](https://github.com/avtomatika-ai/rca)**, handling all communication complexity (polling, heartbeats, S3 offloading) so you can focus on writing your business logic.
36
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
37
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/release/python-3110/)
38
+ [![Code Style: Ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://github.com/astral-sh/ruff)
39
+
40
+ This is the official SDK for creating workers compatible with the **[Avtomatika Orchestrator](https://github.com/avtomatika-ai/avtomatika)**. It is built upon the **[Avtomatika Protocol](https://github.com/avtomatika-ai/rxon)** and implements the **[HLN Protocol](https://github.com/avtomatika-ai/hln)**, handling all communication complexity (polling, heartbeats, S3 offloading) so you can focus on writing your business logic.
32
41
 
33
42
  ## Installation
34
43
 
@@ -286,7 +295,7 @@ async def image_resizer(params: ResizeParams, **kwargs):
286
295
 
287
296
  ### 1. Task Handlers
288
297
 
289
- Each handler is an asynchronous function that accepts two arguments:
298
+ Each handler is a function (either `async def` or `def`) that accepts two arguments:
290
299
 
291
300
  - `params` (`dict`, `dataclass`, or `pydantic.BaseModel`): The parameters for the task, automatically validated and instantiated based on your type hint.
292
301
  - `**kwargs`: Additional metadata about the task, including:
@@ -294,6 +303,18 @@ Each handler is an asynchronous function that accepts two arguments:
294
303
  - `job_id` (`str`): The ID of the parent `Job` to which the task belongs.
295
304
  - `priority` (`int`): The execution priority of the task.
296
305
 
306
+ **Synchronous Handlers:**
307
+ If you define your handler as a standard synchronous function (`def handler(...)`), the SDK will automatically execute it in a separate thread using `asyncio.to_thread`. This ensures that CPU-intensive operations (like model inference) do not block the worker's main event loop, allowing heartbeats and other background tasks to continue running smoothly.
308
+
309
+ ```python
310
+ @worker.task("cpu_heavy_task")
311
+ def heavy_computation(params: dict, **kwargs):
312
+ # This will run in a thread, not blocking the loop
313
+ import time
314
+ time.sleep(10)
315
+ return {"status": "success"}
316
+ ```
317
+
297
318
  ### 2. Concurrency Limiting
298
319
 
299
320
  The worker allows you to control how many tasks are executed in parallel. This can be configured at two levels:
@@ -521,6 +542,48 @@ This only requires configuring environment variables for S3 access (see Full Con
521
542
 
522
543
  ### 7. WebSocket Support
523
544
 
545
+ For real-time communication (e.g., immediate task cancellation), the worker supports WebSocket connections. This is enabled by setting `WORKER_ENABLE_WEBSOCKETS=true`. When connected, the orchestrator can push commands like `cancel_task` directly to the worker.
546
+
547
+ ### 8. Middleware
548
+
549
+ The worker supports a middleware system, allowing you to wrap task executions with custom logic. This is particularly useful for resource management (e.g., acquiring GPU locks), logging, error handling, or **Dependency Injection**.
550
+
551
+ Middleware functions wrap the execution of the task handler (and any subsequent middlewares). They receive a context dictionary and the next handler in the chain.
552
+
553
+ The `context` dictionary contains:
554
+ - `task_id`, `job_id`, `task_name`: Metadata.
555
+ - `params`: The validated parameters object.
556
+ - `handler_kwargs`: A dictionary of arguments that will be passed to the handler. **Middleware can modify this dictionary to inject dependencies.**
557
+
558
+ **Example: GPU Resource Manager & Dependency Injection**
559
+
560
+ ```python
561
+ async def gpu_lock_middleware(context: dict, next_handler: callable):
562
+ # Pre-processing: Acquire resource
563
+ print(f"Acquiring GPU for task {context['task_id']}...")
564
+ model_path = await resource_manager.allocate()
565
+
566
+ # Inject the model path into the handler's arguments
567
+ context["handler_kwargs"]["model_path"] = model_path
568
+
569
+ try:
570
+ # Execute the next handler in the chain
571
+ result = await next_handler()
572
+ return result
573
+ finally:
574
+ # Post-processing: Release resource
575
+ print(f"Releasing GPU for task {context['task_id']}...")
576
+ resource_manager.release()
577
+
578
+ # Register the middleware
579
+ worker.add_middleware(gpu_lock_middleware)
580
+
581
+ # Handler now receives 'model_path' automatically
582
+ @worker.task("generate")
583
+ def generate(params, model_path, **kwargs):
584
+ print(f"Using model at: {model_path}")
585
+ ```
586
+
524
587
  ## Advanced Features
525
588
 
526
589
  ### Reporting Skill & Model Dependencies
@@ -577,8 +640,11 @@ The worker is fully configured via environment variables.
577
640
  | `WORKER_TYPE` | A string identifying the type of the worker. | `generic-cpu-worker` |
578
641
  | `WORKER_PORT` | The port for the worker's health check server. | `8083` |
579
642
  | `WORKER_TOKEN` | A common authentication token used to connect to orchestrators. | `your-secret-worker-token` |
580
- | `WORKER_INDIVIDUAL_TOKEN` | An individual token for this worker, which overrides `WORKER_TOKEN` if set. | - |
581
- | `ORCHESTRATOR_URL` | The URL of a single orchestrator (used if `ORCHESTRATORS_CONFIG` is not set). | `http://localhost:8080` |
643
+ - **`WORKER_INDIVIDUAL_TOKEN`**: An individual token for this worker, which overrides `WORKER_TOKEN` if set.
644
+ - **`TLS_CA_PATH`**: Path to the CA certificate to verify the orchestrator.
645
+ - **`TLS_CERT_PATH`**: Path to the client certificate for mTLS.
646
+ - **`TLS_KEY_PATH`**: Path to the client private key for mTLS.
647
+ - **`ORCHESTRATOR_URL`**: The address of the Avtomatika orchestrator.
582
648
  | `ORCHESTRATORS_CONFIG` | A JSON string with a list of orchestrators for multi-orchestrator modes. | `[]` |
583
649
  | `MULTI_ORCHESTRATOR_MODE` | The mode for handling multiple orchestrators. Possible values: `FAILOVER`, `ROUND_ROBIN`. | `FAILOVER` |
584
650
  | `MAX_CONCURRENT_TASKS` | The maximum number of tasks the worker can execute simultaneously. | `10` |
@@ -605,8 +671,9 @@ The worker is fully configured via environment variables.
605
671
 
606
672
  ## Development
607
673
 
608
- To install the necessary dependencies for running tests, use the following command:
674
+ To install the necessary dependencies for running tests (assuming you are in the package root):
609
675
 
610
- ```bash
611
- pip install .[test]
612
- ```
676
+ 1. Install the worker in editable mode with test dependencies:
677
+ ```bash
678
+ pip install -e .[test]
679
+ ```
@@ -0,0 +1,12 @@
1
+ avtomatika_worker/__init__.py,sha256=YYxuVc3EUabivZe6ZXNyFDUZH97qTKIQwxKZxV4klDc,342
2
+ avtomatika_worker/config.py,sha256=wXmtQTABhHEl0vwxQdWgSfmCFsKNEH-lF5CF70VV2SU,6114
3
+ avtomatika_worker/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ avtomatika_worker/s3.py,sha256=v_xSMVsPRgjkhQZWJreGjMSYCIfH_n7BHo5GMqmPw5k,10371
5
+ avtomatika_worker/task_files.py,sha256=9WmvVt4iZP65CA3lTttOmVnW8tlvfQMwutfTla4yJyY,5858
6
+ avtomatika_worker/types.py,sha256=cHzpvdJahAV6RlirlWcQ0xhxCrwNlrCalbsUL49t_ko,581
7
+ avtomatika_worker/worker.py,sha256=oMuMiPGKUYKwCq18T11zQeT36OBSGfxPNd5unVEEWsk,28250
8
+ avtomatika_worker-1.0b4.dist-info/licenses/LICENSE,sha256=J19fUi8XywciRuLLWHvcPQGc0Uo-9K1a0dKaIbzw_Yg,1092
9
+ avtomatika_worker-1.0b4.dist-info/METADATA,sha256=DZHKg_Yxl0dTraVoLfj29lOPvVzAJD8taF8VjxxLlUU,32868
10
+ avtomatika_worker-1.0b4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
11
+ avtomatika_worker-1.0b4.dist-info/top_level.txt,sha256=d3b5BUeUrHM1Cn-cbStz-hpucikEBlPOvtcmQ_j3qAs,18
12
+ avtomatika_worker-1.0b4.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Dmitrii Gagarin
3
+ Copyright (c) 2025-2026 Dmitrii Gagarin aka madgagarin
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,93 +0,0 @@
1
- from asyncio import sleep
2
- from logging import getLogger
3
- from typing import Any
4
-
5
- from aiohttp import ClientError, ClientSession, ClientTimeout, ClientWebSocketResponse
6
-
7
- from .constants import AUTH_HEADER_WORKER
8
-
9
- logger = getLogger(__name__)
10
-
11
-
12
- class OrchestratorClient:
13
- """
14
- Dedicated client for communicating with a single Avtomatika Orchestrator instance.
15
- Handles HTTP requests, retries, and authentication.
16
- """
17
-
18
- def __init__(self, session: ClientSession, base_url: str, worker_id: str, token: str):
19
- self.session = session
20
- self.base_url = base_url.rstrip("/")
21
- self.worker_id = worker_id
22
- self.token = token
23
- self._headers = {AUTH_HEADER_WORKER: self.token}
24
-
25
- async def register(self, payload: dict[str, Any]) -> bool:
26
- """Registers the worker with the orchestrator."""
27
- url = f"{self.base_url}/_worker/workers/register"
28
- try:
29
- async with self.session.post(url, json=payload, headers=self._headers) as resp:
30
- if resp.status >= 400:
31
- logger.error(f"Error registering with {self.base_url}: {resp.status}")
32
- return False
33
- return True
34
- except ClientError as e:
35
- logger.error(f"Error registering with orchestrator {self.base_url}: {e}")
36
- return False
37
-
38
- async def poll_task(self, timeout: float) -> dict[str, Any] | None:
39
- """Polls for the next available task."""
40
- url = f"{self.base_url}/_worker/workers/{self.worker_id}/tasks/next"
41
- client_timeout = ClientTimeout(total=timeout + 5)
42
- try:
43
- async with self.session.get(url, headers=self._headers, timeout=client_timeout) as resp:
44
- if resp.status == 200:
45
- return await resp.json()
46
- elif resp.status != 204:
47
- logger.warning(f"Unexpected status from {self.base_url} during poll: {resp.status}")
48
- except ClientError as e:
49
- logger.error(f"Error polling for tasks from {self.base_url}: {e}")
50
- except Exception as e:
51
- logger.exception(f"Unexpected error polling from {self.base_url}: {e}")
52
- return None
53
-
54
- async def send_heartbeat(self, payload: dict[str, Any]) -> bool:
55
- """Sends a heartbeat message to update worker state."""
56
- url = f"{self.base_url}/_worker/workers/{self.worker_id}"
57
- try:
58
- async with self.session.patch(url, json=payload, headers=self._headers) as resp:
59
- if resp.status >= 400:
60
- logger.warning(f"Heartbeat to {self.base_url} failed with status: {resp.status}")
61
- return False
62
- return True
63
- except ClientError as e:
64
- logger.error(f"Error sending heartbeat to orchestrator {self.base_url}: {e}")
65
- return False
66
-
67
- async def send_result(self, payload: dict[str, Any], max_retries: int, initial_delay: float) -> bool:
68
- """Sends task result with retries and exponential backoff."""
69
- url = f"{self.base_url}/_worker/tasks/result"
70
- delay = initial_delay
71
- for i in range(max_retries):
72
- try:
73
- async with self.session.post(url, json=payload, headers=self._headers) as resp:
74
- if resp.status == 200:
75
- return True
76
- logger.error(f"Error sending result to {self.base_url}: {resp.status}")
77
- except ClientError as e:
78
- logger.error(f"Error sending result to {self.base_url}: {e}")
79
-
80
- if i < max_retries - 1:
81
- await sleep(delay * (2**i))
82
- return False
83
-
84
- async def connect_websocket(self) -> ClientWebSocketResponse | None:
85
- """Establishes a WebSocket connection for real-time commands."""
86
- ws_url = self.base_url.replace("http", "ws", 1) + "/_worker/ws"
87
- try:
88
- ws = await self.session.ws_connect(ws_url, headers=self._headers)
89
- logger.info(f"WebSocket connection established to {ws_url}")
90
- return ws
91
- except Exception as e:
92
- logger.warning(f"WebSocket connection to {ws_url} failed: {e}")
93
- return None
@@ -1,22 +0,0 @@
1
- """
2
- Centralized constants for the Avtomatika protocol (Worker SDK).
3
- These should match the constants in the core `avtomatika` package.
4
- """
5
-
6
- # --- Auth Headers ---
7
- AUTH_HEADER_CLIENT = "X-Avtomatika-Token"
8
- AUTH_HEADER_WORKER = "X-Worker-Token"
9
-
10
- # --- Error Codes ---
11
- ERROR_CODE_TRANSIENT = "TRANSIENT_ERROR"
12
- ERROR_CODE_PERMANENT = "PERMANENT_ERROR"
13
- ERROR_CODE_INVALID_INPUT = "INVALID_INPUT_ERROR"
14
-
15
- # --- Task Statuses ---
16
- TASK_STATUS_SUCCESS = "success"
17
- TASK_STATUS_FAILURE = "failure"
18
- TASK_STATUS_CANCELLED = "cancelled"
19
- TASK_STATUS_NEEDS_REVIEW = "needs_review" # Example of a common custom status
20
-
21
- # --- Commands (WebSocket) ---
22
- COMMAND_CANCEL_TASK = "cancel_task"
@@ -1,14 +0,0 @@
1
- avtomatika_worker/__init__.py,sha256=y_s5KlsgFu7guemZfjLVQ3Jzq7DyLG168-maVGwWRC4,334
2
- avtomatika_worker/client.py,sha256=mkvwrMY8tAaZN_lwMSxWHmAoWsDemD-WiKSeH5fM6GI,4173
3
- avtomatika_worker/config.py,sha256=NaAhufpwyG6CsHW-cXmqR3MfGp_5SdDZ_vEhmmV8G3g,5819
4
- avtomatika_worker/constants.py,sha256=DfGR_YkW9rbioCorKpNGfZ0i_0iGgMq2swyJhVl9nNA,669
5
- avtomatika_worker/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- avtomatika_worker/s3.py,sha256=sAuXBp__XTVhyNwK9gsxy1Jm_udJM6ypssGTZ00pa6U,8847
7
- avtomatika_worker/task_files.py,sha256=ucjBuI78UmtMvfucTzDTNJ1g0KJaRIwyshRNTipIZSU,3351
8
- avtomatika_worker/types.py,sha256=dSNsHgqV6hZhOt4eUK2PDWB6lrrwCA5_T_iIBI_wTZ0,442
9
- avtomatika_worker/worker.py,sha256=XSRfLO-W0J6WG128Iu-rL_w3-PqmsWQMUElVLi3Z1gk,21904
10
- avtomatika_worker-1.0b3.dist-info/licenses/LICENSE,sha256=tqCjw9Y1vbU-hLcWi__7wQstLbt2T1XWPdbQYqCxuWY,1072
11
- avtomatika_worker-1.0b3.dist-info/METADATA,sha256=yiEtJuMv5WHYHfScna7cF5QAvAUhMCJdUDENHvrMRFY,29601
12
- avtomatika_worker-1.0b3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
13
- avtomatika_worker-1.0b3.dist-info/top_level.txt,sha256=d3b5BUeUrHM1Cn-cbStz-hpucikEBlPOvtcmQ_j3qAs,18
14
- avtomatika_worker-1.0b3.dist-info/RECORD,,