pymmary 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.
- pymmary-0.1.0/.github/dependabot.yml +35 -0
- pymmary-0.1.0/.github/workflows/release.yml +166 -0
- pymmary-0.1.0/.github/workflows/test.yml +242 -0
- pymmary-0.1.0/.gitignore +90 -0
- pymmary-0.1.0/LICENSE +21 -0
- pymmary-0.1.0/PKG-INFO +136 -0
- pymmary-0.1.0/README.md +107 -0
- pymmary-0.1.0/pyproject.toml +115 -0
- pymmary-0.1.0/sonar-project.properties +19 -0
- pymmary-0.1.0/src/pymmary/__init__.py +13 -0
- pymmary-0.1.0/src/pymmary/_version.py +24 -0
- pymmary-0.1.0/src/pymmary/adapters/__init__.py +0 -0
- pymmary-0.1.0/src/pymmary/adapters/pytest.py +212 -0
- pymmary-0.1.0/src/pymmary/detector.py +44 -0
- pymmary-0.1.0/src/pymmary/emitter.py +80 -0
- pymmary-0.1.0/src/pymmary/py.typed +0 -0
- pymmary-0.1.0/src/pymmary/schema.py +37 -0
- pymmary-0.1.0/tests/__init__.py +0 -0
- pymmary-0.1.0/tests/adapters/__init__.py +0 -0
- pymmary-0.1.0/tests/adapters/test_pytest.py +435 -0
- pymmary-0.1.0/tests/conftest.py +5 -0
- pymmary-0.1.0/tests/test_detector.py +74 -0
- pymmary-0.1.0/tests/test_emitter.py +223 -0
- pymmary-0.1.0/tests/test_package.py +17 -0
- pymmary-0.1.0/tests/test_schema.py +44 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
- package-ecosystem: "github-actions"
|
|
4
|
+
directory: "/"
|
|
5
|
+
schedule:
|
|
6
|
+
interval: "weekly"
|
|
7
|
+
commit-message:
|
|
8
|
+
prefix: "ci"
|
|
9
|
+
labels:
|
|
10
|
+
- "dependencies"
|
|
11
|
+
groups:
|
|
12
|
+
actions:
|
|
13
|
+
patterns:
|
|
14
|
+
- "*"
|
|
15
|
+
|
|
16
|
+
- package-ecosystem: "uv"
|
|
17
|
+
directory: "/"
|
|
18
|
+
schedule:
|
|
19
|
+
interval: "weekly"
|
|
20
|
+
commit-message:
|
|
21
|
+
prefix: "deps"
|
|
22
|
+
labels:
|
|
23
|
+
- "dependencies"
|
|
24
|
+
- "python"
|
|
25
|
+
# pytest appears in [project.optional-dependencies], which is the contract with
|
|
26
|
+
# anyone installing pymmary[pytest], not a development pin. Its floor is a
|
|
27
|
+
# researched compatibility boundary and moving it silently drops users. There
|
|
28
|
+
# is no way to scope dependabot to a single table, so pytest is off limits
|
|
29
|
+
# entirely; CI installs the newest one regardless, since every pin is a floor.
|
|
30
|
+
ignore:
|
|
31
|
+
- dependency-name: "pytest"
|
|
32
|
+
groups:
|
|
33
|
+
python-deps:
|
|
34
|
+
patterns:
|
|
35
|
+
- "*"
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*.*.*"
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
validate:
|
|
13
|
+
name: Validate Release
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
|
|
16
|
+
outputs:
|
|
17
|
+
version: ${{ steps.version.outputs.version }}
|
|
18
|
+
|
|
19
|
+
steps:
|
|
20
|
+
- name: Checkout
|
|
21
|
+
uses: actions/checkout@v7
|
|
22
|
+
with:
|
|
23
|
+
fetch-depth: 0
|
|
24
|
+
persist-credentials: false
|
|
25
|
+
|
|
26
|
+
- name: Validate Branch
|
|
27
|
+
run: |
|
|
28
|
+
TAG_COMMIT=$(git rev-list -n 1 ${{ github.ref_name }})
|
|
29
|
+
|
|
30
|
+
if ! git merge-base --is-ancestor $TAG_COMMIT origin/master; then
|
|
31
|
+
echo "❌ ERROR: Tag ${{ github.ref_name }} is not on master branch"
|
|
32
|
+
echo "Tags must be created from commits that are on master."
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
echo "✅ Tag ${{ github.ref_name }} is on master branch"
|
|
37
|
+
|
|
38
|
+
- name: Extract Version
|
|
39
|
+
id: version
|
|
40
|
+
run: |
|
|
41
|
+
VERSION="${{ github.ref_name }}"
|
|
42
|
+
VERSION="${VERSION#v}"
|
|
43
|
+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
44
|
+
echo "📦 Version: $VERSION"
|
|
45
|
+
|
|
46
|
+
- name: Validate Version Format
|
|
47
|
+
run: |
|
|
48
|
+
VERSION="${{ steps.version.outputs.version }}"
|
|
49
|
+
if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$'; then
|
|
50
|
+
echo "❌ ERROR: Invalid version format: $VERSION"
|
|
51
|
+
echo "Expected: X.Y.Z or X.Y.Z-suffix"
|
|
52
|
+
exit 1
|
|
53
|
+
fi
|
|
54
|
+
echo "✅ Version format valid: $VERSION"
|
|
55
|
+
|
|
56
|
+
build:
|
|
57
|
+
name: Build Package
|
|
58
|
+
runs-on: ubuntu-latest
|
|
59
|
+
needs: [validate]
|
|
60
|
+
|
|
61
|
+
steps:
|
|
62
|
+
- name: Checkout
|
|
63
|
+
uses: actions/checkout@v7
|
|
64
|
+
with:
|
|
65
|
+
fetch-depth: 0
|
|
66
|
+
persist-credentials: false
|
|
67
|
+
|
|
68
|
+
- name: Install uv
|
|
69
|
+
uses: astral-sh/setup-uv@v7
|
|
70
|
+
|
|
71
|
+
- name: Set up Python
|
|
72
|
+
uses: actions/setup-python@v7
|
|
73
|
+
with:
|
|
74
|
+
python-version: "3.12"
|
|
75
|
+
|
|
76
|
+
- name: Build Package
|
|
77
|
+
run: uv build
|
|
78
|
+
|
|
79
|
+
- name: Verify Package
|
|
80
|
+
run: |
|
|
81
|
+
echo "📦 Built packages:"
|
|
82
|
+
ls -la dist/
|
|
83
|
+
|
|
84
|
+
- name: Upload Artifacts
|
|
85
|
+
uses: actions/upload-artifact@v7
|
|
86
|
+
with:
|
|
87
|
+
name: dist
|
|
88
|
+
path: dist/
|
|
89
|
+
retention-days: 5
|
|
90
|
+
|
|
91
|
+
test-install:
|
|
92
|
+
name: Test Install (py${{ matrix.python-version }})
|
|
93
|
+
runs-on: ubuntu-latest
|
|
94
|
+
needs: [build]
|
|
95
|
+
|
|
96
|
+
strategy:
|
|
97
|
+
matrix:
|
|
98
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
99
|
+
|
|
100
|
+
steps:
|
|
101
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
102
|
+
uses: actions/setup-python@v7
|
|
103
|
+
with:
|
|
104
|
+
python-version: ${{ matrix.python-version }}
|
|
105
|
+
|
|
106
|
+
- name: Download Artifacts
|
|
107
|
+
uses: actions/download-artifact@v8
|
|
108
|
+
with:
|
|
109
|
+
name: dist
|
|
110
|
+
path: dist/
|
|
111
|
+
|
|
112
|
+
- name: Install Package
|
|
113
|
+
run: |
|
|
114
|
+
pip install dist/*.whl
|
|
115
|
+
pip show pymmary
|
|
116
|
+
|
|
117
|
+
publish-pypi:
|
|
118
|
+
name: Publish to PyPI
|
|
119
|
+
runs-on: ubuntu-latest
|
|
120
|
+
needs: [validate, build, test-install]
|
|
121
|
+
environment: pypi
|
|
122
|
+
|
|
123
|
+
permissions:
|
|
124
|
+
id-token: write
|
|
125
|
+
|
|
126
|
+
steps:
|
|
127
|
+
- name: Download Artifacts
|
|
128
|
+
uses: actions/download-artifact@v8
|
|
129
|
+
with:
|
|
130
|
+
name: dist
|
|
131
|
+
path: dist/
|
|
132
|
+
|
|
133
|
+
- name: Publish to PyPI
|
|
134
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
135
|
+
with:
|
|
136
|
+
print-hash: true
|
|
137
|
+
|
|
138
|
+
github-release:
|
|
139
|
+
name: Create GitHub Release
|
|
140
|
+
runs-on: ubuntu-latest
|
|
141
|
+
needs: [validate, publish-pypi]
|
|
142
|
+
|
|
143
|
+
permissions:
|
|
144
|
+
contents: write
|
|
145
|
+
|
|
146
|
+
steps:
|
|
147
|
+
- name: Checkout
|
|
148
|
+
uses: actions/checkout@v7
|
|
149
|
+
with:
|
|
150
|
+
persist-credentials: false
|
|
151
|
+
|
|
152
|
+
- name: Download Artifacts
|
|
153
|
+
uses: actions/download-artifact@v8
|
|
154
|
+
with:
|
|
155
|
+
name: dist
|
|
156
|
+
path: dist/
|
|
157
|
+
|
|
158
|
+
- name: Create Release
|
|
159
|
+
uses: softprops/action-gh-release@v3
|
|
160
|
+
with:
|
|
161
|
+
tag_name: ${{ github.ref_name }}
|
|
162
|
+
name: Release ${{ needs.validate.outputs.version }}
|
|
163
|
+
draft: false
|
|
164
|
+
prerelease: ${{ contains(github.ref_name, '-') }}
|
|
165
|
+
generate_release_notes: true
|
|
166
|
+
files: dist/*
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [master]
|
|
8
|
+
types: [opened, synchronize, reopened]
|
|
9
|
+
|
|
10
|
+
concurrency:
|
|
11
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
12
|
+
cancel-in-progress: true
|
|
13
|
+
|
|
14
|
+
permissions:
|
|
15
|
+
contents: read
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
lint:
|
|
19
|
+
name: Lint
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
|
|
22
|
+
steps:
|
|
23
|
+
- name: Checkout
|
|
24
|
+
uses: actions/checkout@v7
|
|
25
|
+
with:
|
|
26
|
+
fetch-depth: 0
|
|
27
|
+
persist-credentials: false
|
|
28
|
+
|
|
29
|
+
- name: Install uv
|
|
30
|
+
uses: astral-sh/setup-uv@v7
|
|
31
|
+
|
|
32
|
+
- name: Set up Python
|
|
33
|
+
uses: actions/setup-python@v7
|
|
34
|
+
with:
|
|
35
|
+
python-version: "3.12"
|
|
36
|
+
|
|
37
|
+
- name: Install dependencies
|
|
38
|
+
run: uv sync --all-extras
|
|
39
|
+
|
|
40
|
+
- name: Check formatting
|
|
41
|
+
run: uv run ruff format --check src/ tests/
|
|
42
|
+
|
|
43
|
+
- name: Check linting
|
|
44
|
+
run: uv run ruff check src/ tests/
|
|
45
|
+
|
|
46
|
+
typecheck:
|
|
47
|
+
name: Type Check
|
|
48
|
+
runs-on: ubuntu-latest
|
|
49
|
+
|
|
50
|
+
steps:
|
|
51
|
+
- name: Checkout
|
|
52
|
+
uses: actions/checkout@v7
|
|
53
|
+
with:
|
|
54
|
+
fetch-depth: 0
|
|
55
|
+
persist-credentials: false
|
|
56
|
+
|
|
57
|
+
- name: Install uv
|
|
58
|
+
uses: astral-sh/setup-uv@v7
|
|
59
|
+
|
|
60
|
+
- name: Set up Python
|
|
61
|
+
uses: actions/setup-python@v7
|
|
62
|
+
with:
|
|
63
|
+
python-version: "3.12"
|
|
64
|
+
|
|
65
|
+
- name: Install dependencies
|
|
66
|
+
run: uv sync --all-extras
|
|
67
|
+
|
|
68
|
+
- name: Run mypy
|
|
69
|
+
run: uv run mypy src/pymmary
|
|
70
|
+
|
|
71
|
+
test:
|
|
72
|
+
name: Test (py${{ matrix.python-version }})
|
|
73
|
+
runs-on: ubuntu-latest
|
|
74
|
+
needs: [lint]
|
|
75
|
+
|
|
76
|
+
strategy:
|
|
77
|
+
fail-fast: false
|
|
78
|
+
matrix:
|
|
79
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
80
|
+
|
|
81
|
+
steps:
|
|
82
|
+
- name: Checkout
|
|
83
|
+
uses: actions/checkout@v7
|
|
84
|
+
with:
|
|
85
|
+
fetch-depth: 0
|
|
86
|
+
persist-credentials: false
|
|
87
|
+
|
|
88
|
+
- name: Install uv
|
|
89
|
+
uses: astral-sh/setup-uv@v7
|
|
90
|
+
|
|
91
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
92
|
+
uses: actions/setup-python@v7
|
|
93
|
+
with:
|
|
94
|
+
python-version: ${{ matrix.python-version }}
|
|
95
|
+
|
|
96
|
+
- name: Install dependencies
|
|
97
|
+
run: uv sync --all-extras
|
|
98
|
+
|
|
99
|
+
- name: Verify core does not import any adapter
|
|
100
|
+
# pymmary's core promises zero runtime dependencies. pytest is always
|
|
101
|
+
# present here (it is our test runner), so we cannot check for its
|
|
102
|
+
# absence the way pyssertive checks for django/httpx. What we can
|
|
103
|
+
# guarantee is that `import pymmary` never drags an adapter in.
|
|
104
|
+
# Append new adapter modules to `forbidden` as they are added.
|
|
105
|
+
run: |
|
|
106
|
+
uv run python - <<'PY'
|
|
107
|
+
import sys
|
|
108
|
+
|
|
109
|
+
forbidden = ("pytest", "unittest", "mypy")
|
|
110
|
+
before = set(sys.modules)
|
|
111
|
+
import pymmary # noqa: F401
|
|
112
|
+
|
|
113
|
+
pulled = [m for m in forbidden if m not in before and m in sys.modules]
|
|
114
|
+
if pulled:
|
|
115
|
+
print(f"ERROR: importing pymmary pulled in {pulled}")
|
|
116
|
+
sys.exit(1)
|
|
117
|
+
print("OK: pymmary core imports without any adapter")
|
|
118
|
+
PY
|
|
119
|
+
|
|
120
|
+
- name: Run tests
|
|
121
|
+
run: uv run pytest --cov=pymmary --cov-report=term-missing --cov-report=xml:coverage.xml --cov-fail-under=100
|
|
122
|
+
|
|
123
|
+
- name: Upload coverage artifact
|
|
124
|
+
if: matrix.python-version == '3.14'
|
|
125
|
+
uses: actions/upload-artifact@v7
|
|
126
|
+
with:
|
|
127
|
+
name: coverage-report
|
|
128
|
+
path: coverage.xml
|
|
129
|
+
|
|
130
|
+
test-min-pytest:
|
|
131
|
+
name: Test (oldest supported pytest, py${{ matrix.python-version }})
|
|
132
|
+
runs-on: ubuntu-latest
|
|
133
|
+
needs: [lint]
|
|
134
|
+
|
|
135
|
+
strategy:
|
|
136
|
+
fail-fast: false
|
|
137
|
+
matrix:
|
|
138
|
+
python-version: ["3.10", "3.14"]
|
|
139
|
+
|
|
140
|
+
steps:
|
|
141
|
+
- name: Checkout
|
|
142
|
+
uses: actions/checkout@v7
|
|
143
|
+
with:
|
|
144
|
+
fetch-depth: 0
|
|
145
|
+
persist-credentials: false
|
|
146
|
+
|
|
147
|
+
- name: Install uv
|
|
148
|
+
uses: astral-sh/setup-uv@v7
|
|
149
|
+
|
|
150
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
151
|
+
uses: actions/setup-python@v7
|
|
152
|
+
with:
|
|
153
|
+
python-version: ${{ matrix.python-version }}
|
|
154
|
+
|
|
155
|
+
- name: Install dependencies
|
|
156
|
+
run: uv sync --all-extras
|
|
157
|
+
|
|
158
|
+
- name: Pin pytest to the floor declared in pyproject
|
|
159
|
+
# The `test` matrix always resolves to the newest pytest, so nothing there
|
|
160
|
+
# can catch a broken floor. The adapter unregisters the terminal reporter,
|
|
161
|
+
# and before 9.1 that silently destroyed every assertion explanation —
|
|
162
|
+
# exactly the payload this library exists to produce.
|
|
163
|
+
# Bump this in lockstep with the `pytest` extra, never separately.
|
|
164
|
+
run: uv pip install "pytest==9.1.0"
|
|
165
|
+
|
|
166
|
+
- name: Run tests
|
|
167
|
+
# --no-sync or uv would resolve pytest straight back to latest.
|
|
168
|
+
# Coverage is enforced by the `test` job; this one is about compatibility.
|
|
169
|
+
run: uv run --no-sync pytest --cov-fail-under=0 --cov-report=
|
|
170
|
+
|
|
171
|
+
sonarcloud:
|
|
172
|
+
name: SonarCloud Analysis
|
|
173
|
+
runs-on: ubuntu-latest
|
|
174
|
+
needs: [test]
|
|
175
|
+
|
|
176
|
+
steps:
|
|
177
|
+
- name: Checkout
|
|
178
|
+
uses: actions/checkout@v7
|
|
179
|
+
with:
|
|
180
|
+
fetch-depth: 0
|
|
181
|
+
persist-credentials: false
|
|
182
|
+
|
|
183
|
+
- name: Download coverage artifact
|
|
184
|
+
uses: actions/download-artifact@v8
|
|
185
|
+
with:
|
|
186
|
+
name: coverage-report
|
|
187
|
+
|
|
188
|
+
- name: SonarCloud Scan
|
|
189
|
+
uses: SonarSource/sonarqube-scan-action@v8
|
|
190
|
+
env:
|
|
191
|
+
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
|
192
|
+
|
|
193
|
+
ci-success:
|
|
194
|
+
name: CI Success
|
|
195
|
+
runs-on: ubuntu-latest
|
|
196
|
+
needs: [lint, typecheck, test, test-min-pytest, sonarcloud]
|
|
197
|
+
if: always()
|
|
198
|
+
|
|
199
|
+
steps:
|
|
200
|
+
- name: Check all jobs
|
|
201
|
+
run: |
|
|
202
|
+
echo "===== Job Results ====="
|
|
203
|
+
echo "Lint: ${{ needs.lint.result }}"
|
|
204
|
+
echo "Typecheck: ${{ needs.typecheck.result }}"
|
|
205
|
+
echo "Test: ${{ needs.test.result }}"
|
|
206
|
+
echo "Test (min pytest): ${{ needs.test-min-pytest.result }}"
|
|
207
|
+
echo "SonarCloud: ${{ needs.sonarcloud.result }}"
|
|
208
|
+
echo "======================="
|
|
209
|
+
|
|
210
|
+
fail=0
|
|
211
|
+
|
|
212
|
+
if [ "${{ needs.lint.result }}" != "success" ]; then
|
|
213
|
+
echo "❌ Lint failed"
|
|
214
|
+
fail=1
|
|
215
|
+
fi
|
|
216
|
+
|
|
217
|
+
if [ "${{ needs.typecheck.result }}" != "success" ]; then
|
|
218
|
+
echo "❌ Typecheck failed"
|
|
219
|
+
fail=1
|
|
220
|
+
fi
|
|
221
|
+
|
|
222
|
+
if [ "${{ needs.test.result }}" != "success" ]; then
|
|
223
|
+
echo "❌ Tests failed"
|
|
224
|
+
fail=1
|
|
225
|
+
fi
|
|
226
|
+
|
|
227
|
+
if [ "${{ needs.test-min-pytest.result }}" != "success" ]; then
|
|
228
|
+
echo "❌ Tests on the oldest supported pytest failed"
|
|
229
|
+
fail=1
|
|
230
|
+
fi
|
|
231
|
+
|
|
232
|
+
if [ "${{ needs.sonarcloud.result }}" != "success" ]; then
|
|
233
|
+
echo "❌ SonarCloud failed"
|
|
234
|
+
fail=1
|
|
235
|
+
fi
|
|
236
|
+
|
|
237
|
+
if [ "$fail" -ne 0 ]; then
|
|
238
|
+
echo "🛑 Release blocked — fix the failures above."
|
|
239
|
+
exit 1
|
|
240
|
+
fi
|
|
241
|
+
|
|
242
|
+
echo "✅ All required checks passed — release-ready."
|
pymmary-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
env/
|
|
12
|
+
build/
|
|
13
|
+
develop-eggs/
|
|
14
|
+
dist/
|
|
15
|
+
downloads/
|
|
16
|
+
eggs/
|
|
17
|
+
.eggs/
|
|
18
|
+
lib/
|
|
19
|
+
lib64/
|
|
20
|
+
parts/
|
|
21
|
+
sdist/
|
|
22
|
+
var/
|
|
23
|
+
*.egg-info/
|
|
24
|
+
.installed.cfg
|
|
25
|
+
*.egg
|
|
26
|
+
|
|
27
|
+
# PyInstaller
|
|
28
|
+
# Usually these files are written by a python script from a template
|
|
29
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
30
|
+
*.manifest
|
|
31
|
+
*.spec
|
|
32
|
+
|
|
33
|
+
# Installer logs
|
|
34
|
+
pip-log.txt
|
|
35
|
+
pip-delete-this-directory.txt
|
|
36
|
+
|
|
37
|
+
# Unit test / coverage reports
|
|
38
|
+
htmlcov/
|
|
39
|
+
.tox/
|
|
40
|
+
.coverage
|
|
41
|
+
.coverage.*
|
|
42
|
+
.cache
|
|
43
|
+
nosetests.xml
|
|
44
|
+
coverage.xml
|
|
45
|
+
*,cover
|
|
46
|
+
.hypothesis/
|
|
47
|
+
.pytest_cache
|
|
48
|
+
|
|
49
|
+
# Translations
|
|
50
|
+
*.mo
|
|
51
|
+
*.pot
|
|
52
|
+
|
|
53
|
+
# Django stuff:
|
|
54
|
+
*.log
|
|
55
|
+
local_settings.py
|
|
56
|
+
|
|
57
|
+
# Flask instance folder
|
|
58
|
+
instance/
|
|
59
|
+
|
|
60
|
+
# Sphinx documentation
|
|
61
|
+
docs/_build/
|
|
62
|
+
|
|
63
|
+
# MkDocs documentation
|
|
64
|
+
/site/
|
|
65
|
+
|
|
66
|
+
# PyBuilder
|
|
67
|
+
target/
|
|
68
|
+
|
|
69
|
+
# IPython Notebook
|
|
70
|
+
.ipynb_checkpoints
|
|
71
|
+
|
|
72
|
+
# pyenv
|
|
73
|
+
.python-version
|
|
74
|
+
|
|
75
|
+
# virtual env
|
|
76
|
+
.venv/
|
|
77
|
+
|
|
78
|
+
# Auto-generated version file (hatch-vcs)
|
|
79
|
+
src/pymmary/_version.py
|
|
80
|
+
|
|
81
|
+
# uv lock file
|
|
82
|
+
uv.lock
|
|
83
|
+
|
|
84
|
+
# claude and other AI tool files
|
|
85
|
+
.claude/
|
|
86
|
+
.serena/
|
|
87
|
+
.omc/
|
|
88
|
+
|
|
89
|
+
# Mac OS files
|
|
90
|
+
.DS_Store
|
pymmary-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Unay Santisteban
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pymmary-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pymmary
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Agent-optimized output compressor for Python tooling.
|
|
5
|
+
Project-URL: Homepage, https://github.com/othercodes/pymmary
|
|
6
|
+
Project-URL: Repository, https://github.com/othercodes/pymmary.git
|
|
7
|
+
Project-URL: Issues, https://github.com/othercodes/pymmary/issues
|
|
8
|
+
Author-email: Unay Santisteban <usantisteban@othercode.io>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai-agents,compression,output,pytest,testing
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Framework :: Pytest
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Topic :: Software Development :: Testing
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Provides-Extra: pytest
|
|
27
|
+
Requires-Dist: pytest>=9.1; extra == 'pytest'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# pymmary
|
|
31
|
+
|
|
32
|
+
[](https://github.com/othercodes/pymmary/actions/workflows/test.yml)
|
|
33
|
+
[](https://sonarcloud.io/summary/new_code?id=othercodes_pymmary)
|
|
34
|
+
|
|
35
|
+
Agent-optimized output compressor for Python tooling.
|
|
36
|
+
|
|
37
|
+
> **Status: early development.** The pytest adapter works; nothing is published to PyPI yet.
|
|
38
|
+
|
|
39
|
+
## Why
|
|
40
|
+
|
|
41
|
+
When an AI agent runs `pytest`, it pays for output written for humans — progress dots, a full traceback per failure, colour codes, a summary table. A green run of a thousand tests tells the agent one thing ("everything passed") and charges thousands of tokens to say it.
|
|
42
|
+
|
|
43
|
+
Pymmary detects that a coding agent is running the tool and replaces that output with compact JSON. Outside an agent, nothing changes: no agent detected, no compression, byte-identical output for humans.
|
|
44
|
+
|
|
45
|
+
It is a decision of the *project*, not of the agent's environment. Add it as a dev dependency and any agent that clones the repo and runs `pytest` benefits, with no per-machine setup.
|
|
46
|
+
|
|
47
|
+
## Features
|
|
48
|
+
|
|
49
|
+
- Automatic agent detection via environment variables — no configuration
|
|
50
|
+
- Strict no-op fallback: no agent, no change to output
|
|
51
|
+
- Compact JSON keyed by pytest `nodeid`, so failures are pasteable straight back into the CLI
|
|
52
|
+
- Zero runtime dependencies in the core; each adapter ships behind its own extra
|
|
53
|
+
- Hooks into the host tool's native extension points — no global monkey-patching
|
|
54
|
+
|
|
55
|
+
## Requirements
|
|
56
|
+
|
|
57
|
+
- Python 3.10+
|
|
58
|
+
- pytest 9.1+ (optional, for `pymmary[pytest]`)
|
|
59
|
+
|
|
60
|
+
## Installation
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install pymmary
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
With the pytest adapter:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install pymmary[pytest]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Usage
|
|
73
|
+
|
|
74
|
+
Nothing to wire up. Install it as a dev dependency and run your tools as usual — when a supported agent is detected, output is compressed.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pytest
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{"tool":"pytest","result":"passed","exit_code":0,"duration":0.32,"summary":{"collected":1002,"passed":1002}}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
On failure, only what the agent needs to act:
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"tool": "pytest",
|
|
89
|
+
"result": "failed",
|
|
90
|
+
"exit_code": 1,
|
|
91
|
+
"duration": 0.32,
|
|
92
|
+
"summary": { "collected": 1002, "passed": 999, "failed": 2, "error": 1 },
|
|
93
|
+
"failures": [
|
|
94
|
+
{
|
|
95
|
+
"nodeid": "tests/test_api.py::TestAuth::test_login[user-2]",
|
|
96
|
+
"phase": "call",
|
|
97
|
+
"file": "tests/test_api.py",
|
|
98
|
+
"line": 42,
|
|
99
|
+
"type": "AssertionError",
|
|
100
|
+
"message": "assert 401 == 200"
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
A run that fails to collect is never reported as a pass — the verdict follows pytest's exit code, not our own tally:
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{"tool":"pytest","result":"failed","exit_code":2,"duration":0.008,"summary":{"error":1},"failures":[{"nodeid":"test_broken.py","phase":"collect","file":"test_broken.py","line":1,"type":"ModuleNotFoundError","message":"No module named 'requests'"}]}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Configuration
|
|
113
|
+
|
|
114
|
+
Two environment variables, no config file and no CLI flags:
|
|
115
|
+
|
|
116
|
+
| Variable | Effect |
|
|
117
|
+
|---|---|
|
|
118
|
+
| `PYMMARY_FORCE=1` | Compress even when no agent is detected — useful to see what an agent sees |
|
|
119
|
+
| `PYMMARY_MAX_FAILURES=N` | How many failures to spell out. Default 20; `0` keeps every one of them |
|
|
120
|
+
|
|
121
|
+
The cap is about diminishing returns, not size: an agent facing 400 failures fixes a handful and runs again, so the rest cost context and buy nothing. `summary` always counts the whole run, and whatever was left out is declared in `failures_omitted`. On a 400-failure suite: 98,671 bytes of human output, 56,826 uncapped, **2,900 by default**.
|
|
122
|
+
|
|
123
|
+
## Limitations
|
|
124
|
+
|
|
125
|
+
- **pytest-xdist**: pymmary stands down completely under `-n`, leaving normal pytest output. The controller never runs the tests itself, so a compressed summary would count none of them. Aggregating the worker streams is planned.
|
|
126
|
+
- **pytest 9.1 is a hard floor.** The adapter unregisters pytest's terminal reporter to own the output. Before 9.1, pytest built assertion explanations through `config.get_terminal_writer()`, which asserts that reporter is still registered — so on older versions every `assert` failure degrades to a bare `AssertionError` pointing into pytest's internals. That is the one payload this library exists to produce, so the floor is enforced rather than worked around.
|
|
127
|
+
|
|
128
|
+
## Related
|
|
129
|
+
|
|
130
|
+
Companion to [pyssertive](https://github.com/othercodes/pyssertive) (assert phase) and [pyrrange](https://github.com/othercodes/pyrrange) (arrange phase). Pymmary covers the report phase, for AI consumers.
|
|
131
|
+
|
|
132
|
+
Inspired by [laravel/pao](https://github.com/laravel/pao) — the PHP original. Pymmary keeps its envelope recognizable but speaks pytest's own vocabulary rather than PHPUnit's.
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
MIT
|