Skip to main content

Using the SDK

Creating a client​

DimeSchedulerClient is the single entry point. It needs an API keyAPI keyA credential that authenticates calls to the Dime.Scheduler API. The preferred way to integrate over the deprecated JWT flow. and, optionally, an environment:

from dimescheduler import DimeSchedulerClient, Environment

client = DimeSchedulerClient(api_key="MY_API_KEY") # production
client = DimeSchedulerClient(api_key="MY_API_KEY", environment=Environment.Sandbox)

Environment has two members: Production (the default) and Sandbox. Each one maps to the base URL of that environment, which you can read back from client.base_url; client.environment returns the enum member itself. An empty api_key raises ValueError straight away rather than failing on the first request.

The client owns an httpx.Client under the hood and should be closed when you're done. Use it as a context manager so the connection pool is released for you automatically:

with DimeSchedulerClient(api_key="MY_API_KEY") as client:
client.categories.create({"name": "INSTALL", "displayName": "Install", "color": "#22d3ee"})

For long-lived processes (web apps, workers) instantiate the client once at startup, reuse it across requests, and call client.close() on shutdown.

Configuration​

Everything after environment is keyword-only. The client sets X-API-KEY and User-Agent for you; any headers you pass are merged on top and win on conflict, so you can override the user agent or add a correlation header. Keyword arguments the client doesn't recognize are forwarded to httpx.Client as-is:

import httpx
from dimescheduler import DimeSchedulerClient, Environment

client = DimeSchedulerClient(
api_key="MY_API_KEY",
environment=Environment.Sandbox,
timeout=60.0, # seconds, default 30.0
headers={"X-Correlation-Id": "nightly-sync"}, # merged into every request
transport=httpx.HTTPTransport(local_address="0.0.0.0"),
proxy="http://proxy.internal:8080", # any other httpx.Client kwarg
)

timeout is the client-level default for every request; see Timeouts for per-call overrides. transport is the inner httpx.BaseTransport the client sends requests through. Unless you pass retry=False, the SDK wraps it in its own retrying transport, so a custom transport and retries compose.

Domain-grouped accessors​

Every entity hangs off a typed accessor on the client. The naming mirrors the .NET and JavaScript SDKs 1:1, with idiomatic snake_case:

# CRUD: create / update / delete / get_all
client.categories # /category
client.resources # /resource
client.filter_groups # /filterGroup
client.filter_values # /filterValue
client.tags # /tag
client.project_members # /projectMember
client.task_dependencies # /taskDependency

# CRUD plus a purpose-built read
client.appointments # /appointment get(start_date, end_date, resources=None), get_by_id(id)
client.jobs # /job get(job_no)
client.tasks # /task get(is_open=, page=, page_size=)
client.time_entries # /timeEntry get(start, end)
client.time_sessions # /timeSession get(start, end)
client.resource_capacities # /resourceCapacity get(start, end)
client.notifications # /notification get(page, limit, sort=, group=, filter=)

# create / delete only
client.task_assignments # /taskAssignment
client.task_budget_entries # /taskBudgetEntry

# read-only: get_all only
client.appointment_dependencies, client.appointment_fields, client.calendars, client.resource_types

# their own shape
client.messages, client.geocoding, client.optimization, client.recommendation, client.imports

jobs and tasks are full CRUD accessors (JobApi, TaskApi) with a get added on top, as are appointments, time_entries and time_sessions. task_assignments and task_budget_entries only expose create and delete: the API has no update or list for them. The read-only accessors expose get_all and nothing else.

CRUD-shaped entities (most of them) expose create / update / delete / get_all:

client.categories.create(category)
client.categories.update(category)
client.categories.delete(category)
client.categories.get_all()

Endpoints with their own shape get purpose-built methods:

from datetime import date

