nimax 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nimax-1.0.0/.github/workflows/ci.yml +49 -0
- nimax-1.0.0/.github/workflows/publish.yml +35 -0
- nimax-1.0.0/.gitignore +12 -0
- nimax-1.0.0/.pre-commit-config.yaml +37 -0
- nimax-1.0.0/.taplo.toml +3 -0
- nimax-1.0.0/LICENSE +21 -0
- nimax-1.0.0/PKG-INFO +157 -0
- nimax-1.0.0/README.md +104 -0
- nimax-1.0.0/conftest.py +1 -0
- nimax-1.0.0/pyproject.toml +110 -0
- nimax-1.0.0/src/nimax/__init__.py +19 -0
- nimax-1.0.0/src/nimax/_adapter.py +94 -0
- nimax-1.0.0/src/nimax/_cassette.py +487 -0
- nimax-1.0.0/src/nimax/_matchers.py +123 -0
- nimax-1.0.0/src/nimax/_placeholders.py +31 -0
- nimax-1.0.0/src/nimax/_record_mode.py +20 -0
- nimax-1.0.0/src/nimax/_serializers.py +54 -0
- nimax-1.0.0/src/nimax/_websocket.py +240 -0
- nimax-1.0.0/src/nimax/plugin.py +185 -0
- nimax-1.0.0/tests/__init__.py +0 -0
- nimax-1.0.0/tests/_utils.py +72 -0
- nimax-1.0.0/tests/conftest.py +60 -0
- nimax-1.0.0/tests/integration/__init__.py +0 -0
- nimax-1.0.0/tests/integration/test_adapter.py +165 -0
- nimax-1.0.0/tests/integration/test_cassette.py +417 -0
- nimax-1.0.0/tests/integration/test_websocket.py +264 -0
- nimax-1.0.0/tests/plugin/__init__.py +0 -0
- nimax-1.0.0/tests/plugin/test_plugin.py +228 -0
- nimax-1.0.0/tests/unit/__init__.py +0 -0
- nimax-1.0.0/tests/unit/test_cassette_helpers.py +153 -0
- nimax-1.0.0/tests/unit/test_matchers.py +231 -0
- nimax-1.0.0/tests/unit/test_placeholders.py +55 -0
- nimax-1.0.0/tests/unit/test_record_mode.py +36 -0
- nimax-1.0.0/tests/unit/test_serializers.py +84 -0
- nimax-1.0.0/tests/unit/test_websocket_models.py +147 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
lint:
|
|
11
|
+
name: Lint
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v6
|
|
15
|
+
- uses: astral-sh/setup-uv@v8.1.0
|
|
16
|
+
with:
|
|
17
|
+
enable-cache: false
|
|
18
|
+
- run: uv run --extra dev ruff check .
|
|
19
|
+
- run: uv run --extra dev ruff format --check .
|
|
20
|
+
|
|
21
|
+
typecheck:
|
|
22
|
+
name: Type check
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v6
|
|
26
|
+
- uses: astral-sh/setup-uv@v8.1.0
|
|
27
|
+
with:
|
|
28
|
+
enable-cache: false
|
|
29
|
+
- run: uv run --extra dev ty check src/
|
|
30
|
+
|
|
31
|
+
test:
|
|
32
|
+
name: Test (Python ${{ matrix.python-version }})
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
strategy:
|
|
35
|
+
fail-fast: false
|
|
36
|
+
matrix:
|
|
37
|
+
python-version: ["3.11", "3.12", "3.13", "3.14"]
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@v6
|
|
40
|
+
- uses: astral-sh/setup-uv@v8.1.0
|
|
41
|
+
with:
|
|
42
|
+
python-version: ${{ matrix.python-version }}
|
|
43
|
+
enable-cache: false
|
|
44
|
+
- run: uv run --extra dev pytest --cov=nimax --cov-report=xml
|
|
45
|
+
- uses: codecov/codecov-action@v6
|
|
46
|
+
if: matrix.python-version == '3.14'
|
|
47
|
+
with:
|
|
48
|
+
files: coverage.xml
|
|
49
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build:
|
|
10
|
+
name: Build
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v6
|
|
14
|
+
- uses: astral-sh/setup-uv@v8.1.0
|
|
15
|
+
with:
|
|
16
|
+
enable-cache: false
|
|
17
|
+
- run: uv build
|
|
18
|
+
- uses: actions/upload-artifact@v7.0.1
|
|
19
|
+
with:
|
|
20
|
+
name: dist
|
|
21
|
+
path: dist/
|
|
22
|
+
|
|
23
|
+
publish:
|
|
24
|
+
name: Publish to PyPI
|
|
25
|
+
needs: build
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
environment: pypi
|
|
28
|
+
permissions:
|
|
29
|
+
id-token: write
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/download-artifact@v8.0.1
|
|
32
|
+
with:
|
|
33
|
+
name: dist
|
|
34
|
+
path: dist/
|
|
35
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
nimax-1.0.0/.gitignore
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: local
|
|
3
|
+
hooks:
|
|
4
|
+
- id: ruff-format
|
|
5
|
+
name: ruff format
|
|
6
|
+
entry: uv run --extra dev ruff format
|
|
7
|
+
language: system
|
|
8
|
+
types: [python]
|
|
9
|
+
|
|
10
|
+
- id: ruff-check
|
|
11
|
+
name: ruff check --fix
|
|
12
|
+
entry: uv run --extra dev ruff check --fix --exit-non-zero-on-fix
|
|
13
|
+
language: system
|
|
14
|
+
types: [python]
|
|
15
|
+
|
|
16
|
+
- id: ruff-format-post
|
|
17
|
+
name: ruff format (post-fix)
|
|
18
|
+
entry: uv run --extra dev ruff format
|
|
19
|
+
language: system
|
|
20
|
+
types: [python]
|
|
21
|
+
|
|
22
|
+
- id: ty
|
|
23
|
+
name: ty check
|
|
24
|
+
entry: uv run --extra dev ty check src/
|
|
25
|
+
language: system
|
|
26
|
+
pass_filenames: false
|
|
27
|
+
types: [python]
|
|
28
|
+
|
|
29
|
+
- repo: https://github.com/hukkin/mdformat
|
|
30
|
+
rev: 0.7.22
|
|
31
|
+
hooks:
|
|
32
|
+
- id: mdformat
|
|
33
|
+
|
|
34
|
+
- repo: https://github.com/ComPWA/taplo-pre-commit
|
|
35
|
+
rev: v0.9.3
|
|
36
|
+
hooks:
|
|
37
|
+
- id: taplo-format
|
nimax-1.0.0/.taplo.toml
ADDED
nimax-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adam Logan
|
|
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.
|
nimax-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nimax
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Record and replay niquests HTTP and WebSocket interactions in pytest
|
|
5
|
+
Project-URL: Homepage, https://github.com/adamlogan73/nimax
|
|
6
|
+
Project-URL: Repository, https://github.com/adamlogan73/nimax
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/adamlogan73/nimax/issues
|
|
8
|
+
Author-email: Adam Logan <adamlogan73@gmail.com>
|
|
9
|
+
License: MIT License
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2026 Adam Logan
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
in the Software without restriction, including without limitation the rights
|
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
furnished to do so, subject to the following conditions:
|
|
19
|
+
|
|
20
|
+
The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
copies or substantial portions of the Software.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
+
SOFTWARE.
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Keywords: cassette,http,niquests,pytest,testing,vcr,websocket
|
|
32
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
33
|
+
Classifier: Framework :: Pytest
|
|
34
|
+
Classifier: Intended Audience :: Developers
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
40
|
+
Classifier: Topic :: Software Development :: Testing
|
|
41
|
+
Requires-Python: >=3.11
|
|
42
|
+
Requires-Dist: niquests>=3
|
|
43
|
+
Requires-Dist: pyyaml>=6
|
|
44
|
+
Provides-Extra: dev
|
|
45
|
+
Requires-Dist: niquests[ws]>=3; extra == 'dev'
|
|
46
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
47
|
+
Requires-Dist: pytest-cov>=6; extra == 'dev'
|
|
48
|
+
Requires-Dist: pytest>=9; extra == 'dev'
|
|
49
|
+
Requires-Dist: ruff>=0.9; extra == 'dev'
|
|
50
|
+
Requires-Dist: ty>=0; extra == 'dev'
|
|
51
|
+
Requires-Dist: websockets>=13; extra == 'dev'
|
|
52
|
+
Description-Content-Type: text/markdown
|
|
53
|
+
|
|
54
|
+
# nimax
|
|
55
|
+
|
|
56
|
+
[](https://github.com/adamlogan73/nimax/actions/workflows/ci.yml)
|
|
57
|
+
[](https://codecov.io/gh/adamlogan73/nimax)
|
|
58
|
+
[](https://pypi.org/project/nimax/)
|
|
59
|
+
|
|
60
|
+
Record and replay [niquests](https://github.com/jawah/niquests) HTTP and WebSocket interactions in pytest.
|
|
61
|
+
|
|
62
|
+
nimax is a VCR-style cassette library built natively for niquests — supporting lazy responses, multiplexed connections, `AsyncSession`, and WebSockets. It is to niquests what [betamax](https://github.com/betamax/betamax) is to requests.
|
|
63
|
+
|
|
64
|
+
## Installation
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install nimax
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Quick start
|
|
71
|
+
|
|
72
|
+
### Automatic fixture
|
|
73
|
+
|
|
74
|
+
nimax registers a `nimax_session` pytest fixture automatically. Use it instead of `niquests.Session()` in your tests:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
def test_my_api(nimax_session):
|
|
78
|
+
resp = nimax_session.get("https://api.example.com/users")
|
|
79
|
+
assert resp.status_code == 200
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
On the first run nimax records the real HTTP response to a cassette file under `cassettes/<test_module>/<test_name>.json`. Subsequent runs replay from the cassette — no network required.
|
|
83
|
+
|
|
84
|
+
### Async sessions
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
import pytest
|
|
88
|
+
import niquests
|
|
89
|
+
|
|
90
|
+
async def test_async(nimax_session):
|
|
91
|
+
async with niquests.AsyncSession() as session:
|
|
92
|
+
with NimaxRecorder(session).use_cassette("my_cassette.json"):
|
|
93
|
+
resp = await session.get("https://api.example.com/data")
|
|
94
|
+
assert resp.status_code == 200
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Programmatic API
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
import niquests
|
|
101
|
+
from nimax import NimaxRecorder, RecordMode
|
|
102
|
+
|
|
103
|
+
def test_programmatic(tmp_path):
|
|
104
|
+
session = niquests.Session()
|
|
105
|
+
cassette_path = tmp_path / "my_cassette.json"
|
|
106
|
+
with NimaxRecorder(session).use_cassette(cassette_path, record_mode=RecordMode.ONCE):
|
|
107
|
+
resp = session.get("https://api.example.com/users")
|
|
108
|
+
assert resp.status_code == 200
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Record modes
|
|
112
|
+
|
|
113
|
+
| Mode | Behaviour |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `once` | Record on first run, replay on subsequent runs (default) |
|
|
116
|
+
| `none` | Never record — raise an error if no matching interaction exists |
|
|
117
|
+
| `new_episodes` | Replay existing interactions; record any unmatched requests |
|
|
118
|
+
| `all` | Always record, overwriting the cassette each run |
|
|
119
|
+
|
|
120
|
+
## Placeholders
|
|
121
|
+
|
|
122
|
+
Scrub sensitive values (tokens, API keys) from cassettes before they are written:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from nimax import Placeholder
|
|
126
|
+
|
|
127
|
+
recorder = NimaxRecorder(session, placeholders=[
|
|
128
|
+
Placeholder(placeholder="<AUTH_TOKEN>", replace="Bearer secret123"),
|
|
129
|
+
])
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Custom matchers and serializers
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from nimax import BaseMatcher, NimaxRecorder
|
|
136
|
+
|
|
137
|
+
class BodyMatcher(BaseMatcher):
|
|
138
|
+
name = "body"
|
|
139
|
+
|
|
140
|
+
def match(self, recorded: dict, live: object) -> bool:
|
|
141
|
+
return recorded.get("body") == live.body # type: ignore[union-attr]
|
|
142
|
+
|
|
143
|
+
NimaxRecorder.register_matcher(BodyMatcher)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
YAML cassettes are supported out of the box — use a `.yaml` extension for the cassette path.
|
|
147
|
+
|
|
148
|
+
## Requirements
|
|
149
|
+
|
|
150
|
+
- Python ≥ 3.11
|
|
151
|
+
- niquests ≥ 3
|
|
152
|
+
- pytest ≥ 8
|
|
153
|
+
- PyYAML ≥ 6
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
MIT
|
nimax-1.0.0/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# nimax
|
|
2
|
+
|
|
3
|
+
[](https://github.com/adamlogan73/nimax/actions/workflows/ci.yml)
|
|
4
|
+
[](https://codecov.io/gh/adamlogan73/nimax)
|
|
5
|
+
[](https://pypi.org/project/nimax/)
|
|
6
|
+
|
|
7
|
+
Record and replay [niquests](https://github.com/jawah/niquests) HTTP and WebSocket interactions in pytest.
|
|
8
|
+
|
|
9
|
+
nimax is a VCR-style cassette library built natively for niquests — supporting lazy responses, multiplexed connections, `AsyncSession`, and WebSockets. It is to niquests what [betamax](https://github.com/betamax/betamax) is to requests.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install nimax
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
### Automatic fixture
|
|
20
|
+
|
|
21
|
+
nimax registers a `nimax_session` pytest fixture automatically. Use it instead of `niquests.Session()` in your tests:
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
def test_my_api(nimax_session):
|
|
25
|
+
resp = nimax_session.get("https://api.example.com/users")
|
|
26
|
+
assert resp.status_code == 200
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
On the first run nimax records the real HTTP response to a cassette file under `cassettes/<test_module>/<test_name>.json`. Subsequent runs replay from the cassette — no network required.
|
|
30
|
+
|
|
31
|
+
### Async sessions
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import pytest
|
|
35
|
+
import niquests
|
|
36
|
+
|
|
37
|
+
async def test_async(nimax_session):
|
|
38
|
+
async with niquests.AsyncSession() as session:
|
|
39
|
+
with NimaxRecorder(session).use_cassette("my_cassette.json"):
|
|
40
|
+
resp = await session.get("https://api.example.com/data")
|
|
41
|
+
assert resp.status_code == 200
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Programmatic API
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
import niquests
|
|
48
|
+
from nimax import NimaxRecorder, RecordMode
|
|
49
|
+
|
|
50
|
+
def test_programmatic(tmp_path):
|
|
51
|
+
session = niquests.Session()
|
|
52
|
+
cassette_path = tmp_path / "my_cassette.json"
|
|
53
|
+
with NimaxRecorder(session).use_cassette(cassette_path, record_mode=RecordMode.ONCE):
|
|
54
|
+
resp = session.get("https://api.example.com/users")
|
|
55
|
+
assert resp.status_code == 200
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Record modes
|
|
59
|
+
|
|
60
|
+
| Mode | Behaviour |
|
|
61
|
+
|---|---|
|
|
62
|
+
| `once` | Record on first run, replay on subsequent runs (default) |
|
|
63
|
+
| `none` | Never record — raise an error if no matching interaction exists |
|
|
64
|
+
| `new_episodes` | Replay existing interactions; record any unmatched requests |
|
|
65
|
+
| `all` | Always record, overwriting the cassette each run |
|
|
66
|
+
|
|
67
|
+
## Placeholders
|
|
68
|
+
|
|
69
|
+
Scrub sensitive values (tokens, API keys) from cassettes before they are written:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from nimax import Placeholder
|
|
73
|
+
|
|
74
|
+
recorder = NimaxRecorder(session, placeholders=[
|
|
75
|
+
Placeholder(placeholder="<AUTH_TOKEN>", replace="Bearer secret123"),
|
|
76
|
+
])
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Custom matchers and serializers
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from nimax import BaseMatcher, NimaxRecorder
|
|
83
|
+
|
|
84
|
+
class BodyMatcher(BaseMatcher):
|
|
85
|
+
name = "body"
|
|
86
|
+
|
|
87
|
+
def match(self, recorded: dict, live: object) -> bool:
|
|
88
|
+
return recorded.get("body") == live.body # type: ignore[union-attr]
|
|
89
|
+
|
|
90
|
+
NimaxRecorder.register_matcher(BodyMatcher)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
YAML cassettes are supported out of the box — use a `.yaml` extension for the cassette path.
|
|
94
|
+
|
|
95
|
+
## Requirements
|
|
96
|
+
|
|
97
|
+
- Python ≥ 3.11
|
|
98
|
+
- niquests ≥ 3
|
|
99
|
+
- pytest ≥ 8
|
|
100
|
+
- PyYAML ≥ 6
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
MIT
|
nimax-1.0.0/conftest.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pytest_plugins = ["pytester"]
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
build-backend = "hatchling.build"
|
|
3
|
+
requires = ["hatchling"]
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nimax"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Record and replay niquests HTTP and WebSocket interactions in pytest"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
|
+
authors = [{ name = "Adam Logan", email = "adamlogan73@gmail.com" }]
|
|
12
|
+
keywords = [
|
|
13
|
+
"pytest",
|
|
14
|
+
"niquests",
|
|
15
|
+
"vcr",
|
|
16
|
+
"cassette",
|
|
17
|
+
"http",
|
|
18
|
+
"websocket",
|
|
19
|
+
"testing",
|
|
20
|
+
]
|
|
21
|
+
classifiers = [
|
|
22
|
+
"Development Status :: 5 - Production/Stable",
|
|
23
|
+
"Framework :: Pytest",
|
|
24
|
+
"Intended Audience :: Developers",
|
|
25
|
+
"License :: OSI Approved :: MIT License",
|
|
26
|
+
"Programming Language :: Python :: 3",
|
|
27
|
+
"Programming Language :: Python :: 3.11",
|
|
28
|
+
"Programming Language :: Python :: 3.12",
|
|
29
|
+
"Programming Language :: Python :: 3.13",
|
|
30
|
+
"Topic :: Software Development :: Testing",
|
|
31
|
+
]
|
|
32
|
+
requires-python = ">=3.11"
|
|
33
|
+
dependencies = [
|
|
34
|
+
"niquests>=3",
|
|
35
|
+
"PyYAML>=6",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/adamlogan73/nimax"
|
|
40
|
+
Repository = "https://github.com/adamlogan73/nimax"
|
|
41
|
+
"Bug Tracker" = "https://github.com/adamlogan73/nimax/issues"
|
|
42
|
+
|
|
43
|
+
[project.optional-dependencies]
|
|
44
|
+
dev = [
|
|
45
|
+
"niquests[ws]>=3",
|
|
46
|
+
"websockets>=13",
|
|
47
|
+
"ruff>=0.9",
|
|
48
|
+
"ty>=0",
|
|
49
|
+
"pytest>=9",
|
|
50
|
+
"pytest-asyncio>=0.24",
|
|
51
|
+
"pytest-cov>=6",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
[project.entry-points.pytest11]
|
|
55
|
+
nimax = "nimax.plugin"
|
|
56
|
+
|
|
57
|
+
[tool.hatch.build.targets.wheel]
|
|
58
|
+
packages = ["src/nimax"]
|
|
59
|
+
|
|
60
|
+
# ── pytest ────────────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
[tool.pytest.ini_options]
|
|
63
|
+
asyncio_mode = "auto"
|
|
64
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
65
|
+
testpaths = ["tests"]
|
|
66
|
+
pythonpath = ["."]
|
|
67
|
+
|
|
68
|
+
# ── ruff ──────────────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
[tool.ruff]
|
|
71
|
+
target-version = "py311"
|
|
72
|
+
line-length = 100
|
|
73
|
+
|
|
74
|
+
[tool.ruff.lint]
|
|
75
|
+
select = ["ALL"]
|
|
76
|
+
ignore = [
|
|
77
|
+
# Formatter conflicts
|
|
78
|
+
"ISC001", # single-line-implicit-string-concatenation
|
|
79
|
+
"D",
|
|
80
|
+
"ANN401", # Any annotations are legitimate for niquests internals
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
[tool.ruff.lint.per-file-ignores]
|
|
84
|
+
"tests/**" = [
|
|
85
|
+
"S101", # assert OK in tests
|
|
86
|
+
"SLF001", # private member access OK in tests
|
|
87
|
+
"ARG001", # unused function args (fixtures)
|
|
88
|
+
"ARG002", # unused method args (fixtures)
|
|
89
|
+
"PLR2004", # magic values OK in tests
|
|
90
|
+
"PLC0415", # conditional imports OK in tests (optional deps)
|
|
91
|
+
"ASYNC110", # asyncio.sleep polling OK for cross-thread shutdown coordination
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
[tool.ruff.lint.isort]
|
|
95
|
+
known-first-party = ["nimax"]
|
|
96
|
+
|
|
97
|
+
# ── coverage ──────────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
[tool.coverage.run]
|
|
100
|
+
source = ["nimax"]
|
|
101
|
+
omit = ["tests/*"]
|
|
102
|
+
|
|
103
|
+
[tool.coverage.report]
|
|
104
|
+
show_missing = true
|
|
105
|
+
skip_covered = true
|
|
106
|
+
|
|
107
|
+
# ── ty ────────────────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
[tool.ty.environment]
|
|
110
|
+
python-version = "3.11"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""nimax: record and replay niquests HTTP/WebSocket interactions in pytest."""
|
|
2
|
+
|
|
3
|
+
from ._adapter import NimaxRecorder
|
|
4
|
+
from ._cassette import Cassette
|
|
5
|
+
from ._matchers import BaseMatcher
|
|
6
|
+
from ._placeholders import Placeholder
|
|
7
|
+
from ._record_mode import RecordMode
|
|
8
|
+
from ._serializers import BaseSerializer, JSONSerializer, YAMLSerializer
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"BaseMatcher",
|
|
12
|
+
"BaseSerializer",
|
|
13
|
+
"Cassette",
|
|
14
|
+
"JSONSerializer",
|
|
15
|
+
"NimaxRecorder",
|
|
16
|
+
"Placeholder",
|
|
17
|
+
"RecordMode",
|
|
18
|
+
"YAMLSerializer",
|
|
19
|
+
]
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""NimaxRecorder: programmatic API for wrapping a session with a cassette."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
8
|
+
|
|
9
|
+
from ._cassette import DEFAULT_MATCH_ON, Cassette
|
|
10
|
+
from ._matchers import BUILTIN_MATCHERS, BaseMatcher
|
|
11
|
+
from ._record_mode import RecordMode
|
|
12
|
+
from ._serializers import BUILTIN_SERIALIZERS, BaseSerializer, JSONSerializer
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from collections.abc import Generator, Iterable
|
|
16
|
+
|
|
17
|
+
import niquests
|
|
18
|
+
|
|
19
|
+
from ._placeholders import Placeholder
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class NimaxRecorder:
|
|
23
|
+
"""Wraps a niquests session to provide cassette recording and replay.
|
|
24
|
+
|
|
25
|
+
Usage::
|
|
26
|
+
|
|
27
|
+
recorder = NimaxRecorder(session)
|
|
28
|
+
with recorder.use_cassette("my_test") as cassette:
|
|
29
|
+
resp = session.get("https://example.com")
|
|
30
|
+
|
|
31
|
+
The recorder patches ``niquests.Session.send`` and
|
|
32
|
+
``niquests.AsyncSession.send`` class-wide for the duration of the context,
|
|
33
|
+
so any sessions created inside the block are also intercepted.
|
|
34
|
+
|
|
35
|
+
Custom matchers and serializers can be registered at the class level::
|
|
36
|
+
|
|
37
|
+
NimaxRecorder.register_matcher(MyMatcher)
|
|
38
|
+
NimaxRecorder.register_serializer(MySerializer)
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
_matchers: ClassVar[dict[str, type[BaseMatcher]]] = dict(BUILTIN_MATCHERS)
|
|
42
|
+
_serializers: ClassVar[dict[str, type[BaseSerializer]]] = dict(BUILTIN_SERIALIZERS)
|
|
43
|
+
|
|
44
|
+
def __init__(self, session: niquests.Session | niquests.AsyncSession) -> None:
|
|
45
|
+
self._session = session
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def register_matcher(cls, matcher: type[BaseMatcher]) -> None:
|
|
49
|
+
"""Register a custom matcher, making it available by name."""
|
|
50
|
+
cls._matchers[matcher.name] = matcher
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def register_serializer(cls, serializer: type[BaseSerializer]) -> None:
|
|
54
|
+
"""Register a custom serializer, making it available by extension."""
|
|
55
|
+
cls._serializers[serializer.extension] = serializer
|
|
56
|
+
|
|
57
|
+
@contextlib.contextmanager
|
|
58
|
+
def use_cassette( # noqa: PLR0913
|
|
59
|
+
self,
|
|
60
|
+
name: str,
|
|
61
|
+
*,
|
|
62
|
+
cassette_dir: Path | str = "cassettes",
|
|
63
|
+
record_mode: RecordMode = RecordMode.ONCE,
|
|
64
|
+
match_on: Iterable[str] = DEFAULT_MATCH_ON,
|
|
65
|
+
serializer: BaseSerializer | None = None,
|
|
66
|
+
placeholders: list[Placeholder] | None = None,
|
|
67
|
+
) -> Generator[Cassette, None, None]:
|
|
68
|
+
"""Context manager that activates a named cassette for the session.
|
|
69
|
+
|
|
70
|
+
:param name: Cassette name (used as the filename stem).
|
|
71
|
+
:param cassette_dir: Directory to store cassette files.
|
|
72
|
+
:param record_mode: When to record vs replay.
|
|
73
|
+
:param match_on: Iterable of matcher names.
|
|
74
|
+
:param serializer: Explicit serializer (defaults to JSON).
|
|
75
|
+
:param placeholders: Sensitive values to redact in the cassette.
|
|
76
|
+
"""
|
|
77
|
+
resolved_serializer = serializer or JSONSerializer()
|
|
78
|
+
path = Path(cassette_dir) / f"{name}.{resolved_serializer.extension}"
|
|
79
|
+
cassette = Cassette(
|
|
80
|
+
path=path,
|
|
81
|
+
record_mode=record_mode,
|
|
82
|
+
match_on=frozenset(match_on),
|
|
83
|
+
serializer=resolved_serializer,
|
|
84
|
+
placeholders=placeholders,
|
|
85
|
+
matcher_registry=type(self)._matchers, # noqa: SLF001
|
|
86
|
+
serializer_registry=type(self)._serializers, # noqa: SLF001
|
|
87
|
+
)
|
|
88
|
+
with cassette:
|
|
89
|
+
yield cassette
|
|
90
|
+
|
|
91
|
+
# Convenience: expose session on the recorder for use inside the context
|
|
92
|
+
@property
|
|
93
|
+
def session(self) -> niquests.Session | niquests.AsyncSession:
|
|
94
|
+
return self._session
|