runqy-python 0.2.0__tar.gz
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.
- runqy_python-0.2.0/LICENSE +21 -0
- runqy_python-0.2.0/PKG-INFO +176 -0
- runqy_python-0.2.0/README.md +153 -0
- runqy_python-0.2.0/pyproject.toml +35 -0
- runqy_python-0.2.0/setup.cfg +4 -0
- runqy_python-0.2.0/src/runqy_python/__init__.py +32 -0
- runqy_python-0.2.0/src/runqy_python/client.py +240 -0
- runqy_python-0.2.0/src/runqy_python/decorator.py +53 -0
- runqy_python-0.2.0/src/runqy_python/runner.py +127 -0
- runqy_python-0.2.0/src/runqy_python.egg-info/PKG-INFO +176 -0
- runqy_python-0.2.0/src/runqy_python.egg-info/SOURCES.txt +11 -0
- runqy_python-0.2.0/src/runqy_python.egg-info/dependency_links.txt +1 -0
- runqy_python-0.2.0/src/runqy_python.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Publikey
|
|
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.
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: runqy-python
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Python SDK for runqy - write distributed task handlers with simple decorators
|
|
5
|
+
Author: Publikey
|
|
6
|
+
Project-URL: Documentation, https://docs.runqy.com
|
|
7
|
+
Project-URL: Repository, https://github.com/Publikey/runqy-python
|
|
8
|
+
Project-URL: Issues, https://github.com/Publikey/runqy-python/issues
|
|
9
|
+
Keywords: task-queue,distributed,worker,redis,async,client
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.8
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
|
|
24
|
+
<p align="center">
|
|
25
|
+
<img src="assets/logo.svg" alt="runqy logo" width="80" height="80">
|
|
26
|
+
</p>
|
|
27
|
+
|
|
28
|
+
<h1 align="center">runqy-python</h1>
|
|
29
|
+
|
|
30
|
+
<p align="center">
|
|
31
|
+
Python SDK for <a href="https://runqy.com">runqy</a> - write distributed task handlers with simple decorators.
|
|
32
|
+
<br>
|
|
33
|
+
<a href="https://docs.runqy.com"><strong>Documentation</strong></a> · <a href="https://runqy.com"><strong>Website</strong></a>
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install runqy-python
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Task Handlers
|
|
43
|
+
|
|
44
|
+
Create tasks that run on [runqy-worker](https://github.com/publikey/runqy-worker) using simple decorators:
|
|
45
|
+
|
|
46
|
+
### Simple Task
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from runqy_python import task, run
|
|
50
|
+
|
|
51
|
+
@task
|
|
52
|
+
def process(payload: dict) -> dict:
|
|
53
|
+
return {"message": "Hello!", "received": payload}
|
|
54
|
+
|
|
55
|
+
if __name__ == "__main__":
|
|
56
|
+
run()
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### With Model Loading
|
|
60
|
+
|
|
61
|
+
For ML inference tasks, use `@load` to load models once at startup:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from runqy_python import task, load, run
|
|
65
|
+
|
|
66
|
+
@load
|
|
67
|
+
def setup():
|
|
68
|
+
"""Runs once before ready signal. Return value is passed to @task as ctx."""
|
|
69
|
+
model = load_heavy_model() # Load weights, etc.
|
|
70
|
+
return {"model": model}
|
|
71
|
+
|
|
72
|
+
@task
|
|
73
|
+
def process(payload: dict, ctx: dict) -> dict:
|
|
74
|
+
"""Process tasks using the loaded model."""
|
|
75
|
+
prediction = ctx["model"].predict(payload["input"])
|
|
76
|
+
return {"prediction": prediction}
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__":
|
|
79
|
+
run()
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### One-Shot Tasks
|
|
83
|
+
|
|
84
|
+
For lightweight tasks that don't need to stay loaded in memory, use `run_once()`:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from runqy_python import task, run_once
|
|
88
|
+
|
|
89
|
+
@task
|
|
90
|
+
def process(payload: dict) -> dict:
|
|
91
|
+
return {"result": payload["x"] * 2}
|
|
92
|
+
|
|
93
|
+
if __name__ == "__main__":
|
|
94
|
+
run_once() # Process one task and exit
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
| Function | Behavior | Use case |
|
|
98
|
+
|----------|----------|----------|
|
|
99
|
+
| `run()` | Loops forever, handles many tasks | ML inference (expensive load) |
|
|
100
|
+
| `run_once()` | Handles ONE task, exits | Lightweight tasks |
|
|
101
|
+
|
|
102
|
+
## Protocol
|
|
103
|
+
|
|
104
|
+
The SDK handles the runqy-worker stdin/stdout JSON protocol:
|
|
105
|
+
|
|
106
|
+
1. **Load phase**: Calls `@load` function (if registered)
|
|
107
|
+
2. **Ready signal**: Sends `{"status": "ready"}` after load completes
|
|
108
|
+
3. **Task input**: Reads JSON from stdin: `{"task_id": "...", "payload": {...}}`
|
|
109
|
+
4. **Response**: Writes JSON to stdout: `{"task_id": "...", "result": {...}, "error": null, "retry": false}`
|
|
110
|
+
|
|
111
|
+
## Client (Optional)
|
|
112
|
+
|
|
113
|
+
The SDK also includes a client for enqueuing tasks to a runqy server:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from runqy_python import RunqyClient
|
|
117
|
+
|
|
118
|
+
client = RunqyClient("http://localhost:3000", api_key="your-api-key")
|
|
119
|
+
|
|
120
|
+
# Enqueue a task
|
|
121
|
+
task = client.enqueue("inference_default", {"input": "hello"})
|
|
122
|
+
print(f"Task ID: {task.task_id}")
|
|
123
|
+
|
|
124
|
+
# Check result
|
|
125
|
+
result = client.get_task(task.task_id)
|
|
126
|
+
print(f"State: {result.state}, Result: {result.result}")
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Or use the convenience function:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from runqy_python import enqueue
|
|
133
|
+
|
|
134
|
+
task = enqueue(
|
|
135
|
+
"inference_default",
|
|
136
|
+
{"input": "hello"},
|
|
137
|
+
server_url="http://localhost:3000",
|
|
138
|
+
api_key="your-api-key"
|
|
139
|
+
)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Client API
|
|
143
|
+
|
|
144
|
+
**RunqyClient(server_url, api_key, timeout=30)**
|
|
145
|
+
- `server_url`: Base URL of the runqy server
|
|
146
|
+
- `api_key`: API key for authentication
|
|
147
|
+
- `timeout`: Default request timeout in seconds
|
|
148
|
+
|
|
149
|
+
**client.enqueue(queue, payload, timeout=300)**
|
|
150
|
+
- `queue`: Queue name (e.g., `"inference_default"`)
|
|
151
|
+
- `payload`: Task payload as a dict
|
|
152
|
+
- `timeout`: Task execution timeout in seconds
|
|
153
|
+
- Returns: `TaskInfo` with `task_id`, `queue`, `state`
|
|
154
|
+
|
|
155
|
+
**client.get_task(task_id)**
|
|
156
|
+
- `task_id`: Task ID from enqueue
|
|
157
|
+
- Returns: `TaskInfo` with `task_id`, `queue`, `state`, `result`, `error`
|
|
158
|
+
|
|
159
|
+
### Exceptions
|
|
160
|
+
|
|
161
|
+
- `RunqyError`: Base exception for all client errors
|
|
162
|
+
- `AuthenticationError`: Invalid or missing API key
|
|
163
|
+
- `TaskNotFoundError`: Task ID doesn't exist
|
|
164
|
+
|
|
165
|
+
## Development
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
# Install in editable mode
|
|
169
|
+
pip install -e .
|
|
170
|
+
|
|
171
|
+
# Test task execution
|
|
172
|
+
echo '{"task_id":"t1","payload":{"foo":"bar"}}' | python your_model.py
|
|
173
|
+
|
|
174
|
+
# Test client import
|
|
175
|
+
python -c "from runqy_python import RunqyClient; print('OK')"
|
|
176
|
+
```
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/logo.svg" alt="runqy logo" width="80" height="80">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">runqy-python</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
Python SDK for <a href="https://runqy.com">runqy</a> - write distributed task handlers with simple decorators.
|
|
9
|
+
<br>
|
|
10
|
+
<a href="https://docs.runqy.com"><strong>Documentation</strong></a> · <a href="https://runqy.com"><strong>Website</strong></a>
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install runqy-python
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Task Handlers
|
|
20
|
+
|
|
21
|
+
Create tasks that run on [runqy-worker](https://github.com/publikey/runqy-worker) using simple decorators:
|
|
22
|
+
|
|
23
|
+
### Simple Task
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from runqy_python import task, run
|
|
27
|
+
|
|
28
|
+
@task
|
|
29
|
+
def process(payload: dict) -> dict:
|
|
30
|
+
return {"message": "Hello!", "received": payload}
|
|
31
|
+
|
|
32
|
+
if __name__ == "__main__":
|
|
33
|
+
run()
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### With Model Loading
|
|
37
|
+
|
|
38
|
+
For ML inference tasks, use `@load` to load models once at startup:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from runqy_python import task, load, run
|
|
42
|
+
|
|
43
|
+
@load
|
|
44
|
+
def setup():
|
|
45
|
+
"""Runs once before ready signal. Return value is passed to @task as ctx."""
|
|
46
|
+
model = load_heavy_model() # Load weights, etc.
|
|
47
|
+
return {"model": model}
|
|
48
|
+
|
|
49
|
+
@task
|
|
50
|
+
def process(payload: dict, ctx: dict) -> dict:
|
|
51
|
+
"""Process tasks using the loaded model."""
|
|
52
|
+
prediction = ctx["model"].predict(payload["input"])
|
|
53
|
+
return {"prediction": prediction}
|
|
54
|
+
|
|
55
|
+
if __name__ == "__main__":
|
|
56
|
+
run()
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### One-Shot Tasks
|
|
60
|
+
|
|
61
|
+
For lightweight tasks that don't need to stay loaded in memory, use `run_once()`:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from runqy_python import task, run_once
|
|
65
|
+
|
|
66
|
+
@task
|
|
67
|
+
def process(payload: dict) -> dict:
|
|
68
|
+
return {"result": payload["x"] * 2}
|
|
69
|
+
|
|
70
|
+
if __name__ == "__main__":
|
|
71
|
+
run_once() # Process one task and exit
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
| Function | Behavior | Use case |
|
|
75
|
+
|----------|----------|----------|
|
|
76
|
+
| `run()` | Loops forever, handles many tasks | ML inference (expensive load) |
|
|
77
|
+
| `run_once()` | Handles ONE task, exits | Lightweight tasks |
|
|
78
|
+
|
|
79
|
+
## Protocol
|
|
80
|
+
|
|
81
|
+
The SDK handles the runqy-worker stdin/stdout JSON protocol:
|
|
82
|
+
|
|
83
|
+
1. **Load phase**: Calls `@load` function (if registered)
|
|
84
|
+
2. **Ready signal**: Sends `{"status": "ready"}` after load completes
|
|
85
|
+
3. **Task input**: Reads JSON from stdin: `{"task_id": "...", "payload": {...}}`
|
|
86
|
+
4. **Response**: Writes JSON to stdout: `{"task_id": "...", "result": {...}, "error": null, "retry": false}`
|
|
87
|
+
|
|
88
|
+
## Client (Optional)
|
|
89
|
+
|
|
90
|
+
The SDK also includes a client for enqueuing tasks to a runqy server:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from runqy_python import RunqyClient
|
|
94
|
+
|
|
95
|
+
client = RunqyClient("http://localhost:3000", api_key="your-api-key")
|
|
96
|
+
|
|
97
|
+
# Enqueue a task
|
|
98
|
+
task = client.enqueue("inference_default", {"input": "hello"})
|
|
99
|
+
print(f"Task ID: {task.task_id}")
|
|
100
|
+
|
|
101
|
+
# Check result
|
|
102
|
+
result = client.get_task(task.task_id)
|
|
103
|
+
print(f"State: {result.state}, Result: {result.result}")
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Or use the convenience function:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from runqy_python import enqueue
|
|
110
|
+
|
|
111
|
+
task = enqueue(
|
|
112
|
+
"inference_default",
|
|
113
|
+
{"input": "hello"},
|
|
114
|
+
server_url="http://localhost:3000",
|
|
115
|
+
api_key="your-api-key"
|
|
116
|
+
)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Client API
|
|
120
|
+
|
|
121
|
+
**RunqyClient(server_url, api_key, timeout=30)**
|
|
122
|
+
- `server_url`: Base URL of the runqy server
|
|
123
|
+
- `api_key`: API key for authentication
|
|
124
|
+
- `timeout`: Default request timeout in seconds
|
|
125
|
+
|
|
126
|
+
**client.enqueue(queue, payload, timeout=300)**
|
|
127
|
+
- `queue`: Queue name (e.g., `"inference_default"`)
|
|
128
|
+
- `payload`: Task payload as a dict
|
|
129
|
+
- `timeout`: Task execution timeout in seconds
|
|
130
|
+
- Returns: `TaskInfo` with `task_id`, `queue`, `state`
|
|
131
|
+
|
|
132
|
+
**client.get_task(task_id)**
|
|
133
|
+
- `task_id`: Task ID from enqueue
|
|
134
|
+
- Returns: `TaskInfo` with `task_id`, `queue`, `state`, `result`, `error`
|
|
135
|
+
|
|
136
|
+
### Exceptions
|
|
137
|
+
|
|
138
|
+
- `RunqyError`: Base exception for all client errors
|
|
139
|
+
- `AuthenticationError`: Invalid or missing API key
|
|
140
|
+
- `TaskNotFoundError`: Task ID doesn't exist
|
|
141
|
+
|
|
142
|
+
## Development
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
# Install in editable mode
|
|
146
|
+
pip install -e .
|
|
147
|
+
|
|
148
|
+
# Test task execution
|
|
149
|
+
echo '{"task_id":"t1","payload":{"foo":"bar"}}' | python your_model.py
|
|
150
|
+
|
|
151
|
+
# Test client import
|
|
152
|
+
python -c "from runqy_python import RunqyClient; print('OK')"
|
|
153
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "runqy-python"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "Python SDK for runqy - write distributed task handlers with simple decorators"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.8"
|
|
7
|
+
dependencies = []
|
|
8
|
+
authors = [
|
|
9
|
+
{name = "Publikey"}
|
|
10
|
+
]
|
|
11
|
+
keywords = ["task-queue", "distributed", "worker", "redis", "async", "client"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 4 - Beta",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.8",
|
|
18
|
+
"Programming Language :: Python :: 3.9",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Documentation = "https://docs.runqy.com"
|
|
27
|
+
Repository = "https://github.com/Publikey/runqy-python"
|
|
28
|
+
Issues = "https://github.com/Publikey/runqy-python/issues"
|
|
29
|
+
|
|
30
|
+
[build-system]
|
|
31
|
+
requires = ["setuptools>=61.0,<75.0", "wheel"]
|
|
32
|
+
build-backend = "setuptools.build_meta"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.packages.find]
|
|
35
|
+
where = ["src"]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""runqy-python: Python SDK for runqy - write distributed task handlers with simple decorators."""
|
|
2
|
+
|
|
3
|
+
# Task execution (for workers)
|
|
4
|
+
from .decorator import task, load
|
|
5
|
+
from .runner import run, run_once
|
|
6
|
+
|
|
7
|
+
# Client (for enqueuing tasks)
|
|
8
|
+
from .client import (
|
|
9
|
+
RunqyClient,
|
|
10
|
+
TaskInfo,
|
|
11
|
+
RunqyError,
|
|
12
|
+
AuthenticationError,
|
|
13
|
+
TaskNotFoundError,
|
|
14
|
+
enqueue,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
# Task execution
|
|
19
|
+
"task",
|
|
20
|
+
"load",
|
|
21
|
+
"run",
|
|
22
|
+
"run_once",
|
|
23
|
+
# Client
|
|
24
|
+
"RunqyClient",
|
|
25
|
+
"TaskInfo",
|
|
26
|
+
"RunqyError",
|
|
27
|
+
"AuthenticationError",
|
|
28
|
+
"TaskNotFoundError",
|
|
29
|
+
"enqueue",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
__version__ = "0.2.0"
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""Client for enqueuing tasks to runqy server."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import urllib.request
|
|
5
|
+
import urllib.error
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, Dict, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RunqyError(Exception):
|
|
11
|
+
"""Base exception for runqy client errors."""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AuthenticationError(RunqyError):
|
|
16
|
+
"""Raised when API key is invalid or missing."""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TaskNotFoundError(RunqyError):
|
|
21
|
+
"""Raised when a task is not found."""
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class TaskInfo:
|
|
27
|
+
"""Task information returned from server.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
task_id: Unique identifier for the task
|
|
31
|
+
queue: Queue name the task was submitted to
|
|
32
|
+
state: Task state (pending, active, completed, etc.)
|
|
33
|
+
result: Task result data (if completed)
|
|
34
|
+
error: Error message (if failed)
|
|
35
|
+
payload: Original task payload
|
|
36
|
+
"""
|
|
37
|
+
task_id: str
|
|
38
|
+
queue: str
|
|
39
|
+
state: str
|
|
40
|
+
result: Optional[Any] = None
|
|
41
|
+
error: Optional[str] = None
|
|
42
|
+
payload: Optional[Dict[str, Any]] = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class RunqyClient:
|
|
46
|
+
"""Client for interacting with runqy server.
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
client = RunqyClient("http://localhost:3000", api_key="your-api-key")
|
|
50
|
+
|
|
51
|
+
# Enqueue a task
|
|
52
|
+
task = client.enqueue("inference_default", {"input": "hello"})
|
|
53
|
+
print(f"Task ID: {task.task_id}")
|
|
54
|
+
|
|
55
|
+
# Check result
|
|
56
|
+
result = client.get_task(task.task_id)
|
|
57
|
+
print(f"State: {result.state}, Result: {result.result}")
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(self, server_url: str, api_key: str, timeout: int = 30):
|
|
61
|
+
"""Initialize the client.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
server_url: Base URL of the runqy server (e.g., "http://localhost:3000")
|
|
65
|
+
api_key: API key for authentication
|
|
66
|
+
timeout: Default request timeout in seconds
|
|
67
|
+
"""
|
|
68
|
+
self.server_url = server_url.rstrip("/")
|
|
69
|
+
self.api_key = api_key
|
|
70
|
+
self.timeout = timeout
|
|
71
|
+
|
|
72
|
+
def _request(
|
|
73
|
+
self,
|
|
74
|
+
method: str,
|
|
75
|
+
path: str,
|
|
76
|
+
data: Optional[Dict[str, Any]] = None,
|
|
77
|
+
timeout: Optional[int] = None
|
|
78
|
+
) -> Dict[str, Any]:
|
|
79
|
+
"""Make an HTTP request to the server.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
method: HTTP method (GET, POST, etc.)
|
|
83
|
+
path: API path (e.g., "/queue/add")
|
|
84
|
+
data: Request body data (for POST)
|
|
85
|
+
timeout: Request timeout in seconds
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Parsed JSON response
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
AuthenticationError: If API key is invalid
|
|
92
|
+
RunqyError: For other HTTP errors
|
|
93
|
+
"""
|
|
94
|
+
url = f"{self.server_url}{path}"
|
|
95
|
+
headers = {
|
|
96
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
97
|
+
"Content-Type": "application/json",
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
body = None
|
|
101
|
+
if data is not None:
|
|
102
|
+
body = json.dumps(data).encode("utf-8")
|
|
103
|
+
|
|
104
|
+
req = urllib.request.Request(url, data=body, headers=headers, method=method)
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
with urllib.request.urlopen(req, timeout=timeout or self.timeout) as resp:
|
|
108
|
+
response_data = resp.read().decode("utf-8")
|
|
109
|
+
return json.loads(response_data) if response_data else {}
|
|
110
|
+
except urllib.error.HTTPError as e:
|
|
111
|
+
response_body = e.read().decode("utf-8") if e.fp else ""
|
|
112
|
+
|
|
113
|
+
if e.code == 401:
|
|
114
|
+
raise AuthenticationError(f"Authentication failed: {response_body}")
|
|
115
|
+
elif e.code == 404:
|
|
116
|
+
raise TaskNotFoundError(f"Task not found: {response_body}")
|
|
117
|
+
else:
|
|
118
|
+
raise RunqyError(f"HTTP {e.code}: {response_body}")
|
|
119
|
+
except urllib.error.URLError as e:
|
|
120
|
+
raise RunqyError(f"Connection error: {e.reason}")
|
|
121
|
+
|
|
122
|
+
def enqueue(
|
|
123
|
+
self,
|
|
124
|
+
queue: str,
|
|
125
|
+
payload: Dict[str, Any],
|
|
126
|
+
timeout: int = 300
|
|
127
|
+
) -> TaskInfo:
|
|
128
|
+
"""Enqueue a task to a queue.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
queue: Queue name (e.g., "inference_default")
|
|
132
|
+
payload: Task payload data
|
|
133
|
+
timeout: Task execution timeout in seconds (default: 300)
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
TaskInfo with task_id and initial state
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
AuthenticationError: If API key is invalid
|
|
140
|
+
RunqyError: For other errors
|
|
141
|
+
|
|
142
|
+
Example:
|
|
143
|
+
task = client.enqueue("inference_default", {"input": "hello"})
|
|
144
|
+
print(f"Enqueued task: {task.task_id}")
|
|
145
|
+
"""
|
|
146
|
+
data = {
|
|
147
|
+
"queue": queue,
|
|
148
|
+
"timeout": timeout,
|
|
149
|
+
"data": payload,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
response = self._request("POST", "/queue/add", data)
|
|
153
|
+
|
|
154
|
+
info = response.get("info", {})
|
|
155
|
+
return TaskInfo(
|
|
156
|
+
task_id=info.get("id", ""),
|
|
157
|
+
queue=info.get("queue", queue),
|
|
158
|
+
state=info.get("state", "pending"),
|
|
159
|
+
payload=payload,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def get_task(self, task_id: str) -> TaskInfo:
|
|
163
|
+
"""Get task status and result.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
task_id: Task ID returned from enqueue()
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
TaskInfo with current state and result (if completed)
|
|
170
|
+
|
|
171
|
+
Raises:
|
|
172
|
+
TaskNotFoundError: If task doesn't exist
|
|
173
|
+
RunqyError: For other errors
|
|
174
|
+
|
|
175
|
+
Example:
|
|
176
|
+
result = client.get_task(task.task_id)
|
|
177
|
+
if result.state == "completed":
|
|
178
|
+
print(f"Result: {result.result}")
|
|
179
|
+
"""
|
|
180
|
+
response = self._request("GET", f"/queue/{task_id}")
|
|
181
|
+
|
|
182
|
+
info = response.get("Info", response.get("info", {}))
|
|
183
|
+
|
|
184
|
+
# Parse result if it's a string (JSON)
|
|
185
|
+
result = info.get("Result", info.get("result"))
|
|
186
|
+
if isinstance(result, str) and result:
|
|
187
|
+
try:
|
|
188
|
+
result = json.loads(result)
|
|
189
|
+
except json.JSONDecodeError:
|
|
190
|
+
pass # Keep as string if not valid JSON
|
|
191
|
+
|
|
192
|
+
# Parse payload if it's a string (JSON)
|
|
193
|
+
payload = info.get("Payload", info.get("payload"))
|
|
194
|
+
if isinstance(payload, str) and payload:
|
|
195
|
+
try:
|
|
196
|
+
payload = json.loads(payload)
|
|
197
|
+
except json.JSONDecodeError:
|
|
198
|
+
pass # Keep as string if not valid JSON
|
|
199
|
+
|
|
200
|
+
return TaskInfo(
|
|
201
|
+
task_id=info.get("ID", info.get("id", task_id)),
|
|
202
|
+
queue=info.get("Queue", info.get("queue", "")),
|
|
203
|
+
state=info.get("State", info.get("state", "")),
|
|
204
|
+
result=result,
|
|
205
|
+
error=info.get("LastErr", info.get("last_err")),
|
|
206
|
+
payload=payload,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def enqueue(
|
|
211
|
+
queue: str,
|
|
212
|
+
payload: Dict[str, Any],
|
|
213
|
+
server_url: str,
|
|
214
|
+
api_key: str,
|
|
215
|
+
timeout: int = 300
|
|
216
|
+
) -> TaskInfo:
|
|
217
|
+
"""Quick enqueue without creating a client instance.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
queue: Queue name (e.g., "inference_default")
|
|
221
|
+
payload: Task payload data
|
|
222
|
+
server_url: Base URL of the runqy server
|
|
223
|
+
api_key: API key for authentication
|
|
224
|
+
timeout: Task execution timeout in seconds (default: 300)
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
TaskInfo with task_id and initial state
|
|
228
|
+
|
|
229
|
+
Example:
|
|
230
|
+
from runqy_python import enqueue
|
|
231
|
+
|
|
232
|
+
task = enqueue(
|
|
233
|
+
"inference_default",
|
|
234
|
+
{"input": "hello"},
|
|
235
|
+
server_url="http://localhost:3000",
|
|
236
|
+
api_key="your-api-key"
|
|
237
|
+
)
|
|
238
|
+
print(f"Task ID: {task.task_id}")
|
|
239
|
+
"""
|
|
240
|
+
return RunqyClient(server_url, api_key).enqueue(queue, payload, timeout)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Task decorators for registering handler and load functions."""
|
|
2
|
+
|
|
3
|
+
_registered_handler = None
|
|
4
|
+
_registered_loader = None
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def task(func):
|
|
8
|
+
"""Decorator to register a function as the task handler.
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
@task
|
|
12
|
+
def process(payload: dict) -> dict:
|
|
13
|
+
return {"result": "..."}
|
|
14
|
+
|
|
15
|
+
# With context from @load:
|
|
16
|
+
@task
|
|
17
|
+
def process(payload: dict, ctx: dict) -> dict:
|
|
18
|
+
return ctx["model"].predict(payload)
|
|
19
|
+
"""
|
|
20
|
+
global _registered_handler
|
|
21
|
+
_registered_handler = func
|
|
22
|
+
return func
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def load(func):
|
|
26
|
+
"""Decorator to register a function that runs once at startup.
|
|
27
|
+
|
|
28
|
+
The load function is called before the ready signal is sent.
|
|
29
|
+
Its return value is passed as the second argument (ctx) to the task handler.
|
|
30
|
+
|
|
31
|
+
Usage:
|
|
32
|
+
@load
|
|
33
|
+
def setup():
|
|
34
|
+
model = load_heavy_model()
|
|
35
|
+
return {"model": model}
|
|
36
|
+
|
|
37
|
+
@task
|
|
38
|
+
def process(payload: dict, ctx: dict) -> dict:
|
|
39
|
+
return ctx["model"].predict(payload)
|
|
40
|
+
"""
|
|
41
|
+
global _registered_loader
|
|
42
|
+
_registered_loader = func
|
|
43
|
+
return func
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_handler():
|
|
47
|
+
"""Get the registered task handler."""
|
|
48
|
+
return _registered_handler
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_loader():
|
|
52
|
+
"""Get the registered load function."""
|
|
53
|
+
return _registered_loader
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Runner loop for processing tasks from runqy-worker."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import json
|
|
5
|
+
from .decorator import get_handler, get_loader
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def run():
|
|
9
|
+
"""Main loop: load, ready signal, read tasks, call handler, write responses.
|
|
10
|
+
|
|
11
|
+
This function:
|
|
12
|
+
1. Calls the @load function if registered (for model loading, etc.)
|
|
13
|
+
2. Sends {"status": "ready"} to signal readiness to runqy-worker
|
|
14
|
+
3. Reads JSON task requests from stdin (one per line)
|
|
15
|
+
4. Calls the registered @task handler with the payload (and context if @load was used)
|
|
16
|
+
5. Writes JSON responses to stdout
|
|
17
|
+
"""
|
|
18
|
+
handler = get_handler()
|
|
19
|
+
if handler is None:
|
|
20
|
+
raise RuntimeError("No task handler registered. Use @task decorator.")
|
|
21
|
+
|
|
22
|
+
# Run load function if registered (before ready signal)
|
|
23
|
+
loader = get_loader()
|
|
24
|
+
ctx = None
|
|
25
|
+
if loader is not None:
|
|
26
|
+
ctx = loader()
|
|
27
|
+
|
|
28
|
+
# Ready signal
|
|
29
|
+
print(json.dumps({"status": "ready"}))
|
|
30
|
+
sys.stdout.flush()
|
|
31
|
+
|
|
32
|
+
# Process tasks from stdin
|
|
33
|
+
for line in sys.stdin:
|
|
34
|
+
line = line.strip()
|
|
35
|
+
if not line:
|
|
36
|
+
continue
|
|
37
|
+
|
|
38
|
+
task_id = "unknown"
|
|
39
|
+
try:
|
|
40
|
+
task_data = json.loads(line)
|
|
41
|
+
task_id = task_data.get("task_id", "unknown")
|
|
42
|
+
payload = task_data.get("payload", {})
|
|
43
|
+
|
|
44
|
+
# Call handler with or without context
|
|
45
|
+
if ctx is not None:
|
|
46
|
+
result = handler(payload, ctx)
|
|
47
|
+
else:
|
|
48
|
+
result = handler(payload)
|
|
49
|
+
|
|
50
|
+
response = {
|
|
51
|
+
"task_id": task_id,
|
|
52
|
+
"result": result,
|
|
53
|
+
"error": None,
|
|
54
|
+
"retry": False
|
|
55
|
+
}
|
|
56
|
+
except Exception as e:
|
|
57
|
+
response = {
|
|
58
|
+
"task_id": task_id,
|
|
59
|
+
"result": None,
|
|
60
|
+
"error": str(e),
|
|
61
|
+
"retry": False
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
print(json.dumps(response))
|
|
65
|
+
sys.stdout.flush()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def run_once():
|
|
69
|
+
"""Process a single task from stdin and exit.
|
|
70
|
+
|
|
71
|
+
Use this for lightweight tasks that don't need to stay loaded in memory.
|
|
72
|
+
|
|
73
|
+
Flow:
|
|
74
|
+
1. Calls @load function if registered
|
|
75
|
+
2. Sends {"status": "ready"}
|
|
76
|
+
3. Reads ONE JSON task from stdin
|
|
77
|
+
4. Calls @task handler
|
|
78
|
+
5. Writes response to stdout
|
|
79
|
+
6. Exits
|
|
80
|
+
"""
|
|
81
|
+
handler = get_handler()
|
|
82
|
+
if handler is None:
|
|
83
|
+
raise RuntimeError("No task handler registered. Use @task decorator.")
|
|
84
|
+
|
|
85
|
+
# Run load function if registered (before ready signal)
|
|
86
|
+
loader = get_loader()
|
|
87
|
+
ctx = None
|
|
88
|
+
if loader is not None:
|
|
89
|
+
ctx = loader()
|
|
90
|
+
|
|
91
|
+
# Ready signal
|
|
92
|
+
print(json.dumps({"status": "ready"}))
|
|
93
|
+
sys.stdout.flush()
|
|
94
|
+
|
|
95
|
+
# Read ONE task
|
|
96
|
+
line = sys.stdin.readline().strip()
|
|
97
|
+
if not line:
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
task_id = "unknown"
|
|
101
|
+
try:
|
|
102
|
+
task_data = json.loads(line)
|
|
103
|
+
task_id = task_data.get("task_id", "unknown")
|
|
104
|
+
payload = task_data.get("payload", {})
|
|
105
|
+
|
|
106
|
+
# Call handler with or without context
|
|
107
|
+
if ctx is not None:
|
|
108
|
+
result = handler(payload, ctx)
|
|
109
|
+
else:
|
|
110
|
+
result = handler(payload)
|
|
111
|
+
|
|
112
|
+
response = {
|
|
113
|
+
"task_id": task_id,
|
|
114
|
+
"result": result,
|
|
115
|
+
"error": None,
|
|
116
|
+
"retry": False
|
|
117
|
+
}
|
|
118
|
+
except Exception as e:
|
|
119
|
+
response = {
|
|
120
|
+
"task_id": task_id,
|
|
121
|
+
"result": None,
|
|
122
|
+
"error": str(e),
|
|
123
|
+
"retry": False
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
print(json.dumps(response))
|
|
127
|
+
sys.stdout.flush()
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: runqy-python
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Python SDK for runqy - write distributed task handlers with simple decorators
|
|
5
|
+
Author: Publikey
|
|
6
|
+
Project-URL: Documentation, https://docs.runqy.com
|
|
7
|
+
Project-URL: Repository, https://github.com/Publikey/runqy-python
|
|
8
|
+
Project-URL: Issues, https://github.com/Publikey/runqy-python/issues
|
|
9
|
+
Keywords: task-queue,distributed,worker,redis,async,client
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.8
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
|
|
24
|
+
<p align="center">
|
|
25
|
+
<img src="assets/logo.svg" alt="runqy logo" width="80" height="80">
|
|
26
|
+
</p>
|
|
27
|
+
|
|
28
|
+
<h1 align="center">runqy-python</h1>
|
|
29
|
+
|
|
30
|
+
<p align="center">
|
|
31
|
+
Python SDK for <a href="https://runqy.com">runqy</a> - write distributed task handlers with simple decorators.
|
|
32
|
+
<br>
|
|
33
|
+
<a href="https://docs.runqy.com"><strong>Documentation</strong></a> · <a href="https://runqy.com"><strong>Website</strong></a>
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install runqy-python
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Task Handlers
|
|
43
|
+
|
|
44
|
+
Create tasks that run on [runqy-worker](https://github.com/publikey/runqy-worker) using simple decorators:
|
|
45
|
+
|
|
46
|
+
### Simple Task
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from runqy_python import task, run
|
|
50
|
+
|
|
51
|
+
@task
|
|
52
|
+
def process(payload: dict) -> dict:
|
|
53
|
+
return {"message": "Hello!", "received": payload}
|
|
54
|
+
|
|
55
|
+
if __name__ == "__main__":
|
|
56
|
+
run()
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### With Model Loading
|
|
60
|
+
|
|
61
|
+
For ML inference tasks, use `@load` to load models once at startup:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from runqy_python import task, load, run
|
|
65
|
+
|
|
66
|
+
@load
|
|
67
|
+
def setup():
|
|
68
|
+
"""Runs once before ready signal. Return value is passed to @task as ctx."""
|
|
69
|
+
model = load_heavy_model() # Load weights, etc.
|
|
70
|
+
return {"model": model}
|
|
71
|
+
|
|
72
|
+
@task
|
|
73
|
+
def process(payload: dict, ctx: dict) -> dict:
|
|
74
|
+
"""Process tasks using the loaded model."""
|
|
75
|
+
prediction = ctx["model"].predict(payload["input"])
|
|
76
|
+
return {"prediction": prediction}
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__":
|
|
79
|
+
run()
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### One-Shot Tasks
|
|
83
|
+
|
|
84
|
+
For lightweight tasks that don't need to stay loaded in memory, use `run_once()`:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from runqy_python import task, run_once
|
|
88
|
+
|
|
89
|
+
@task
|
|
90
|
+
def process(payload: dict) -> dict:
|
|
91
|
+
return {"result": payload["x"] * 2}
|
|
92
|
+
|
|
93
|
+
if __name__ == "__main__":
|
|
94
|
+
run_once() # Process one task and exit
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
| Function | Behavior | Use case |
|
|
98
|
+
|----------|----------|----------|
|
|
99
|
+
| `run()` | Loops forever, handles many tasks | ML inference (expensive load) |
|
|
100
|
+
| `run_once()` | Handles ONE task, exits | Lightweight tasks |
|
|
101
|
+
|
|
102
|
+
## Protocol
|
|
103
|
+
|
|
104
|
+
The SDK handles the runqy-worker stdin/stdout JSON protocol:
|
|
105
|
+
|
|
106
|
+
1. **Load phase**: Calls `@load` function (if registered)
|
|
107
|
+
2. **Ready signal**: Sends `{"status": "ready"}` after load completes
|
|
108
|
+
3. **Task input**: Reads JSON from stdin: `{"task_id": "...", "payload": {...}}`
|
|
109
|
+
4. **Response**: Writes JSON to stdout: `{"task_id": "...", "result": {...}, "error": null, "retry": false}`
|
|
110
|
+
|
|
111
|
+
## Client (Optional)
|
|
112
|
+
|
|
113
|
+
The SDK also includes a client for enqueuing tasks to a runqy server:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from runqy_python import RunqyClient
|
|
117
|
+
|
|
118
|
+
client = RunqyClient("http://localhost:3000", api_key="your-api-key")
|
|
119
|
+
|
|
120
|
+
# Enqueue a task
|
|
121
|
+
task = client.enqueue("inference_default", {"input": "hello"})
|
|
122
|
+
print(f"Task ID: {task.task_id}")
|
|
123
|
+
|
|
124
|
+
# Check result
|
|
125
|
+
result = client.get_task(task.task_id)
|
|
126
|
+
print(f"State: {result.state}, Result: {result.result}")
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Or use the convenience function:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from runqy_python import enqueue
|
|
133
|
+
|
|
134
|
+
task = enqueue(
|
|
135
|
+
"inference_default",
|
|
136
|
+
{"input": "hello"},
|
|
137
|
+
server_url="http://localhost:3000",
|
|
138
|
+
api_key="your-api-key"
|
|
139
|
+
)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Client API
|
|
143
|
+
|
|
144
|
+
**RunqyClient(server_url, api_key, timeout=30)**
|
|
145
|
+
- `server_url`: Base URL of the runqy server
|
|
146
|
+
- `api_key`: API key for authentication
|
|
147
|
+
- `timeout`: Default request timeout in seconds
|
|
148
|
+
|
|
149
|
+
**client.enqueue(queue, payload, timeout=300)**
|
|
150
|
+
- `queue`: Queue name (e.g., `"inference_default"`)
|
|
151
|
+
- `payload`: Task payload as a dict
|
|
152
|
+
- `timeout`: Task execution timeout in seconds
|
|
153
|
+
- Returns: `TaskInfo` with `task_id`, `queue`, `state`
|
|
154
|
+
|
|
155
|
+
**client.get_task(task_id)**
|
|
156
|
+
- `task_id`: Task ID from enqueue
|
|
157
|
+
- Returns: `TaskInfo` with `task_id`, `queue`, `state`, `result`, `error`
|
|
158
|
+
|
|
159
|
+
### Exceptions
|
|
160
|
+
|
|
161
|
+
- `RunqyError`: Base exception for all client errors
|
|
162
|
+
- `AuthenticationError`: Invalid or missing API key
|
|
163
|
+
- `TaskNotFoundError`: Task ID doesn't exist
|
|
164
|
+
|
|
165
|
+
## Development
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
# Install in editable mode
|
|
169
|
+
pip install -e .
|
|
170
|
+
|
|
171
|
+
# Test task execution
|
|
172
|
+
echo '{"task_id":"t1","payload":{"foo":"bar"}}' | python your_model.py
|
|
173
|
+
|
|
174
|
+
# Test client import
|
|
175
|
+
python -c "from runqy_python import RunqyClient; print('OK')"
|
|
176
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/runqy_python/__init__.py
|
|
5
|
+
src/runqy_python/client.py
|
|
6
|
+
src/runqy_python/decorator.py
|
|
7
|
+
src/runqy_python/runner.py
|
|
8
|
+
src/runqy_python.egg-info/PKG-INFO
|
|
9
|
+
src/runqy_python.egg-info/SOURCES.txt
|
|
10
|
+
src/runqy_python.egg-info/dependency_links.txt
|
|
11
|
+
src/runqy_python.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
runqy_python
|