Error Handling
Exceptions, pagination, and response patterns for the Strongly Python SDK.
Exception Hierarchy
All SDK exceptions inherit from a common base, making it easy to catch specific errors or handle broad categories.
StronglyError
|-- APIError
| |-- ValidationError (400)
| |-- AuthenticationError (401)
| |-- PermissionDeniedError (403)
| |-- NotFoundError (404)
| |-- RequestTimeoutError (408)
| |-- ConflictError (409)
| |-- PayloadTooLargeError (413)
| |-- UnsupportedMediaTypeError (415)
| |-- UnprocessableEntityError (422)
| |-- RateLimitError (429)
| |-- InternalServerError (500)
| |-- BadGatewayError (502)
| |-- ServiceUnavailableError (503)
|
|-- TimeoutError
|-- ConnectionError
|-- ConfigurationError
All exceptions are available at the top level:
from strongly import (
StronglyError,
APIError,
ValidationError,
AuthenticationError,
PermissionDeniedError,
NotFoundError,
RequestTimeoutError,
ConflictError,
PayloadTooLargeError,
UnsupportedMediaTypeError,
UnprocessableEntityError,
RateLimitError,
InternalServerError,
BadGatewayError,
ServiceUnavailableError,
TimeoutError,
ConnectionError,
ConfigurationError,
)
Base Exception
StronglyError
The root exception for all SDK errors.
| Field | Type | Description |
|---|---|---|
message | str | Human-readable error message |
from strongly import Strongly, StronglyError
client = Strongly()
try:
client.apps.retrieve("app-abc123")
except StronglyError as e:
print(f"Something went wrong: {e.message}")
API Errors
APIError
Base class for all HTTP errors returned by the Strongly API. Extends StronglyError.
| Field | Type | Description |
|---|---|---|
message | str | Human-readable error message |
status_code | int | HTTP status code |
error_code | Optional[str] | Machine-readable error code from the API |
request_id | Optional[str] | Request ID for support and debugging |
body | Optional[dict] | Full response body from the API |
from strongly import Strongly, APIError
client = Strongly()
try:
client.apps.retrieve("app-abc123")
except APIError as e:
print(f"HTTP {e.status_code}: {e.message}")
if e.request_id:
print(f"Request ID: {e.request_id}")
if e.body:
print(f"Response body: {e.body}")
HTTP Exceptions
All HTTP exceptions extend APIError and carry the same fields. Each has a default status code and message.
| Exception | Status Code | Default Message | Extra Fields |
|---|---|---|---|
ValidationError | 400 | "Validation error" | details: List[dict] (defaults to []) |
AuthenticationError | 401 | "Invalid or missing API key" | -- |
PermissionDeniedError | 403 | "Permission denied" | -- |
NotFoundError | 404 | "Resource not found" | -- |
RequestTimeoutError | 408 | "Request timeout" | -- |
ConflictError | 409 | "Resource conflict" | -- |
PayloadTooLargeError | 413 | "Payload too large" | -- |
UnsupportedMediaTypeError | 415 | "Unsupported media type" | -- |
UnprocessableEntityError | 422 | "Unprocessable entity" | -- |
RateLimitError | 429 | "Rate limit exceeded" | retry_after: Optional[float] |
InternalServerError | 500 | "Internal server error" | -- |
BadGatewayError | 502 | "Bad gateway" | -- |
ServiceUnavailableError | 503 | "Service unavailable" | -- |
Transport Errors
Transport errors extend StronglyError directly. They do not have a status_code because the request never received an HTTP response.
| Exception | Description |
|---|---|
TimeoutError | The request timed out before the server responded |
ConnectionError | Could not connect to the Strongly API server |
ConfigurationError | Missing credentials, invalid base URL, or other configuration problem |
from strongly import Strongly, TimeoutError, ConnectionError, ConfigurationError
try:
client = Strongly(api_key="invalid", base_url="https://bad-url.example.com")
client.apps.list().to_list()
except ConfigurationError as e:
print(f"Configuration problem: {e.message}")
except ConnectionError as e:
print(f"Cannot reach server: {e.message}")
except TimeoutError as e:
print(f"Request timed out: {e.message}")
Comprehensive Error Handling
Catching Specific Errors
Handle each error type with the appropriate recovery strategy:
from strongly import (
Strongly,
NotFoundError,
AuthenticationError,
PermissionDeniedError,
RateLimitError,
ValidationError,
ConflictError,
InternalServerError,
ServiceUnavailableError,
TimeoutError,
ConnectionError,
)
client = Strongly()
try:
app = client.apps.retrieve("app-abc123")
except NotFoundError as e:
print(f"Not found: {e.message}")
except AuthenticationError:
print("Check your API key")
except PermissionDeniedError:
print("Your key does not have permission for this operation")
except ValidationError as e:
print(f"Invalid input: {e.message}")
for detail in e.details:
print(f" Field: {detail.get('field')} — {detail.get('message')}")
except ConflictError as e:
print(f"Conflict: {e.message}")
except RateLimitError as e:
print(f"Rate limited — retry after {e.retry_after}s")
except InternalServerError as e:
print(f"Server error (request_id={e.request_id}): {e.message}")
except ServiceUnavailableError:
print("Service is temporarily unavailable, try again later")
except TimeoutError:
print("Request timed out — consider increasing the timeout")
except ConnectionError:
print("Cannot reach the API — check your network")
Broad Catch with APIError
If you want a single handler for all HTTP errors:
from strongly import Strongly, APIError
client = Strongly()
try:
client.workflows.execute("wf-abc123")
except APIError as e:
print(f"API error {e.status_code}: {e.message}")
if e.request_id:
print(f"Include this in support requests: {e.request_id}")
Broadest Catch with StronglyError
Catch everything (HTTP, transport, and configuration errors):
from strongly import Strongly, StronglyError
client = Strongly()
try:
client.workflows.execute("wf-abc123")
except StronglyError as e:
print(f"Error: {e.message}")
Retry Patterns
Manual Retry with Rate Limiting
import time
from strongly import Strongly, RateLimitError, APIError
client = Strongly()
def call_with_retry(fn, max_retries=3):
for attempt in range(max_retries):
try:
return fn()
except RateLimitError as e:
wait = e.retry_after or (2 ** attempt)
print(f"Rate limited. Waiting {wait}s (attempt {attempt + 1}/{max_retries})")
time.sleep(wait)
except APIError as e:
if e.status_code >= 500 and attempt < max_retries - 1:
wait = 2 ** attempt
print(f"Server error {e.status_code}. Retrying in {wait}s...")
time.sleep(wait)
else:
raise
raise RuntimeError("Max retries exceeded")
# Usage
app = call_with_retry(lambda: client.apps.retrieve("app-abc123"))
print(f"App: {app.name}")
Exponential Backoff for Server Errors
import time
from strongly import (
Strongly,
RateLimitError,
InternalServerError,
BadGatewayError,
ServiceUnavailableError,
TimeoutError,
)
RETRYABLE = (RateLimitError, InternalServerError, BadGatewayError, ServiceUnavailableError, TimeoutError)
client = Strongly()
def robust_call(fn, max_retries=5, base_delay=1.0):
for attempt in range(max_retries):
try:
return fn()
except RateLimitError as e:
delay = e.retry_after or base_delay * (2 ** attempt)
print(f"Rate limited. Waiting {delay:.1f}s...")
time.sleep(delay)
except RETRYABLE:
if attempt == max_retries - 1:
raise
delay = base_delay * (2 ** attempt)
print(f"Retryable error. Waiting {delay:.1f}s...")
time.sleep(delay)
# Usage
result = robust_call(lambda: client.workflows.execute("wf-abc123"))
print(f"Execution started: {result['executionId']}")
Built-in Retries
The client itself retries on server errors (5xx) automatically:
from strongly import Strongly
# Default: 3 retries with exponential backoff
client = Strongly()
# Custom retry count
client = Strongly(max_retries=5)
# Disable retries entirely
client = Strongly(max_retries=0)
Built-in retries apply to transient server errors (408, 429, 500, 502, 503) and connection timeouts. They do not retry other client errors (4xx). Retry delays use exponential backoff with jitter.
Pagination
List methods return auto-paginating iterators that fetch results in batches automatically. The SDK provides SyncPaginator[T] for synchronous code and AsyncPaginator[T] for async.
SyncPaginator
SyncPaginator[T](
*,
fetch_page, # Callable that fetches a page of results
model, # The model class for deserialization
params=None, # Optional query parameters
page_size=50, # Number of items per page
)
| Method / Property | Return Type | Description |
|---|---|---|
__iter__() | Iterator[T] | Iterate over all items across all pages |
__next__() | T | Get the next item |
to_list() | List[T] | Eagerly fetch all pages and return a flat list |
first() | Optional[T] | Return the first item, or None if empty |
total | Optional[int] | Total item count (available after the first page loads) |
AsyncPaginator
Same interface but with async methods:
| Method / Property | Return Type | Description |
|---|---|---|
__aiter__() | AsyncIterator[T] | Async iterate over all items |
__anext__() | T | Get the next item asynchronously |
async to_list() | List[T] | Eagerly fetch all pages asynchronously |
async first() | Optional[T] | Return the first item, or None if empty |
total | Optional[int] | Total item count (available after the first page loads) |
Iterating with a For Loop
The most common pattern -- iterate through all results automatically:
from strongly import Strongly
client = Strongly()
for app in client.apps.list():
print(f"{app.name} — {app.status}")
The paginator fetches pages behind the scenes as you iterate. You never need to manage page tokens or offsets.
Getting All Results as a List
Use to_list() to eagerly consume all pages into a Python list:
from strongly import Strongly
client = Strongly()
all_workflows = client.workflows.list(status="active").to_list()
print(f"Found {len(all_workflows)} active workflows")
Getting the First Item
Use first() to grab just the first matching result without fetching everything:
from strongly import Strongly
client = Strongly()
latest = client.executions.list(workflow_id="wf-abc123", status="completed").first()
if latest:
print(f"Last execution: {latest.id} — {latest.ended_at}")
else:
print("No completed executions found")
Checking the Total Count
The total property is available after the first page has been fetched:
from strongly import Strongly
client = Strongly()
paginator = client.addons.list(type="postgresql")
next(paginator) # fetch the first page
print(f"Total PostgreSQL addons: {paginator.total}")
Controlling Batch Size
Pass limit to control how many items are fetched per page:
from strongly import Strongly
client = Strongly()
# Fetch in batches of 10
for addon in client.addons.list(limit=10):
print(addon.label)
Async Iteration
With AsyncStrongly, use async for and await:
import asyncio
from strongly import AsyncStrongly
async def main():
async with AsyncStrongly() as client:
# Async for loop
async for wf in client.workflows.list(status="active"):
print(wf.name)
# Async to_list
all_apps = await client.apps.list().to_list()
print(f"Total apps: {len(all_apps)}")
# Async first
latest = await client.executions.list(status="running").first()
if latest:
print(f"Running: {latest.id}")
asyncio.run(main())
Response Patterns
Dictionary Responses
Some methods return plain dict objects (create, delete, deploy, and action methods):
from strongly import Strongly
client = Strongly()
result = client.workflows.execute("wf-abc123")
execution_id = result["executionId"]
print(f"Execution started: {execution_id}")
Model Responses
Retrieve and update methods return typed model objects with attribute access:
from strongly import Strongly
client = Strongly()
app = client.apps.retrieve("app-abc123")
print(app.name) # Attribute access
print(app.status)
print(app.created_at)
Combining Patterns
A typical flow mixing both response types:
from strongly import Strongly, NotFoundError, RateLimitError
import time
client = Strongly()
# Create returns a model
app = client.apps.create({
"name": "my-api",
"image": "my-org/my-api:latest",
"port": 8080,
})
# Deploy returns a dict
client.apps.deploy(app.id)
# Poll status (returns a model)
while True:
try:
status = client.apps.status(app.id)
if status.ready_replicas and status.ready_replicas > 0:
print(f"Live at {status.url}")
break
print(f"State: {status.state}...")
time.sleep(5)
except RateLimitError as e:
time.sleep(e.retry_after or 5)
Complete Example
import time
from strongly import (
Strongly,
NotFoundError,
AuthenticationError,
RateLimitError,
ValidationError,
APIError,
StronglyError,
)
def main():
# --- Initialize client ---
try:
client = Strongly()
me = client.auth.whoami()
print(f"Authenticated as {me.email}")
except AuthenticationError:
print("Invalid API key. Check STRONGLY_API_KEY.")
return
# --- List with pagination ---
print("\nActive workflows:")
paginator = client.workflows.list(status="active")
all_workflows = paginator.to_list()
print(f"Found {len(all_workflows)} active workflows")
for wf in all_workflows[:5]:
print(f" {wf.name} (ID: {wf.id})")
# --- Handle specific errors ---
print("\nLooking up a workflow...")
try:
wf = client.workflows.retrieve("wf-does-not-exist")
except NotFoundError as e:
print(f"Not found: {e.message}")
# --- Handle validation errors ---
print("\nCreating an invalid workflow...")
try:
client.workflows.create({}) # missing required fields
except ValidationError as e:
print(f"Validation failed: {e.message}")
for detail in e.details:
print(f" {detail}")
# --- Retry on rate limiting ---
print("\nExecuting workflows with retry...")
if all_workflows:
wf_id = all_workflows[0].id
for attempt in range(3):
try:
result = client.workflows.execute(wf_id)
print(f"Execution started: {result['executionId']}")
break
except RateLimitError as e:
wait = e.retry_after or 2
print(f"Rate limited. Waiting {wait}s...")
time.sleep(wait)
# --- Pagination patterns ---
print("\nPagination examples:")
# for loop
count = 0
for app in client.apps.list(limit=5):
count += 1
print(f" Iterated over {count} apps")
# first()
first_addon = client.addons.list().first()
if first_addon:
print(f" First addon: {first_addon.label}")
# total count
pg = client.apps.list()
next(pg)
print(f" Total apps: {pg.total}")
# --- Catch-all ---
print("\nDone.")
if __name__ == "__main__":
main()