cognitive3dpy 1.0.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.
@@ -0,0 +1,203 @@
1
+ Metadata-Version: 2.3
2
+ Name: cognitive3dpy
3
+ Version: 1.0.0
4
+ Summary: Python client for the Cognitive3D analytics API.
5
+ Author: Mona
6
+ Author-email: Mona <monajhzhu@gmail.com>
7
+ Requires-Dist: httpx>=0.28.1
8
+ Requires-Dist: polars>=1.38.1
9
+ Requires-Dist: python-dateutil>=2.9.0.post0
10
+ Requires-Dist: pandas>=3.0 ; extra == 'pandas'
11
+ Requires-Dist: pyarrow>=23.0.1 ; extra == 'pandas'
12
+ Requires-Python: >=3.11
13
+ Provides-Extra: pandas
14
+ Description-Content-Type: text/markdown
15
+
16
+ # cognitive3dpy
17
+
18
+ `cognitive3dpy` is a Python client for the [Cognitive3D](https://cognitive3d.com) analytics API. It turns raw JSON responses into a Polars or Panadas DataFrames ready for analysis, handling authentication, pagination, property flattening, name resolution, and type coercion so you can go from API key to analysis-ready data in a few lines of code.
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ pip install cognitive3dpy
24
+ ```
25
+
26
+ To enable pandas output (`output="pandas"`), install with the optional extra:
27
+
28
+ ```bash
29
+ pip install "cognitive3dpy[pandas]"
30
+ ```
31
+
32
+ ## Getting an API Key
33
+
34
+ 1. Log in to your [Cognitive3D Dashboard](https://app.cognitive3d.com)
35
+ 2. Click your profile icon in the top-right corner and select **Settings**
36
+ 3. Navigate to the **API Keys** tab
37
+ 4. Click **Create API Key**, give it a name, and copy the generated key
38
+
39
+ Store the key in a `.env` file (never commit this to version control):
40
+
41
+ ```bash
42
+ C3D_API_KEY=your_api_key_here
43
+ ```
44
+
45
+ Or set it as an environment variable in your shell:
46
+
47
+ ```bash
48
+ export C3D_API_KEY=your_api_key_here
49
+ ```
50
+
51
+ ## Quick Start
52
+
53
+ ```python
54
+ import cognitive3dpy as c3d
55
+
56
+ # Authenticate and set project
57
+ c3d.c3d_auth() # reads C3D_API_KEY env var, or pass key directly
58
+ c3d.c3d_project(4460) # set default project ID
59
+
60
+ # Pull data
61
+ sessions = c3d.c3d_sessions(n=100)
62
+ events = c3d.c3d_events()
63
+ results = c3d.c3d_objective_results(group_by="steps")
64
+ session_steps = c3d.c3d_session_objectives()
65
+ polls = c3d.c3d_exitpoll()
66
+ ```
67
+
68
+ ## Data Streams
69
+
70
+ | Function | Description | Key output columns |
71
+ |---|---|---|
72
+ | `c3d_sessions()` | Session-level metrics and properties | `session_id`, `duration_s`, `hmd`, `c3d_metrics_*`, `c3d_device_*`, `c3d_geo_*` |
73
+ | `c3d_sessions(session_type="scene")` | Sessions split by scene visited — one row per session-scene | Adds `scene_id`, `scene_version_id`, `scene_name` |
74
+ | `c3d_events()` | One row per in-session event with session context | `event_name`, `event_date`, `position_x/y/z`, `object`, `scene_name`, `prop_*` |
75
+ | `c3d_objective_results()` | Objective success/failure counts by version | `objective_name`, `version_number`, `succeeded`, `failed`, `completion_rate` |
76
+ | `c3d_objective_results(group_by="steps")` | Step-level detail for each objective | Adds `step_name`, `step_type`, `step_detail`, `avg_completion_time_s` |
77
+ | `c3d_session_objectives()` | Per-session objective step results — one row per step per session | `session_id`, `participant_id`, `objective_name`, `step_number`, `step_description`, `step_result`, `step_duration_sec` |
78
+ | `c3d_exitpoll()` | Exit poll survey responses | `question_title`, `value`, `value_label`, per hook/version |
79
+
80
+ ## Sessions
81
+
82
+ Retrieve session-level data with optional date filtering and compact/full column modes.
83
+
84
+ ```python
85
+ # Last 30 days, compact columns (default)
86
+ sessions = c3d.c3d_sessions(n=100)
87
+
88
+ # Custom date range, all columns
89
+ sessions = c3d.c3d_sessions(
90
+ n=50,
91
+ start_date="2025-01-01",
92
+ end_date="2025-06-01",
93
+ compact=False,
94
+ )
95
+ ```
96
+
97
+ ### Scene Sessions
98
+
99
+ Use `session_type="scene"` to get session data broken out by scene — one row per session-scene combination. This is useful for comparing metrics across scenes within the same session. Defaults to the latest version of each scene.
100
+
101
+ ```python
102
+ # All scenes, latest versions (default)
103
+ scene_sessions = c3d.c3d_sessions(session_type="scene")
104
+
105
+ # Filter to a specific scene
106
+ scene_sessions = c3d.c3d_sessions(
107
+ session_type="scene",
108
+ scene_id="de704574-b03f-424e-be87-4985f85ed2e8",
109
+ )
110
+
111
+ # Filter to a specific scene version
112
+ scene_sessions = c3d.c3d_sessions(
113
+ session_type="scene",
114
+ scene_version_id=7011,
115
+ )
116
+ ```
117
+
118
+ ## Events
119
+
120
+ Retrieve per-event data with session context attached. Events are unnested from sessions — one row per event. Dynamic object IDs are resolved to friendly names, and scene version IDs are resolved to scene names.
121
+
122
+ ```python
123
+ events = c3d.c3d_events(
124
+ start_date="2025-01-01",
125
+ n=20,
126
+ )
127
+ ```
128
+
129
+ ## Objective Results
130
+
131
+ Query objective success/failure counts, optionally sliced by version or with step-level detail.
132
+
133
+ ```python
134
+ # By version (default)
135
+ results = c3d.c3d_objective_results(group_by="version")
136
+
137
+ # Step-level detail
138
+ detailed = c3d.c3d_objective_results(group_by="steps")
139
+ ```
140
+
141
+ ## Session Objectives
142
+
143
+ Retrieve per-session objective step results. Returns one row per step per session, with step descriptions and outcomes.
144
+
145
+ ```python
146
+ session_steps = c3d.c3d_session_objectives(
147
+ start_date="2025-01-01",
148
+ end_date="2025-06-01",
149
+ )
150
+ ```
151
+
152
+ ## Exit Polls
153
+
154
+ Retrieve exit poll response counts across all hooks and versions. Returns one row per response option per question per version, with human-readable value labels.
155
+
156
+ ```python
157
+ # All hooks and versions
158
+ polls = c3d.c3d_exitpoll()
159
+
160
+ # Filter to a specific hook and version
161
+ polls = c3d.c3d_exitpoll(hook="end_questions", version=3)
162
+ ```
163
+
164
+ ## Configuration
165
+
166
+ ### Timeout
167
+
168
+ The default request timeout is 30 seconds per API call. To increase it:
169
+
170
+ ```python
171
+ import cognitive3dpy as c3d
172
+
173
+ c3d.c3d_set_timeout(60) # 60 seconds
174
+ ```
175
+
176
+ Or set the `C3D_TIMEOUT` environment variable before your session starts:
177
+
178
+ ```bash
179
+ export C3D_TIMEOUT=60
180
+ ```
181
+
182
+ `c3d_set_timeout()` takes effect immediately. The environment variable is read once on first import, so it must be set before importing the package.
183
+
184
+ ## Key Features
185
+
186
+ - **Compact mode** — `c3d_sessions(compact=True)` (default) returns ~40 curated columns; `compact=False` returns everything
187
+ - **Scene sessions** — `session_type="scene"` queries the latest version of each scene by default, giving the full project picture split by scene
188
+ - **Automatic name resolution** — dynamic object SDK IDs are resolved to friendly names in events and objective steps; scene version IDs are resolved to scene names
189
+ - **Date defaults** — all functions default to the last 30 days when no date range is specified
190
+ - **Column naming** — top-level API fields use `snake_case`; Cognitive3D properties retain their `c3d_` prefix (e.g., `c3d_metrics_fps_score`)
191
+ - **Polars-native** — returns `polars.DataFrame` by default; pass `output="pandas"` for a pandas DataFrame
192
+ - **Session filtering** — `exclude_test`, `exclude_idle`, `min_duration` parameters across functions
193
+
194
+ ## Common Options
195
+
196
+ All data-fetching functions support these session filters:
197
+
198
+ - `exclude_test` / `exclude_idle` — filter out test and junk sessions (default `True`)
199
+ - `start_date` / `end_date` — date range as a `date`/`datetime` object, epoch timestamp, or `"YYYY-MM-DD"` string
200
+ - `min_duration` — minimum session duration in seconds
201
+ - `project_id` — override the default set by `c3d_project()`
202
+ - `output` — `"polars"` (default) or `"pandas"`
203
+ - `warn_empty` — emit a `UserWarning` when 0 rows are returned (default `True`); set to `False` to suppress
@@ -0,0 +1,188 @@
1
+ # cognitive3dpy
2
+
3
+ `cognitive3dpy` is a Python client for the [Cognitive3D](https://cognitive3d.com) analytics API. It turns raw JSON responses into a Polars or Panadas DataFrames ready for analysis, handling authentication, pagination, property flattening, name resolution, and type coercion so you can go from API key to analysis-ready data in a few lines of code.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install cognitive3dpy
9
+ ```
10
+
11
+ To enable pandas output (`output="pandas"`), install with the optional extra:
12
+
13
+ ```bash
14
+ pip install "cognitive3dpy[pandas]"
15
+ ```
16
+
17
+ ## Getting an API Key
18
+
19
+ 1. Log in to your [Cognitive3D Dashboard](https://app.cognitive3d.com)
20
+ 2. Click your profile icon in the top-right corner and select **Settings**
21
+ 3. Navigate to the **API Keys** tab
22
+ 4. Click **Create API Key**, give it a name, and copy the generated key
23
+
24
+ Store the key in a `.env` file (never commit this to version control):
25
+
26
+ ```bash
27
+ C3D_API_KEY=your_api_key_here
28
+ ```
29
+
30
+ Or set it as an environment variable in your shell:
31
+
32
+ ```bash
33
+ export C3D_API_KEY=your_api_key_here
34
+ ```
35
+
36
+ ## Quick Start
37
+
38
+ ```python
39
+ import cognitive3dpy as c3d
40
+
41
+ # Authenticate and set project
42
+ c3d.c3d_auth() # reads C3D_API_KEY env var, or pass key directly
43
+ c3d.c3d_project(4460) # set default project ID
44
+
45
+ # Pull data
46
+ sessions = c3d.c3d_sessions(n=100)
47
+ events = c3d.c3d_events()
48
+ results = c3d.c3d_objective_results(group_by="steps")
49
+ session_steps = c3d.c3d_session_objectives()
50
+ polls = c3d.c3d_exitpoll()
51
+ ```
52
+
53
+ ## Data Streams
54
+
55
+ | Function | Description | Key output columns |
56
+ |---|---|---|
57
+ | `c3d_sessions()` | Session-level metrics and properties | `session_id`, `duration_s`, `hmd`, `c3d_metrics_*`, `c3d_device_*`, `c3d_geo_*` |
58
+ | `c3d_sessions(session_type="scene")` | Sessions split by scene visited — one row per session-scene | Adds `scene_id`, `scene_version_id`, `scene_name` |
59
+ | `c3d_events()` | One row per in-session event with session context | `event_name`, `event_date`, `position_x/y/z`, `object`, `scene_name`, `prop_*` |
60
+ | `c3d_objective_results()` | Objective success/failure counts by version | `objective_name`, `version_number`, `succeeded`, `failed`, `completion_rate` |
61
+ | `c3d_objective_results(group_by="steps")` | Step-level detail for each objective | Adds `step_name`, `step_type`, `step_detail`, `avg_completion_time_s` |
62
+ | `c3d_session_objectives()` | Per-session objective step results — one row per step per session | `session_id`, `participant_id`, `objective_name`, `step_number`, `step_description`, `step_result`, `step_duration_sec` |
63
+ | `c3d_exitpoll()` | Exit poll survey responses | `question_title`, `value`, `value_label`, per hook/version |
64
+
65
+ ## Sessions
66
+
67
+ Retrieve session-level data with optional date filtering and compact/full column modes.
68
+
69
+ ```python
70
+ # Last 30 days, compact columns (default)
71
+ sessions = c3d.c3d_sessions(n=100)
72
+
73
+ # Custom date range, all columns
74
+ sessions = c3d.c3d_sessions(
75
+ n=50,
76
+ start_date="2025-01-01",
77
+ end_date="2025-06-01",
78
+ compact=False,
79
+ )
80
+ ```
81
+
82
+ ### Scene Sessions
83
+
84
+ Use `session_type="scene"` to get session data broken out by scene — one row per session-scene combination. This is useful for comparing metrics across scenes within the same session. Defaults to the latest version of each scene.
85
+
86
+ ```python
87
+ # All scenes, latest versions (default)
88
+ scene_sessions = c3d.c3d_sessions(session_type="scene")
89
+
90
+ # Filter to a specific scene
91
+ scene_sessions = c3d.c3d_sessions(
92
+ session_type="scene",
93
+ scene_id="de704574-b03f-424e-be87-4985f85ed2e8",
94
+ )
95
+
96
+ # Filter to a specific scene version
97
+ scene_sessions = c3d.c3d_sessions(
98
+ session_type="scene",
99
+ scene_version_id=7011,
100
+ )
101
+ ```
102
+
103
+ ## Events
104
+
105
+ Retrieve per-event data with session context attached. Events are unnested from sessions — one row per event. Dynamic object IDs are resolved to friendly names, and scene version IDs are resolved to scene names.
106
+
107
+ ```python
108
+ events = c3d.c3d_events(
109
+ start_date="2025-01-01",
110
+ n=20,
111
+ )
112
+ ```
113
+
114
+ ## Objective Results
115
+
116
+ Query objective success/failure counts, optionally sliced by version or with step-level detail.
117
+
118
+ ```python
119
+ # By version (default)
120
+ results = c3d.c3d_objective_results(group_by="version")
121
+
122
+ # Step-level detail
123
+ detailed = c3d.c3d_objective_results(group_by="steps")
124
+ ```
125
+
126
+ ## Session Objectives
127
+
128
+ Retrieve per-session objective step results. Returns one row per step per session, with step descriptions and outcomes.
129
+
130
+ ```python
131
+ session_steps = c3d.c3d_session_objectives(
132
+ start_date="2025-01-01",
133
+ end_date="2025-06-01",
134
+ )
135
+ ```
136
+
137
+ ## Exit Polls
138
+
139
+ Retrieve exit poll response counts across all hooks and versions. Returns one row per response option per question per version, with human-readable value labels.
140
+
141
+ ```python
142
+ # All hooks and versions
143
+ polls = c3d.c3d_exitpoll()
144
+
145
+ # Filter to a specific hook and version
146
+ polls = c3d.c3d_exitpoll(hook="end_questions", version=3)
147
+ ```
148
+
149
+ ## Configuration
150
+
151
+ ### Timeout
152
+
153
+ The default request timeout is 30 seconds per API call. To increase it:
154
+
155
+ ```python
156
+ import cognitive3dpy as c3d
157
+
158
+ c3d.c3d_set_timeout(60) # 60 seconds
159
+ ```
160
+
161
+ Or set the `C3D_TIMEOUT` environment variable before your session starts:
162
+
163
+ ```bash
164
+ export C3D_TIMEOUT=60
165
+ ```
166
+
167
+ `c3d_set_timeout()` takes effect immediately. The environment variable is read once on first import, so it must be set before importing the package.
168
+
169
+ ## Key Features
170
+
171
+ - **Compact mode** — `c3d_sessions(compact=True)` (default) returns ~40 curated columns; `compact=False` returns everything
172
+ - **Scene sessions** — `session_type="scene"` queries the latest version of each scene by default, giving the full project picture split by scene
173
+ - **Automatic name resolution** — dynamic object SDK IDs are resolved to friendly names in events and objective steps; scene version IDs are resolved to scene names
174
+ - **Date defaults** — all functions default to the last 30 days when no date range is specified
175
+ - **Column naming** — top-level API fields use `snake_case`; Cognitive3D properties retain their `c3d_` prefix (e.g., `c3d_metrics_fps_score`)
176
+ - **Polars-native** — returns `polars.DataFrame` by default; pass `output="pandas"` for a pandas DataFrame
177
+ - **Session filtering** — `exclude_test`, `exclude_idle`, `min_duration` parameters across functions
178
+
179
+ ## Common Options
180
+
181
+ All data-fetching functions support these session filters:
182
+
183
+ - `exclude_test` / `exclude_idle` — filter out test and junk sessions (default `True`)
184
+ - `start_date` / `end_date` — date range as a `date`/`datetime` object, epoch timestamp, or `"YYYY-MM-DD"` string
185
+ - `min_duration` — minimum session duration in seconds
186
+ - `project_id` — override the default set by `c3d_project()`
187
+ - `output` — `"polars"` (default) or `"pandas"`
188
+ - `warn_empty` — emit a `UserWarning` when 0 rows are returned (default `True`); set to `False` to suppress
@@ -0,0 +1,53 @@
1
+ [project]
2
+ name = "cognitive3dpy"
3
+ version = "1.0.0"
4
+ description = "Python client for the Cognitive3D analytics API."
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Mona", email = "monajhzhu@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.11"
10
+ dependencies = [
11
+ "httpx>=0.28.1",
12
+ "polars>=1.38.1",
13
+ "python-dateutil>=2.9.0.post0",
14
+ ]
15
+
16
+ [project.optional-dependencies]
17
+ pandas = [
18
+ "pandas>=3.0",
19
+ "pyarrow>=23.0.1",
20
+ ]
21
+
22
+ [build-system]
23
+ requires = ["uv_build>=0.10.6,<0.11.0"]
24
+ build-backend = "uv_build"
25
+
26
+ [dependency-groups]
27
+ dev = [
28
+ "ipykernel>=7.2.0",
29
+ "mypy>=1.19.1",
30
+ "pytest>=9.0.2",
31
+ "pytest-cov>=7.0.0",
32
+ "python-semantic-release>=9.0.0",
33
+ "respx>=0.22.0",
34
+ "ruff>=0.15.2",
35
+ ]
36
+
37
+ [tool.ruff]
38
+ line-length = 88
39
+ target-version = "py311"
40
+
41
+ [tool.ruff.lint]
42
+ select = ["E", "F", "I", "UP"]
43
+
44
+ [tool.pytest.ini_options]
45
+ testpaths = ["tests"]
46
+
47
+ [tool.semantic_release]
48
+ version_toml = ["pyproject.toml:project.version"]
49
+ branch = "main"
50
+ commit_message = "chore(release): v{version} [skip ci]"
51
+
52
+ [tool.semantic_release.changelog]
53
+ exclude_commit_patterns = ["^chore"]
@@ -0,0 +1,38 @@
1
+ """cognitive3dpy — Python client for the Cognitive3D Analytics REST API."""
2
+
3
+ from importlib.metadata import version
4
+
5
+ __version__ = version("cognitive3dpy")
6
+
7
+ from cognitive3dpy._client import (
8
+ C3DAPIError,
9
+ C3DAuthError,
10
+ C3DError,
11
+ C3DNotFoundError,
12
+ c3d_set_timeout,
13
+ )
14
+ from cognitive3dpy.auth import c3d_auth, c3d_project
15
+ from cognitive3dpy.events import c3d_events
16
+ from cognitive3dpy.exitpoll import c3d_exitpoll
17
+ from cognitive3dpy.objectives import c3d_objective_results
18
+ from cognitive3dpy.session_objectives import c3d_session_objectives
19
+ from cognitive3dpy.sessions import c3d_sessions
20
+
21
+ __all__ = [
22
+ # Auth
23
+ "c3d_auth",
24
+ "c3d_project",
25
+ # Data functions
26
+ "c3d_sessions",
27
+ "c3d_events",
28
+ "c3d_objective_results",
29
+ "c3d_session_objectives",
30
+ "c3d_exitpoll",
31
+ # Config
32
+ "c3d_set_timeout",
33
+ # Exceptions
34
+ "C3DError",
35
+ "C3DAuthError",
36
+ "C3DNotFoundError",
37
+ "C3DAPIError",
38
+ ]
@@ -0,0 +1,139 @@
1
+ """HTTP client wrapper for the Cognitive3D API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import atexit
6
+ import logging
7
+ import os
8
+ import time
9
+
10
+ import httpx
11
+
12
+ from cognitive3dpy.auth import get_api_key
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ _RETRYABLE_STATUS = {429, 500, 502, 503, 504}
17
+ _MAX_RETRIES = 3
18
+ _BACKOFF_BASE = 1.0 # seconds
19
+
20
+ BASE_URL = "https://api.cognitive3d.com"
21
+ _DEFAULT_TIMEOUT = 30.0
22
+
23
+ _client: httpx.Client | None = None
24
+ _timeout: float = float(os.environ.get("C3D_TIMEOUT", _DEFAULT_TIMEOUT))
25
+
26
+
27
+ def _close_client() -> None:
28
+ if _client is not None and not _client.is_closed:
29
+ _client.close()
30
+
31
+
32
+ atexit.register(_close_client)
33
+
34
+
35
+ class C3DError(Exception):
36
+ """Base exception for Cognitive3D API errors."""
37
+
38
+
39
+ class C3DAuthError(C3DError):
40
+ """Raised on 401 Unauthorized."""
41
+
42
+
43
+ class C3DNotFoundError(C3DError):
44
+ """Raised on 404 Not Found."""
45
+
46
+
47
+ class C3DAPIError(C3DError):
48
+ """Raised on any other non-2xx response."""
49
+
50
+
51
+ def _get_client() -> httpx.Client:
52
+ """Return a reusable httpx client, creating one if needed."""
53
+ global _client
54
+ if _client is None or _client.is_closed:
55
+ _client = httpx.Client(base_url=BASE_URL, timeout=_timeout)
56
+ return _client
57
+
58
+
59
+ def c3d_set_timeout(seconds: float) -> None:
60
+ """Set the HTTP request timeout for all API calls.
61
+
62
+ Takes effect immediately by recreating the underlying HTTP client.
63
+ Can also be configured via the ``C3D_TIMEOUT`` environment variable
64
+ before the first API call.
65
+
66
+ Parameters
67
+ ----------
68
+ seconds : float
69
+ Timeout in seconds per request. Default is 30.0.
70
+ """
71
+ global _timeout, _client
72
+ _timeout = float(seconds)
73
+ if _client is not None and not _client.is_closed:
74
+ _client.close()
75
+ _client = None
76
+
77
+
78
+ def _build_headers() -> dict[str, str]:
79
+ """Build request headers with the stored API key."""
80
+ return {
81
+ "Authorization": get_api_key(),
82
+ "Content-Type": "application/json",
83
+ }
84
+
85
+
86
+ def _raise_for_status(response: httpx.Response) -> None:
87
+ """Raise a typed exception for non-2xx responses."""
88
+ if response.is_success:
89
+ return
90
+ msg = f"{response.status_code}: {response.text}"
91
+ if response.status_code == 401:
92
+ raise C3DAuthError(msg)
93
+ if response.status_code == 404:
94
+ raise C3DNotFoundError(msg)
95
+ raise C3DAPIError(msg)
96
+
97
+
98
+ def _execute_with_retry(fn) -> httpx.Response:
99
+ """Execute a request callable with exponential backoff on transient errors."""
100
+ last_exc: Exception | None = None
101
+ for attempt in range(_MAX_RETRIES):
102
+ try:
103
+ response = fn()
104
+ if response.status_code not in _RETRYABLE_STATUS:
105
+ return response
106
+ last_exc = C3DAPIError(f"{response.status_code}: {response.text}")
107
+ except httpx.TransportError as exc:
108
+ last_exc = exc
109
+
110
+ wait = _BACKOFF_BASE * (2**attempt)
111
+ logger.warning(
112
+ "Request failed (attempt %d/%d), retrying in %.1fs...",
113
+ attempt + 1,
114
+ _MAX_RETRIES,
115
+ wait,
116
+ )
117
+ time.sleep(wait)
118
+
119
+ raise last_exc
120
+
121
+
122
+ def c3d_request(endpoint: str, body: dict) -> dict | list:
123
+ """POST to the API and return the parsed JSON response."""
124
+ client = _get_client()
125
+ response = _execute_with_retry(
126
+ lambda: client.post(endpoint, headers=_build_headers(), json=body)
127
+ )
128
+ _raise_for_status(response)
129
+ return response.json()
130
+
131
+
132
+ def c3d_get(endpoint: str) -> dict | list:
133
+ """GET from the API and return the parsed JSON response."""
134
+ client = _get_client()
135
+ response = _execute_with_retry(
136
+ lambda: client.get(endpoint, headers=_build_headers())
137
+ )
138
+ _raise_for_status(response)
139
+ return response.json()