pytest-devant-cloud 0.1.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,8 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ .pytest_cache/
6
+ *.egg-info/
7
+ dist/
8
+ build/
@@ -0,0 +1,88 @@
1
+ DevQ Cloud Enterprise License
2
+
3
+ Copyright (c) 2026 DevQ Cloud. All rights reserved.
4
+
5
+ ================================================================================
6
+ THIS IS A TEMPLATE. Review with legal counsel before publishing to npm.
7
+ ================================================================================
8
+
9
+ 1. DEFINITIONS
10
+
11
+ "Software" means this npm package and its source code, including all
12
+ modifications and derivative works.
13
+
14
+ "Service" means the DevQ Cloud hosted product made available by DevQ Cloud
15
+ to its customers.
16
+
17
+ "Subscription" means a current, paid commercial agreement between you and
18
+ DevQ Cloud authorising use of the Service, OR a free-tier registration
19
+ accepted by DevQ Cloud.
20
+
21
+ "You" means the individual or legal entity exercising rights under this
22
+ License.
23
+
24
+ 2. GRANT OF USE
25
+
26
+ Subject to the terms below and the existence of an active Subscription,
27
+ DevQ Cloud grants You a non-exclusive, non-transferable, revocable license
28
+ to:
29
+
30
+ (a) install and run the Software on Your own systems and continuous
31
+ integration infrastructure;
32
+ (b) use the Software solely to connect to and interact with the Service;
33
+ (c) make modifications to the Software for Your own internal use, provided
34
+ such modifications are not distributed.
35
+
36
+ 3. RESTRICTIONS
37
+
38
+ You may not, except to the extent expressly permitted by applicable law:
39
+
40
+ (a) redistribute, sublicense, sell, rent, lease, or otherwise transfer the
41
+ Software or any modified version of the Software to any third party;
42
+ (b) use the Software to provide a managed, hosted, or commercial service
43
+ that competes with the Service;
44
+ (c) remove, alter, or obscure any proprietary notices in the Software;
45
+ (d) reverse engineer, decompile, or disassemble the Software, except as
46
+ expressly permitted by applicable law notwithstanding this limitation;
47
+ (e) use the Software in violation of any applicable law or regulation.
48
+
49
+ 4. NO TRANSFER OF OWNERSHIP
50
+
51
+ The Software is licensed, not sold. DevQ Cloud retains all right, title,
52
+ and interest in and to the Software, including all intellectual property
53
+ rights.
54
+
55
+ 5. TERMINATION
56
+
57
+ This License terminates automatically and immediately if Your Subscription
58
+ ends, expires, or is terminated for any reason. Upon termination, You must
59
+ cease all use of the Software and destroy all copies in Your possession.
60
+
61
+ 6. NO WARRANTY
62
+
63
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
64
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
65
+ FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
66
+
67
+ 7. LIMITATION OF LIABILITY
68
+
69
+ IN NO EVENT SHALL DEVQ CLOUD OR ITS CONTRIBUTORS BE LIABLE FOR ANY CLAIM,
70
+ DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR
71
+ OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE
72
+ USE OR OTHER DEALINGS IN THE SOFTWARE. DEVQ CLOUD'S TOTAL LIABILITY FOR
73
+ ALL CLAIMS RELATED TO THE SOFTWARE SHALL NOT EXCEED THE FEES PAID BY YOU
74
+ FOR THE SUBSCRIPTION IN THE TWELVE (12) MONTHS PRECEDING THE CLAIM.
75
+
76
+ 8. GOVERNING LAW
77
+
78
+ This License is governed by the laws of the jurisdiction in which DevQ
79
+ Cloud is incorporated, without regard to its conflict of law principles.
80
+
81
+ 9. ENTIRE AGREEMENT
82
+
83
+ This License, together with the Subscription terms, constitutes the
84
+ entire agreement between You and DevQ Cloud concerning the Software and
85
+ supersedes all prior or contemporaneous agreements, proposals, or
86
+ communications.
87
+
88
+ For commercial licensing inquiries, contact: licensing@devq.cloud
@@ -0,0 +1,189 @@
1
+ Metadata-Version: 2.4
2
+ Name: pytest-devant-cloud
3
+ Version: 0.1.0
4
+ Summary: pytest plugin that streams runs, results, and step trees to Devant Cloud's /v1/runs API.
5
+ Project-URL: Homepage, https://github.com/devant-net/devq-cloud/tree/main/packages/pytest-devant-cloud
6
+ Project-URL: Repository, https://github.com/devant-net/devq-cloud
7
+ Author: Devant Cloud
8
+ License: DevQ Cloud Enterprise License
9
+
10
+ Copyright (c) 2026 DevQ Cloud. All rights reserved.
11
+
12
+ ================================================================================
13
+ THIS IS A TEMPLATE. Review with legal counsel before publishing to npm.
14
+ ================================================================================
15
+
16
+ 1. DEFINITIONS
17
+
18
+ "Software" means this npm package and its source code, including all
19
+ modifications and derivative works.
20
+
21
+ "Service" means the DevQ Cloud hosted product made available by DevQ Cloud
22
+ to its customers.
23
+
24
+ "Subscription" means a current, paid commercial agreement between you and
25
+ DevQ Cloud authorising use of the Service, OR a free-tier registration
26
+ accepted by DevQ Cloud.
27
+
28
+ "You" means the individual or legal entity exercising rights under this
29
+ License.
30
+
31
+ 2. GRANT OF USE
32
+
33
+ Subject to the terms below and the existence of an active Subscription,
34
+ DevQ Cloud grants You a non-exclusive, non-transferable, revocable license
35
+ to:
36
+
37
+ (a) install and run the Software on Your own systems and continuous
38
+ integration infrastructure;
39
+ (b) use the Software solely to connect to and interact with the Service;
40
+ (c) make modifications to the Software for Your own internal use, provided
41
+ such modifications are not distributed.
42
+
43
+ 3. RESTRICTIONS
44
+
45
+ You may not, except to the extent expressly permitted by applicable law:
46
+
47
+ (a) redistribute, sublicense, sell, rent, lease, or otherwise transfer the
48
+ Software or any modified version of the Software to any third party;
49
+ (b) use the Software to provide a managed, hosted, or commercial service
50
+ that competes with the Service;
51
+ (c) remove, alter, or obscure any proprietary notices in the Software;
52
+ (d) reverse engineer, decompile, or disassemble the Software, except as
53
+ expressly permitted by applicable law notwithstanding this limitation;
54
+ (e) use the Software in violation of any applicable law or regulation.
55
+
56
+ 4. NO TRANSFER OF OWNERSHIP
57
+
58
+ The Software is licensed, not sold. DevQ Cloud retains all right, title,
59
+ and interest in and to the Software, including all intellectual property
60
+ rights.
61
+
62
+ 5. TERMINATION
63
+
64
+ This License terminates automatically and immediately if Your Subscription
65
+ ends, expires, or is terminated for any reason. Upon termination, You must
66
+ cease all use of the Software and destroy all copies in Your possession.
67
+
68
+ 6. NO WARRANTY
69
+
70
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
71
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
72
+ FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
73
+
74
+ 7. LIMITATION OF LIABILITY
75
+
76
+ IN NO EVENT SHALL DEVQ CLOUD OR ITS CONTRIBUTORS BE LIABLE FOR ANY CLAIM,
77
+ DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR
78
+ OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE
79
+ USE OR OTHER DEALINGS IN THE SOFTWARE. DEVQ CLOUD'S TOTAL LIABILITY FOR
80
+ ALL CLAIMS RELATED TO THE SOFTWARE SHALL NOT EXCEED THE FEES PAID BY YOU
81
+ FOR THE SUBSCRIPTION IN THE TWELVE (12) MONTHS PRECEDING THE CLAIM.
82
+
83
+ 8. GOVERNING LAW
84
+
85
+ This License is governed by the laws of the jurisdiction in which DevQ
86
+ Cloud is incorporated, without regard to its conflict of law principles.
87
+
88
+ 9. ENTIRE AGREEMENT
89
+
90
+ This License, together with the Subscription terms, constitutes the
91
+ entire agreement between You and DevQ Cloud concerning the Software and
92
+ supersedes all prior or contemporaneous agreements, proposals, or
93
+ communications.
94
+
95
+ For commercial licensing inquiries, contact: licensing@devq.cloud
96
+ License-File: LICENSE
97
+ Keywords: ci,devant,devq,pytest,reporter,test-reporting
98
+ Classifier: Framework :: Pytest
99
+ Classifier: Intended Audience :: Developers
100
+ Classifier: Operating System :: OS Independent
101
+ Classifier: Programming Language :: Python
102
+ Classifier: Programming Language :: Python :: 3
103
+ Classifier: Programming Language :: Python :: 3.10
104
+ Classifier: Programming Language :: Python :: 3.11
105
+ Classifier: Programming Language :: Python :: 3.12
106
+ Classifier: Programming Language :: Python :: 3.13
107
+ Classifier: Topic :: Software Development :: Quality Assurance
108
+ Classifier: Topic :: Software Development :: Testing
109
+ Requires-Python: >=3.10
110
+ Requires-Dist: httpx>=0.24
111
+ Requires-Dist: pytest>=7.0
112
+ Description-Content-Type: text/markdown
113
+
114
+ # pytest-devant-cloud
115
+
116
+ pytest plugin that streams runs, results, and per-test step trees into
117
+ [Devant Cloud](https://github.com/devant-net/devq-cloud) as your suite
118
+ executes — the Python sibling of `@devant-net/playwright-reporter`.
119
+
120
+ ## Install
121
+
122
+ ```bash
123
+ pip install pytest-devant-cloud
124
+ ```
125
+
126
+ The plugin auto-registers via the `pytest11` entry-point group. No
127
+ `conftest.py` changes are needed.
128
+
129
+ ## Configure
130
+
131
+ Set env vars before invoking pytest:
132
+
133
+ | Var | Default | Notes |
134
+ |---|---|---|
135
+ | `DEVQ_API_URL` | `http://localhost:32124` | Your tenant's URL |
136
+ | `DEVQ_TOKEN` | `dev-admin-token` | Bearer token (CI/CD settings) |
137
+ | `DEVQ_PROJECT_ID` | `1` | Devant Cloud project id |
138
+ | `DEVQ_RUN_NAME` | `pytest — <ISO date>` | Display name on the run |
139
+ | `DEVQ_RUN_ID` | _(unset)_ | Attach to an externally-created run instead of creating one |
140
+
141
+ Or with CLI flags:
142
+
143
+ ```bash
144
+ pytest \
145
+ --devant-api-url=https://acme.devq.cloud \
146
+ --devant-token=$DEVQ_TOKEN \
147
+ --devant-project-id=1
148
+ ```
149
+
150
+ To disable the plugin for one run without uninstalling:
151
+
152
+ ```bash
153
+ pytest -p no:devant_cloud
154
+ ```
155
+
156
+ ## How tests bind to test cases
157
+
158
+ Each pytest item resolves to a Devant Cloud `test_case` row in this order:
159
+
160
+ 1. **`@pytest.mark.devant("DEF-AB12")` marker** on the test — looked up
161
+ via `GET /v1/test-cases/by-key/DEF-AB12`.
162
+ 2. **Exact name match** (`<file>::<test>` nodeid) in the project →
163
+ `GET /v1/test-cases?search=…`.
164
+ 3. **Auto-create** → `POST /v1/test-cases`. The plugin prints the new key:
165
+ ```
166
+ [devant] minted DEF-XYZ9 for "tests/test_auth.py::test_login"
167
+ — add @pytest.mark.devant("DEF-XYZ9") to bind it
168
+ ```
169
+
170
+ Bind the key in source once and future runs (even with renames) reuse the
171
+ same case.
172
+
173
+ ## What gets sent
174
+
175
+ | pytest hook | Devant Cloud call |
176
+ |---|---|
177
+ | `pytest_sessionstart` | `POST /v1/runs` (skipped if `DEVQ_RUN_ID` is set) |
178
+ | `pytest_runtest_makereport` (wrapper) | stashes setup/call/teardown reports on the item |
179
+ | `pytest_runtest_logreport` (teardown phase) | resolve test case → `POST /v1/runs/:id/results` with step tree |
180
+ | `pytest_sessionfinish` | `POST /v1/runs/:id/complete` (skipped if `DEVQ_RUN_ID` is set) |
181
+
182
+ The step tree includes one node per phase (setup / call / teardown) with
183
+ status, duration, and longrepr captured as `error_message`.
184
+
185
+ ## CI metadata
186
+
187
+ Auto-detected for GitHub Actions, GitLab CI, CircleCI, Jenkins, and Azure
188
+ DevOps, plus a generic `CI=true` fallback. Populates the `ci_*` columns
189
+ on the run so the dashboard can deep-link to commits and PRs.
@@ -0,0 +1,76 @@
1
+ # pytest-devant-cloud
2
+
3
+ pytest plugin that streams runs, results, and per-test step trees into
4
+ [Devant Cloud](https://github.com/devant-net/devq-cloud) as your suite
5
+ executes — the Python sibling of `@devant-net/playwright-reporter`.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install pytest-devant-cloud
11
+ ```
12
+
13
+ The plugin auto-registers via the `pytest11` entry-point group. No
14
+ `conftest.py` changes are needed.
15
+
16
+ ## Configure
17
+
18
+ Set env vars before invoking pytest:
19
+
20
+ | Var | Default | Notes |
21
+ |---|---|---|
22
+ | `DEVQ_API_URL` | `http://localhost:32124` | Your tenant's URL |
23
+ | `DEVQ_TOKEN` | `dev-admin-token` | Bearer token (CI/CD settings) |
24
+ | `DEVQ_PROJECT_ID` | `1` | Devant Cloud project id |
25
+ | `DEVQ_RUN_NAME` | `pytest — <ISO date>` | Display name on the run |
26
+ | `DEVQ_RUN_ID` | _(unset)_ | Attach to an externally-created run instead of creating one |
27
+
28
+ Or with CLI flags:
29
+
30
+ ```bash
31
+ pytest \
32
+ --devant-api-url=https://acme.devq.cloud \
33
+ --devant-token=$DEVQ_TOKEN \
34
+ --devant-project-id=1
35
+ ```
36
+
37
+ To disable the plugin for one run without uninstalling:
38
+
39
+ ```bash
40
+ pytest -p no:devant_cloud
41
+ ```
42
+
43
+ ## How tests bind to test cases
44
+
45
+ Each pytest item resolves to a Devant Cloud `test_case` row in this order:
46
+
47
+ 1. **`@pytest.mark.devant("DEF-AB12")` marker** on the test — looked up
48
+ via `GET /v1/test-cases/by-key/DEF-AB12`.
49
+ 2. **Exact name match** (`<file>::<test>` nodeid) in the project →
50
+ `GET /v1/test-cases?search=…`.
51
+ 3. **Auto-create** → `POST /v1/test-cases`. The plugin prints the new key:
52
+ ```
53
+ [devant] minted DEF-XYZ9 for "tests/test_auth.py::test_login"
54
+ — add @pytest.mark.devant("DEF-XYZ9") to bind it
55
+ ```
56
+
57
+ Bind the key in source once and future runs (even with renames) reuse the
58
+ same case.
59
+
60
+ ## What gets sent
61
+
62
+ | pytest hook | Devant Cloud call |
63
+ |---|---|
64
+ | `pytest_sessionstart` | `POST /v1/runs` (skipped if `DEVQ_RUN_ID` is set) |
65
+ | `pytest_runtest_makereport` (wrapper) | stashes setup/call/teardown reports on the item |
66
+ | `pytest_runtest_logreport` (teardown phase) | resolve test case → `POST /v1/runs/:id/results` with step tree |
67
+ | `pytest_sessionfinish` | `POST /v1/runs/:id/complete` (skipped if `DEVQ_RUN_ID` is set) |
68
+
69
+ The step tree includes one node per phase (setup / call / teardown) with
70
+ status, duration, and longrepr captured as `error_message`.
71
+
72
+ ## CI metadata
73
+
74
+ Auto-detected for GitHub Actions, GitLab CI, CircleCI, Jenkins, and Azure
75
+ DevOps, plus a generic `CI=true` fallback. Populates the `ci_*` columns
76
+ on the run so the dashboard can deep-link to commits and PRs.
@@ -0,0 +1,58 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "pytest-devant-cloud"
7
+ version = "0.1.0"
8
+ description = "pytest plugin that streams runs, results, and step trees to Devant Cloud's /v1/runs API."
9
+ readme = "README.md"
10
+ license = { file = "LICENSE" }
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "Devant Cloud" }]
13
+ keywords = ["pytest", "reporter", "devant", "devq", "test-reporting", "ci"]
14
+ classifiers = [
15
+ "Framework :: Pytest",
16
+ "Intended Audience :: Developers",
17
+ "Operating System :: OS Independent",
18
+ "Programming Language :: Python",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Software Development :: Testing",
25
+ "Topic :: Software Development :: Quality Assurance",
26
+ ]
27
+ dependencies = [
28
+ "pytest>=7.0",
29
+ "httpx>=0.24",
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/devant-net/devq-cloud/tree/main/packages/pytest-devant-cloud"
34
+ Repository = "https://github.com/devant-net/devq-cloud"
35
+
36
+ # Pytest discovers this plugin automatically via the `pytest11` entry-point
37
+ # group. `plugin` is the module name pytest imports; pytest then registers
38
+ # every hook function it finds at module scope.
39
+ [project.entry-points."pytest11"]
40
+ devant_cloud = "pytest_devant_cloud.plugin"
41
+
42
+ [tool.hatch.build.targets.wheel]
43
+ packages = ["src/pytest_devant_cloud"]
44
+
45
+ [tool.hatch.build.targets.sdist]
46
+ include = [
47
+ "/src",
48
+ "/tests",
49
+ "/README.md",
50
+ "/LICENSE",
51
+ "/pyproject.toml",
52
+ ]
53
+
54
+ [tool.pytest.ini_options]
55
+ testpaths = ["tests"]
56
+ # Don't auto-load the plugin under test against its own unit tests — they
57
+ # only exercise pure helpers and shouldn't open HTTP connections.
58
+ addopts = "-p no:devant_cloud"
@@ -0,0 +1,12 @@
1
+ """pytest plugin that streams runs, results, and step trees to Devant Cloud.
2
+
3
+ Public API is intentionally tiny — the plugin is registered via the
4
+ `pytest11` entry-point group and configured via env vars or CLI flags. See
5
+ the README for the contract.
6
+ """
7
+
8
+ from . import mapping
9
+ from .client import DevqClient
10
+
11
+ __all__ = ["DevqClient", "mapping"]
12
+ __version__ = "0.1.0"
@@ -0,0 +1,238 @@
1
+ """HTTP client for Devant Cloud's `/v1/runs/*` endpoints.
2
+
3
+ Thin wrapper around `httpx` with bearer auth, retry on 5xx/429/network
4
+ errors, and the resolve-test-case fallback chain. The plugin owns the
5
+ lifecycle (createRun → submitResults → completeRun); the client owns the
6
+ wire-level concerns (auth, retries, JSON encoding).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import time
12
+ from dataclasses import dataclass
13
+ from typing import Any
14
+ from urllib.parse import quote
15
+
16
+ import httpx
17
+
18
+ from .mapping import CIInfo
19
+
20
+
21
+ # ── data classes ─────────────────────────────────────────────────────────
22
+
23
+
24
+ @dataclass
25
+ class ResolvedCase:
26
+ """Result of resolveTestCase — matches JS reporter-core's ResolvedCase."""
27
+
28
+ id: int
29
+ key: str
30
+ minted: bool
31
+
32
+
33
+ # ── client ───────────────────────────────────────────────────────────────
34
+
35
+
36
+ class DevqClient:
37
+ """Devant Cloud REST client. One instance per pytest session."""
38
+
39
+ def __init__(
40
+ self,
41
+ api_url: str,
42
+ api_token: str,
43
+ project_id: int,
44
+ *,
45
+ timeout: float = 30.0,
46
+ max_retries: int = 3,
47
+ ) -> None:
48
+ # Strip trailing slash so url joins don't double up.
49
+ self.api_url = api_url.rstrip("/")
50
+ self.project_id = project_id
51
+ self.max_retries = max_retries
52
+ # Re-use one Connection pool for the whole session — pytest can
53
+ # easily emit hundreds of POSTs in a fast suite, and TCP setup
54
+ # cost dwarfs the request itself otherwise.
55
+ self._http = httpx.Client(
56
+ headers={
57
+ "Authorization": f"Bearer {api_token}",
58
+ "Content-Type": "application/json",
59
+ "Accept": "application/json",
60
+ },
61
+ timeout=timeout,
62
+ )
63
+
64
+ # ── lifecycle ────────────────────────────────────────────────────────
65
+
66
+ def close(self) -> None:
67
+ self._http.close()
68
+
69
+ def __enter__(self) -> "DevqClient":
70
+ return self
71
+
72
+ def __exit__(self, *_: object) -> None:
73
+ self.close()
74
+
75
+ # ── low-level request with retry ─────────────────────────────────────
76
+
77
+ def _request(
78
+ self,
79
+ method: str,
80
+ path: str,
81
+ *,
82
+ json_body: Any | None = None,
83
+ ) -> httpx.Response:
84
+ """One request with bounded retry.
85
+
86
+ Retries on 5xx, 429, and network errors with exponential backoff
87
+ (0.25s, 0.5s, 1s). 4xx errors fail fast so callers see schema
88
+ problems immediately.
89
+ """
90
+ url = f"{self.api_url}{path}"
91
+ last_exc: Exception | None = None
92
+ for attempt in range(self.max_retries + 1):
93
+ try:
94
+ resp = self._http.request(method, url, json=json_body)
95
+ except httpx.RequestError as exc:
96
+ last_exc = exc
97
+ if attempt >= self.max_retries:
98
+ raise
99
+ time.sleep(0.25 * (2 ** attempt))
100
+ continue
101
+
102
+ if resp.status_code >= 500 or resp.status_code == 429:
103
+ if attempt >= self.max_retries:
104
+ resp.raise_for_status()
105
+ time.sleep(0.25 * (2 ** attempt))
106
+ continue
107
+
108
+ resp.raise_for_status()
109
+ return resp
110
+
111
+ # Should be unreachable — every path above either returns or raises.
112
+ if last_exc:
113
+ raise last_exc
114
+ raise RuntimeError("retry loop exited without response")
115
+
116
+ # ── API surface ──────────────────────────────────────────────────────
117
+
118
+ def create_run(
119
+ self,
120
+ *,
121
+ name: str,
122
+ framework: str = "pytest",
123
+ ci: CIInfo | None = None,
124
+ mode: str = "automated",
125
+ ) -> dict[str, Any]:
126
+ payload: dict[str, Any] = {
127
+ "project_id": self.project_id,
128
+ "mode": mode,
129
+ "name": name,
130
+ "framework": framework,
131
+ }
132
+ if ci:
133
+ payload["ci"] = ci
134
+ resp = self._request("POST", "/v1/runs", json_body=payload)
135
+ return resp.json()
136
+
137
+ def resolve_test_case(
138
+ self,
139
+ *,
140
+ explicit_key: str | None,
141
+ full_name: str,
142
+ ) -> ResolvedCase:
143
+ """Mirrors reporter-core/src/resolve.ts.
144
+
145
+ Order: explicit @KEY → exact name search → auto-create.
146
+ """
147
+ # 1. explicit @KEY.
148
+ if explicit_key:
149
+ try:
150
+ resp = self._http.get(
151
+ f"{self.api_url}/v1/test-cases/by-key/{quote(explicit_key)}",
152
+ params={"project_id": self.project_id},
153
+ )
154
+ if resp.status_code == 200:
155
+ body = resp.json()
156
+ return ResolvedCase(
157
+ id=int(body["id"]),
158
+ key=str(body["key"]),
159
+ minted=False,
160
+ )
161
+ except httpx.HTTPError:
162
+ # Fall through to search → mint.
163
+ pass
164
+
165
+ # 2. exact name search.
166
+ try:
167
+ resp = self._http.get(
168
+ f"{self.api_url}/v1/test-cases",
169
+ params={"project_id": self.project_id, "search": full_name},
170
+ )
171
+ if resp.status_code == 200:
172
+ items = resp.json().get("items", [])
173
+ for it in items:
174
+ if it.get("name") == full_name:
175
+ return ResolvedCase(
176
+ id=int(it["id"]),
177
+ key=str(it["key"]),
178
+ minted=False,
179
+ )
180
+ except httpx.HTTPError:
181
+ pass
182
+
183
+ # 3. auto-create.
184
+ resp = self._request(
185
+ "POST",
186
+ "/v1/test-cases",
187
+ json_body={
188
+ "project_id": self.project_id,
189
+ "name": full_name,
190
+ "is_automated": True,
191
+ },
192
+ )
193
+ body = resp.json()
194
+ return ResolvedCase(
195
+ id=int(body["id"]),
196
+ key=str(body["key"]),
197
+ minted=True,
198
+ )
199
+
200
+ def submit_results(
201
+ self,
202
+ run_id: int,
203
+ results: list[dict[str, Any]],
204
+ ) -> list[dict[str, Any]]:
205
+ resp = self._request(
206
+ "POST",
207
+ f"/v1/runs/{run_id}/results",
208
+ json_body={"results": results},
209
+ )
210
+ body = resp.json()
211
+ return body.get("results", [])
212
+
213
+ def submit_coverage(
214
+ self,
215
+ run_id: int,
216
+ summary: dict[str, Any],
217
+ ) -> None:
218
+ self._request(
219
+ "POST",
220
+ f"/v1/runs/{run_id}/coverage",
221
+ json_body=summary,
222
+ )
223
+
224
+ def complete_run(
225
+ self,
226
+ run_id: int,
227
+ *,
228
+ status: str = "complete",
229
+ html_report_url: str | None = None,
230
+ ) -> None:
231
+ payload: dict[str, Any] = {"status": status}
232
+ if html_report_url is not None:
233
+ payload["html_report_url"] = html_report_url
234
+ self._request(
235
+ "POST",
236
+ f"/v1/runs/{run_id}/complete",
237
+ json_body=payload,
238
+ )