rest-api-mocker 0.1.0__py3-none-any.whl

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,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,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,8 @@
1
+ rest_api_mocker/__init__.py,sha256=uAOZvUicJ3PId1pyKKQtnoagP5jsCCkZNDg6MWO3FmE,427
2
+ rest_api_mocker/client.py,sha256=bc5kiee20kZ2VjLisUKAKKm4DjFr_kC6xTOL9X7-4us,5543
3
+ rest_api_mocker/exceptions.py,sha256=Vz6dhaoSF5MVkr3vvphltoS9TPGMS6YSz4GGlBMa3zo,689
4
+ rest_api_mocker/models.py,sha256=P744NvqI51TedClje_aC-MXl9NX6cobZmJa1GDJBiOA,1576
5
+ rest_api_mocker-0.1.0.dist-info/METADATA,sha256=2moHoMzs9QidxNiSg94DfXzGgooNz3Q3tFXKvRYna8Y,6248
6
+ rest_api_mocker-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
7
+ rest_api_mocker-0.1.0.dist-info/licenses/LICENSE,sha256=eTbYou-toxlnYebu1uvLqlKtNK_RgQ-qNtYdgX9OFjs,1069
8
+ rest_api_mocker-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.