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.
Files changed (35) hide show
  1. nimax-1.0.0/.github/workflows/ci.yml +49 -0
  2. nimax-1.0.0/.github/workflows/publish.yml +35 -0
  3. nimax-1.0.0/.gitignore +12 -0
  4. nimax-1.0.0/.pre-commit-config.yaml +37 -0
  5. nimax-1.0.0/.taplo.toml +3 -0
  6. nimax-1.0.0/LICENSE +21 -0
  7. nimax-1.0.0/PKG-INFO +157 -0
  8. nimax-1.0.0/README.md +104 -0
  9. nimax-1.0.0/conftest.py +1 -0
  10. nimax-1.0.0/pyproject.toml +110 -0
  11. nimax-1.0.0/src/nimax/__init__.py +19 -0
  12. nimax-1.0.0/src/nimax/_adapter.py +94 -0
  13. nimax-1.0.0/src/nimax/_cassette.py +487 -0
  14. nimax-1.0.0/src/nimax/_matchers.py +123 -0
  15. nimax-1.0.0/src/nimax/_placeholders.py +31 -0
  16. nimax-1.0.0/src/nimax/_record_mode.py +20 -0
  17. nimax-1.0.0/src/nimax/_serializers.py +54 -0
  18. nimax-1.0.0/src/nimax/_websocket.py +240 -0
  19. nimax-1.0.0/src/nimax/plugin.py +185 -0
  20. nimax-1.0.0/tests/__init__.py +0 -0
  21. nimax-1.0.0/tests/_utils.py +72 -0
  22. nimax-1.0.0/tests/conftest.py +60 -0
  23. nimax-1.0.0/tests/integration/__init__.py +0 -0
  24. nimax-1.0.0/tests/integration/test_adapter.py +165 -0
  25. nimax-1.0.0/tests/integration/test_cassette.py +417 -0
  26. nimax-1.0.0/tests/integration/test_websocket.py +264 -0
  27. nimax-1.0.0/tests/plugin/__init__.py +0 -0
  28. nimax-1.0.0/tests/plugin/test_plugin.py +228 -0
  29. nimax-1.0.0/tests/unit/__init__.py +0 -0
  30. nimax-1.0.0/tests/unit/test_cassette_helpers.py +153 -0
  31. nimax-1.0.0/tests/unit/test_matchers.py +231 -0
  32. nimax-1.0.0/tests/unit/test_placeholders.py +55 -0
  33. nimax-1.0.0/tests/unit/test_record_mode.py +36 -0
  34. nimax-1.0.0/tests/unit/test_serializers.py +84 -0
  35. 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,12 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ .idea/
8
+ .ruff_cache/
9
+ .code-graph/
10
+ .coverage
11
+ coverage.xml
12
+ uv.lock
@@ -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
@@ -0,0 +1,3 @@
1
+ [formatting]
2
+ array_trailing_comma = true
3
+ array_auto_collapse = false
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
+ [![CI](https://github.com/adamlogan73/nimax/actions/workflows/ci.yml/badge.svg)](https://github.com/adamlogan73/nimax/actions/workflows/ci.yml)
57
+ [![codecov](https://codecov.io/gh/adamlogan73/nimax/graph/badge.svg)](https://codecov.io/gh/adamlogan73/nimax)
58
+ [![PyPI](https://img.shields.io/pypi/v/nimax)](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
+ [![CI](https://github.com/adamlogan73/nimax/actions/workflows/ci.yml/badge.svg)](https://github.com/adamlogan73/nimax/actions/workflows/ci.yml)
4
+ [![codecov](https://codecov.io/gh/adamlogan73/nimax/graph/badge.svg)](https://codecov.io/gh/adamlogan73/nimax)
5
+ [![PyPI](https://img.shields.io/pypi/v/nimax)](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
@@ -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