ppio-gpus 0.0.1__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.
- ppio_gpus-0.0.1/.gitignore +56 -0
- ppio_gpus-0.0.1/PKG-INFO +100 -0
- ppio_gpus-0.0.1/README.md +71 -0
- ppio_gpus-0.0.1/ppio_gpus-0.1.0.tar.gz +0 -0
- ppio_gpus-0.0.1/pyproject.toml +43 -0
- ppio_gpus-0.0.1/src/ppio_gpus/__init__.py +99 -0
- ppio_gpus-0.0.1/src/ppio_gpus/client.py +264 -0
- ppio_gpus-0.0.1/src/ppio_gpus/http_client.py +40 -0
- ppio_gpus-0.0.1/src/ppio_gpus/modules/__init__.py +1 -0
- ppio_gpus-0.0.1/src/ppio_gpus/modules/handler.py +9 -0
- ppio_gpus-0.0.1/src/ppio_gpus/modules/http.py +81 -0
- ppio_gpus-0.0.1/src/ppio_gpus/modules/job.py +211 -0
- ppio_gpus-0.0.1/src/ppio_gpus/modules/logger.py +90 -0
- ppio_gpus-0.0.1/src/ppio_gpus/modules/ping.py +90 -0
- ppio_gpus-0.0.1/src/ppio_gpus/modules/progress.py +44 -0
- ppio_gpus-0.0.1/src/ppio_gpus/modules/scale.py +182 -0
- ppio_gpus-0.0.1/src/ppio_gpus/modules/upload.py +266 -0
- ppio_gpus-0.0.1/src/ppio_gpus/modules/worker_state.py +147 -0
- ppio_gpus-0.0.1/src/ppio_gpus/worker.py +25 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# If you prefer the allow list template instead of the deny list, see community template:
|
|
2
|
+
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
|
3
|
+
#
|
|
4
|
+
# Binaries for programs and plugins
|
|
5
|
+
*.exe
|
|
6
|
+
*.exe~
|
|
7
|
+
*.dll
|
|
8
|
+
*.so
|
|
9
|
+
*.dylib
|
|
10
|
+
|
|
11
|
+
# Test binary, built with `go test -c`
|
|
12
|
+
*.test
|
|
13
|
+
|
|
14
|
+
# Output of the go coverage tool, specifically when used with LiteIDE
|
|
15
|
+
*.out
|
|
16
|
+
|
|
17
|
+
# Dependency directories (remove the comment below to include it)
|
|
18
|
+
# vendor/
|
|
19
|
+
|
|
20
|
+
# Go workspace file
|
|
21
|
+
go.work
|
|
22
|
+
go.work.sum
|
|
23
|
+
|
|
24
|
+
# env file
|
|
25
|
+
.env
|
|
26
|
+
vendor/
|
|
27
|
+
|
|
28
|
+
# controller-gen
|
|
29
|
+
bin/
|
|
30
|
+
|
|
31
|
+
.stfolder
|
|
32
|
+
.stignore
|
|
33
|
+
.idea/
|
|
34
|
+
pkg/mod/
|
|
35
|
+
pkg/sumdb/
|
|
36
|
+
|
|
37
|
+
cmd/autoscaler/autoscaler
|
|
38
|
+
cmd/datawatcher/datawatcher
|
|
39
|
+
cmd/gateway/gateway
|
|
40
|
+
cmd/monolithic-gateway/gateway
|
|
41
|
+
cmd/taskproxy/taskproxy
|
|
42
|
+
cmd/taskscheduler/taskscheduler
|
|
43
|
+
cmd/metricserver/metricserver
|
|
44
|
+
deploy/nitor-*.tgz
|
|
45
|
+
deploy/manifests.yaml
|
|
46
|
+
|
|
47
|
+
main
|
|
48
|
+
*.pkl
|
|
49
|
+
*.pkl.lock
|
|
50
|
+
|
|
51
|
+
# Python
|
|
52
|
+
__pycache__/
|
|
53
|
+
*.pyc
|
|
54
|
+
*.egg-info/
|
|
55
|
+
dist/
|
|
56
|
+
.venv/
|
ppio_gpus-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ppio-gpus
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: PPIO GPUs SDK - Python SDK for PPIO serverless GPU endpoints
|
|
5
|
+
Project-URL: Homepage, https://ppio.com/
|
|
6
|
+
Author-email: PPIO <support@ppio.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Keywords: ai,gpu,inference,ppio,sdk,serverless
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
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 :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Requires-Dist: aiohttp-retry>=2.8.3
|
|
23
|
+
Requires-Dist: aiohttp>=3.9.0
|
|
24
|
+
Requires-Dist: boto3>=1.28.0
|
|
25
|
+
Requires-Dist: filelock>=3.12.0
|
|
26
|
+
Requires-Dist: requests>=2.25.0
|
|
27
|
+
Requires-Dist: urllib3>=2.0.0
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# ppio-gpus
|
|
31
|
+
|
|
32
|
+
Python SDK for PPIO serverless GPU endpoints.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install ppio-gpus
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick Start (Client)
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import ppio_gpus
|
|
44
|
+
|
|
45
|
+
ppio_gpus.api_key = "your-api-key"
|
|
46
|
+
|
|
47
|
+
endpoint = ppio_gpus.Endpoint("your-endpoint-id")
|
|
48
|
+
job = endpoint.run({"prompt": "hello world"})
|
|
49
|
+
|
|
50
|
+
print(job.status())
|
|
51
|
+
output = job.output(timeout=60)
|
|
52
|
+
print(output)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Quick Start (Worker)
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
import ppio_gpus
|
|
59
|
+
|
|
60
|
+
def handler(job):
|
|
61
|
+
job_input = job["input"]
|
|
62
|
+
# ... process ...
|
|
63
|
+
return {"result": "done"}
|
|
64
|
+
|
|
65
|
+
ppio_gpus.start({"handler": handler})
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Client API
|
|
69
|
+
|
|
70
|
+
- `Endpoint(endpoint_id)` -- create an endpoint client
|
|
71
|
+
- `Endpoint.run(input)` -- async submit a task, returns a `Job` immediately
|
|
72
|
+
- `Endpoint.health()` -- check endpoint health
|
|
73
|
+
- `Endpoint.purge_queue()` -- cancel all pending tasks
|
|
74
|
+
- `Job.status()` -- get current task status
|
|
75
|
+
- `Job.output(timeout=0)` -- get task output, optionally poll until complete
|
|
76
|
+
- `Job.cancel()` -- cancel a running task
|
|
77
|
+
|
|
78
|
+
## Worker API
|
|
79
|
+
|
|
80
|
+
- `ppio_gpus.start(config)` -- start the worker loop (blocks until SIGTERM)
|
|
81
|
+
- `ppio_gpus.progress_update(job, data)` -- send in-progress status update
|
|
82
|
+
- `ppio_gpus.upload_image(job_id, path)` -- upload image to S3, returns presigned URL
|
|
83
|
+
- `ppio_gpus.upload_file(name, path)` -- upload file to S3
|
|
84
|
+
- `ppio_gpus.upload_bytes(name, data)` -- upload bytes to S3
|
|
85
|
+
|
|
86
|
+
## Configuration
|
|
87
|
+
|
|
88
|
+
Set credentials via environment variables:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
export PPIO_API_KEY="your-api-key"
|
|
92
|
+
export PPIO_ENDPOINT_BASE_URL="https://async-public.serverless.ppinfra.com/v1" # default
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Or set them directly:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
ppio_gpus.api_key = "your-api-key"
|
|
99
|
+
ppio_gpus.endpoint_url_base = "https://your-custom-url/v1"
|
|
100
|
+
```
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# ppio-gpus
|
|
2
|
+
|
|
3
|
+
Python SDK for PPIO serverless GPU endpoints.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install ppio-gpus
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start (Client)
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
import ppio_gpus
|
|
15
|
+
|
|
16
|
+
ppio_gpus.api_key = "your-api-key"
|
|
17
|
+
|
|
18
|
+
endpoint = ppio_gpus.Endpoint("your-endpoint-id")
|
|
19
|
+
job = endpoint.run({"prompt": "hello world"})
|
|
20
|
+
|
|
21
|
+
print(job.status())
|
|
22
|
+
output = job.output(timeout=60)
|
|
23
|
+
print(output)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick Start (Worker)
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
import ppio_gpus
|
|
30
|
+
|
|
31
|
+
def handler(job):
|
|
32
|
+
job_input = job["input"]
|
|
33
|
+
# ... process ...
|
|
34
|
+
return {"result": "done"}
|
|
35
|
+
|
|
36
|
+
ppio_gpus.start({"handler": handler})
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Client API
|
|
40
|
+
|
|
41
|
+
- `Endpoint(endpoint_id)` -- create an endpoint client
|
|
42
|
+
- `Endpoint.run(input)` -- async submit a task, returns a `Job` immediately
|
|
43
|
+
- `Endpoint.health()` -- check endpoint health
|
|
44
|
+
- `Endpoint.purge_queue()` -- cancel all pending tasks
|
|
45
|
+
- `Job.status()` -- get current task status
|
|
46
|
+
- `Job.output(timeout=0)` -- get task output, optionally poll until complete
|
|
47
|
+
- `Job.cancel()` -- cancel a running task
|
|
48
|
+
|
|
49
|
+
## Worker API
|
|
50
|
+
|
|
51
|
+
- `ppio_gpus.start(config)` -- start the worker loop (blocks until SIGTERM)
|
|
52
|
+
- `ppio_gpus.progress_update(job, data)` -- send in-progress status update
|
|
53
|
+
- `ppio_gpus.upload_image(job_id, path)` -- upload image to S3, returns presigned URL
|
|
54
|
+
- `ppio_gpus.upload_file(name, path)` -- upload file to S3
|
|
55
|
+
- `ppio_gpus.upload_bytes(name, data)` -- upload bytes to S3
|
|
56
|
+
|
|
57
|
+
## Configuration
|
|
58
|
+
|
|
59
|
+
Set credentials via environment variables:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
export PPIO_API_KEY="your-api-key"
|
|
63
|
+
export PPIO_ENDPOINT_BASE_URL="https://async-public.serverless.ppinfra.com/v1" # default
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Or set them directly:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
ppio_gpus.api_key = "your-api-key"
|
|
70
|
+
ppio_gpus.endpoint_url_base = "https://your-custom-url/v1"
|
|
71
|
+
```
|
|
Binary file
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling >= 1.26"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ppio-gpus"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "PPIO GPUs SDK - Python SDK for PPIO serverless GPU endpoints"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "PPIO", email = "support@ppio.com" },
|
|
13
|
+
]
|
|
14
|
+
keywords = ["ppio", "serverless", "gpu", "ai", "inference", "sdk"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.8",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
27
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
28
|
+
]
|
|
29
|
+
requires-python = ">=3.8"
|
|
30
|
+
dependencies = [
|
|
31
|
+
"requests>=2.25.0",
|
|
32
|
+
"aiohttp>=3.9.0",
|
|
33
|
+
"aiohttp-retry>=2.8.3",
|
|
34
|
+
"boto3>=1.28.0",
|
|
35
|
+
"urllib3>=2.0.0",
|
|
36
|
+
"filelock>=3.12.0",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://ppio.com/"
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.wheel]
|
|
43
|
+
packages = ["src/ppio_gpus"]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PPIO GPUs SDK
|
|
3
|
+
|
|
4
|
+
Usage (client)::
|
|
5
|
+
|
|
6
|
+
import ppio_gpus
|
|
7
|
+
|
|
8
|
+
ppio_gpus.api_key = "your-api-key"
|
|
9
|
+
endpoint = ppio_gpus.Endpoint("your-endpoint-id")
|
|
10
|
+
job = endpoint.run({"prompt": "hello"})
|
|
11
|
+
print(job.output(timeout=60))
|
|
12
|
+
|
|
13
|
+
Usage (worker)::
|
|
14
|
+
|
|
15
|
+
import ppio_gpus
|
|
16
|
+
|
|
17
|
+
def handler(job):
|
|
18
|
+
return {"result": job["input"]}
|
|
19
|
+
|
|
20
|
+
ppio_gpus.start({"handler": handler})
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import os
|
|
25
|
+
import signal
|
|
26
|
+
import sys
|
|
27
|
+
from typing import Any, Dict
|
|
28
|
+
|
|
29
|
+
from .client import Endpoint, Job, HTTPClient
|
|
30
|
+
from .modules.logger import PPIOLogger
|
|
31
|
+
from .modules.progress import progress_update
|
|
32
|
+
from .modules.upload import upload_image, upload_images, upload_file, upload_bytes
|
|
33
|
+
from .worker import run_worker
|
|
34
|
+
|
|
35
|
+
api_key = os.environ.get("PPIO_API_KEY", None)
|
|
36
|
+
endpoint_url_base = os.environ.get(
|
|
37
|
+
"PPIO_ENDPOINT_BASE_URL", "https://async-public.serverless.ppinfra.com/v1"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
# client
|
|
42
|
+
"Endpoint", "Job", "HTTPClient",
|
|
43
|
+
"api_key", "endpoint_url_base",
|
|
44
|
+
# worker
|
|
45
|
+
"start", "progress_update",
|
|
46
|
+
"upload_image", "upload_images", "upload_file", "upload_bytes",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
log = PPIOLogger()
|
|
50
|
+
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
# CLI arguments
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
_parser = argparse.ArgumentParser(prog="ppio_gpus", description="PPIO Serverless Worker")
|
|
55
|
+
_parser.add_argument(
|
|
56
|
+
"--log_level",
|
|
57
|
+
type=str,
|
|
58
|
+
default=None,
|
|
59
|
+
help="Log level: ERROR, WARN, INFO, DEBUG (default: DEBUG).",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _set_config_args(config: dict) -> dict:
|
|
64
|
+
args, unknown = _parser.parse_known_args()
|
|
65
|
+
sys.argv = [sys.argv[0]] + unknown
|
|
66
|
+
config["rp_args"] = vars(args)
|
|
67
|
+
|
|
68
|
+
if config["rp_args"].get("log_level"):
|
|
69
|
+
log.set_level(config["rp_args"]["log_level"])
|
|
70
|
+
|
|
71
|
+
return config
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _signal_handler(sig, frame):
|
|
75
|
+
del sig, frame
|
|
76
|
+
log.info("SIGINT received, shutting down.")
|
|
77
|
+
sys.exit(0)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ---------------------------------------------------------------------------
|
|
81
|
+
# Public API
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
def start(config: Dict[str, Any]) -> None:
|
|
84
|
+
"""
|
|
85
|
+
Start the PPIO serverless worker.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
config: dict with at least ``"handler"`` (Callable).
|
|
89
|
+
Optional keys:
|
|
90
|
+
- ``"concurrency_modifier"`` -- callable(int) -> int
|
|
91
|
+
- ``"refresh_worker"`` -- bool, restart worker after each job
|
|
92
|
+
- ``"return_aggregate_stream"`` -- bool, accumulate stream chunks
|
|
93
|
+
"""
|
|
94
|
+
print("--- Starting PPIO Serverless Worker ---")
|
|
95
|
+
|
|
96
|
+
signal.signal(signal.SIGINT, _signal_handler)
|
|
97
|
+
config = _set_config_args(config)
|
|
98
|
+
|
|
99
|
+
run_worker(config)
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Async GPU Serverless Client SDK
|
|
3
|
+
|
|
4
|
+
Shared client module for submitting tasks to the async gateway.
|
|
5
|
+
Provides Endpoint, Job, and HTTPClient classes for async task submission.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
from typing import Any, Dict, Optional
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
from requests.adapters import HTTPAdapter, Retry
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
API_KEY_NOT_SET_MSG = (
|
|
16
|
+
"API key has not been set. Please set it via the api_key parameter "
|
|
17
|
+
"or the appropriate environment variable."
|
|
18
|
+
)
|
|
19
|
+
UNAUTHORIZED_MSG = "Unauthorized — invalid or expired API key."
|
|
20
|
+
FINAL_STATES = {"COMPLETED", "FAILED", "TIMED_OUT", "CANCELLED"}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _is_completed(status: str) -> bool:
|
|
24
|
+
return status in FINAL_STATES
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------------------- #
|
|
28
|
+
# Client #
|
|
29
|
+
# ---------------------------------------------------------------------------- #
|
|
30
|
+
class HTTPClient:
|
|
31
|
+
"""A client for making requests to the async-gateway."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, api_key: str, endpoint_url_base: str):
|
|
34
|
+
"""
|
|
35
|
+
Initialize an HTTPClient instance.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
api_key: The API key for authentication.
|
|
39
|
+
endpoint_url_base: The base URL for the async-gateway v1 API.
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
RuntimeError: If the API key is not provided.
|
|
43
|
+
"""
|
|
44
|
+
if api_key is None:
|
|
45
|
+
raise RuntimeError(API_KEY_NOT_SET_MSG)
|
|
46
|
+
|
|
47
|
+
self.api_key = api_key
|
|
48
|
+
self.endpoint_url_base = endpoint_url_base.rstrip("/")
|
|
49
|
+
|
|
50
|
+
self.session = requests.Session()
|
|
51
|
+
retries = Retry(total=5, backoff_factor=1, status_forcelist=[408, 429])
|
|
52
|
+
self.session.mount("http://", HTTPAdapter(max_retries=retries))
|
|
53
|
+
self.session.mount("https://", HTTPAdapter(max_retries=retries))
|
|
54
|
+
|
|
55
|
+
self.headers = {
|
|
56
|
+
"Content-Type": "application/json",
|
|
57
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
def _request(
|
|
61
|
+
self,
|
|
62
|
+
method: str,
|
|
63
|
+
endpoint: str,
|
|
64
|
+
data: Optional[dict] = None,
|
|
65
|
+
timeout: int = 10,
|
|
66
|
+
):
|
|
67
|
+
"""
|
|
68
|
+
Make a request to the specified endpoint using the given HTTP method.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
method: The HTTP method to use ('GET' or 'POST').
|
|
72
|
+
endpoint: The endpoint path appended to endpoint_url_base.
|
|
73
|
+
data: The JSON payload to send with the request.
|
|
74
|
+
timeout: Seconds to wait before giving up.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
The JSON response from the server.
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
RuntimeError: If the response returns a 401 Unauthorized status.
|
|
81
|
+
requests.HTTPError: If the response contains an unsuccessful status code.
|
|
82
|
+
"""
|
|
83
|
+
url = f"{self.endpoint_url_base}/{endpoint}"
|
|
84
|
+
response = self.session.request(
|
|
85
|
+
method, url, headers=self.headers, json=data, timeout=timeout
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if response.status_code == 401:
|
|
89
|
+
raise RuntimeError(UNAUTHORIZED_MSG)
|
|
90
|
+
|
|
91
|
+
response.raise_for_status()
|
|
92
|
+
return response.json()
|
|
93
|
+
|
|
94
|
+
def post(self, endpoint: str, data: Optional[dict] = None, timeout: int = 10):
|
|
95
|
+
"""Post to the endpoint."""
|
|
96
|
+
return self._request("POST", endpoint, data, timeout)
|
|
97
|
+
|
|
98
|
+
def get(self, endpoint: str, timeout: int = 10):
|
|
99
|
+
"""Get from the endpoint."""
|
|
100
|
+
return self._request("GET", endpoint, timeout=timeout)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ---------------------------------------------------------------------------- #
|
|
104
|
+
# Job #
|
|
105
|
+
# ---------------------------------------------------------------------------- #
|
|
106
|
+
class Job:
|
|
107
|
+
"""Represents a submitted task on the async-gateway."""
|
|
108
|
+
|
|
109
|
+
def __init__(self, endpoint_id: str, job_id: str, client: HTTPClient):
|
|
110
|
+
"""
|
|
111
|
+
Initialize a Job instance.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
endpoint_id: The identifier for the endpoint.
|
|
115
|
+
job_id: The identifier for the task.
|
|
116
|
+
client: An HTTPClient instance to make requests with.
|
|
117
|
+
"""
|
|
118
|
+
self.endpoint_id = endpoint_id
|
|
119
|
+
self.job_id = job_id
|
|
120
|
+
self._client = client
|
|
121
|
+
|
|
122
|
+
self._status: Optional[str] = None
|
|
123
|
+
self._output: Optional[Any] = None
|
|
124
|
+
|
|
125
|
+
def _fetch_job(self) -> Dict[str, Any]:
|
|
126
|
+
"""Fetch current task state from the gateway."""
|
|
127
|
+
status_url = f"{self.endpoint_id}/status/{self.job_id}"
|
|
128
|
+
job_state = self._client.get(endpoint=status_url)
|
|
129
|
+
|
|
130
|
+
if _is_completed(job_state["status"]):
|
|
131
|
+
self._status = job_state["status"]
|
|
132
|
+
self._output = job_state.get("output", None)
|
|
133
|
+
|
|
134
|
+
return job_state
|
|
135
|
+
|
|
136
|
+
def status(self) -> str:
|
|
137
|
+
"""
|
|
138
|
+
Returns the status of the task.
|
|
139
|
+
|
|
140
|
+
Terminal states are cached so subsequent calls avoid extra requests.
|
|
141
|
+
"""
|
|
142
|
+
if self._status is not None:
|
|
143
|
+
return self._status
|
|
144
|
+
|
|
145
|
+
return self._fetch_job()["status"]
|
|
146
|
+
|
|
147
|
+
def output(self, timeout: int = 0) -> Any:
|
|
148
|
+
"""
|
|
149
|
+
Returns the output of the task.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
timeout: If > 0, polls every second until the task reaches a
|
|
153
|
+
terminal state or the timeout is exceeded.
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
TimeoutError: If the timeout is exceeded before the task completes.
|
|
157
|
+
"""
|
|
158
|
+
if timeout > 0:
|
|
159
|
+
remaining = timeout
|
|
160
|
+
while not _is_completed(self.status()):
|
|
161
|
+
time.sleep(1)
|
|
162
|
+
remaining -= 1
|
|
163
|
+
if remaining <= 0:
|
|
164
|
+
raise TimeoutError("Job timed out.")
|
|
165
|
+
|
|
166
|
+
if self._output is not None:
|
|
167
|
+
return self._output
|
|
168
|
+
|
|
169
|
+
return self._fetch_job().get("output", None)
|
|
170
|
+
|
|
171
|
+
def cancel(self, timeout: int = 3) -> Any:
|
|
172
|
+
"""
|
|
173
|
+
Cancel the task.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
timeout: Seconds to wait for the server to respond.
|
|
177
|
+
"""
|
|
178
|
+
return self._client.post(
|
|
179
|
+
f"{self.endpoint_id}/cancel/{self.job_id}",
|
|
180
|
+
data=None,
|
|
181
|
+
timeout=timeout,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ---------------------------------------------------------------------------- #
|
|
186
|
+
# Endpoint #
|
|
187
|
+
# ---------------------------------------------------------------------------- #
|
|
188
|
+
class Endpoint:
|
|
189
|
+
"""Manages an endpoint for submitting tasks to the async-gateway."""
|
|
190
|
+
|
|
191
|
+
def __init__(
|
|
192
|
+
self,
|
|
193
|
+
endpoint_id: str,
|
|
194
|
+
api_key: Optional[str] = None,
|
|
195
|
+
endpoint_url_base: Optional[str] = None,
|
|
196
|
+
):
|
|
197
|
+
"""
|
|
198
|
+
Initialize an Endpoint instance.
|
|
199
|
+
|
|
200
|
+
If api_key or endpoint_url_base are not provided, the constructor
|
|
201
|
+
reads module-level globals ``ppio_gpus.api_key`` and
|
|
202
|
+
``ppio_gpus.endpoint_url_base``.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
endpoint_id: The identifier for the endpoint.
|
|
206
|
+
api_key: Optional API key. Falls back to ppio_gpus.api_key.
|
|
207
|
+
endpoint_url_base: Optional base URL. Falls back to ppio_gpus.endpoint_url_base.
|
|
208
|
+
"""
|
|
209
|
+
self.endpoint_id = endpoint_id
|
|
210
|
+
|
|
211
|
+
resolved_key = api_key
|
|
212
|
+
resolved_base = endpoint_url_base
|
|
213
|
+
|
|
214
|
+
if resolved_key is None or resolved_base is None:
|
|
215
|
+
import ppio_gpus as _pkg
|
|
216
|
+
if resolved_key is None:
|
|
217
|
+
resolved_key = getattr(_pkg, "api_key", None)
|
|
218
|
+
if resolved_base is None:
|
|
219
|
+
resolved_base = getattr(_pkg, "endpoint_url_base", None)
|
|
220
|
+
|
|
221
|
+
self._client = HTTPClient(api_key=resolved_key, endpoint_url_base=resolved_base)
|
|
222
|
+
|
|
223
|
+
def run(self, request_input: Dict[str, Any]) -> Job:
|
|
224
|
+
"""
|
|
225
|
+
Submit a task to the endpoint.
|
|
226
|
+
|
|
227
|
+
Automatically wraps the payload in ``{"input": ...}`` if the dict
|
|
228
|
+
does not already contain an ``"input"`` key.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
request_input: The input payload for the task.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
A Job instance for tracking the submitted task.
|
|
235
|
+
"""
|
|
236
|
+
if "input" not in request_input:
|
|
237
|
+
request_input = {"input": request_input}
|
|
238
|
+
|
|
239
|
+
job_request = self._client.post(
|
|
240
|
+
f"{self.endpoint_id}/run", request_input
|
|
241
|
+
)
|
|
242
|
+
return Job(self.endpoint_id, job_request["id"], self._client)
|
|
243
|
+
|
|
244
|
+
def health(self, timeout: int = 3) -> Dict[str, Any]:
|
|
245
|
+
"""
|
|
246
|
+
Check the health of the endpoint.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
timeout: Seconds to wait for the server to respond.
|
|
250
|
+
"""
|
|
251
|
+
return self._client.get(
|
|
252
|
+
f"{self.endpoint_id}/health", timeout=timeout
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
def purge_queue(self, timeout: int = 3) -> Dict[str, Any]:
|
|
256
|
+
"""
|
|
257
|
+
Purge all pending tasks from the endpoint's queue.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
timeout: Seconds to wait for the server to respond.
|
|
261
|
+
"""
|
|
262
|
+
return self._client.post(
|
|
263
|
+
f"{self.endpoint_id}/purge-queue", data=None, timeout=timeout
|
|
264
|
+
)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HTTP Client abstractions
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from aiohttp import ClientSession, ClientTimeout, TCPConnector, ClientResponseError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TooManyRequests(ClientResponseError):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _get_auth_header():
|
|
16
|
+
# PPIO_AI_API_KEY already contains "Bearer <jwt>" when JWT auth is configured.
|
|
17
|
+
auth_value = os.environ.get("PPIO_AI_API_KEY", "")
|
|
18
|
+
return {
|
|
19
|
+
"Content-Type": "application/json",
|
|
20
|
+
"Authorization": auth_value,
|
|
21
|
+
"Accept-Encoding": "gzip, deflate",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def AsyncClientSession(*args, **kwargs): # pylint: disable=invalid-name
|
|
26
|
+
"""
|
|
27
|
+
Factory for aiohttp.ClientSession pre-configured for the PPIO async gateway.
|
|
28
|
+
"""
|
|
29
|
+
return ClientSession(
|
|
30
|
+
connector=TCPConnector(limit=0),
|
|
31
|
+
headers=_get_auth_header(),
|
|
32
|
+
timeout=ClientTimeout(600, ceil_threshold=400),
|
|
33
|
+
*args,
|
|
34
|
+
**kwargs,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SyncClientSession(requests.Session):
|
|
39
|
+
"""Thin wrapper around requests.Session."""
|
|
40
|
+
pass
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from ppio_gpus.modules import handler, job, logger, ping, progress, scale, upload, worker_state
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Handler introspection utilities."""
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from typing import Callable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def is_generator(handler: Callable) -> bool:
|
|
8
|
+
"""Return True if handler is a (async) generator function."""
|
|
9
|
+
return inspect.isgeneratorfunction(handler) or inspect.isasyncgenfunction(handler)
|