superstep 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.
superstep/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """Superstep Python SDK: start workflow runs over the public API and read their results."""
2
+ from ._client import AsyncClient, Client
3
+ from ._errors import (APIConnectionError, AuthenticationError, ConfigError, InvalidRequestError, NotFoundError, PayloadTooLargeError,
4
+ RateLimitError, ServerError, SuperstepError, WaitTimeoutError)
5
+ from ._models import Run, RunMetrics, RunStatus, RunWaiting
6
+ from ._version import __version__
7
+
8
+ __all__ = ["Client", "AsyncClient", "Run", "RunMetrics", "RunStatus", "RunWaiting", "SuperstepError", "ConfigError",
9
+ "APIConnectionError", "AuthenticationError", "NotFoundError", "InvalidRequestError", "PayloadTooLargeError",
10
+ "RateLimitError", "ServerError", "WaitTimeoutError", "__version__"]
superstep/_base.py ADDED
@@ -0,0 +1,64 @@
1
+ """What the sync and async clients share: configuration, request building, envelope parsing and the poll schedule."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ from dataclasses import dataclass
6
+ from typing import Any, Dict, Iterator, Optional
7
+ from urllib.parse import quote
8
+
9
+ from ._errors import APIConnectionError, ConfigError, error_for
10
+ from ._models import Run
11
+ from ._version import __version__
12
+
13
+ ENV_BASE_URL, ENV_TOKEN, ENV_WORKFLOW_ID = "SUPERSTEP_BASE_URL", "SUPERSTEP_TOKEN", "SUPERSTEP_WORKFLOW_ID"
14
+ DEFAULT_TIMEOUT = 60.0 # per HTTP request; a `sync` start may hold the connection up to STUDIO_SYNC_WAIT_MAX (30 s)
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class Config:
19
+ base_url: str
20
+ token: str
21
+ workflow_id: str
22
+
23
+ @classmethod
24
+ def resolve(cls, base_url: Optional[str], token: Optional[str], workflow_id: Optional[str]) -> "Config":
25
+ values = {"base_url": base_url or os.environ.get(ENV_BASE_URL), "token": token or os.environ.get(ENV_TOKEN),
26
+ "workflow_id": workflow_id or os.environ.get(ENV_WORKFLOW_ID)}
27
+ env = {"base_url": ENV_BASE_URL, "token": ENV_TOKEN, "workflow_id": ENV_WORKFLOW_ID}
28
+ missing = [f"{k} (or {env[k]})" for k, v in values.items() if not v]
29
+ if missing: raise ConfigError("Missing " + ", ".join(missing), code="CONFIG_MISSING")
30
+ return cls(base_url=str(values["base_url"]).rstrip("/"), token=str(values["token"]), workflow_id=str(values["workflow_id"]))
31
+
32
+ @property
33
+ def headers(self) -> Dict[str, str]:
34
+ return {"X-Workflow-Token": self.token, "Accept": "application/json", "User-Agent": f"superstep-python/{__version__}"}
35
+
36
+ def runs_url(self, run_id: Optional[str] = None) -> str:
37
+ url = f"{self.base_url}/api/hooks/workflows/{quote(self.workflow_id, safe='')}/runs"
38
+ return f"{url}/{quote(run_id, safe='')}" if run_id else url
39
+
40
+
41
+ def start_body(input: Optional[Dict[str, Any]], mode: str, callback_url: Optional[str]) -> Dict[str, Any]:
42
+ if mode not in ("async", "sync"): raise ValueError("mode must be 'async' or 'sync'")
43
+ body: Dict[str, Any] = {"input": input or {}, "mode": mode}
44
+ if callback_url: body["callback_url"] = callback_url
45
+ return body
46
+
47
+
48
+ def parse(status: int, headers: Dict[str, str], payload: Any) -> Run:
49
+ """Unwrap the envelope; raise the typed error for an error envelope."""
50
+ if not isinstance(payload, dict) or "success" not in payload:
51
+ raise APIConnectionError(f"Unexpected response (HTTP {status}); is base_url a Superstep server?", code="BAD_RESPONSE", status=status)
52
+ if not payload["success"]:
53
+ err = payload.get("error") or {}
54
+ raise error_for(status, err.get("code", "UNKNOWN"), err.get("message", f"HTTP {status}"), err.get("details"), headers)
55
+ return Run.from_dict(payload["data"])
56
+
57
+
58
+ def poll_delays(initial: float, maximum: float, factor: float) -> Iterator[float]:
59
+ """initial, initial*factor, ... capped at maximum."""
60
+ if initial <= 0 or factor < 1: raise ValueError("poll_interval must be > 0 and backoff >= 1")
61
+ delay = initial
62
+ while True:
63
+ yield min(delay, maximum)
64
+ delay *= factor
superstep/_client.py ADDED
@@ -0,0 +1,125 @@
1
+ """Sync and async clients. Both send the same requests (`_base`); only the transport and sleeping differ."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import time
6
+ from typing import Any, Dict, Optional
7
+
8
+ import httpx
9
+
10
+ from ._base import DEFAULT_TIMEOUT, Config, parse, poll_delays, start_body
11
+ from ._errors import APIConnectionError, WaitTimeoutError
12
+ from ._models import Run
13
+
14
+
15
+ def _json(r: httpx.Response) -> Any:
16
+ try: return r.json()
17
+ except ValueError: return None
18
+
19
+
20
+ class _Common:
21
+ def __init__(self, workflow_id: Optional[str], token: Optional[str], base_url: Optional[str]):
22
+ self.config = Config.resolve(base_url, token, workflow_id)
23
+
24
+ def _deadline(self, timeout: Optional[float]) -> Optional[float]:
25
+ return None if timeout is None else time.monotonic() + timeout
26
+
27
+ @staticmethod
28
+ def _sleep_for(delay: float, deadline: Optional[float], run: Run) -> float:
29
+ if deadline is None: return delay
30
+ left = deadline - time.monotonic()
31
+ if left <= 0: raise WaitTimeoutError(f"Run {run.run_id} still {run.status} after the timeout", run)
32
+ return min(delay, left)
33
+
34
+
35
+ class Client(_Common):
36
+ """Synchronous client.
37
+
38
+ client = Client(workflow_id="example", token="wft_...", base_url="http://localhost:8080")
39
+ run = client.run_and_wait({"message": "hi"}, timeout=60)
40
+
41
+ Unset arguments fall back to SUPERSTEP_WORKFLOW_ID, SUPERSTEP_TOKEN and SUPERSTEP_BASE_URL.
42
+ """
43
+
44
+ def __init__(self, workflow_id: Optional[str] = None, token: Optional[str] = None, base_url: Optional[str] = None, *,
45
+ timeout: float = DEFAULT_TIMEOUT, http_client: Optional[httpx.Client] = None):
46
+ super().__init__(workflow_id, token, base_url)
47
+ self._http = http_client or httpx.Client(timeout=timeout)
48
+ self._owns_http = http_client is None
49
+
50
+ def _send(self, method: str, url: str, body: Optional[Dict[str, Any]] = None) -> Run:
51
+ try: r = self._http.request(method, url, json=body, headers=self.config.headers)
52
+ except httpx.HTTPError as e: raise APIConnectionError(str(e) or type(e).__name__, code="CONNECTION_ERROR") from e
53
+ return parse(r.status_code, dict(r.headers), _json(r))
54
+
55
+ def run(self, input: Optional[Dict[str, Any]] = None, *, mode: str = "async", callback_url: Optional[str] = None) -> Run:
56
+ """Start a run. `async` returns at once (status `queued`); `sync` waits up to the server's limit."""
57
+ return self._send("POST", self.config.runs_url(), start_body(input, mode, callback_url))
58
+
59
+ def get_run(self, run_id: str) -> Run:
60
+ return self._send("GET", self.config.runs_url(run_id))
61
+
62
+ def wait(self, run_id: str, *, timeout: Optional[float] = 300.0, poll_interval: float = 1.0, max_poll_interval: float = 10.0,
63
+ backoff: float = 1.5) -> Run:
64
+ """Poll until the run is done. Raises WaitTimeoutError after `timeout` seconds (None waits forever)."""
65
+ deadline, delays = self._deadline(timeout), poll_delays(poll_interval, max_poll_interval, backoff)
66
+ while True:
67
+ run = self.get_run(run_id)
68
+ if run.done: return run
69
+ time.sleep(self._sleep_for(next(delays), deadline, run))
70
+
71
+ def run_and_wait(self, input: Optional[Dict[str, Any]] = None, *, timeout: Optional[float] = 300.0, poll_interval: float = 1.0,
72
+ max_poll_interval: float = 10.0, backoff: float = 1.5, callback_url: Optional[str] = None) -> Run:
73
+ """Start a run and poll (interval growing by `backoff` up to `max_poll_interval`) until it is done."""
74
+ started = self.run(input, callback_url=callback_url)
75
+ if started.done: return started
76
+ return self.wait(started.run_id, timeout=timeout, poll_interval=poll_interval, max_poll_interval=max_poll_interval, backoff=backoff)
77
+
78
+ def close(self) -> None:
79
+ if self._owns_http: self._http.close()
80
+
81
+ def __enter__(self) -> "Client": return self
82
+
83
+ def __exit__(self, *exc: Any) -> None: self.close()
84
+
85
+
86
+ class AsyncClient(_Common):
87
+ """Asynchronous client with the same methods as `Client`, as coroutines."""
88
+
89
+ def __init__(self, workflow_id: Optional[str] = None, token: Optional[str] = None, base_url: Optional[str] = None, *,
90
+ timeout: float = DEFAULT_TIMEOUT, http_client: Optional[httpx.AsyncClient] = None):
91
+ super().__init__(workflow_id, token, base_url)
92
+ self._http = http_client or httpx.AsyncClient(timeout=timeout)
93
+ self._owns_http = http_client is None
94
+
95
+ async def _send(self, method: str, url: str, body: Optional[Dict[str, Any]] = None) -> Run:
96
+ try: r = await self._http.request(method, url, json=body, headers=self.config.headers)
97
+ except httpx.HTTPError as e: raise APIConnectionError(str(e) or type(e).__name__, code="CONNECTION_ERROR") from e
98
+ return parse(r.status_code, dict(r.headers), _json(r))
99
+
100
+ async def run(self, input: Optional[Dict[str, Any]] = None, *, mode: str = "async", callback_url: Optional[str] = None) -> Run:
101
+ return await self._send("POST", self.config.runs_url(), start_body(input, mode, callback_url))
102
+
103
+ async def get_run(self, run_id: str) -> Run:
104
+ return await self._send("GET", self.config.runs_url(run_id))
105
+
106
+ async def wait(self, run_id: str, *, timeout: Optional[float] = 300.0, poll_interval: float = 1.0, max_poll_interval: float = 10.0,
107
+ backoff: float = 1.5) -> Run:
108
+ deadline, delays = self._deadline(timeout), poll_delays(poll_interval, max_poll_interval, backoff)
109
+ while True:
110
+ run = await self.get_run(run_id)
111
+ if run.done: return run
112
+ await asyncio.sleep(self._sleep_for(next(delays), deadline, run))
113
+
114
+ async def run_and_wait(self, input: Optional[Dict[str, Any]] = None, *, timeout: Optional[float] = 300.0, poll_interval: float = 1.0,
115
+ max_poll_interval: float = 10.0, backoff: float = 1.5, callback_url: Optional[str] = None) -> Run:
116
+ started = await self.run(input, callback_url=callback_url)
117
+ if started.done: return started
118
+ return await self.wait(started.run_id, timeout=timeout, poll_interval=poll_interval, max_poll_interval=max_poll_interval, backoff=backoff)
119
+
120
+ async def aclose(self) -> None:
121
+ if self._owns_http: await self._http.aclose()
122
+
123
+ async def __aenter__(self) -> "AsyncClient": return self
124
+
125
+ async def __aexit__(self, *exc: Any) -> None: await self.aclose()
superstep/_errors.py ADDED
@@ -0,0 +1,85 @@
1
+ """Typed errors, mapped from the API's stable error codes (docs/api-errors.md), then from the HTTP status."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Dict, Optional, Type
5
+
6
+
7
+ class SuperstepError(Exception):
8
+ """Base class. `code` is the API error code (or a client-side one such as CONNECTION_ERROR)."""
9
+
10
+ def __init__(self, message: str, *, code: str = "UNKNOWN", status: Optional[int] = None, details: Any = None,
11
+ headers: Optional[Dict[str, str]] = None):
12
+ super().__init__(message)
13
+ self.message, self.code, self.status, self.details, self.headers = message, code, status, details, headers or {}
14
+
15
+ def __repr__(self) -> str:
16
+ return f"{type(self).__name__}(code={self.code!r}, status={self.status!r}, message={self.message!r})"
17
+
18
+
19
+ class ConfigError(SuperstepError):
20
+ """Missing base URL, token or workflow id."""
21
+
22
+
23
+ class APIConnectionError(SuperstepError):
24
+ """The request did not reach the server or the response was not the API's envelope."""
25
+
26
+
27
+ class AuthenticationError(SuperstepError):
28
+ """401 TRIGGER_TOKEN_INVALID: missing, invalid or revoked token, or a token of another workflow."""
29
+
30
+
31
+ class NotFoundError(SuperstepError):
32
+ """404, e.g. RUN_NOT_FOUND."""
33
+
34
+
35
+ class InvalidRequestError(SuperstepError):
36
+ """400 / 422: RUN_INPUT_INVALID, CALLBACK_URL_INVALID, VALIDATION_ERROR (see `details`)."""
37
+
38
+
39
+ class PayloadTooLargeError(SuperstepError):
40
+ """413 RUN_INPUT_TOO_LARGE."""
41
+
42
+
43
+ class RateLimitError(SuperstepError):
44
+ """429 RATE_LIMITED. `retry_after` is the number of seconds to wait."""
45
+
46
+ @property
47
+ def retry_after(self) -> Optional[float]:
48
+ value = self.headers.get("retry-after") or self.headers.get("Retry-After")
49
+ try: return float(value) if value is not None else None
50
+ except ValueError: return None
51
+
52
+
53
+ class ServerError(SuperstepError):
54
+ """5xx, e.g. INTERNAL_ERROR."""
55
+
56
+
57
+ class WaitTimeoutError(SuperstepError):
58
+ """`run_and_wait` gave up before the run finished. `run` is the last state seen."""
59
+
60
+ def __init__(self, message: str, run: Any):
61
+ super().__init__(message, code="WAIT_TIMEOUT")
62
+ self.run = run
63
+
64
+
65
+ BY_CODE: Dict[str, Type[SuperstepError]] = {
66
+ "TRIGGER_TOKEN_INVALID": AuthenticationError,
67
+ "RUN_NOT_FOUND": NotFoundError,
68
+ "WORKFLOW_NOT_FOUND": NotFoundError,
69
+ "NOT_FOUND": NotFoundError,
70
+ "RUN_INPUT_INVALID": InvalidRequestError,
71
+ "CALLBACK_URL_INVALID": InvalidRequestError,
72
+ "VALIDATION_ERROR": InvalidRequestError,
73
+ "BAD_REQUEST": InvalidRequestError,
74
+ "RUN_INPUT_TOO_LARGE": PayloadTooLargeError,
75
+ "PAYLOAD_TOO_LARGE": PayloadTooLargeError,
76
+ "RATE_LIMITED": RateLimitError,
77
+ "INTERNAL_ERROR": ServerError,
78
+ }
79
+ BY_STATUS: Dict[int, Type[SuperstepError]] = {401: AuthenticationError, 404: NotFoundError, 400: InvalidRequestError,
80
+ 422: InvalidRequestError, 413: PayloadTooLargeError, 429: RateLimitError}
81
+
82
+
83
+ def error_for(status: int, code: str, message: str, details: Any = None, headers: Optional[Dict[str, str]] = None) -> SuperstepError:
84
+ cls = BY_CODE.get(code) or BY_STATUS.get(status) or (ServerError if status >= 500 else SuperstepError)
85
+ return cls(message, code=code, status=status, details=details, headers=headers)
superstep/_models.py ADDED
@@ -0,0 +1,59 @@
1
+ """Typed models of the public API payloads (docs/api/openapi.public.json; tests/test_spec_sync.py keeps them in step)."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass, field, fields
5
+ from typing import Any, Dict, Literal, Optional, Tuple
6
+
7
+ RunStatus = Literal["queued", "running", "ok", "error", "cancelled", "timeout", "waiting", "expired"]
8
+ PENDING: Tuple[str, ...] = ("queued", "running")
9
+
10
+
11
+ def _pick(cls, data: Optional[Dict[str, Any]]):
12
+ if data is None: return None
13
+ names = {f.name for f in fields(cls)}
14
+ return cls(**{k: v for k, v in data.items() if k in names})
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class RunMetrics:
19
+ duration_ms: Optional[float] = None
20
+ tokens_in: Optional[int] = None
21
+ tokens_out: Optional[int] = None
22
+ cost_usd: Optional[float] = None
23
+ error_count: Optional[int] = None
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class RunWaiting:
28
+ """The Human node a `waiting` run is paused at."""
29
+ node: Optional[str] = None
30
+ prompt: Optional[str] = None
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class Run:
35
+ """A run as returned by `run()` and `get_run()`. A just-queued run has only `run_id`, `status` and `poll_url`."""
36
+ run_id: str
37
+ status: RunStatus
38
+ workflow_id: Optional[str] = None
39
+ source: Optional[str] = None
40
+ final_state: Optional[Dict[str, Any]] = None
41
+ error: Optional[Any] = None
42
+ waiting: Optional[RunWaiting] = None
43
+ metrics: Optional[RunMetrics] = None
44
+ trace_url: Optional[str] = None
45
+ started: Optional[float] = None
46
+ ended: Optional[float] = None
47
+ poll_url: Optional[str] = None
48
+ raw: Dict[str, Any] = field(default_factory=dict, repr=False, compare=False)
49
+
50
+ @property
51
+ def done(self) -> bool:
52
+ """True once the run has stopped changing (any status but `queued` / `running`)."""
53
+ return self.status not in PENDING
54
+
55
+ @classmethod
56
+ def from_dict(cls, data: Dict[str, Any]) -> "Run":
57
+ plain = {f.name for f in fields(cls)} - {"waiting", "metrics", "raw"}
58
+ return cls(**{k: v for k, v in data.items() if k in plain}, waiting=_pick(RunWaiting, data.get("waiting")),
59
+ metrics=_pick(RunMetrics, data.get("metrics")), raw=dict(data))
superstep/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
superstep/py.typed ADDED
File without changes
@@ -0,0 +1,143 @@
1
+ Metadata-Version: 2.5
2
+ Name: superstep
3
+ Version: 0.1.0
4
+ Summary: Python client for the Superstep public API: start workflow runs and read their results.
5
+ Project-URL: Homepage, https://github.com/quinguyen1502/superstep
6
+ Project-URL: Documentation, https://quinguyen1502.github.io/superstep/run/public-api/
7
+ Author: Qui Nguyen
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Framework :: AsyncIO
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Typing :: Typed
13
+ Requires-Python: >=3.9
14
+ Requires-Dist: httpx<1,>=0.25
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
17
+ Requires-Dist: pytest>=8; extra == 'dev'
18
+ Provides-Extra: test
19
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'test'
20
+ Requires-Dist: pytest>=8; extra == 'test'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # superstep (Python SDK)
24
+
25
+ Python client for the Superstep public API: start a workflow run from your own
26
+ service, poll it, and read its final state. Sync and async clients, typed
27
+ models and typed errors; the only dependency is `httpx`.
28
+
29
+ - Guide: <https://quinguyen1502.github.io/superstep/run/public-api/>
30
+ - API reference: <https://quinguyen1502.github.io/superstep/reference/public-api/>
31
+ - Spec: [`docs/api/openapi.public.json`](../../docs/api/openapi.public.json)
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ pip install superstep
37
+ # from the repository, before the first release:
38
+ pip install ./sdk/python
39
+ ```
40
+
41
+ Python 3.9 or later.
42
+
43
+ ## Configure
44
+
45
+ A workflow token (`wft_...`) starts runs of one workflow. The workflow owner
46
+ creates it in the editor under **More (⋯) → API → Create token**; it is shown
47
+ once.
48
+
49
+ ```python
50
+ from superstep import Client
51
+
52
+ client = Client(
53
+ workflow_id="example",
54
+ token="wft_...",
55
+ base_url="https://superstep.example.com",
56
+ )
57
+ ```
58
+
59
+ Arguments left out are read from `SUPERSTEP_WORKFLOW_ID`, `SUPERSTEP_TOKEN`
60
+ and `SUPERSTEP_BASE_URL`. A missing value raises `ConfigError`.
61
+
62
+ ## Run a workflow
63
+
64
+ ```python
65
+ # Start and wait: polls with a growing interval (1 s, 1.5 s, ... up to 10 s).
66
+ run = client.run_and_wait({"message": "where is my order A100"}, timeout=120)
67
+ print(run.status, run.final_state)
68
+
69
+ # Or step by step.
70
+ started = client.run({"message": "hi"}) # status "queued", poll_url set
71
+ current = client.get_run(started.run_id) # status, final_state, metrics ...
72
+ done = client.wait(started.run_id, timeout=60, poll_interval=0.5)
73
+
74
+ # Let the server wait (up to STUDIO_SYNC_WAIT_MAX seconds, default 30).
75
+ run = client.run({"message": "hi"}, mode="sync")
76
+ ```
77
+
78
+ `Run.done` is true for every status except `queued` and `running`. Finished
79
+ statuses are `ok`, `error`, `cancelled`, `timeout`, `waiting` (paused at a
80
+ Human node, see `run.waiting`) and `expired`. `run_and_wait` returns the run
81
+ whatever its final status; check `run.status`.
82
+
83
+ `callback_url=` on `run()` and `run_and_wait()` asks the server to POST the
84
+ result to your URL when the run ends.
85
+
86
+ ## Async
87
+
88
+ ```python
89
+ import asyncio
90
+ from superstep import AsyncClient
91
+
92
+ async def main():
93
+ async with AsyncClient() as client: # configured from the environment
94
+ run = await client.run_and_wait({"message": "hi"})
95
+ print(run.final_state)
96
+
97
+ asyncio.run(main())
98
+ ```
99
+
100
+ ## Errors
101
+
102
+ Every API error raises a subclass of `SuperstepError` with `code` (the
103
+ API's stable error code), `status`, `message` and `details`.
104
+
105
+ | Exception | When |
106
+ | --- | --- |
107
+ | `AuthenticationError` | `401 TRIGGER_TOKEN_INVALID`: missing, wrong or revoked token, or a token of another workflow |
108
+ | `NotFoundError` | `404 RUN_NOT_FOUND` |
109
+ | `InvalidRequestError` | `400 RUN_INPUT_INVALID`, `CALLBACK_URL_INVALID`; `422 VALIDATION_ERROR` |
110
+ | `PayloadTooLargeError` | `413 RUN_INPUT_TOO_LARGE` |
111
+ | `RateLimitError` | `429 RATE_LIMITED`; `retry_after` holds the seconds to wait |
112
+ | `ServerError` | `5xx` |
113
+ | `APIConnectionError` | Network failure, or a response that is not the API envelope |
114
+ | `WaitTimeoutError` | `wait` / `run_and_wait` ran out of time; `err.run` is the last state |
115
+ | `ConfigError` | Missing base URL, token or workflow id |
116
+
117
+ ```python
118
+ from superstep import RateLimitError
119
+ import time
120
+
121
+ try:
122
+ run = client.run({"message": "hi"})
123
+ except RateLimitError as err:
124
+ time.sleep(err.retry_after or 1)
125
+ ```
126
+
127
+ ## Development
128
+
129
+ ```bash
130
+ cd sdk/python
131
+ pip install -e ".[dev]"
132
+ pytest # unit tests, mocked HTTP
133
+ ```
134
+
135
+ `tests/test_spec_sync.py` checks the models against the public spec. The live
136
+ test (`tests/test_integration.py`) runs when `SUPERSTEP_IT_BASE_URL`,
137
+ `SUPERSTEP_IT_TOKEN` and `SUPERSTEP_IT_WORKFLOW_ID` are set;
138
+ `scripts/sdk_integration.sh` at the repository root starts a local backend,
139
+ creates a token and runs it for both SDKs.
140
+
141
+ ## License
142
+
143
+ MIT
@@ -0,0 +1,11 @@
1
+ superstep/__init__.py,sha256=0kWl8LmEknJg09fuoC6AwzV5mf47JfkmCK90fBD1r6g,763
2
+ superstep/_base.py,sha256=ehhUlq2JgixDp4fS7KE19KHIlRKeM8DtdX5--h63Zvw,3117
3
+ superstep/_client.py,sha256=tJT2g7tQ6EmR485Sf1vCKBwmjKAr7O2OeXddn58H7fY,6444
4
+ superstep/_errors.py,sha256=RtWlbElpSqsdkUibcHY9iMel1mA6Gha-UhsDI-Pi9cM,3278
5
+ superstep/_models.py,sha256=AdYV_oZPnb-90FSq9f1i7ucu25x4bFfgApqtYz17mvU,2174
6
+ superstep/_version.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
7
+ superstep/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ superstep-0.1.0.dist-info/METADATA,sha256=-Md1hk3QIC7iqZNx4bLXQRa2aRLvD73VNywijhtG4zw,4655
9
+ superstep-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
10
+ superstep-0.1.0.dist-info/licenses/LICENSE,sha256=WWmmGBy5D7v1yYo4J74h1wAwOWyUzdfK7l9y_z5IFXs,1076
11
+ superstep-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Thái Quí Nguyễn
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.