rest-api-mocker 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.
@@ -0,0 +1,56 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ lint:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: actions/setup-python@v5
14
+ with:
15
+ python-version: "3.12"
16
+ - run: python -m pip install --upgrade pip
17
+ - run: pip install -e ".[dev]"
18
+ - name: Ruff (lint)
19
+ run: ruff check src tests
20
+ - name: Ruff (format)
21
+ run: ruff format --check src tests
22
+ - name: Mypy
23
+ run: mypy src
24
+
25
+ test:
26
+ runs-on: ubuntu-latest
27
+ strategy:
28
+ fail-fast: false
29
+ matrix:
30
+ python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
31
+ steps:
32
+ - uses: actions/checkout@v4
33
+ - uses: actions/setup-python@v5
34
+ with:
35
+ python-version: ${{ matrix.python-version }}
36
+ - run: python -m pip install --upgrade pip
37
+ - run: pip install -e ".[test]"
38
+ - name: Pytest
39
+ run: pytest
40
+
41
+ build:
42
+ runs-on: ubuntu-latest
43
+ steps:
44
+ - uses: actions/checkout@v4
45
+ - uses: actions/setup-python@v5
46
+ with:
47
+ python-version: "3.12"
48
+ - run: python -m pip install --upgrade pip build twine
49
+ - name: Build distribution
50
+ run: python -m build
51
+ - name: Check distribution
52
+ run: twine check dist/*
53
+ - uses: actions/upload-artifact@v4
54
+ with:
55
+ name: dist
56
+ path: dist/
@@ -0,0 +1,39 @@
1
+ name: Publish to PyPI
2
+
3
+ # Publishes when you cut a GitHub Release. Uses PyPI Trusted Publishing
4
+ # (OIDC) — no API tokens or secrets to manage. See README for the one-time
5
+ # PyPI setup this requires.
6
+
7
+ on:
8
+ release:
9
+ types: [published]
10
+
11
+ jobs:
12
+ build:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: "3.12"
19
+ - run: python -m pip install --upgrade pip build twine
20
+ - run: python -m build
21
+ - run: twine check dist/*
22
+ - uses: actions/upload-artifact@v4
23
+ with:
24
+ name: dist
25
+ path: dist/
26
+
27
+ publish:
28
+ needs: build
29
+ runs-on: ubuntu-latest
30
+ environment: pypi
31
+ permissions:
32
+ id-token: write # required for trusted publishing
33
+ steps:
34
+ - uses: actions/download-artifact@v4
35
+ with:
36
+ name: dist
37
+ path: dist/
38
+ - name: Publish to PyPI
39
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,218 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ # Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ # poetry.lock
109
+ # poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ # pdm.lock
116
+ # pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ # pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # Redis
135
+ *.rdb
136
+ *.aof
137
+ *.pid
138
+
139
+ # RabbitMQ
140
+ mnesia/
141
+ rabbitmq/
142
+ rabbitmq-data/
143
+
144
+ # ActiveMQ
145
+ activemq-data/
146
+
147
+ # SageMath parsed files
148
+ *.sage.py
149
+
150
+ # Environments
151
+ .env
152
+ .envrc
153
+ .venv
154
+ env/
155
+ venv/
156
+ ENV/
157
+ env.bak/
158
+ venv.bak/
159
+
160
+ # Spyder project settings
161
+ .spyderproject
162
+ .spyproject
163
+
164
+ # Rope project settings
165
+ .ropeproject
166
+
167
+ # mkdocs documentation
168
+ /site
169
+
170
+ # mypy
171
+ .mypy_cache/
172
+ .dmypy.json
173
+ dmypy.json
174
+
175
+ # Pyre type checker
176
+ .pyre/
177
+
178
+ # pytype static type analyzer
179
+ .pytype/
180
+
181
+ # Cython debug symbols
182
+ cython_debug/
183
+
184
+ # PyCharm
185
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
186
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
187
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
188
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
189
+ # .idea/
190
+
191
+ # Abstra
192
+ # Abstra is an AI-powered process automation framework.
193
+ # Ignore directories containing user credentials, local state, and settings.
194
+ # Learn more at https://abstra.io/docs
195
+ .abstra/
196
+
197
+ # Visual Studio Code
198
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
199
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
200
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
201
+ # you could uncomment the following to ignore the entire vscode folder
202
+ # .vscode/
203
+ # Temporary file for partial code execution
204
+ tempCodeRunnerFile.py
205
+
206
+ # Ruff stuff:
207
+ .ruff_cache/
208
+
209
+ # PyPI configuration file
210
+ .pypirc
211
+
212
+ # Marimo
213
+ marimo/_static/
214
+ marimo/_lsp/
215
+ __marimo__/
216
+
217
+ # Streamlit
218
+ .streamlit/secrets.toml
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 julien lopez
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.
@@ -0,0 +1,175 @@
1
+ Metadata-Version: 2.4
2
+ Name: rest-api-mocker
3
+ Version: 0.1.0
4
+ Summary: A small Python wrapper for RestApiMocker.
5
+ Project-URL: Homepage, https://github.com/julienlopez/RestApiMockerPythonAPI
6
+ Project-URL: Repository, https://github.com/julienlopez/RestApiMockerPythonAPI
7
+ Author-email: Julien Lopez <julien.lopez51@gmail.com>
8
+ License: MIT License
9
+
10
+ Copyright (c) 2026 julien lopez
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
29
+ License-File: LICENSE
30
+ Keywords: api,http,mock,rest,testing
31
+ Classifier: Development Status :: 3 - Alpha
32
+ Classifier: Intended Audience :: Developers
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Programming Language :: Python :: 3
35
+ Classifier: Programming Language :: Python :: 3.8
36
+ Classifier: Programming Language :: Python :: 3.9
37
+ Classifier: Programming Language :: Python :: 3.10
38
+ Classifier: Programming Language :: Python :: 3.11
39
+ Classifier: Programming Language :: Python :: 3.12
40
+ Classifier: Programming Language :: Python :: 3.13
41
+ Classifier: Topic :: Software Development :: Testing :: Mocking
42
+ Requires-Python: >=3.8
43
+ Requires-Dist: requests>=2.20
44
+ Provides-Extra: dev
45
+ Requires-Dist: mypy>=1.8; extra == 'dev'
46
+ Requires-Dist: pytest>=7; extra == 'dev'
47
+ Requires-Dist: responses>=0.23; extra == 'dev'
48
+ Requires-Dist: ruff>=0.4; extra == 'dev'
49
+ Requires-Dist: types-requests; extra == 'dev'
50
+ Provides-Extra: test
51
+ Requires-Dist: pytest>=7; extra == 'test'
52
+ Requires-Dist: responses>=0.23; extra == 'test'
53
+ Description-Content-Type: text/markdown
54
+
55
+ # rest-api-mocker
56
+
57
+ A small Python wrapper for [RestApiMocker](https://github.com/julienlopez/RestApiMockerPythonAPI).
58
+
59
+ ## Installation
60
+
61
+ ```bash
62
+ pip install rest-api-mocker
63
+ ```
64
+
65
+ Until it's published, install from a checkout:
66
+
67
+ ```bash
68
+ pip install -e ".[test]"
69
+ ```
70
+
71
+ ## Usage
72
+
73
+ ```python
74
+ from rest_api_mocker import RestApiMocker
75
+
76
+ mocker = RestApiMocker("http://localhost", 8080)
77
+ mocker.add_mock(
78
+ method="GET",
79
+ path_pattern="/users/.*",
80
+ status=200,
81
+ body={"id": 1, "name": "Ada"},
82
+ )
83
+ ```
84
+
85
+ `RestApiMocker` can also be used as a context manager so the underlying HTTP
86
+ session is closed for you:
87
+
88
+ ```python
89
+ with RestApiMocker("http://localhost", 8080) as mocker:
90
+ mocker.add_mock("GET", "/health", 200, {"ok": True})
91
+ ```
92
+
93
+ ### API
94
+
95
+ The client mirrors the server's `/internal` control plane:
96
+
97
+ | Method | Description |
98
+ | --- | --- |
99
+ | `add_mock(method, path_pattern, status, body, conditions=None)` | Register a mock response. |
100
+ | `get_mocks() -> list[MockConfig]` | List all configured mocks. |
101
+ | `delete_mock(index)` | Delete a mock by its 0-based index. |
102
+ | `delete_all_mocks()` | Delete every configured mock. |
103
+ | `delete_mocks_by_pattern(path_pattern)` | Delete all mocks matching a path pattern. |
104
+ | `get_config() -> ServerConfig` | Get the server's public/private ports. |
105
+ | `get_history() -> list[RequestRecord]` | Get the recorded request history. |
106
+
107
+ ```python
108
+ mocker.add_mock("GET", "/users/.*", 200, {"id": 1})
109
+
110
+ config = mocker.get_config() # ServerConfig(public_port=9090, private_port=80)
111
+ mocks = mocker.get_mocks() # [MockConfig(...)]
112
+ history = mocker.get_history() # [RequestRecord(method=..., path=..., timestamp=...)]
113
+
114
+ mocker.delete_mocks_by_pattern("/users/.*")
115
+ mocker.delete_all_mocks()
116
+ ```
117
+
118
+ The `MockConfig`, `ServerConfig` and `RequestRecord` dataclasses are importable
119
+ from the top-level package.
120
+
121
+ ### Errors
122
+
123
+ A non-success response from the mocker server raises `MockRequestError`
124
+ (a subclass of `RestApiMockerError`):
125
+
126
+ ```python
127
+ from rest_api_mocker import MockRequestError
128
+
129
+ try:
130
+ mocker.add_mock("GET", "/x", 200, {})
131
+ except MockRequestError as exc:
132
+ print(exc.status_code, exc.response_text)
133
+ ```
134
+
135
+ ## Development
136
+
137
+ ```bash
138
+ pip install -e ".[dev]"
139
+ pytest # tests
140
+ ruff check . # lint
141
+ ruff format . # format
142
+ mypy src # type-check
143
+ ```
144
+
145
+ CI (`.github/workflows/ci.yml`) runs the tests on Python 3.8–3.13 plus lint,
146
+ format, and type checks on every push and pull request.
147
+
148
+ ## Releasing to PyPI
149
+
150
+ Publishing is automated by `.github/workflows/publish.yml`, which runs when you
151
+ publish a GitHub Release. It uses PyPI **Trusted Publishing** (OIDC), so there
152
+ are no API tokens or secrets to store.
153
+
154
+ One-time setup:
155
+
156
+ 1. Create an account at <https://pypi.org/account/register/>.
157
+ 2. On PyPI, go to your account → *Publishing* → *Add a pending publisher* and
158
+ register this repository as a trusted publisher:
159
+ - PyPI Project Name: `rest-api-mocker`
160
+ - Owner / Repository: your GitHub `owner` / `RestApiMockerPythonAPI`
161
+ - Workflow name: `publish.yml`
162
+ - Environment name: `pypi`
163
+ 3. (Recommended) In the GitHub repo settings, create an Environment named
164
+ `pypi` to gate releases.
165
+
166
+ To cut a release:
167
+
168
+ 1. Bump `version` in `pyproject.toml` (and `__version__` in
169
+ `src/rest_api_mocker/__init__.py`).
170
+ 2. Tag and push, then publish a GitHub Release for that tag. The workflow
171
+ builds the package and uploads it to PyPI.
172
+
173
+ > Tip: to rehearse without affecting the real index, register the same trusted
174
+ > publisher on <https://test.pypi.org> and point the publish step at it with
175
+ > `with: { repository-url: https://test.pypi.org/legacy/ }`.
@@ -0,0 +1,121 @@
1
+ # rest-api-mocker
2
+
3
+ A small Python wrapper for [RestApiMocker](https://github.com/julienlopez/RestApiMockerPythonAPI).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install rest-api-mocker
9
+ ```
10
+
11
+ Until it's published, install from a checkout:
12
+
13
+ ```bash
14
+ pip install -e ".[test]"
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```python
20
+ from rest_api_mocker import RestApiMocker
21
+
22
+ mocker = RestApiMocker("http://localhost", 8080)
23
+ mocker.add_mock(
24
+ method="GET",
25
+ path_pattern="/users/.*",
26
+ status=200,
27
+ body={"id": 1, "name": "Ada"},
28
+ )
29
+ ```
30
+
31
+ `RestApiMocker` can also be used as a context manager so the underlying HTTP
32
+ session is closed for you:
33
+
34
+ ```python
35
+ with RestApiMocker("http://localhost", 8080) as mocker:
36
+ mocker.add_mock("GET", "/health", 200, {"ok": True})
37
+ ```
38
+
39
+ ### API
40
+
41
+ The client mirrors the server's `/internal` control plane:
42
+
43
+ | Method | Description |
44
+ | --- | --- |
45
+ | `add_mock(method, path_pattern, status, body, conditions=None)` | Register a mock response. |
46
+ | `get_mocks() -> list[MockConfig]` | List all configured mocks. |
47
+ | `delete_mock(index)` | Delete a mock by its 0-based index. |
48
+ | `delete_all_mocks()` | Delete every configured mock. |
49
+ | `delete_mocks_by_pattern(path_pattern)` | Delete all mocks matching a path pattern. |
50
+ | `get_config() -> ServerConfig` | Get the server's public/private ports. |
51
+ | `get_history() -> list[RequestRecord]` | Get the recorded request history. |
52
+
53
+ ```python
54
+ mocker.add_mock("GET", "/users/.*", 200, {"id": 1})
55
+
56
+ config = mocker.get_config() # ServerConfig(public_port=9090, private_port=80)
57
+ mocks = mocker.get_mocks() # [MockConfig(...)]
58
+ history = mocker.get_history() # [RequestRecord(method=..., path=..., timestamp=...)]
59
+
60
+ mocker.delete_mocks_by_pattern("/users/.*")
61
+ mocker.delete_all_mocks()
62
+ ```
63
+
64
+ The `MockConfig`, `ServerConfig` and `RequestRecord` dataclasses are importable
65
+ from the top-level package.
66
+
67
+ ### Errors
68
+
69
+ A non-success response from the mocker server raises `MockRequestError`
70
+ (a subclass of `RestApiMockerError`):
71
+
72
+ ```python
73
+ from rest_api_mocker import MockRequestError
74
+
75
+ try:
76
+ mocker.add_mock("GET", "/x", 200, {})
77
+ except MockRequestError as exc:
78
+ print(exc.status_code, exc.response_text)
79
+ ```
80
+
81
+ ## Development
82
+
83
+ ```bash
84
+ pip install -e ".[dev]"
85
+ pytest # tests
86
+ ruff check . # lint
87
+ ruff format . # format
88
+ mypy src # type-check
89
+ ```
90
+
91
+ CI (`.github/workflows/ci.yml`) runs the tests on Python 3.8–3.13 plus lint,
92
+ format, and type checks on every push and pull request.
93
+
94
+ ## Releasing to PyPI
95
+
96
+ Publishing is automated by `.github/workflows/publish.yml`, which runs when you
97
+ publish a GitHub Release. It uses PyPI **Trusted Publishing** (OIDC), so there
98
+ are no API tokens or secrets to store.
99
+
100
+ One-time setup:
101
+
102
+ 1. Create an account at <https://pypi.org/account/register/>.
103
+ 2. On PyPI, go to your account → *Publishing* → *Add a pending publisher* and
104
+ register this repository as a trusted publisher:
105
+ - PyPI Project Name: `rest-api-mocker`
106
+ - Owner / Repository: your GitHub `owner` / `RestApiMockerPythonAPI`
107
+ - Workflow name: `publish.yml`
108
+ - Environment name: `pypi`
109
+ 3. (Recommended) In the GitHub repo settings, create an Environment named
110
+ `pypi` to gate releases.
111
+
112
+ To cut a release:
113
+
114
+ 1. Bump `version` in `pyproject.toml` (and `__version__` in
115
+ `src/rest_api_mocker/__init__.py`).
116
+ 2. Tag and push, then publish a GitHub Release for that tag. The workflow
117
+ builds the package and uploads it to PyPI.
118
+
119
+ > Tip: to rehearse without affecting the real index, register the same trusted
120
+ > publisher on <https://test.pypi.org> and point the publish step at it with
121
+ > `with: { repository-url: https://test.pypi.org/legacy/ }`.
@@ -0,0 +1,58 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "rest-api-mocker"
7
+ version = "0.1.0"
8
+ description = "A small Python wrapper for RestApiMocker."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { file = "LICENSE" }
12
+ authors = [{ name = "Julien Lopez", email = "julien.lopez51@gmail.com" }]
13
+ keywords = ["mock", "rest", "api", "testing", "http"]
14
+ dependencies = ["requests>=2.20"]
15
+
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.8",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Topic :: Software Development :: Testing :: Mocking",
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ test = ["pytest>=7", "responses>=0.23"]
32
+ dev = [
33
+ "pytest>=7",
34
+ "responses>=0.23",
35
+ "ruff>=0.4",
36
+ "mypy>=1.8",
37
+ "types-requests",
38
+ ]
39
+
40
+ [project.urls]
41
+ Homepage = "https://github.com/julienlopez/RestApiMockerPythonAPI"
42
+ Repository = "https://github.com/julienlopez/RestApiMockerPythonAPI"
43
+
44
+ [tool.hatch.build.targets.wheel]
45
+ packages = ["src/rest_api_mocker"]
46
+
47
+ [tool.pytest.ini_options]
48
+ testpaths = ["tests"]
49
+
50
+ [tool.ruff]
51
+ target-version = "py38"
52
+ line-length = 88
53
+
54
+ [tool.ruff.lint]
55
+ select = ["E", "F", "I", "UP", "B"]
56
+
57
+ [tool.mypy]
58
+ strict = true
@@ -0,0 +1,17 @@
1
+ """rest_api_mocker — a small Python wrapper for RestApiMocker."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .client import RestApiMocker
6
+ from .exceptions import MockRequestError, RestApiMockerError
7
+ from .models import MockConfig, RequestRecord, ServerConfig
8
+
9
+ __all__ = [
10
+ "RestApiMocker",
11
+ "RestApiMockerError",
12
+ "MockRequestError",
13
+ "ServerConfig",
14
+ "RequestRecord",
15
+ "MockConfig",
16
+ ]
17
+ __version__ = "0.1.0"
@@ -0,0 +1,164 @@
1
+ """Client for talking to a RestApiMocker server."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any, Sequence
7
+
8
+ import requests
9
+
10
+ from .exceptions import MockRequestError
11
+ from .models import MockConfig, RequestRecord, ServerConfig
12
+
13
+ DEFAULT_TIMEOUT = 30.0
14
+
15
+
16
+ class RestApiMocker:
17
+ """A thin wrapper around the RestApiMocker HTTP control API.
18
+
19
+ All methods talk to the server's ``/internal`` control plane.
20
+
21
+ Example:
22
+ >>> mocker = RestApiMocker("http://localhost", 8080)
23
+ >>> mocker.add_mock(
24
+ ... method="GET",
25
+ ... path_pattern="/users/.*",
26
+ ... status=200,
27
+ ... body={"id": 1, "name": "Ada"},
28
+ ... )
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ url: str,
34
+ port: int,
35
+ *,
36
+ timeout: float = DEFAULT_TIMEOUT,
37
+ session: requests.Session | None = None,
38
+ ) -> None:
39
+ """Create a client.
40
+
41
+ Args:
42
+ url: Base URL of the mocker server, e.g. ``"http://localhost"``.
43
+ port: Port the mocker server's internal API listens on.
44
+ timeout: Per-request timeout in seconds.
45
+ session: Optional pre-configured :class:`requests.Session`. When
46
+ omitted, a new session is created and owned by this instance.
47
+ """
48
+ self.url = url.rstrip("/")
49
+ self.port = port
50
+ self.timeout = timeout
51
+ self._session = session or requests.Session()
52
+ self._owns_session = session is None
53
+
54
+ @property
55
+ def base_url(self) -> str:
56
+ """The fully-qualified base URL, including port."""
57
+ return f"{self.url}:{self.port}"
58
+
59
+ # -- low-level helpers ------------------------------------------------
60
+
61
+ def _request(self, method: str, path: str, **kwargs: Any) -> requests.Response:
62
+ """Send a request to ``/internal`` and raise on a non-success status.
63
+
64
+ Raises:
65
+ MockRequestError: If the server returns a 4xx/5xx response.
66
+ """
67
+ response = self._session.request(
68
+ method,
69
+ f"{self.base_url}/internal{path}",
70
+ timeout=self.timeout,
71
+ **kwargs,
72
+ )
73
+ if not response.ok:
74
+ raise MockRequestError(response.status_code, response.text)
75
+ return response
76
+
77
+ # -- mocks ------------------------------------------------------------
78
+
79
+ def add_mock(
80
+ self,
81
+ method: str,
82
+ path_pattern: str,
83
+ status: int,
84
+ body: Any,
85
+ conditions: Sequence[dict[str, Any]] | None = None,
86
+ ) -> None:
87
+ """Register a mock response on the server (``POST /internal/mock``).
88
+
89
+ Args:
90
+ method: HTTP method to match, e.g. ``"GET"``.
91
+ path_pattern: Path (or pattern) the mock should match.
92
+ status: HTTP status code the mock should return.
93
+ body: Response body. Serialized to a JSON string before sending.
94
+ conditions: Optional list of matching conditions.
95
+
96
+ Raises:
97
+ MockRequestError: If the server returns a non-success response.
98
+ """
99
+ mock_definition = {
100
+ "method": method,
101
+ "path_pattern": path_pattern,
102
+ "status": status,
103
+ "body": json.dumps(body),
104
+ "conditions": list(conditions) if conditions is not None else [],
105
+ }
106
+ self._request("POST", "/mock", json=mock_definition)
107
+
108
+ def get_mocks(self) -> list[MockConfig]:
109
+ """Return all configured mocks (``GET /internal/mocks``)."""
110
+ response = self._request("GET", "/mocks")
111
+ return [MockConfig.from_dict(item) for item in response.json()]
112
+
113
+ def delete_mock(self, index: int) -> None:
114
+ """Delete a mock by its 0-based index (``DELETE /internal/mock/<index>``).
115
+
116
+ Raises:
117
+ MockRequestError: If the index is out of range (404) or the request
118
+ otherwise fails.
119
+ """
120
+ self._request("DELETE", f"/mock/{index}")
121
+
122
+ def delete_all_mocks(self) -> None:
123
+ """Delete every configured mock (``DELETE /internal/mocks``)."""
124
+ self._request("DELETE", "/mocks")
125
+
126
+ def delete_mocks_by_pattern(self, path_pattern: str) -> None:
127
+ """Delete all mocks whose path pattern matches ``path_pattern``.
128
+
129
+ Maps to ``DELETE /internal/mocks/by-pattern?path_pattern=...``.
130
+
131
+ Raises:
132
+ MockRequestError: If no mock matches the pattern (404) or the
133
+ request otherwise fails.
134
+ """
135
+ self._request(
136
+ "DELETE",
137
+ "/mocks/by-pattern",
138
+ params={"path_pattern": path_pattern},
139
+ )
140
+
141
+ # -- introspection ----------------------------------------------------
142
+
143
+ def get_config(self) -> ServerConfig:
144
+ """Return the server's port configuration (``GET /internal/config``)."""
145
+ response = self._request("GET", "/config")
146
+ return ServerConfig.from_dict(response.json())
147
+
148
+ def get_history(self) -> list[RequestRecord]:
149
+ """Return the recorded request history (``GET /internal/history``)."""
150
+ response = self._request("GET", "/history")
151
+ return [RequestRecord.from_dict(item) for item in response.json()]
152
+
153
+ # -- lifecycle --------------------------------------------------------
154
+
155
+ def close(self) -> None:
156
+ """Close the underlying session, if this instance owns it."""
157
+ if self._owns_session:
158
+ self._session.close()
159
+
160
+ def __enter__(self) -> RestApiMocker:
161
+ return self
162
+
163
+ def __exit__(self, *exc_info: object) -> None:
164
+ self.close()
@@ -0,0 +1,21 @@
1
+ """Exceptions raised by :mod:`rest_api_mocker`."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class RestApiMockerError(Exception):
7
+ """Base class for all errors raised by this library."""
8
+
9
+
10
+ class MockRequestError(RestApiMockerError):
11
+ """Raised when the mocker server returns a non-success response.
12
+
13
+ Attributes:
14
+ status_code: HTTP status code returned by the server.
15
+ response_text: Raw response body returned by the server.
16
+ """
17
+
18
+ def __init__(self, status_code: int, response_text: str) -> None:
19
+ self.status_code = status_code
20
+ self.response_text = response_text
21
+ super().__init__(f"Mocker server returned {status_code}: {response_text}")
@@ -0,0 +1,66 @@
1
+ """Dataclasses mirroring the RestApiMocker server's JSON types."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any
7
+
8
+
9
+ @dataclass
10
+ class ServerConfig:
11
+ """Server configuration, as returned by ``GET /internal/config``."""
12
+
13
+ public_port: int
14
+ private_port: int
15
+
16
+ @classmethod
17
+ def from_dict(cls, data: dict[str, Any]) -> ServerConfig:
18
+ return cls(
19
+ public_port=data["public_port"],
20
+ private_port=data["private_port"],
21
+ )
22
+
23
+
24
+ @dataclass
25
+ class RequestRecord:
26
+ """A single request recorded by the public server.
27
+
28
+ Returned by ``GET /internal/history``.
29
+ """
30
+
31
+ method: str
32
+ path: str
33
+ timestamp: int
34
+
35
+ @classmethod
36
+ def from_dict(cls, data: dict[str, Any]) -> RequestRecord:
37
+ return cls(
38
+ method=data["method"],
39
+ path=data["path"],
40
+ timestamp=data["timestamp"],
41
+ )
42
+
43
+
44
+ @dataclass
45
+ class MockConfig:
46
+ """A configured mock response.
47
+
48
+ Returned by ``GET /internal/mocks``. Note that ``body`` is the JSON string
49
+ the server stores, not a decoded object.
50
+ """
51
+
52
+ method: str
53
+ path_pattern: str
54
+ status: int
55
+ body: str
56
+ conditions: list[Any] = field(default_factory=list)
57
+
58
+ @classmethod
59
+ def from_dict(cls, data: dict[str, Any]) -> MockConfig:
60
+ return cls(
61
+ method=data["method"],
62
+ path_pattern=data["path_pattern"],
63
+ status=data["status"],
64
+ body=data.get("body", ""),
65
+ conditions=list(data.get("conditions") or []),
66
+ )
@@ -0,0 +1,180 @@
1
+ import json
2
+
3
+ import pytest
4
+ import responses
5
+
6
+ from rest_api_mocker import (
7
+ MockConfig,
8
+ MockRequestError,
9
+ RequestRecord,
10
+ RestApiMocker,
11
+ ServerConfig,
12
+ )
13
+
14
+ BASE = "http://localhost:8080/internal"
15
+
16
+
17
+ def make_mocker():
18
+ return RestApiMocker("http://localhost", 8080)
19
+
20
+
21
+ @responses.activate
22
+ def test_add_mock_posts_expected_payload():
23
+ responses.add(responses.POST, f"{BASE}/mock", status=200)
24
+
25
+ make_mocker().add_mock(
26
+ method="GET",
27
+ path_pattern="/users/.*",
28
+ status=200,
29
+ body={"id": 1, "name": "Ada"},
30
+ )
31
+
32
+ assert len(responses.calls) == 1
33
+ sent = json.loads(responses.calls[0].request.body)
34
+ assert sent == {
35
+ "method": "GET",
36
+ "path_pattern": "/users/.*",
37
+ "status": 200,
38
+ "body": json.dumps({"id": 1, "name": "Ada"}),
39
+ "conditions": [],
40
+ }
41
+
42
+
43
+ @responses.activate
44
+ def test_add_mock_raises_on_non_200():
45
+ responses.add(responses.POST, f"{BASE}/mock", status=500, body="boom")
46
+
47
+ with pytest.raises(MockRequestError) as exc_info:
48
+ make_mocker().add_mock("GET", "/x", 200, {})
49
+
50
+ assert exc_info.value.status_code == 500
51
+ assert exc_info.value.response_text == "boom"
52
+
53
+
54
+ def test_base_url_strips_trailing_slash():
55
+ mocker = RestApiMocker("http://localhost/", 8080)
56
+ assert mocker.base_url == "http://localhost:8080"
57
+
58
+
59
+ @responses.activate
60
+ def test_conditions_default_is_not_shared():
61
+ responses.add(responses.POST, f"{BASE}/mock", status=200)
62
+ mocker = make_mocker()
63
+
64
+ mocker.add_mock("GET", "/a", 200, {})
65
+ mocker.add_mock("GET", "/b", 200, {})
66
+
67
+ first = json.loads(responses.calls[0].request.body)["conditions"]
68
+ second = json.loads(responses.calls[1].request.body)["conditions"]
69
+ assert first == [] and second == []
70
+
71
+
72
+ @responses.activate
73
+ def test_get_config():
74
+ responses.add(
75
+ responses.GET,
76
+ f"{BASE}/config",
77
+ json={"public_port": 9090, "private_port": 80},
78
+ status=200,
79
+ )
80
+
81
+ config = make_mocker().get_config()
82
+ assert config == ServerConfig(public_port=9090, private_port=80)
83
+
84
+
85
+ @responses.activate
86
+ def test_get_history():
87
+ responses.add(
88
+ responses.GET,
89
+ f"{BASE}/history",
90
+ json=[{"method": "GET", "path": "/users/1", "timestamp": 1700000000}],
91
+ status=200,
92
+ )
93
+
94
+ history = make_mocker().get_history()
95
+ assert history == [
96
+ RequestRecord(method="GET", path="/users/1", timestamp=1700000000)
97
+ ]
98
+
99
+
100
+ @responses.activate
101
+ def test_get_mocks():
102
+ responses.add(
103
+ responses.GET,
104
+ f"{BASE}/mocks",
105
+ json=[
106
+ {
107
+ "method": "GET",
108
+ "path_pattern": "/users/.*",
109
+ "status": 200,
110
+ "body": "{}",
111
+ "conditions": [],
112
+ }
113
+ ],
114
+ status=200,
115
+ )
116
+
117
+ mocks = make_mocker().get_mocks()
118
+ assert mocks == [
119
+ MockConfig(
120
+ method="GET",
121
+ path_pattern="/users/.*",
122
+ status=200,
123
+ body="{}",
124
+ conditions=[],
125
+ )
126
+ ]
127
+
128
+
129
+ @responses.activate
130
+ def test_delete_mock_by_index():
131
+ responses.add(responses.DELETE, f"{BASE}/mock/0", status=200, body="Mock deleted")
132
+
133
+ make_mocker().delete_mock(0)
134
+ assert responses.calls[0].request.url == f"{BASE}/mock/0"
135
+
136
+
137
+ @responses.activate
138
+ def test_delete_mock_out_of_range_raises():
139
+ responses.add(responses.DELETE, f"{BASE}/mock/99", status=404, body="out of range")
140
+
141
+ with pytest.raises(MockRequestError) as exc_info:
142
+ make_mocker().delete_mock(99)
143
+ assert exc_info.value.status_code == 404
144
+
145
+
146
+ @responses.activate
147
+ def test_delete_all_mocks():
148
+ responses.add(
149
+ responses.DELETE, f"{BASE}/mocks", status=200, body="All mocks deleted"
150
+ )
151
+
152
+ make_mocker().delete_all_mocks()
153
+ assert len(responses.calls) == 1
154
+
155
+
156
+ @responses.activate
157
+ def test_delete_mocks_by_pattern():
158
+ responses.add(
159
+ responses.DELETE,
160
+ f"{BASE}/mocks/by-pattern",
161
+ status=200,
162
+ body="1 mock(s) deleted",
163
+ )
164
+
165
+ make_mocker().delete_mocks_by_pattern("/users/:id")
166
+
167
+ assert responses.calls[0].request.params == {"path_pattern": "/users/:id"}
168
+
169
+
170
+ @responses.activate
171
+ def test_delete_mocks_by_pattern_no_match_raises():
172
+ responses.add(
173
+ responses.DELETE,
174
+ f"{BASE}/mocks/by-pattern",
175
+ status=404,
176
+ body="No mocks found",
177
+ )
178
+
179
+ with pytest.raises(MockRequestError):
180
+ make_mocker().delete_mocks_by_pattern("/nope")