hyperstudy 0.2.3__tar.gz → 0.3.0__tar.gz
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.
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/CHANGELOG.md +19 -0
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/PKG-INFO +32 -2
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/README.md +30 -1
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/pyproject.toml +2 -1
- hyperstudy-0.3.0/src/hyperstudy/__init__.py +93 -0
- hyperstudy-0.3.0/src/hyperstudy/experiments.py +222 -0
- hyperstudy-0.3.0/src/hyperstudy/models.py +327 -0
- hyperstudy-0.3.0/tests/fixtures/experiment.schema.json +227 -0
- hyperstudy-0.3.0/tests/test_experiments.py +398 -0
- hyperstudy-0.3.0/tests/test_models.py +348 -0
- hyperstudy-0.2.3/src/hyperstudy/__init__.py +0 -38
- hyperstudy-0.2.3/src/hyperstudy/experiments.py +0 -125
- hyperstudy-0.2.3/tests/test_experiments.py +0 -197
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/.github/workflows/publish.yml +0 -0
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/.github/workflows/sync-release-notes.yml +0 -0
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/.github/workflows/test.yml +0 -0
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/.gitignore +0 -0
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/LICENSE +0 -0
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/docs/superpowers/specs/2026-04-10-recording-downloads-design.md +0 -0
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/src/hyperstudy/_dataframe.py +0 -0
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/src/hyperstudy/_display.py +0 -0
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/src/hyperstudy/_downloads.py +0 -0
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/src/hyperstudy/_http.py +0 -0
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/src/hyperstudy/_pagination.py +0 -0
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/src/hyperstudy/_types.py +0 -0
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/src/hyperstudy/client.py +0 -0
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/src/hyperstudy/exceptions.py +0 -0
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/tests/__init__.py +0 -0
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/tests/conftest.py +0 -0
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/tests/fixtures/deployment_sessions_response.json +0 -0
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/tests/fixtures/deployment_single_response.json +0 -0
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/tests/fixtures/deployments_list_response.json +0 -0
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/tests/fixtures/error_401.json +0 -0
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/tests/fixtures/error_403.json +0 -0
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/tests/fixtures/events_response.json +0 -0
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/tests/fixtures/experiment_single_response.json +0 -0
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/tests/fixtures/experiments_list_response.json +0 -0
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/tests/fixtures/paginated_page1.json +0 -0
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/tests/fixtures/paginated_page2.json +0 -0
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/tests/fixtures/pre_experiment_response.json +0 -0
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/tests/fixtures/recordings_response.json +0 -0
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/tests/fixtures/sparse_ratings_response.json +0 -0
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/tests/fixtures/warnings_response.json +0 -0
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/tests/test_client.py +0 -0
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/tests/test_dataframe.py +0 -0
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/tests/test_downloads.py +0 -0
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/tests/test_http.py +0 -0
- {hyperstudy-0.2.3 → hyperstudy-0.3.0}/tests/test_pagination.py +0 -0
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.3.0
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- Typed experiment builders for programmatic authoring. New top-level exports: `Experiment`, `State`, `FocusComponent`, `Role`, `WaitingRoomConfig`, `DisconnectTimeout`, `InstructionsPage`, `PostExperimentQuestionnaire`, `TransitionRules`, `ComponentType`, `GlobalComponentType`. Snake_case Python fields are converted to the camelCase wire format automatically; unknown top-level fields are preserved via `extra="allow"` so the SDK doesn't block on backend schema additions.
|
|
8
|
+
- Component factory helpers: `show_text`, `show_image`, `show_video`, `vas_rating`, `text_input`, `multiple_choice`, `waiting`, `likert_scale`, `ranking`. Extra kwargs pass through to the component `config` for any backend-supported field.
|
|
9
|
+
- `create_experiment` and `update_experiment` now accept an `experiment=Experiment(...)` argument alongside the existing `**kwargs` form. When both are given, kwargs override builder fields.
|
|
10
|
+
- New `validate_experiment(experiment)` method — dry-run against `POST /experiments/validate`.
|
|
11
|
+
- Schema-drift guard test: vendored copy of `experiment.schema.json` is checked against `ComponentType` / `GlobalComponentType` / required-field declarations so backend additions surface in CI.
|
|
12
|
+
|
|
13
|
+
### Backwards Compatibility
|
|
14
|
+
|
|
15
|
+
- `create_experiment(name=..., **kwargs)` continues to work unchanged.
|
|
16
|
+
- `update_experiment(experiment_id, **kwargs)` continues to work unchanged.
|
|
17
|
+
|
|
18
|
+
### Dependencies
|
|
19
|
+
|
|
20
|
+
- Adds `pydantic>=2.5`.
|
|
21
|
+
|
|
3
22
|
## v0.2.0
|
|
4
23
|
|
|
5
24
|
### Features
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hyperstudy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Python SDK for the HyperStudy experiment platform API
|
|
5
5
|
Project-URL: Homepage, https://hyperstudy.io
|
|
6
6
|
Project-URL: Documentation, https://docs.hyperstudy.io/developers/python-sdk
|
|
@@ -21,6 +21,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
21
21
|
Classifier: Topic :: Scientific/Engineering
|
|
22
22
|
Requires-Python: >=3.9
|
|
23
23
|
Requires-Dist: pandas>=1.5.0
|
|
24
|
+
Requires-Dist: pydantic>=2.5
|
|
24
25
|
Requires-Dist: requests>=2.28.0
|
|
25
26
|
Requires-Dist: tqdm>=4.60.0
|
|
26
27
|
Provides-Extra: dev
|
|
@@ -125,12 +126,41 @@ experiments = hs.list_experiments()
|
|
|
125
126
|
# Get details (with rich display in notebooks)
|
|
126
127
|
exp = hs.get_experiment("exp_id")
|
|
127
128
|
|
|
128
|
-
#
|
|
129
|
+
# Quick create / update / delete (kwargs form, backwards-compatible)
|
|
129
130
|
new_exp = hs.create_experiment(name="My Study", description="...")
|
|
130
131
|
hs.update_experiment("exp_id", name="Updated Name")
|
|
131
132
|
hs.delete_experiment("exp_id")
|
|
132
133
|
```
|
|
133
134
|
|
|
135
|
+
### Building experiments programmatically (0.3.0+)
|
|
136
|
+
|
|
137
|
+
For full experiment definitions — states, components, roles — use the typed `Experiment` builder. Snake_case Python fields convert to the camelCase wire format automatically.
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from hyperstudy import Experiment, State, Role, show_text, vas_rating
|
|
141
|
+
|
|
142
|
+
exp = Experiment(
|
|
143
|
+
name="Two-person study",
|
|
144
|
+
required_participants=2,
|
|
145
|
+
states=[
|
|
146
|
+
State(id="intro", focus_component=show_text("Welcome")),
|
|
147
|
+
State(
|
|
148
|
+
id="rate",
|
|
149
|
+
focus_component=vas_rating("Rate the clip", output_variable="rating"),
|
|
150
|
+
),
|
|
151
|
+
],
|
|
152
|
+
roles={"speaker": Role(name="Speaker", participant_count=1)},
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Dry-run validation against the backend.
|
|
156
|
+
print(hs.validate_experiment(exp)) # → {"valid": True, ...}
|
|
157
|
+
|
|
158
|
+
# Create.
|
|
159
|
+
info = hs.create_experiment(experiment=exp)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Component factories cover `show_text`, `show_image`, `show_video`, `vas_rating`, `text_input`, `multiple_choice`, `waiting`, `likert_scale`, `ranking`. For other component types, construct directly: `FocusComponent(type=ComponentType.X, config={...})`. See the [Experiment Authoring guide](https://docs.hyperstudy.io/experimenters/api-access/experiment-authoring) for the full reference.
|
|
163
|
+
|
|
134
164
|
## Deployments
|
|
135
165
|
|
|
136
166
|
```python
|
|
@@ -91,12 +91,41 @@ experiments = hs.list_experiments()
|
|
|
91
91
|
# Get details (with rich display in notebooks)
|
|
92
92
|
exp = hs.get_experiment("exp_id")
|
|
93
93
|
|
|
94
|
-
#
|
|
94
|
+
# Quick create / update / delete (kwargs form, backwards-compatible)
|
|
95
95
|
new_exp = hs.create_experiment(name="My Study", description="...")
|
|
96
96
|
hs.update_experiment("exp_id", name="Updated Name")
|
|
97
97
|
hs.delete_experiment("exp_id")
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
+
### Building experiments programmatically (0.3.0+)
|
|
101
|
+
|
|
102
|
+
For full experiment definitions — states, components, roles — use the typed `Experiment` builder. Snake_case Python fields convert to the camelCase wire format automatically.
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from hyperstudy import Experiment, State, Role, show_text, vas_rating
|
|
106
|
+
|
|
107
|
+
exp = Experiment(
|
|
108
|
+
name="Two-person study",
|
|
109
|
+
required_participants=2,
|
|
110
|
+
states=[
|
|
111
|
+
State(id="intro", focus_component=show_text("Welcome")),
|
|
112
|
+
State(
|
|
113
|
+
id="rate",
|
|
114
|
+
focus_component=vas_rating("Rate the clip", output_variable="rating"),
|
|
115
|
+
),
|
|
116
|
+
],
|
|
117
|
+
roles={"speaker": Role(name="Speaker", participant_count=1)},
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Dry-run validation against the backend.
|
|
121
|
+
print(hs.validate_experiment(exp)) # → {"valid": True, ...}
|
|
122
|
+
|
|
123
|
+
# Create.
|
|
124
|
+
info = hs.create_experiment(experiment=exp)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Component factories cover `show_text`, `show_image`, `show_video`, `vas_rating`, `text_input`, `multiple_choice`, `waiting`, `likert_scale`, `ranking`. For other component types, construct directly: `FocusComponent(type=ComponentType.X, config={...})`. See the [Experiment Authoring guide](https://docs.hyperstudy.io/experimenters/api-access/experiment-authoring) for the full reference.
|
|
128
|
+
|
|
100
129
|
## Deployments
|
|
101
130
|
|
|
102
131
|
```python
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "hyperstudy"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Python SDK for the HyperStudy experiment platform API"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -28,6 +28,7 @@ dependencies = [
|
|
|
28
28
|
"requests>=2.28.0",
|
|
29
29
|
"pandas>=1.5.0",
|
|
30
30
|
"tqdm>=4.60.0",
|
|
31
|
+
"pydantic>=2.5",
|
|
31
32
|
]
|
|
32
33
|
|
|
33
34
|
[project.optional-dependencies]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""HyperStudy Python SDK — access experiment data from notebooks and scripts.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
import hyperstudy
|
|
6
|
+
|
|
7
|
+
hs = hyperstudy.HyperStudy(api_key="hst_live_...")
|
|
8
|
+
events = hs.get_events("experiment_id")
|
|
9
|
+
|
|
10
|
+
Building experiments programmatically::
|
|
11
|
+
|
|
12
|
+
from hyperstudy import HyperStudy, Experiment, State, show_text
|
|
13
|
+
|
|
14
|
+
exp = Experiment(
|
|
15
|
+
name="Welcome study",
|
|
16
|
+
states=[State(id="s1", focus_component=show_text("Hello"))],
|
|
17
|
+
)
|
|
18
|
+
hs = HyperStudy(api_key="hst_live_...")
|
|
19
|
+
info = hs.create_experiment(experiment=exp)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from ._types import DataType, RatingKind, Scope
|
|
23
|
+
from .client import HyperStudy
|
|
24
|
+
from .exceptions import (
|
|
25
|
+
AuthenticationError,
|
|
26
|
+
ForbiddenError,
|
|
27
|
+
HyperStudyError,
|
|
28
|
+
NotFoundError,
|
|
29
|
+
ServerError,
|
|
30
|
+
ValidationError,
|
|
31
|
+
)
|
|
32
|
+
from .models import (
|
|
33
|
+
ComponentType,
|
|
34
|
+
DisconnectTimeout,
|
|
35
|
+
Experiment,
|
|
36
|
+
FocusComponent,
|
|
37
|
+
GlobalComponentType,
|
|
38
|
+
InstructionsPage,
|
|
39
|
+
PostExperimentQuestionnaire,
|
|
40
|
+
Role,
|
|
41
|
+
State,
|
|
42
|
+
TransitionRules,
|
|
43
|
+
WaitingRoomConfig,
|
|
44
|
+
likert_scale,
|
|
45
|
+
multiple_choice,
|
|
46
|
+
ranking,
|
|
47
|
+
show_image,
|
|
48
|
+
show_text,
|
|
49
|
+
show_video,
|
|
50
|
+
text_input,
|
|
51
|
+
vas_rating,
|
|
52
|
+
waiting,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
__version__ = "0.3.0"
|
|
56
|
+
|
|
57
|
+
__all__ = [
|
|
58
|
+
"HyperStudy",
|
|
59
|
+
"__version__",
|
|
60
|
+
# Exceptions
|
|
61
|
+
"HyperStudyError",
|
|
62
|
+
"AuthenticationError",
|
|
63
|
+
"ForbiddenError",
|
|
64
|
+
"NotFoundError",
|
|
65
|
+
"ServerError",
|
|
66
|
+
"ValidationError",
|
|
67
|
+
# Enums
|
|
68
|
+
"Scope",
|
|
69
|
+
"DataType",
|
|
70
|
+
"RatingKind",
|
|
71
|
+
"ComponentType",
|
|
72
|
+
"GlobalComponentType",
|
|
73
|
+
# Experiment builders
|
|
74
|
+
"Experiment",
|
|
75
|
+
"State",
|
|
76
|
+
"FocusComponent",
|
|
77
|
+
"Role",
|
|
78
|
+
"TransitionRules",
|
|
79
|
+
"WaitingRoomConfig",
|
|
80
|
+
"DisconnectTimeout",
|
|
81
|
+
"InstructionsPage",
|
|
82
|
+
"PostExperimentQuestionnaire",
|
|
83
|
+
# Component factories
|
|
84
|
+
"show_text",
|
|
85
|
+
"show_image",
|
|
86
|
+
"show_video",
|
|
87
|
+
"vas_rating",
|
|
88
|
+
"text_input",
|
|
89
|
+
"multiple_choice",
|
|
90
|
+
"waiting",
|
|
91
|
+
"likert_scale",
|
|
92
|
+
"ranking",
|
|
93
|
+
]
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""Experiment CRUD methods (mixed into HyperStudy via inheritance)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
from pydantic.alias_generators import to_camel
|
|
9
|
+
|
|
10
|
+
from ._display import ExperimentInfo
|
|
11
|
+
from ._http import HttpTransport
|
|
12
|
+
from ._pagination import fetch_all_pages
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from .models import Experiment
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _build_experiment_payload(
|
|
19
|
+
experiment: "Experiment | dict[str, Any] | None" = None,
|
|
20
|
+
**overrides: Any,
|
|
21
|
+
) -> dict[str, Any]:
|
|
22
|
+
"""Build a wire-ready dict from an Experiment instance or raw dict.
|
|
23
|
+
|
|
24
|
+
- ``Experiment`` → ``.model_dump(by_alias=True, exclude_none=True)``
|
|
25
|
+
- ``dict`` → shallow copy
|
|
26
|
+
- ``None`` → empty dict
|
|
27
|
+
|
|
28
|
+
``overrides`` are merged on top (caller wins). Snake_case keys in
|
|
29
|
+
overrides are translated to camelCase so they actually override the
|
|
30
|
+
aliased fields of the dumped ``Experiment`` (otherwise a kwarg like
|
|
31
|
+
``required_participants=5`` would leave the builder's
|
|
32
|
+
``requiredParticipants`` in place and ALSO add ``required_participants``
|
|
33
|
+
to the wire payload — both keys would go).
|
|
34
|
+
"""
|
|
35
|
+
if experiment is None:
|
|
36
|
+
payload: dict[str, Any] = {}
|
|
37
|
+
elif isinstance(experiment, BaseModel):
|
|
38
|
+
payload = experiment.model_dump(by_alias=True, exclude_none=True)
|
|
39
|
+
elif isinstance(experiment, dict):
|
|
40
|
+
payload = dict(experiment)
|
|
41
|
+
else:
|
|
42
|
+
raise TypeError(
|
|
43
|
+
f"experiment must be an Experiment or dict, got {type(experiment).__name__}"
|
|
44
|
+
)
|
|
45
|
+
for key, value in overrides.items():
|
|
46
|
+
payload[to_camel(key)] = value
|
|
47
|
+
return payload
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ExperimentMixin:
|
|
51
|
+
"""Methods for experiment management. Mixed into the main client."""
|
|
52
|
+
|
|
53
|
+
_transport: HttpTransport
|
|
54
|
+
|
|
55
|
+
# ------------------------------------------------------------------
|
|
56
|
+
# Read
|
|
57
|
+
# ------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
def list_experiments(
|
|
60
|
+
self,
|
|
61
|
+
*,
|
|
62
|
+
search: str | None = None,
|
|
63
|
+
limit: int | None = None,
|
|
64
|
+
offset: int = 0,
|
|
65
|
+
output: str = "pandas",
|
|
66
|
+
progress: bool = True,
|
|
67
|
+
):
|
|
68
|
+
"""List experiments accessible to the authenticated user.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
search: Filter experiments by name/description substring.
|
|
72
|
+
limit: Max experiments to return. ``None`` fetches all pages.
|
|
73
|
+
offset: Starting offset for pagination.
|
|
74
|
+
output: ``"pandas"`` (default), ``"polars"``, or ``"dict"``.
|
|
75
|
+
progress: Show progress bar when paginating.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
DataFrame or list of dicts depending on *output*.
|
|
79
|
+
"""
|
|
80
|
+
params: dict[str, Any] = {"offset": offset}
|
|
81
|
+
if search:
|
|
82
|
+
params["search"] = search
|
|
83
|
+
|
|
84
|
+
if limit is not None:
|
|
85
|
+
params["limit"] = limit
|
|
86
|
+
body = self._transport.get("experiments", params=params)
|
|
87
|
+
data = body.get("data", [])
|
|
88
|
+
else:
|
|
89
|
+
data, _ = fetch_all_pages(
|
|
90
|
+
self._transport,
|
|
91
|
+
"experiments",
|
|
92
|
+
params=params,
|
|
93
|
+
page_size=50,
|
|
94
|
+
progress=progress,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return self._convert_output(data, output)
|
|
98
|
+
|
|
99
|
+
def get_experiment(self, experiment_id: str) -> ExperimentInfo:
|
|
100
|
+
"""Get a single experiment with enriched metadata.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
An :class:`ExperimentInfo` object (dict-like, with ``_repr_html_``).
|
|
104
|
+
"""
|
|
105
|
+
body = self._transport.get(f"experiments/{experiment_id}")
|
|
106
|
+
data = body.get("data", [])
|
|
107
|
+
record = data[0] if isinstance(data, list) and data else data
|
|
108
|
+
return ExperimentInfo(record)
|
|
109
|
+
|
|
110
|
+
def get_experiment_config(self, experiment_id: str) -> dict[str, Any]:
|
|
111
|
+
"""Get raw experiment config (lightweight, no enrichment)."""
|
|
112
|
+
body = self._transport.get(f"experiments/{experiment_id}/config")
|
|
113
|
+
data = body.get("data", [])
|
|
114
|
+
return data[0] if isinstance(data, list) and data else data
|
|
115
|
+
|
|
116
|
+
# ------------------------------------------------------------------
|
|
117
|
+
# Write
|
|
118
|
+
# ------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
def create_experiment(
|
|
121
|
+
self,
|
|
122
|
+
*,
|
|
123
|
+
experiment: "Experiment | None" = None,
|
|
124
|
+
name: str | None = None,
|
|
125
|
+
**kwargs,
|
|
126
|
+
) -> ExperimentInfo:
|
|
127
|
+
"""Create a new experiment.
|
|
128
|
+
|
|
129
|
+
Two equivalent ways to call this:
|
|
130
|
+
|
|
131
|
+
- **Typed builder** (recommended): pass an :class:`Experiment` instance
|
|
132
|
+
via the ``experiment`` argument. Snake_case fields on the model are
|
|
133
|
+
converted to the camelCase wire format automatically. Fields added
|
|
134
|
+
via ``extra="allow"`` (unknown to the SDK's typed shape) and entries
|
|
135
|
+
in the per-component ``config`` dict pass through *verbatim* — use
|
|
136
|
+
camelCase keys for those.
|
|
137
|
+
- **Raw kwargs**: pass ``name=`` (required) and any additional fields
|
|
138
|
+
as keyword arguments. Backwards-compatible with hyperstudy <= 0.2.x.
|
|
139
|
+
|
|
140
|
+
When both forms are mixed, keyword arguments override fields from
|
|
141
|
+
``experiment`` — useful for ad-hoc overrides.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
experiment: Typed experiment definition.
|
|
145
|
+
name: Experiment name (required when ``experiment`` is omitted).
|
|
146
|
+
**kwargs: Additional fields, or overrides applied on top of
|
|
147
|
+
``experiment``.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
The created :class:`ExperimentInfo`.
|
|
151
|
+
"""
|
|
152
|
+
if name is not None:
|
|
153
|
+
kwargs["name"] = name
|
|
154
|
+
payload = _build_experiment_payload(experiment, **kwargs)
|
|
155
|
+
if not payload.get("name"):
|
|
156
|
+
raise TypeError(
|
|
157
|
+
"create_experiment requires a 'name' "
|
|
158
|
+
"(pass name=... or experiment=Experiment(name=...))"
|
|
159
|
+
)
|
|
160
|
+
body = self._transport.post("experiments", json=payload)
|
|
161
|
+
data = body.get("data", [])
|
|
162
|
+
record = data[0] if isinstance(data, list) and data else data
|
|
163
|
+
return ExperimentInfo(record)
|
|
164
|
+
|
|
165
|
+
def update_experiment(
|
|
166
|
+
self,
|
|
167
|
+
experiment_id: str,
|
|
168
|
+
*,
|
|
169
|
+
experiment: "Experiment | None" = None,
|
|
170
|
+
**kwargs,
|
|
171
|
+
) -> dict[str, Any]:
|
|
172
|
+
"""Update an experiment.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
experiment_id: ID of the experiment to update.
|
|
176
|
+
experiment: Optional typed :class:`Experiment` carrying the new
|
|
177
|
+
fields. Snake_case → camelCase conversion is applied.
|
|
178
|
+
**kwargs: Fields to update, or overrides applied on top of
|
|
179
|
+
``experiment``.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Update confirmation dict.
|
|
183
|
+
"""
|
|
184
|
+
payload = _build_experiment_payload(experiment, **kwargs)
|
|
185
|
+
body = self._transport.put(f"experiments/{experiment_id}", json=payload)
|
|
186
|
+
data = body.get("data", [])
|
|
187
|
+
return data[0] if isinstance(data, list) and data else data
|
|
188
|
+
|
|
189
|
+
def validate_experiment(
|
|
190
|
+
self,
|
|
191
|
+
experiment: "Experiment | dict[str, Any]",
|
|
192
|
+
) -> dict[str, Any]:
|
|
193
|
+
"""Dry-run validation of an experiment definition against the backend.
|
|
194
|
+
|
|
195
|
+
Returns the ``ValidationResultResponse`` body — typically
|
|
196
|
+
``{"valid": True}`` on success, or ``{"valid": False, "errors": [...]}``
|
|
197
|
+
with a list of issues.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
experiment: An :class:`Experiment` or a raw config dict.
|
|
201
|
+
"""
|
|
202
|
+
if experiment is None:
|
|
203
|
+
raise TypeError("validate_experiment requires an Experiment or dict")
|
|
204
|
+
payload = _build_experiment_payload(experiment)
|
|
205
|
+
body = self._transport.post("experiments/validate", json=payload)
|
|
206
|
+
data = body.get("data", [])
|
|
207
|
+
return data[0] if isinstance(data, list) and data else data
|
|
208
|
+
|
|
209
|
+
def delete_experiment(
|
|
210
|
+
self, experiment_id: str, *, skip_data_check: bool = False
|
|
211
|
+
) -> None:
|
|
212
|
+
"""Delete (soft-delete) an experiment.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
experiment_id: ID of the experiment to delete.
|
|
216
|
+
skip_data_check: If True, skip the confirmation check for experiments
|
|
217
|
+
with collected data.
|
|
218
|
+
"""
|
|
219
|
+
params = {}
|
|
220
|
+
if skip_data_check:
|
|
221
|
+
params["skipDataCheck"] = "true"
|
|
222
|
+
self._transport.delete(f"experiments/{experiment_id}", params=params)
|