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/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"
@@ -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."""