avtomatika-worker 1.0b3__py3-none-any.whl → 1.0b5__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.
- avtomatika_worker/__init__.py +1 -1
- avtomatika_worker/config.py +6 -0
- avtomatika_worker/s3.py +76 -48
- avtomatika_worker/task_files.py +60 -2
- avtomatika_worker/types.py +29 -4
- avtomatika_worker/worker.py +333 -155
- {avtomatika_worker-1.0b3.dist-info → avtomatika_worker-1.0b5.dist-info}/METADATA +82 -21
- avtomatika_worker-1.0b5.dist-info/RECORD +12 -0
- {avtomatika_worker-1.0b3.dist-info → avtomatika_worker-1.0b5.dist-info}/WHEEL +1 -1
- {avtomatika_worker-1.0b3.dist-info → avtomatika_worker-1.0b5.dist-info}/licenses/LICENSE +1 -1
- avtomatika_worker/client.py +0 -93
- avtomatika_worker/constants.py +0 -22
- avtomatika_worker-1.0b3.dist-info/RECORD +0 -14
- {avtomatika_worker-1.0b3.dist-info → avtomatika_worker-1.0b5.dist-info}/top_level.txt +0 -0
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: avtomatika-worker
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.0b5
|
|
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
|
-
|
|
36
|
+
[](https://opensource.org/licenses/MIT)
|
|
37
|
+
[](https://www.python.org/downloads/release/python-3110/)
|
|
38
|
+
[](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,13 +295,26 @@ async def image_resizer(params: ResizeParams, **kwargs):
|
|
|
286
295
|
|
|
287
296
|
### 1. Task Handlers
|
|
288
297
|
|
|
289
|
-
Each handler is
|
|
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:
|
|
293
302
|
- `task_id` (`str`): The unique ID of the task itself.
|
|
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.
|
|
305
|
+
- `send_progress` (`callable`): An async function `await send_progress(progress_float, message_string)` to report task execution progress (0.0 to 1.0) to the orchestrator.
|
|
306
|
+
|
|
307
|
+
**Synchronous Handlers:**
|
|
308
|
+
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.
|
|
309
|
+
|
|
310
|
+
```python
|
|
311
|
+
@worker.task("cpu_heavy_task")
|
|
312
|
+
def heavy_computation(params: dict, **kwargs):
|
|
313
|
+
# This will run in a thread, not blocking the loop
|
|
314
|
+
import time
|
|
315
|
+
time.sleep(10)
|
|
316
|
+
return {"status": "success"}
|
|
317
|
+
```
|
|
296
318
|
|
|
297
319
|
### 2. Concurrency Limiting
|
|
298
320
|
|
|
@@ -383,7 +405,7 @@ return {
|
|
|
383
405
|
|
|
384
406
|
#### Error Handling
|
|
385
407
|
|
|
386
|
-
To control the orchestrator's fault tolerance mechanism, you can return standardized error types.
|
|
408
|
+
To control the orchestrator's fault tolerance mechanism, you can return standardized error types. All error constants can be imported from `avtomatika_worker.typing`.
|
|
387
409
|
|
|
388
410
|
- **Transient Error (`TRANSIENT_ERROR`)**: For issues that might be resolved on a retry (e.g., a network failure).
|
|
389
411
|
```python
|
|
@@ -396,17 +418,10 @@ To control the orchestrator's fault tolerance mechanism, you can return standard
|
|
|
396
418
|
}
|
|
397
419
|
}
|
|
398
420
|
```
|
|
399
|
-
- **Permanent Error (`PERMANENT_ERROR`)**: For unresolvable problems (e.g., an invalid file format).
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
"status": "failure",
|
|
404
|
-
"error": {
|
|
405
|
-
"code": PERMANENT_ERROR,
|
|
406
|
-
"message": "Corrupted input file"
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
```
|
|
421
|
+
- **Permanent Error (`PERMANENT_ERROR`)**: For unresolvable problems (e.g., an invalid file format). Causes immediate quarantine.
|
|
422
|
+
- **Security Error (`SECURITY_ERROR`)**: For security violations. Causes immediate quarantine.
|
|
423
|
+
- **Dependency Error (`DEPENDENCY_ERROR`)**: For missing models or tools. Causes immediate quarantine.
|
|
424
|
+
- **Resource Exhausted (`RESOURCE_EXHAUSTED_ERROR`)**: When resources are temporarily unavailable. Treated as transient (retried).
|
|
410
425
|
|
|
411
426
|
### 4. Failover and Load Balancing
|
|
412
427
|
|
|
@@ -521,6 +536,48 @@ This only requires configuring environment variables for S3 access (see Full Con
|
|
|
521
536
|
|
|
522
537
|
### 7. WebSocket Support
|
|
523
538
|
|
|
539
|
+
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.
|
|
540
|
+
|
|
541
|
+
### 8. Middleware
|
|
542
|
+
|
|
543
|
+
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**.
|
|
544
|
+
|
|
545
|
+
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.
|
|
546
|
+
|
|
547
|
+
The `context` dictionary contains:
|
|
548
|
+
- `task_id`, `job_id`, `task_name`: Metadata.
|
|
549
|
+
- `params`: The validated parameters object.
|
|
550
|
+
- `handler_kwargs`: A dictionary of arguments that will be passed to the handler. **Middleware can modify this dictionary to inject dependencies.**
|
|
551
|
+
|
|
552
|
+
**Example: GPU Resource Manager & Dependency Injection**
|
|
553
|
+
|
|
554
|
+
```python
|
|
555
|
+
async def gpu_lock_middleware(context: dict, next_handler: callable):
|
|
556
|
+
# Pre-processing: Acquire resource
|
|
557
|
+
print(f"Acquiring GPU for task {context['task_id']}...")
|
|
558
|
+
model_path = await resource_manager.allocate()
|
|
559
|
+
|
|
560
|
+
# Inject the model path into the handler's arguments
|
|
561
|
+
context["handler_kwargs"]["model_path"] = model_path
|
|
562
|
+
|
|
563
|
+
try:
|
|
564
|
+
# Execute the next handler in the chain
|
|
565
|
+
result = await next_handler()
|
|
566
|
+
return result
|
|
567
|
+
finally:
|
|
568
|
+
# Post-processing: Release resource
|
|
569
|
+
print(f"Releasing GPU for task {context['task_id']}...")
|
|
570
|
+
resource_manager.release()
|
|
571
|
+
|
|
572
|
+
# Register the middleware
|
|
573
|
+
worker.add_middleware(gpu_lock_middleware)
|
|
574
|
+
|
|
575
|
+
# Handler now receives 'model_path' automatically
|
|
576
|
+
@worker.task("generate")
|
|
577
|
+
def generate(params, model_path, **kwargs):
|
|
578
|
+
print(f"Using model at: {model_path}")
|
|
579
|
+
```
|
|
580
|
+
|
|
524
581
|
## Advanced Features
|
|
525
582
|
|
|
526
583
|
### Reporting Skill & Model Dependencies
|
|
@@ -577,8 +634,11 @@ The worker is fully configured via environment variables.
|
|
|
577
634
|
| `WORKER_TYPE` | A string identifying the type of the worker. | `generic-cpu-worker` |
|
|
578
635
|
| `WORKER_PORT` | The port for the worker's health check server. | `8083` |
|
|
579
636
|
| `WORKER_TOKEN` | A common authentication token used to connect to orchestrators. | `your-secret-worker-token` |
|
|
580
|
-
|
|
581
|
-
|
|
637
|
+
- **`WORKER_INDIVIDUAL_TOKEN`**: An individual token for this worker, which overrides `WORKER_TOKEN` if set.
|
|
638
|
+
- **`TLS_CA_PATH`**: Path to the CA certificate to verify the orchestrator.
|
|
639
|
+
- **`TLS_CERT_PATH`**: Path to the client certificate for mTLS.
|
|
640
|
+
- **`TLS_KEY_PATH`**: Path to the client private key for mTLS.
|
|
641
|
+
- **`ORCHESTRATOR_URL`**: The address of the Avtomatika orchestrator.
|
|
582
642
|
| `ORCHESTRATORS_CONFIG` | A JSON string with a list of orchestrators for multi-orchestrator modes. | `[]` |
|
|
583
643
|
| `MULTI_ORCHESTRATOR_MODE` | The mode for handling multiple orchestrators. Possible values: `FAILOVER`, `ROUND_ROBIN`. | `FAILOVER` |
|
|
584
644
|
| `MAX_CONCURRENT_TASKS` | The maximum number of tasks the worker can execute simultaneously. | `10` |
|
|
@@ -605,8 +665,9 @@ The worker is fully configured via environment variables.
|
|
|
605
665
|
|
|
606
666
|
## Development
|
|
607
667
|
|
|
608
|
-
To install the necessary dependencies for running tests
|
|
668
|
+
To install the necessary dependencies for running tests (assuming you are in the package root):
|
|
609
669
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
670
|
+
1. Install the worker in editable mode with test dependencies:
|
|
671
|
+
```bash
|
|
672
|
+
pip install -e .[test]
|
|
673
|
+
```
|
|
@@ -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=9lFjRIhBBtztsPcnnbQ0ifnikBUQFqf8EeieuOG3MYI,1094
|
|
7
|
+
avtomatika_worker/worker.py,sha256=oMuMiPGKUYKwCq18T11zQeT36OBSGfxPNd5unVEEWsk,28250
|
|
8
|
+
avtomatika_worker-1.0b5.dist-info/licenses/LICENSE,sha256=J19fUi8XywciRuLLWHvcPQGc0Uo-9K1a0dKaIbzw_Yg,1092
|
|
9
|
+
avtomatika_worker-1.0b5.dist-info/METADATA,sha256=qz5Dl8qIubhAPtGSxv0LY7t6pVDDqiLD8wOhSg7I3dY,33240
|
|
10
|
+
avtomatika_worker-1.0b5.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
11
|
+
avtomatika_worker-1.0b5.dist-info/top_level.txt,sha256=d3b5BUeUrHM1Cn-cbStz-hpucikEBlPOvtcmQ_j3qAs,18
|
|
12
|
+
avtomatika_worker-1.0b5.dist-info/RECORD,,
|
|
@@ -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
|
avtomatika_worker/client.py
DELETED
|
@@ -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
|
avtomatika_worker/constants.py
DELETED
|
@@ -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,,
|
|
File without changes
|