wherobots-python-sdk 0.1.0__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.
- wherobots/__init__.py +87 -0
- wherobots/__version__.py +5 -0
- wherobots/api/__init__.py +7 -0
- wherobots/api/base.py +256 -0
- wherobots/api/files.py +386 -0
- wherobots/api/runs.py +255 -0
- wherobots/client.py +640 -0
- wherobots/config.py +165 -0
- wherobots/enums.py +140 -0
- wherobots/exceptions.py +47 -0
- wherobots/models.py +1080 -0
- wherobots/py.typed +0 -0
- wherobots/utils/__init__.py +6 -0
- wherobots/utils/logger.py +34 -0
- wherobots/utils/validation.py +31 -0
- wherobots_python_sdk-0.1.0.dist-info/METADATA +580 -0
- wherobots_python_sdk-0.1.0.dist-info/RECORD +20 -0
- wherobots_python_sdk-0.1.0.dist-info/WHEEL +5 -0
- wherobots_python_sdk-0.1.0.dist-info/licenses/LICENSE +191 -0
- wherobots_python_sdk-0.1.0.dist-info/top_level.txt +1 -0
wherobots/config.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Configuration helpers for the Wherobots Jobs client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import warnings
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
from wherobots.exceptions import WherobotsConfigError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, repr=False)
|
|
13
|
+
class WherobotsConfig:
|
|
14
|
+
"""Immutable configuration for the Wherobots Jobs client.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
api_key: Wherobots API key.
|
|
18
|
+
region: Default deployment region (e.g. ``"aws-us-west-2"``).
|
|
19
|
+
s3_bucket: S3 bucket for script uploads.
|
|
20
|
+
s3_prefix: Key prefix within the bucket.
|
|
21
|
+
base_url: Wherobots API base URL (must use HTTPS).
|
|
22
|
+
version: API version string.
|
|
23
|
+
request_timeout_seconds: HTTP request timeout in seconds.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
api_key: str | None = None
|
|
27
|
+
region: str | None = None
|
|
28
|
+
s3_bucket: str | None = None
|
|
29
|
+
s3_prefix: str = "wherobots-jobs"
|
|
30
|
+
base_url: str = "https://api.cloud.wherobots.com"
|
|
31
|
+
version: str = "latest"
|
|
32
|
+
request_timeout_seconds: int = 30
|
|
33
|
+
|
|
34
|
+
def __repr__(self) -> str:
|
|
35
|
+
"""Return a string representation with the API key masked."""
|
|
36
|
+
masked_key: str | None = None
|
|
37
|
+
if self.api_key:
|
|
38
|
+
# Show the last 4 characters only when the key is long
|
|
39
|
+
# enough that those 4 characters don't meaningfully leak
|
|
40
|
+
# the secret. For keys of 8 characters or fewer, mask the
|
|
41
|
+
# entire value.
|
|
42
|
+
masked_key = f"***{self.api_key[-4:]}" if len(self.api_key) > 8 else "***"
|
|
43
|
+
return (
|
|
44
|
+
f"WherobotsConfig("
|
|
45
|
+
f"api_key={masked_key!r}, "
|
|
46
|
+
f"region={self.region!r}, "
|
|
47
|
+
f"s3_bucket={self.s3_bucket!r}, "
|
|
48
|
+
f"s3_prefix={self.s3_prefix!r}, "
|
|
49
|
+
f"base_url={self.base_url!r}, "
|
|
50
|
+
f"version={self.version!r}, "
|
|
51
|
+
f"request_timeout_seconds={self.request_timeout_seconds!r})"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def __post_init__(self) -> None:
|
|
55
|
+
"""Validate configuration values after dataclass init.
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
WherobotsConfigError: If ``request_timeout_seconds`` is not
|
|
59
|
+
positive, or if ``base_url`` does not use HTTPS.
|
|
60
|
+
"""
|
|
61
|
+
if self.request_timeout_seconds <= 0:
|
|
62
|
+
raise WherobotsConfigError("request_timeout_seconds must be a positive integer")
|
|
63
|
+
|
|
64
|
+
# Fail fast on non-HTTPS base URLs. The low-level ``BaseClient``
|
|
65
|
+
# also rechecks this, but catching it at config-construction
|
|
66
|
+
# time surfaces misconfiguration before any HTTP work begins.
|
|
67
|
+
normalized = self.base_url.rstrip("/")
|
|
68
|
+
if normalized.startswith("http://"):
|
|
69
|
+
raise WherobotsConfigError(
|
|
70
|
+
"base_url must use HTTPS. Sending API keys over plaintext "
|
|
71
|
+
f"HTTP is not allowed. Got: {self.base_url}"
|
|
72
|
+
)
|
|
73
|
+
if not normalized.startswith("https://"):
|
|
74
|
+
raise WherobotsConfigError(f"base_url must be an https:// URL. Got: {self.base_url}")
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def from_env(
|
|
78
|
+
cls,
|
|
79
|
+
api_key: str | None = None,
|
|
80
|
+
region: str | None = None,
|
|
81
|
+
s3_bucket: str | None = None,
|
|
82
|
+
s3_prefix: str | None = None,
|
|
83
|
+
base_url: str | None = None,
|
|
84
|
+
version: str | None = None,
|
|
85
|
+
request_timeout_seconds: int | None = None,
|
|
86
|
+
) -> WherobotsConfig:
|
|
87
|
+
"""Build configuration from environment variables with optional overrides.
|
|
88
|
+
|
|
89
|
+
Explicit parameters take priority over environment variables.
|
|
90
|
+
Empty-string env vars are treated as unset.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
api_key: Override for ``WHEROBOTS_API_KEY``.
|
|
94
|
+
region: Override for ``WHEROBOTS_REGION``.
|
|
95
|
+
s3_bucket: Override for ``WHEROBOTS_S3_BUCKET``.
|
|
96
|
+
s3_prefix: Override for ``WHEROBOTS_S3_PREFIX``.
|
|
97
|
+
base_url: Override for ``WHEROBOTS_API_BASE_URL``.
|
|
98
|
+
version: Override for ``WHEROBOTS_VERSION``.
|
|
99
|
+
request_timeout_seconds: Override for
|
|
100
|
+
``WHEROBOTS_REQUEST_TIMEOUT_SECONDS``.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
A validated ``WherobotsConfig`` instance.
|
|
104
|
+
|
|
105
|
+
Raises:
|
|
106
|
+
WherobotsConfigError: If the timeout env var is not a valid
|
|
107
|
+
integer, or if validation in ``__post_init__`` fails.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def _env(var: str) -> str | None:
|
|
111
|
+
"""Return env var value, treating empty strings as None."""
|
|
112
|
+
val = os.environ.get(var)
|
|
113
|
+
return val if val else None
|
|
114
|
+
|
|
115
|
+
# Parse timeout with error handling for malformed env var
|
|
116
|
+
if request_timeout_seconds is not None:
|
|
117
|
+
timeout = request_timeout_seconds
|
|
118
|
+
else:
|
|
119
|
+
raw_timeout = _env("WHEROBOTS_REQUEST_TIMEOUT_SECONDS")
|
|
120
|
+
if raw_timeout is not None:
|
|
121
|
+
try:
|
|
122
|
+
timeout = int(raw_timeout)
|
|
123
|
+
except ValueError as exc:
|
|
124
|
+
raise WherobotsConfigError(
|
|
125
|
+
f"WHEROBOTS_REQUEST_TIMEOUT_SECONDS must be an integer, "
|
|
126
|
+
f"got: {raw_timeout!r}"
|
|
127
|
+
) from exc
|
|
128
|
+
else:
|
|
129
|
+
timeout = 30
|
|
130
|
+
|
|
131
|
+
# Emit deprecation warnings for S3-related env vars that are no
|
|
132
|
+
# longer used (presigned uploads are the sole upload method).
|
|
133
|
+
resolved_bucket = s3_bucket or _env("WHEROBOTS_S3_BUCKET")
|
|
134
|
+
resolved_prefix = s3_prefix or _env("WHEROBOTS_S3_PREFIX")
|
|
135
|
+
|
|
136
|
+
if resolved_bucket:
|
|
137
|
+
warnings.warn(
|
|
138
|
+
"WHEROBOTS_S3_BUCKET / s3_bucket is deprecated and ignored. "
|
|
139
|
+
"The SDK now uploads files exclusively via presigned URLs "
|
|
140
|
+
"(only an API key is needed). This setting will be removed "
|
|
141
|
+
"in a future release.",
|
|
142
|
+
DeprecationWarning,
|
|
143
|
+
stacklevel=2,
|
|
144
|
+
)
|
|
145
|
+
if resolved_prefix and resolved_prefix != "wherobots-jobs":
|
|
146
|
+
warnings.warn(
|
|
147
|
+
"WHEROBOTS_S3_PREFIX / s3_prefix is deprecated and ignored. "
|
|
148
|
+
"The SDK now uploads files exclusively via presigned URLs "
|
|
149
|
+
"(only an API key is needed). This setting will be removed "
|
|
150
|
+
"in a future release.",
|
|
151
|
+
DeprecationWarning,
|
|
152
|
+
stacklevel=2,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
return cls(
|
|
156
|
+
api_key=api_key or _env("WHEROBOTS_API_KEY"),
|
|
157
|
+
region=region or _env("WHEROBOTS_REGION"),
|
|
158
|
+
s3_bucket=s3_bucket or _env("WHEROBOTS_S3_BUCKET"),
|
|
159
|
+
s3_prefix=s3_prefix or _env("WHEROBOTS_S3_PREFIX") or "wherobots-jobs",
|
|
160
|
+
base_url=base_url
|
|
161
|
+
or _env("WHEROBOTS_API_BASE_URL")
|
|
162
|
+
or "https://api.cloud.wherobots.com",
|
|
163
|
+
version=version or _env("WHEROBOTS_VERSION") or "latest",
|
|
164
|
+
request_timeout_seconds=timeout,
|
|
165
|
+
)
|
wherobots/enums.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Enumeration types for the Wherobots Jobs API."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class JobStatus(Enum):
|
|
7
|
+
"""Job run status values (from OpenAPI ``RunStatus``)."""
|
|
8
|
+
|
|
9
|
+
PENDING = "PENDING"
|
|
10
|
+
RUNNING = "RUNNING"
|
|
11
|
+
COMPLETED = "COMPLETED"
|
|
12
|
+
FAILED = "FAILED"
|
|
13
|
+
CANCELLED = "CANCELLED"
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def is_terminal(self) -> bool:
|
|
17
|
+
"""Return ``True`` if this status represents a finished run."""
|
|
18
|
+
return self in _TERMINAL_STATUSES
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_TERMINAL_STATUSES = frozenset(
|
|
22
|
+
{
|
|
23
|
+
JobStatus.COMPLETED,
|
|
24
|
+
JobStatus.FAILED,
|
|
25
|
+
JobStatus.CANCELLED,
|
|
26
|
+
}
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
_TERMINAL_STATUS_VALUES = frozenset(s.value for s in _TERMINAL_STATUSES)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def is_terminal_status(status: "JobStatus | str | None") -> bool:
|
|
33
|
+
"""Return True if *status* is a known terminal state.
|
|
34
|
+
|
|
35
|
+
Accepts a :class:`JobStatus`, a raw string (e.g. an API-added status
|
|
36
|
+
the SDK doesn't yet know about), or ``None``. Unknown strings are
|
|
37
|
+
treated as **non-terminal** — conservative so callers keep polling
|
|
38
|
+
rather than bailing early on a new running/transitional state.
|
|
39
|
+
"""
|
|
40
|
+
if status is None:
|
|
41
|
+
return False
|
|
42
|
+
if isinstance(status, JobStatus):
|
|
43
|
+
return status in _TERMINAL_STATUSES
|
|
44
|
+
return status in _TERMINAL_STATUS_VALUES
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Runtime(Enum):
|
|
48
|
+
"""Available Wherobots compute runtime sizes.
|
|
49
|
+
|
|
50
|
+
The values match the lowercase ``RuntimeId`` strings accepted by the
|
|
51
|
+
API. This enum is a **convenience subset** — the API also accepts
|
|
52
|
+
legacy uppercase values (``TINY``, ``SMALL``, ``MEDIUM``, ``LARGE``,
|
|
53
|
+
``XLARGE``, ``XXLARGE``) and may add new runtimes at any time. Where
|
|
54
|
+
the SDK accepts a runtime argument (e.g. ``WherobotsJob(runtime=...)``)
|
|
55
|
+
it also accepts a plain string, so unknown runtimes are never
|
|
56
|
+
blocked client-side.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
MICRO = "micro"
|
|
60
|
+
TINY = "tiny"
|
|
61
|
+
SMALL = "small"
|
|
62
|
+
MEDIUM = "medium"
|
|
63
|
+
LARGE = "large"
|
|
64
|
+
X_LARGE = "x-large"
|
|
65
|
+
DOUBLE_X_LARGE = "2x-large"
|
|
66
|
+
QUAD_X_LARGE = "4x-large"
|
|
67
|
+
MEDIUM_HIMEM = "medium-himem"
|
|
68
|
+
LARGE_HIMEM = "large-himem"
|
|
69
|
+
X_LARGE_HIMEM = "x-large-himem"
|
|
70
|
+
DOUBLE_X_LARGE_HIMEM = "2x-large-himem"
|
|
71
|
+
QUAD_X_LARGE_HIMEM = "4x-large-himem"
|
|
72
|
+
X_LARGE_HICPU = "x-large-hicpu"
|
|
73
|
+
DOUBLE_X_LARGE_HICPU = "2x-large-hicpu"
|
|
74
|
+
X_LARGE_MATCHER = "x-large-matcher"
|
|
75
|
+
DOUBLE_X_LARGE_MATCHER = "2x-large-matcher"
|
|
76
|
+
MICRO_A10_GPU = "micro-a10-gpu"
|
|
77
|
+
TINY_A10_GPU = "tiny-a10-gpu"
|
|
78
|
+
SMALL_A10_GPU = "small-a10-gpu"
|
|
79
|
+
MEDIUM_A10_GPU = "medium-a10-gpu"
|
|
80
|
+
LARGE_A10_GPU = "large-a10-gpu"
|
|
81
|
+
X_LARGE_A10_GPU = "x-large-a10-gpu"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class Region(Enum):
|
|
85
|
+
"""Known AWS regions for Wherobots deployments.
|
|
86
|
+
|
|
87
|
+
Region is a **free-form string** in the Wherobots API — this enum
|
|
88
|
+
lists regions that exist today for convenience, but the API may
|
|
89
|
+
add new regions at any time. SDK call sites that accept a region
|
|
90
|
+
(e.g. ``WherobotsJob(region=...)``) always accept a plain string
|
|
91
|
+
as well, so unknown regions are never blocked client-side.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
EU_WEST_1 = "aws-eu-west-1"
|
|
95
|
+
US_WEST_2 = "aws-us-west-2"
|
|
96
|
+
US_EAST_1 = "aws-us-east-1"
|
|
97
|
+
AP_SOUTH_1 = "aws-ap-south-1"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class DependencyType(Enum):
|
|
101
|
+
"""Dependency source types (OpenAPI ``DependencySourceType``)."""
|
|
102
|
+
|
|
103
|
+
PYPI = "PYPI"
|
|
104
|
+
FILE = "FILE"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class DependencyFileType(Enum):
|
|
108
|
+
"""File types for ``FILE`` dependencies (from OpenAPI ``DependencyFileType``)."""
|
|
109
|
+
|
|
110
|
+
JAR = "JAR"
|
|
111
|
+
PYTHON_WHEEL = "PYTHON_WHEEL"
|
|
112
|
+
ZIP = "ZIP"
|
|
113
|
+
OTHER = "OTHER"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class AppType(Enum):
|
|
117
|
+
"""Kube app types attached to a run (OpenAPI ``AppType``)."""
|
|
118
|
+
|
|
119
|
+
JUPYTER = "JUPYTER"
|
|
120
|
+
SQL_SESSION = "SQL_SESSION"
|
|
121
|
+
RUN = "RUN"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class AppStatus(Enum):
|
|
125
|
+
"""Lifecycle status of the Kube app backing a run (OpenAPI ``AppStatus``)."""
|
|
126
|
+
|
|
127
|
+
PENDING = "PENDING"
|
|
128
|
+
PREPARING = "PREPARING"
|
|
129
|
+
PREPARE_FAILED = "PREPARE_FAILED"
|
|
130
|
+
REQUESTED = "REQUESTED"
|
|
131
|
+
DEPLOYING = "DEPLOYING"
|
|
132
|
+
DEPLOY_FAILED = "DEPLOY_FAILED"
|
|
133
|
+
DEPLOYED = "DEPLOYED"
|
|
134
|
+
INITIALIZING = "INITIALIZING"
|
|
135
|
+
INIT_FAILED = "INIT_FAILED"
|
|
136
|
+
READY = "READY"
|
|
137
|
+
DESTROY_REQUESTED = "DESTROY_REQUESTED"
|
|
138
|
+
DESTROYING = "DESTROYING"
|
|
139
|
+
DESTROY_FAILED = "DESTROY_FAILED"
|
|
140
|
+
DESTROYED = "DESTROYED"
|
wherobots/exceptions.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Custom exceptions for the Wherobots Jobs API client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class WherobotsJobError(Exception):
|
|
9
|
+
"""Base exception for all Wherobots job errors."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class WherobotsAPIError(WherobotsJobError):
|
|
13
|
+
"""Raised when the Wherobots API returns an error response."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
message: str,
|
|
18
|
+
status_code: int | None = None,
|
|
19
|
+
response: dict[str, Any] | None = None,
|
|
20
|
+
request_id: str | None = None,
|
|
21
|
+
):
|
|
22
|
+
self.status_code = status_code
|
|
23
|
+
self.response = response
|
|
24
|
+
self.request_id = request_id
|
|
25
|
+
super().__init__(message)
|
|
26
|
+
|
|
27
|
+
def __str__(self) -> str:
|
|
28
|
+
parts = [super().__str__()]
|
|
29
|
+
if self.request_id:
|
|
30
|
+
parts.append(f"[request_id={self.request_id}]")
|
|
31
|
+
return " ".join(parts)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class WherobotsValidationError(WherobotsJobError):
|
|
35
|
+
"""Raised for client-side validation errors before making API calls."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class WherobotsS3Error(WherobotsJobError):
|
|
39
|
+
"""Raised when an S3 upload or access operation fails."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class WherobotsConfigError(WherobotsJobError):
|
|
43
|
+
"""Raised when configuration is missing or invalid."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class WherobotsTimeoutError(WherobotsJobError):
|
|
47
|
+
"""Raised when a job exceeds the specified timeout."""
|