client.appointments.get(start_date, end_date, resources=["R1", "R2"])
client.appointments.get_by_id("8f3c0b1e-...")
client.jobs.get("CASE_123")
client.tasks.get(is_open=True, page=1, page_size=50)
client.time_entries.get(date(2026, 5, 1), date(2026, 5, 31)) # inclusive day range
client.notifications.get(1, 50, sort="createdAt:desc") # page and limit are positional
client.geocoding.geocode_text("221B Baker Street", "GB")
client.optimization.field_service(request)
client.messages.send({"text": "Heads up!", "severity": "Warning"})

time_entries.get and time_sessions.get accept a date, a datetime or a string, but only the date part is sent: the API filters on whole days. notifications.get takes page and limit as required positional arguments; sort, group and filter are optional keywords.

For the full list of accessors, see the API reference or the client source.

Results, not exceptions​

Once the server has answered, every call returns a Result - including on 4xx and 5xx:

result = client.categories.get_all()

result.ok # True on 2xx
result.data # parsed JSON body on success, else None
result.error # parsed JSON error body on 4xx/5xx, else None
result.response # underlying httpx.Response - headers, status code, raw bytes

The dispatcher never raises on a non-2xx response - you decide how to react. If you'd rather work with exceptions, call raise_for_error(); if you want the typed error as a value instead, to_error() returns the same exception object without raising it (or None on success), which is handy for branching on its class:

from dimescheduler import RateLimitError, ValidationError

result = client.categories.create(category)
try:
result.raise_for_error()
except RateLimitError as e:
time.sleep(e.retry_after or 1)
except ValidationError as e:
log.warning("bad payload: %s", e.body)

The typed exception hierarchy covers ValidationError (400/422), AuthenticationError (401), AuthorizationError (403), NotFoundError (404), ConflictError (409), RateLimitError (429) and ServerError (5xx). Any other status falls back to the base class. All of them subclass DimeSchedulerError, which carries status_code, body (the parsed error payload, if any) and response (the raw httpx.Response); RateLimitError adds retry_after in seconds.

Transport failures are the exception to "results, not exceptions". When the connection fails or a request times out and the retries are exhausted, there is no response to wrap, so the underlying httpx exception (httpx.ConnectError, httpx.ReadTimeout, ...) propagates from the call itself. The package exports a NetworkError class, but the SDK does not raise it today - catch the httpx family instead:

import httpx

try:
result = client.categories.get_all()
except httpx.TransportError as e: # ConnectError, ReadTimeout, ... after retries are exhausted
log.error("could not reach Dime.Scheduler: %s", e)

Retries​

The SDK retries transient failures by default: connection errors, timeouts, and HTTP 408 / 429 / 500 / 502 / 503 / 504. A request is retried up to three times (max_retries=3), so it is attempted at most four times. Backoff is exponential with full jitter, starting at 0.5 s and doubling each attempt, capped at 30 seconds, and Retry-After is honored (and capped by the same maximum). When the retries run out on a retryable status code you get that last response back as a Result; when they run out on a connection error or timeout, the httpx exception propagates.

Override the policy via RetryConfig:

from dimescheduler import DimeSchedulerClient, RetryConfig

client = DimeSchedulerClient(
api_key="MY_API_KEY",
retry=RetryConfig(max_retries=5, initial_delay_s=1.0, max_delay_s=60.0),
)

RetryConfig is a frozen dataclass with six fields and these defaults: max_retries=3, initial_delay_s=0.5, max_delay_s=30.0, backoff_multiplier=2.0, retry_status_codes=(408, 429, 500, 502, 503, 504) and respect_retry_after=True. Set respect_retry_after=False to always use the computed backoff, or pass your own retry_status_codes tuple to narrow or widen what counts as retryable.

Disable retries entirely with retry=False, which drops the retrying transport altogether. If you'd rather keep the wrapper in place and turn retries off, pass RetryConfig(max_retries=0) instead.

Timeouts and cancellation​

The client-level timeout (default 30 s) applies to every request. Override it for a single call:

import httpx

client.imports.run(payload, timeout=120.0) # 120 s
client.imports.run(payload, timeout=httpx.Timeout(60.0, connect=5.0))
client.imports.run(payload, timeout=None) # wait indefinitely

Omit timeout to inherit the client-level default.