tauopt.client#
tauopt.Client: one surface, two drivers.
Client(base_url=..., token=...) # hosted: talks to a TauOpt service
Client(driver="local") # local: full engine in-process, no server
A script written against one driver upgrades to the other by changing only the
constructor. The hosted driver is featherweight (httpx + pydantic); the local driver
needs tauopt[local] (and tauopt[botorch] for the GP strategies).
Module Contents#
Classes#
The TauOpt client: one experiment/suggest/observe surface over two interchangeable drivers. |
API#
- class tauopt.client.Client(base_url: str | None = None, token: str | None = None, driver: Literal[hosted, local] = 'hosted', *, timeout_s: float = 60.0, supervisor: Literal[auto, process, off] = 'auto', backstop_alpha: float | None = None, backstop_grace_s: float | None = None)#
The TauOpt client: one experiment/suggest/observe surface over two interchangeable drivers.
Hosted (default):
Client(base_url=..., token=...)talks to a TauOpt service over HTTP with Bearer auth (featherweight: httpx + pydantic only). Local:Client(driver="local")runs the full engine in-process with the same semantics, no server (needstauopt[local]; the GP strategies needtauopt[botorch]).A deadline-bearing ask (
suggest(..., max_wait_s=W)) is BACKSTOPPED on the local driver: a typed answer withinmax(1.25 W, W + 3 s)whatever the engine’s cooperative clocks manage.supervisorpicks the shape –"auto"(default) runs a deadline-bearing ask on a warm worker PROCESS, one per driver, started at the first such ask and killed and replaced behind a refusal;"process"runs every ask there (an ask with no deadline then gets a 375 s backstop, derived from the service’s 300 s substitute, and still carries no deadline of its own);"off"runs it inline, as before.backstop_alphaandbackstop_grace_soverride the protocol constants 1.25 and 3.0 for this driver (None= the constants). Local-driver options only: the hosted driver refuses them withValueError.close()stops the worker. The worker hand-off is POSIX-only in this version: on Windows"auto"runs inline and"process"is refused at construction.On the hosted driver the suggest POST listens for the backstop of its deadline plus a 40 s allowance whenever that exceeds
timeout_s(415 s for an ask with no deadline), so the service’s typed refusal arrives before the client stops listening (the ask runs on its queue lane;provenance.laneworker); every other route keepstimeout_s.Raises ValueError when the hosted driver is missing base_url/token, and ImportError from the local driver when math extras are absent (the message names the exact pip install target).
Initialization
- close() None#
Release the driver’s resources: the hosted driver’s connection pool, or the local driver’s worker process (the next supervised ask starts a fresh one). Idempotent.
- create_experiment(name: str, parameters: list[dict[str, Any]], metric: dict[str, Any] | None = None, metrics: list[dict[str, Any]] | None = None, linear_constraints: list[dict[str, Any]] | None = None, num_solutions: int = 1, observation_budget: int | None = None, parallel_bandwidth: int = 1, metadata: dict[str, Any] | None = None, suggestion_ttl_s: int | None = None) tauopt.models.Experiment#
metric(single-objective) XORmetrics(the metrics cluster: multimetric Pareto, thresholds, strategy=store|constraint).metricis a dict{"name": ..., "objective": "minimize" | "maximize"}; a metric without anobjectivemaximizes. A bare stringmetric="name"is refused before any request leaves (400validation_errornaming the accepted form): the shorthand named no objective and silently maximized a loss.num_solutionsasks best_assignments for k diverse top solutions (single-objective only).observation_budgetis a planning hint rather than an enforced stop: the default routing never reads it; both drivers inject it as thequeue_selectionhorizon when that config block names none; asks past the budget still answer, and your loop decides when to quit.suggestion_ttl_sis the hosted plane’s open-suggestion TTL in seconds: an open suggestion older than it is closed by the next ask on the experiment (expired_atset, its bandwidth slot reissued) and an observation against it answers 409suggestion_expired;Noneleaves the service default (86400 s) in force and sends no key,0never expires. The local driver records and serves the value (the hosted field’s integer domain, 0..2147483647, lax-coerced as the service coerces, else 400validation_error) and never expires a suggestion.
- experiment(experiment_id: str) tauopt.models.Experiment#
Fetch an experiment by id, with live
progress(observation count, best observation, the open-suggestion count, and – on the hosted plane – the next ask’s cost estimateexpected_suggest_sand, for a multimetric experiment, the frontier sizefrontier_count).
- update_experiment(experiment_id: str, name: str | None = None, observation_budget: int | None = None, parallel_bandwidth: int | None = None, metadata: dict[str, Any] | None = None, metric_thresholds: dict[str, float | None] | None = None, suggestion_ttl_s: int | None = None) tauopt.models.Experiment#
PATCH semantics – None leaves a field unchanged. Space and metric structure are immutable;
metric_thresholdsmoves thresholds on declared metrics (SigOpt allowed mid-experiment threshold updates).suggestion_ttl_smoves the hosted plane’s open-suggestion TTL (0never expires; the new value applies from the next ask). BecauseNonemeans unchanged here as for every field, reverting a hosted experiment to the service default TTL has no Client form: send the RESTPATCHwith an explicit"suggestion_ttl_s": null(the MCP server’supdate_experimentdrops null inputs the same way). The local driver records and serves the value (the hosted field’s integer domain, 0..2147483647, lax-coerced as the service coerces, else 400validation_error) and never expires a suggestion.
- archive_experiment(experiment_id: str) None#
Archive (soft-delete) an experiment – the active-experiment-cap remedy on the hosted plane (
DELETE /v1/experiments/{id}, 204).An archived experiment stays READABLE:
experiment()still returns it (statebecomes"deleted"), andbest()plus suggestion dismissals (close_suggestion/close_all_suggestions) keep working, so a finished study remains auditable after it frees its cap slot.suggest/observe/update_experimentrefuse with a 404 :class:~tauopt.ApiError, and archiving twice 404s too – the hosted plane’s exact semantics; the local driver mirrors them. There is no unarchive.Flaky-transport note: the hosted DELETE retries on 429 and on 500, 502, 503, and 504, so a success whose response was lost can surface the RETRY’s 404 even though the archive landed – treat
not_foundfrom this call as already-archived (the states are operationally equivalent: the cap slot is free either way).
- suggest(experiment_id: str, config: dict[str, Any] | None = None, max_wait_s: float | None = None, on_timeout: str | None = None, intents: list[dict[str, Any]] | None = None, tag: str | None = None) tauopt.models.Suggestion#
Ask for the next point.
configpins an explicit optimizer config (the public config stratum); omitted -> the service/engine policy routes.The wait budget is yours:
max_wait_sbounds the whole ask (accepted domain: a number in (0, 300]);on_timeout="ladder"(default) degrades down the robustness ladder with the fallback recorded in provenance,"fail"surfaces an honest timeout (hosted: 503 + Retry-After) so you can run your own backoff. The experiment’sprogress.expected_suggest_sestimates the next ask’s cost from history.Raises :class:
~tauopt.AskDeadlineExceeded(a 503ask_deadline_exceeded:class:~tauopt.ApiError, noRetry-After: a caps re-ask repeats deterministic work) whenconfigsetstimebox_mode: "caps"andmax_wait_sexpires at a rung boundary or at delivery: no suggestion opens and no cheaper rung substitutes; the refusal receipt (point,max_wait_s,elapsed_s,rung) rides the exception’s attributes and itsdetails. Both drivers raise the same class, so oneexcept ApiErrorhandler serves both. On the hosted plane, an ask that first waited for an ask slot reaches the engine with that wait already taken off its budget: the receipt’smax_wait_sis the budget the engine was handed after the wait (the figure you sent, less the queue wait) andelapsed_scounts from the engine’s start. A deadline-bearing ask is also BACKSTOPPED: a typed answer is promised withinmax(1.25 * max_wait_s, max_wait_s + 3 s)whatever the engine’s cooperative clocks manage (a native kernel that never returns). The local driver runs such an ask on a warm worker process (the constructor’ssupervisor); past the backstop it raises :class:~tauopt.BackstopExceeded, the same 503ask_deadline_exceededwithpoint"backstop"and astage(ask,warmup,worker_died,reap,abandoned), the hung work killed behind the response and the state untouched. The hosted plane answers the same class from its own backstop on its queue lane (provenance.laneworker). The suggest POST listens formax(timeout_s, B(W) + 40 s)(415 s with no deadline), so the service’s backstop answers before the client stops listening; a stalled connection ends inReadTimeoutat the derived bound (115 s for a 60 s deadline), or attimeout_swhere the derivation is under it. The engine’s own failures outside the never-fail net (:class:~tauopt.engine.AskFailed: every rung failed, or an integrity postcondition failed after a rung answered) surface on both drivers as a 500 :class:~tauopt.ApiErrorof the sameerror_type(all_rungs_failedwith the attempts indetails,integrity_violationwith the failed check); the caller’s own argument errors (:class:~tauopt.AskValidationError) are the 400validation_errorclass, and any other engine-sideValueErroris a 500internal_error.intents(dedup is the engine-wide default since 0.3.0 – explicitdedup: falsedisables and refuses intents): typed authorized-duplicate requests{kind, source_id, ordinal}– validated against the recomputed policy, never trusted; refusals surface in the suggestion’s provenance dedup record.The ask-loop contract (0.3.0 completion ladder). Exhaustion is COMPLETION rather than failure. On a finite space the engine can prove exhausted (every finite space of at most 1,024 raw points; larger all-finite spaces without conditionals or linear constraints), once every input-feasible point is taken suggest raises :class:
~tauopt.SpaceExhausted– PROVEN exhaustion (the proof receipt ridesdetails.dedup; raised when no draft can be produced, while a batch of authorized replicas returns normally withuniqueness_statusspace_exhausted); outside those proofs a fully covered space surfaces as 409unique_candidate_unavailable::try: sug = client.suggest(exp_id) except OptimizationComplete: # SpaceExhausted narrows the WHICH ... # done -- read the best point
Old
except ApiErrorhandlers keep working (dual lineage). A 409unique_candidate_unavailable(bounded repair ran dry, NO proof – continuous/large spaces) is NOT completion: checkdetails.dedup.unfilled_count/uniqueness_status, then observe or close pending work first – a same-state re-ask is a DETERMINISTIC repeat (P4), never a retry strategy. Authorized replication intents remain the continuation path at proven exhaustion (noisy objectives).tag(an optional worker label: 1 to 128 printable characters, no control or separator characters) is stored on the suggestion, served back asSuggestion.tagon every read, and is the exact-match filter of :meth:suggestions, so a worker that dies and restarts lists its own open suggestions by its tag (the parallel-workers how-to, “Recovering after a worker dies”); a label, never a key for anything (no quota, no admission). Sent only when set; the hosted plane serves the tag and refuses a value outside the rule as 400validation_error. The local driver refuses a value outside the rule as the same 400 with the service’s texts, and a non-string, a lone-surrogate or a non-finite tag is refused by the Client itself before any request, on both drivers, with the service’s text (the wire cannot carry those faithfully).
- observe(experiment_id: str, suggestion_id: str | None = None, assignments: tauopt.models.Assignments | None = None, value: float | None = None, values: dict[str, float] | None = None, value_stddev: float | None = None, values_stddev: dict[str, float] | None = None, failed: bool = False, metadata: dict[str, Any] | None = None) tauopt.models.Observation#
valuein single-metric mode,values(name -> value, every optimize/constraint metric required) in metrics mode.value_stddev/values_stddevreport measurement noise – it enters the GP as fixed per-point variance.
- close_suggestion(experiment_id: str, suggestion_id: str) None#
Dismiss an open suggestion without observing it. Its bandwidth slot frees, and the dismissal advances the sequential streams. Note a dismissed-unobserved point leaves the dedup universe and may be re-suggested later. Unknown id: 404 ApiError; already closed: no-op.
- close_all_suggestions(experiment_id: str) int#
Bulk-dismiss every open suggestion; returns how many were closed.
- best(experiment_id: str) tauopt.models.BestAssignments#
Best result so far: the single best observation (single-objective), k diverse top solutions when the experiment declared num_solutions > 1, or the non-dominated threshold-respecting frontier in metrics mode.
- suggestions(experiment_id: str, state: Literal[_io.open, closed] | None = None, tag: str | None = None, *, limit: int | None = None, offset: int = 0) tauopt.models.Page[tauopt.models.Suggestion]#
List the experiment’s suggestions in id order, as a :class:
~tauopt.Page(alistplustotal, the service’scountunder the filters): every row by default, or only the"open"ones (holding a bandwidth slot, awaiting an observation) or the"closed"ones. Works on archived experiments. This is the state check after a lost response: a POST that died with a 5xx may have committed server-side, so list before re-sending.tagnarrows the list to the suggestions whose worker label is exactlytag(besidestate): the restart step of the recovery how-to. The hosted query?tag=filters on the service; the local driver filters by construction. A value outside the tag’s rule (1 to 128 printable characters) is the same 400validation_erroron both planes; a non-string or a lone-surrogate tag is refused by the Client before any request (httpx would send a number as its decimal string, which the service would accept as a label).limitandoffsetpage a long history (the hosted window,?limit=&offset=):limitabsent lists every row, present it is clamped to 1..200;offsetskips that many rows and must be non-negative – a negative one is a 400validation_erroron both planes (query.offset: Input should be greater than or equal to 0), never floored, and so is a negativelimit(only0clamps up).total(the service’scount) is the total understateandtagwhatever the window (on the hosted plane a paged count and its page are two reads: a concurrent insert can make them differ by one row). A value left at its default stays off the wire. The local driver mirrors the window and the refusals, and astateoutsideopen/closedis the same 400 on both planes.
- observations(experiment_id: str, *, limit: int | None = None, offset: int = 0, sort: str = 'created', dir: Literal[asc, desc] = 'asc') tauopt.models.Page[tauopt.models.Observation]#
List the experiment’s observations (archived experiments included) as a :class:
~tauopt.Page(alistplustotal, the service’scount): in report order by default, every row.sortanddirorder the rows as the hosted list does (?sort=&dir=):createdis id order (report order);valuethe first optimized metric’s value, with failed rows (no value) last in either direction and the id as the secondary key;value-<metric>a declared metric by name (a metric absent from a row’svaluessorts last); any parameter name its assignment (numeric parameters as numbers, categoricals as text; a conditional parameter’s absent assignment last). The two reserved words win over a parameter of the same name. An unknown key is a 400validation_errornaming the accepted keys, on both planes in the service’s text; so is adiroutsideasc/desc.limitandoffsetpage the result:limitabsent lists every row, present it is clamped to 1..200; a negativeoffset(orlimit) is a 400 on both planes, never floored;totalis the service’scountwhatever the window. A value left at its default stays off the wire. The local driver mirrors the whole grammar; its text keys sort by code point where the hosted plane sorts under its database collation.
- experiments(limit: int = 50, offset: int = 0, *, q: str | None = None, sort: Literal[created, updated, name, observation_count] | None = None, dir: Literal[asc, desc] | None = None, include_deleted: bool = False) tauopt.models.ExperimentList#
List your experiments as one page of the
{count, data}envelope (hosted:GET /v1/experiments): by default the ACTIVE ones, newest first, withcountthe active total the hosted plane’s active-experiment cap counts, each row WITH itsprogress(observation count, best observation, open-suggestion count; the hosted plane adds a multimetric experiment’s frontier size; the hosted list leavesexpected_suggest_snull – read one by id for the estimate).limitis clamped to 1..200 on both planes; a negativeoffset(orlimit) is a 400validation_erroron both (query.offset: Input should be greater than or equal to 0), never floored.qnarrows to names containing it, case-insensitively (the%and_wildcards are literal);sortiscreated(the default),updated(the last change to the experiment, its history or its open set),nameorobservation_count,dirascordesc(the default), ties broken by the newest id;include_deletedlists the archived ones too (state"deleted");countfollows every filter. A filter left unset stays off the wire. The local driver mirrors the grammar and the refusals; itsnameorder is by code point where the hosted plane sorts under its database collation. This is the reconciliation step after a lostcreate_experimentresponse: a POST is retried only on a 429 and on the transport’s pre-commit set (experiment_busy,queue_full, andask_slots_busy, retired at deploy 2 with the lane it guarded but kept in the set); a lost response or any other 5xx –connection_pool_busyincluded, which can follow a commit – is never retried, so list before creating again.
- health() tauopt.models.Health#
Liveness probe (hosted:
GET /health, no token needed, so it says nothing about yours):statusis"ok"after the service’s database round-trip. A hosted 5xx (the database round-trip failed) is retried by the transport like any GET – up to five attempts, about eight seconds of backoff – before theApiErrorsurfaces. The local driver answers"ok".
- auth_check() tauopt.models.AuthCheck#
Token probe (hosted:
GET /v1/auth/check, not counted as usage):user_idis the account the token resolves to, or a 401unauthorized:class:~tauopt.ApiErrorfor a missing, malformed, unknown, revoked, or other-lane token. The local driver has no accounts and answersuser_id=None.