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.
- pytest_devant_cloud-0.1.0/.gitignore +8 -0
- pytest_devant_cloud-0.1.0/LICENSE +88 -0
- pytest_devant_cloud-0.1.0/PKG-INFO +189 -0
- pytest_devant_cloud-0.1.0/README.md +76 -0
- pytest_devant_cloud-0.1.0/pyproject.toml +58 -0
- pytest_devant_cloud-0.1.0/src/pytest_devant_cloud/__init__.py +12 -0
- pytest_devant_cloud-0.1.0/src/pytest_devant_cloud/client.py +238 -0
- pytest_devant_cloud-0.1.0/src/pytest_devant_cloud/mapping.py +374 -0
- pytest_devant_cloud-0.1.0/src/pytest_devant_cloud/plugin.py +464 -0
- pytest_devant_cloud-0.1.0/tests/test_mapping.py +346 -0
|
@@ -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
|
+
)
|