docus 0.1.1__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.
docus-0.1.1/PKG-INFO ADDED
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: docus
3
+ Version: 0.1.1
4
+ Summary: Pytest client for the Docus test recorder service
5
+ Project-URL: Homepage, https://github.com/dooblai/pydocusclient
6
+ Project-URL: Source, https://github.com/dooblai/pydocusclient
7
+ Project-URL: Issues, https://github.com/dooblai/pydocusclient/issues
8
+ Classifier: Framework :: Pytest
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3 :: Only
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Topic :: Software Development :: Testing
13
+ Requires-Python: >=3.9
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: pytest>=7
16
+
17
+ # docus
18
+
19
+ An installable pytest plugin that sends test sessions and results to a Docus
20
+ test recorder service. It uses the pytest11 entry point, so existing test
21
+ files do not need to change.
22
+
23
+ ## Install and run
24
+
25
+ ```sh
26
+ pip install docus
27
+ TEST_RECORDER_URL=https://recorder.example.com pytest
28
+ ```
29
+
30
+ The plugin stays inactive until `TEST_RECORDER_URL` or `--recorder-url` is
31
+ provided. Set `TEST_RECORDER_API_TOKEN` when the service requires a bearer
32
+ token. Set `TEST_RECORDER_REPO` to the canonical Git repository identifier;
33
+ otherwise the plugin uses `GITHUB_REPOSITORY` or Git origin. The optional
34
+ `TEST_RECORDER_ACTOR` overrides the current user.
35
+
36
+ The plugin records collected tests, test results, pytest phase durations,
37
+ captured output, Git commit and branch, and a session ID. It sends a batch
38
+ at session end. If delivery fails, pytest results are unchanged and the batch
39
+ is saved under `.test-recorder/spool`. Retry with:
40
+
41
+ ```sh
42
+ TEST_RECORDER_URL=https://recorder.example.com pydocus-resend
43
+ ```
44
+
45
+ To keep a test identity across a pytest function rename, mark it with
46
+ `@pytest.mark.record_id("logical-name")`. Parameter IDs should remain stable.
47
+ Output and failure fields are capped at 64 KiB each; the batch indicates when
48
+ truncation occurred.
49
+
50
+ The Docus service is developed at
51
+ [dooblai/docus](https://github.com/dooblai/docus). This package only depends
52
+ on pytest and the Python standard library. Other languages can send the same
53
+ JSON event format directly to the service.
54
+
55
+ ## Build and release to PyPI
56
+
57
+ 1. Install the package and run `python testing_suite_client.py`.
58
+ 2. Run `python -m pip install build twine`, `python -m build`, and
59
+ `python -m twine check dist/*` from this repository.
60
+ 3. Create a GitHub environment named `pypi` in `dooblai/pydocusclient`.
61
+ 4. In PyPI, configure a **pending trusted publisher** for project name
62
+ `docus`: owner `dooblai`, repository `pydocusclient`,
63
+ workflow filename `release.yml`, environment `pypi`. This must match
64
+ `.github/workflows/release.yml` exactly.
65
+ 5. Push a version tag such as `v0.1.1`, then publish a GitHub Release for
66
+ that tag. The release workflow builds a wheel and source distribution and
67
+ publishes them using GitHub OIDC. No long lived PyPI API token is needed.
68
+ 6. Verify with `pip install docus` in a fresh environment.
69
+
70
+ PyPI does not reserve a name when a pending publisher is configured. Check
71
+ that `docus` is available immediately before the first release.
72
+ For later releases, update the version in `pyproject.toml` and use a new
73
+ matching GitHub tag. Choose and add a license before public distribution if
74
+ you intend to grant reuse rights.
75
+
76
+ See the [PyPI trusted publishing guide](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/)
77
+ and [Python packaging guide](https://packaging.python.org/en/latest/tutorials/packaging-projects/)
78
+ for the current publishing steps.
docus-0.1.1/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # docus
2
+
3
+ An installable pytest plugin that sends test sessions and results to a Docus
4
+ test recorder service. It uses the pytest11 entry point, so existing test
5
+ files do not need to change.
6
+
7
+ ## Install and run
8
+
9
+ ```sh
10
+ pip install docus
11
+ TEST_RECORDER_URL=https://recorder.example.com pytest
12
+ ```
13
+
14
+ The plugin stays inactive until `TEST_RECORDER_URL` or `--recorder-url` is
15
+ provided. Set `TEST_RECORDER_API_TOKEN` when the service requires a bearer
16
+ token. Set `TEST_RECORDER_REPO` to the canonical Git repository identifier;
17
+ otherwise the plugin uses `GITHUB_REPOSITORY` or Git origin. The optional
18
+ `TEST_RECORDER_ACTOR` overrides the current user.
19
+
20
+ The plugin records collected tests, test results, pytest phase durations,
21
+ captured output, Git commit and branch, and a session ID. It sends a batch
22
+ at session end. If delivery fails, pytest results are unchanged and the batch
23
+ is saved under `.test-recorder/spool`. Retry with:
24
+
25
+ ```sh
26
+ TEST_RECORDER_URL=https://recorder.example.com pydocus-resend
27
+ ```
28
+
29
+ To keep a test identity across a pytest function rename, mark it with
30
+ `@pytest.mark.record_id("logical-name")`. Parameter IDs should remain stable.
31
+ Output and failure fields are capped at 64 KiB each; the batch indicates when
32
+ truncation occurred.
33
+
34
+ The Docus service is developed at
35
+ [dooblai/docus](https://github.com/dooblai/docus). This package only depends
36
+ on pytest and the Python standard library. Other languages can send the same
37
+ JSON event format directly to the service.
38
+
39
+ ## Build and release to PyPI
40
+
41
+ 1. Install the package and run `python testing_suite_client.py`.
42
+ 2. Run `python -m pip install build twine`, `python -m build`, and
43
+ `python -m twine check dist/*` from this repository.
44
+ 3. Create a GitHub environment named `pypi` in `dooblai/pydocusclient`.
45
+ 4. In PyPI, configure a **pending trusted publisher** for project name
46
+ `docus`: owner `dooblai`, repository `pydocusclient`,
47
+ workflow filename `release.yml`, environment `pypi`. This must match
48
+ `.github/workflows/release.yml` exactly.
49
+ 5. Push a version tag such as `v0.1.1`, then publish a GitHub Release for
50
+ that tag. The release workflow builds a wheel and source distribution and
51
+ publishes them using GitHub OIDC. No long lived PyPI API token is needed.
52
+ 6. Verify with `pip install docus` in a fresh environment.
53
+
54
+ PyPI does not reserve a name when a pending publisher is configured. Check
55
+ that `docus` is available immediately before the first release.
56
+ For later releases, update the version in `pyproject.toml` and use a new
57
+ matching GitHub tag. Choose and add a license before public distribution if
58
+ you intend to grant reuse rights.
59
+
60
+ See the [PyPI trusted publishing guide](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/)
61
+ and [Python packaging guide](https://packaging.python.org/en/latest/tutorials/packaging-projects/)
62
+ for the current publishing steps.
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: docus
3
+ Version: 0.1.1
4
+ Summary: Pytest client for the Docus test recorder service
5
+ Project-URL: Homepage, https://github.com/dooblai/pydocusclient
6
+ Project-URL: Source, https://github.com/dooblai/pydocusclient
7
+ Project-URL: Issues, https://github.com/dooblai/pydocusclient/issues
8
+ Classifier: Framework :: Pytest
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3 :: Only
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Topic :: Software Development :: Testing
13
+ Requires-Python: >=3.9
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: pytest>=7
16
+
17
+ # docus
18
+
19
+ An installable pytest plugin that sends test sessions and results to a Docus
20
+ test recorder service. It uses the pytest11 entry point, so existing test
21
+ files do not need to change.
22
+
23
+ ## Install and run
24
+
25
+ ```sh
26
+ pip install docus
27
+ TEST_RECORDER_URL=https://recorder.example.com pytest
28
+ ```
29
+
30
+ The plugin stays inactive until `TEST_RECORDER_URL` or `--recorder-url` is
31
+ provided. Set `TEST_RECORDER_API_TOKEN` when the service requires a bearer
32
+ token. Set `TEST_RECORDER_REPO` to the canonical Git repository identifier;
33
+ otherwise the plugin uses `GITHUB_REPOSITORY` or Git origin. The optional
34
+ `TEST_RECORDER_ACTOR` overrides the current user.
35
+
36
+ The plugin records collected tests, test results, pytest phase durations,
37
+ captured output, Git commit and branch, and a session ID. It sends a batch
38
+ at session end. If delivery fails, pytest results are unchanged and the batch
39
+ is saved under `.test-recorder/spool`. Retry with:
40
+
41
+ ```sh
42
+ TEST_RECORDER_URL=https://recorder.example.com pydocus-resend
43
+ ```
44
+
45
+ To keep a test identity across a pytest function rename, mark it with
46
+ `@pytest.mark.record_id("logical-name")`. Parameter IDs should remain stable.
47
+ Output and failure fields are capped at 64 KiB each; the batch indicates when
48
+ truncation occurred.
49
+
50
+ The Docus service is developed at
51
+ [dooblai/docus](https://github.com/dooblai/docus). This package only depends
52
+ on pytest and the Python standard library. Other languages can send the same
53
+ JSON event format directly to the service.
54
+
55
+ ## Build and release to PyPI
56
+
57
+ 1. Install the package and run `python testing_suite_client.py`.
58
+ 2. Run `python -m pip install build twine`, `python -m build`, and
59
+ `python -m twine check dist/*` from this repository.
60
+ 3. Create a GitHub environment named `pypi` in `dooblai/pydocusclient`.
61
+ 4. In PyPI, configure a **pending trusted publisher** for project name
62
+ `docus`: owner `dooblai`, repository `pydocusclient`,
63
+ workflow filename `release.yml`, environment `pypi`. This must match
64
+ `.github/workflows/release.yml` exactly.
65
+ 5. Push a version tag such as `v0.1.1`, then publish a GitHub Release for
66
+ that tag. The release workflow builds a wheel and source distribution and
67
+ publishes them using GitHub OIDC. No long lived PyPI API token is needed.
68
+ 6. Verify with `pip install docus` in a fresh environment.
69
+
70
+ PyPI does not reserve a name when a pending publisher is configured. Check
71
+ that `docus` is available immediately before the first release.
72
+ For later releases, update the version in `pyproject.toml` and use a new
73
+ matching GitHub tag. Choose and add a license before public distribution if
74
+ you intend to grant reuse rights.
75
+
76
+ See the [PyPI trusted publishing guide](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/)
77
+ and [Python packaging guide](https://packaging.python.org/en/latest/tutorials/packaging-projects/)
78
+ for the current publishing steps.
@@ -0,0 +1,9 @@
1
+ README.md
2
+ pyproject.toml
3
+ pytest_recorder_client.py
4
+ docus.egg-info/PKG-INFO
5
+ docus.egg-info/SOURCES.txt
6
+ docus.egg-info/dependency_links.txt
7
+ docus.egg-info/entry_points.txt
8
+ docus.egg-info/requires.txt
9
+ docus.egg-info/top_level.txt
@@ -0,0 +1,6 @@
1
+ [console_scripts]
2
+ pydocus-resend = pytest_recorder_client:resend_main
3
+ test-recorder-resend = pytest_recorder_client:resend_main
4
+
5
+ [pytest11]
6
+ pydocusclient = pytest_recorder_client
@@ -0,0 +1 @@
1
+ pytest>=7
@@ -0,0 +1 @@
1
+ pytest_recorder_client
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "docus"
7
+ version = "0.1.1"
8
+ description = "Pytest client for the Docus test recorder service"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ dependencies = ["pytest>=7"]
12
+ classifiers = [
13
+ "Framework :: Pytest",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3 :: Only",
16
+ "Operating System :: OS Independent",
17
+ "Topic :: Software Development :: Testing",
18
+ ]
19
+
20
+ [project.urls]
21
+ Homepage = "https://github.com/dooblai/pydocusclient"
22
+ Source = "https://github.com/dooblai/pydocusclient"
23
+ Issues = "https://github.com/dooblai/pydocusclient/issues"
24
+
25
+ [project.entry-points.pytest11]
26
+ pydocusclient = "pytest_recorder_client"
27
+
28
+ [project.scripts]
29
+ pydocus-resend = "pytest_recorder_client:resend_main"
30
+ test-recorder-resend = "pytest_recorder_client:resend_main"
31
+
32
+ [tool.setuptools]
33
+ py-modules = ["pytest_recorder_client"]
@@ -0,0 +1,209 @@
1
+ """Installable pytest adapter for the Test Recorder HTTP API."""
2
+ from datetime import datetime, timezone
3
+ import argparse
4
+ import getpass
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ import re
9
+ import socket
10
+ import subprocess
11
+ import sys
12
+ import urllib.request
13
+ from uuid import uuid4
14
+ import warnings
15
+
16
+
17
+ def timestamp():
18
+ return datetime.now(timezone.utc).isoformat(timespec="microseconds")
19
+
20
+
21
+ def repository_id(root, override=None):
22
+ value = override or os.getenv("TEST_RECORDER_REPO")
23
+ if value:
24
+ return value
25
+ if os.getenv("GITHUB_REPOSITORY"):
26
+ return "github.com/" + os.environ["GITHUB_REPOSITORY"]
27
+ try:
28
+ value = subprocess.check_output(["git", "-C", str(root), "remote", "get-url", "origin"],
29
+ text=True, stderr=subprocess.DEVNULL).strip()
30
+ except (OSError, subprocess.CalledProcessError):
31
+ raise ValueError("Set TEST_RECORDER_REPO when Git origin is unavailable")
32
+ value = value.replace("git@", "", 1).replace(":", "/", 1) if value.startswith("git@") else value
33
+ value = re.sub("^https?://", "", value)
34
+ return value.removesuffix(".git").rstrip("/")
35
+
36
+
37
+ def git_info(root):
38
+ info = {}
39
+ for key, args in (("commit", ["rev-parse", "HEAD"]), ("branch", ["branch", "--show-current"])):
40
+ try:
41
+ info[key] = subprocess.check_output(["git", "-C", str(root), *args], text=True,
42
+ stderr=subprocess.DEVNULL).strip() or None
43
+ except (OSError, subprocess.CalledProcessError):
44
+ info[key] = None
45
+ try:
46
+ info["dirty"] = bool(subprocess.check_output(["git", "-C", str(root), "status", "--porcelain"],
47
+ text=True, stderr=subprocess.DEVNULL).strip())
48
+ except (OSError, subprocess.CalledProcessError):
49
+ info["dirty"] = None
50
+ return info
51
+
52
+
53
+ def post_batch(url, token, payload):
54
+ body = json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
55
+ headers = {"Content-Type": "application/json"}
56
+ if token:
57
+ headers["Authorization"] = "Bearer " + token
58
+ request = urllib.request.Request(url.rstrip("/") + "/v1/batches", data=body,
59
+ headers=headers, method="POST")
60
+ with urllib.request.urlopen(request, timeout=10) as response:
61
+ return json.load(response)
62
+
63
+
64
+ def pytest_addoption(parser):
65
+ group = parser.getgroup("test-recorder")
66
+ group.addoption("--recorder-url", default=None, help="Test Recorder service URL")
67
+ group.addoption("--recorder-repo", default=None, help="Canonical repository ID")
68
+ group.addoption("--recorder-spool", default=None, help="Directory for unsent result batches")
69
+
70
+
71
+ def pytest_configure(config):
72
+ config.addinivalue_line("markers", "record_id(value): stable case name for recorder identity")
73
+ url = config.getoption("--recorder-url") or os.getenv("TEST_RECORDER_URL")
74
+ if url:
75
+ config.pluginmanager.register(RecorderPlugin(config, url), "test-recorder-client-active")
76
+
77
+
78
+ class RecorderPlugin:
79
+ def __init__(self, config, url):
80
+ self.root = Path(str(config.rootpath)).resolve()
81
+ self.repository = repository_id(self.root, config.getoption("--recorder-repo"))
82
+ self.url = url
83
+ self.token = os.getenv("TEST_RECORDER_API_TOKEN")
84
+ self.spool = Path(config.getoption("--recorder-spool") or
85
+ os.getenv("TEST_RECORDER_SPOOL_DIR") or
86
+ self.root / ".test-recorder" / "spool")
87
+ self.sid = str(uuid4())
88
+ self.items = {}
89
+ self.reports = {}
90
+ self.error = None
91
+ git = git_info(self.root)
92
+ self.events = [{
93
+ "type": "session_start", "id": self.sid, "repository": self.repository,
94
+ "actor": os.getenv("TEST_RECORDER_ACTOR") or getpass.getuser(),
95
+ "commit": git["commit"], "started_at": timestamp(), "git": git,
96
+ "host": socket.gethostname(),
97
+ "framework": {"name": "pytest", "version": __import__("pytest").__version__},
98
+ "ci": {"run_id": os.getenv("GITHUB_RUN_ID")} if os.getenv("GITHUB_RUN_ID") else None,
99
+ }]
100
+
101
+ def pytest_collection_finish(self, session):
102
+ for item in session.items:
103
+ try:
104
+ path = Path(str(item.path)).resolve().relative_to(self.root).as_posix()
105
+ except ValueError:
106
+ warnings.warn("Recorder skipped test outside repository: " + item.nodeid)
107
+ continue
108
+ parts = item.nodeid.split("::")
109
+ display = parts[-1]
110
+ name, separator, variant = display.partition("[")
111
+ if not separator:
112
+ name, variant = display, ""
113
+ elif variant.endswith("]"):
114
+ variant = variant[:-1]
115
+ marker = item.get_closest_marker("record_id")
116
+ if marker and marker.args:
117
+ name = str(marker.args[0])
118
+ test = {"repository": self.repository, "path": path,
119
+ "namespace": "::".join(parts[1:-1]), "name": name, "variant": variant}
120
+ self.items[item.nodeid] = test
121
+ self.events.append({"type": "observe", "test": test,
122
+ "commit": self.events[0]["commit"], "seen_at": timestamp()})
123
+
124
+ def pytest_runtest_logreport(self, report):
125
+ self.reports.setdefault(report.nodeid, []).append(report)
126
+ if report.when == "teardown":
127
+ self._record(report.nodeid, self.reports.pop(report.nodeid))
128
+
129
+ def _record(self, nodeid, reports):
130
+ test = self.items.get(nodeid)
131
+ if test is None or not reports:
132
+ return
133
+ failures = [report for report in reports if report.failed]
134
+ skips = [report for report in reports if report.skipped]
135
+ if failures:
136
+ status = "xpassed" if any(getattr(report, "wasxfail", None) for report in failures) else (
137
+ "failed" if any(report.when == "call" for report in failures) else "error")
138
+ elif any(getattr(report, "wasxfail", None) for report in reports if report.passed):
139
+ status = "xpassed"
140
+ elif skips:
141
+ status = "xfailed" if any(getattr(report, "wasxfail", None) for report in skips) else "skipped"
142
+ else:
143
+ status = "passed"
144
+ at = lambda seconds: datetime.fromtimestamp(seconds, timezone.utc).isoformat(timespec="microseconds")
145
+ output = {}
146
+ for key, field in (("stdout", "capstdout"), ("stderr", "capstderr")):
147
+ value = chr(10).join(getattr(report, field, "") for report in reports
148
+ if getattr(report, field, ""))
149
+ output[key] = value[-65536:] if value else None
150
+ output[key + "_truncated"] = len(value) > 65536
151
+ trace = chr(10).join(report.longreprtext for report in failures
152
+ if getattr(report, "longreprtext", ""))
153
+ self.events.append({
154
+ "type": "result", "id": str(uuid4()), "test": test, "sid": self.sid,
155
+ "status": status, "started_at": at(min(report.start for report in reports)),
156
+ "finished_at": at(max(report.stop for report in reports)),
157
+ "nodeid": nodeid, "phases": [
158
+ {"phase": report.when, "outcome": report.outcome, "duration_s": report.duration}
159
+ for report in reports],
160
+ "duration_s": sum(report.duration for report in reports),
161
+ "failure": trace[-65536:] if trace else None,
162
+ "failure_truncated": len(trace) > 65536,
163
+ **output,
164
+ })
165
+
166
+ def pytest_sessionfinish(self, session, exitstatus):
167
+ for nodeid, reports in list(self.reports.items()):
168
+ self._record(nodeid, reports)
169
+ self.reports.clear()
170
+ self.events.append({"type": "session_finish", "sid": self.sid,
171
+ "finished_at": timestamp(), "exit_code": int(exitstatus),
172
+ "collected": len(self.items)})
173
+ payload = {"events": self.events}
174
+ try:
175
+ post_batch(self.url, self.token, payload)
176
+ except Exception as error:
177
+ self.error = str(error)
178
+ try:
179
+ self.spool.mkdir(parents=True, exist_ok=True)
180
+ target = self.spool / (self.sid + ".json")
181
+ target.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
182
+ warnings.warn("Test Recorder delivery failed; saved " + str(target) +
183
+ ": " + self.error)
184
+ except OSError as spool_error:
185
+ warnings.warn("Test Recorder delivery and spooling failed: " +
186
+ str(error) + "; " + str(spool_error))
187
+
188
+ def pytest_terminal_summary(self, terminalreporter):
189
+ terminalreporter.write_line("Test Recorder session: " + self.sid)
190
+ if self.error:
191
+ terminalreporter.write_line("Test Recorder delivery failed: " + self.error)
192
+
193
+
194
+ def resend_main(argv=None):
195
+ parser = argparse.ArgumentParser(description="Resend saved Test Recorder batches")
196
+ parser.add_argument("--url", default=os.getenv("TEST_RECORDER_URL"), required=False)
197
+ parser.add_argument("--spool", default=os.getenv("TEST_RECORDER_SPOOL_DIR", ".test-recorder/spool"))
198
+ args = parser.parse_args(argv)
199
+ if not args.url:
200
+ parser.error("Set --url or TEST_RECORDER_URL")
201
+ for path in sorted(Path(args.spool).glob("*.json")):
202
+ payload = json.loads(path.read_text(encoding="utf-8"))
203
+ post_batch(args.url, os.getenv("TEST_RECORDER_API_TOKEN"), payload)
204
+ path.unlink()
205
+ print("Sent " + str(path))
206
+
207
+
208
+ if __name__ == "__main__":
209
+ resend_main()
docus-0.1.1/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+