proxy-mock 2.12.0__tar.gz → 2.13.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.
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/CHANGELOG.md +19 -0
- proxy_mock-2.13.0/MIGRATING.md +128 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/PKG-INFO +11 -4
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/README.md +10 -3
- proxy_mock-2.13.0/proxy_mock/api/routes/admin.py +25 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/api/routes/configure.py +2 -1
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/api/routes/service.py +2 -1
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/api/routes/storage.py +3 -3
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/api/routes/traffic.py +5 -5
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/app/factory.py +6 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/client/async_client.py +36 -14
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/client/client.py +31 -13
- proxy_mock-2.13.0/proxy_mock/client/migration.py +42 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/client/route.py +14 -1
- proxy_mock-2.13.0/proxy_mock/core/deprecation.py +51 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/pyproject.toml +2 -2
- proxy_mock-2.12.0/proxy_mock/core/deprecation.py +0 -36
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/.gitignore +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/LICENSE +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/examples/README.md +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/examples/compose/README.md +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/__init__.py +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/__main__.py +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/any_catcher.py +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/api/__init__.py +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/api/deps.py +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/api/routes/__init__.py +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/api/schemas.py +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/app/__init__.py +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/app/state.py +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/cli.py +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/client/__init__.py +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/client/service_endpoints.py +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/core/__init__.py +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/core/logging.py +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/core/serializers.py +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/core/settings.py +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/core/time.py +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/core/urls.py +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/domain/__init__.py +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/domain/constants.py +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/domain/models.py +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/pytest_plugin.py +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/repositories/__init__.py +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/repositories/mock_storage.py +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/repositories/traffic_store.py +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/repositories/ttl_cache.py +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/services/__init__.py +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/services/cache_service.py +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/services/mock_service.py +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/services/proxy_service.py +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/services/request_parser.py +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/services/response_factory.py +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/services/rule_engine.py +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/services/snapshot.py +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/services/traffic_service.py +0 -0
- {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/utils.py +0 -0
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
Version 2.13.0
|
|
2
|
+
==============
|
|
3
|
+
|
|
4
|
+
Released on 2026-09-18.
|
|
5
|
+
|
|
6
|
+
* Added opt-in administrative resource paths, enabled by `PROXY_MOCK_ADMIN_PREFIX`, with
|
|
7
|
+
matching `admin_prefix` options on `ProxyMock` and `AsyncProxyMock`. Legacy paths and their
|
|
8
|
+
JSON/msgpack contracts remain available. Aliases are disabled by default to avoid shadowing
|
|
9
|
+
existing user mocks. Invalid or overlapping prefixes fail at startup.
|
|
10
|
+
* Every legacy administrative operation now carries a migration link and a RFC 9745
|
|
11
|
+
`Deprecation` date. This corrects the previous non-standard `true` value; handled error
|
|
12
|
+
responses are marked as well. Server warnings are limited to once per operation per process.
|
|
13
|
+
* Added Python `DeprecationWarning` notices for the upcoming sync-client transport change and
|
|
14
|
+
explicit `cache_time` use. The synchronous client still returns `requests.Response`, and
|
|
15
|
+
cached responses and existing deprecated operations retain their 2.x behaviour.
|
|
16
|
+
* Added `MIGRATING.md` covering the staged HTTP migration, transport compatibility, planned
|
|
17
|
+
installation split, cache removal and the required 3.0 record/replay and response sequences.
|
|
18
|
+
No 3.0 release date or automatic update check is introduced.
|
|
19
|
+
|
|
1
20
|
Version 2.12.0
|
|
2
21
|
==============
|
|
3
22
|
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Migrating from 2.x to 3.0
|
|
2
|
+
|
|
3
|
+
Version 2.13 is the compatibility preparation release. **3.0 is planned, not released.**
|
|
4
|
+
All existing 2.x routes, response bodies, status codes and client transports still work in
|
|
5
|
+
2.13. Record & replay, response sequences and the client/server installation split belong to
|
|
6
|
+
3.0; they are not available in 2.13.
|
|
7
|
+
|
|
8
|
+
## Stay on 2.x until you are ready
|
|
9
|
+
|
|
10
|
+
Pin `proxy_mock>=2.13,<3` in a test project that needs the existing contract. Upgrade the server
|
|
11
|
+
first, then the clients. A 2.13 client without `admin_prefix` still uses the legacy paths and
|
|
12
|
+
can talk to an older 2.x server. Keep `requests` behaviour until you have checked your callers.
|
|
13
|
+
Already installed old versions do not acquire warnings remotely: read release notes and update
|
|
14
|
+
to 2.13 to see these notices. There is no network update check or telemetry.
|
|
15
|
+
|
|
16
|
+
## Try the new administrative paths in 2.13
|
|
17
|
+
|
|
18
|
+
Aliases are **opt-in** so that an existing mock at `/__admin` is not unexpectedly shadowed.
|
|
19
|
+
Start the server with:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
PROXY_MOCK_ADMIN_PREFIX=/__admin proxy-mock --host 127.0.0.1 --port 5000
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Use the same prefix explicitly in either client:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from proxy_mock.client import ProxyMock, AsyncProxyMock
|
|
29
|
+
|
|
30
|
+
with ProxyMock("http://127.0.0.1:5000", admin_prefix="/__admin") as client:
|
|
31
|
+
client.configure_mock(path="/inventory", body={"available": True})
|
|
32
|
+
assert client.execute_request("GET", "/inventory").json()["available"]
|
|
33
|
+
|
|
34
|
+
# In async code:
|
|
35
|
+
# async with AsyncProxyMock("http://127.0.0.1:5000", admin_prefix="/__admin") as client:
|
|
36
|
+
# await client.get_storage()
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The prefix must be an absolute path of letters, digits, underscores, hyphens and optional
|
|
40
|
+
path segments, without a trailing slash. It must not overlap an existing service path such as
|
|
41
|
+
`/storage`, `/traffic`, `/docs` or their children. A prefix is not authentication. Reserve its
|
|
42
|
+
administrative paths for the server and choose a prefix that does not collide with your mocks.
|
|
43
|
+
Leaving the environment variable unset disables aliases; an explicitly empty value is invalid.
|
|
44
|
+
Generic `execute_request()` calls always use the route you provide, without rewriting mock URLs.
|
|
45
|
+
The pytest fixture continues using the legacy routes in 2.13; construct a client explicitly to
|
|
46
|
+
exercise the aliases.
|
|
47
|
+
|
|
48
|
+
| Legacy operation | 2.13 opt-in alias / planned 3.0 path |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| `GET /proxy_mock` | `GET /__admin` |
|
|
51
|
+
| `POST /configure_mock` | `POST /__admin/mocks` |
|
|
52
|
+
| `PATCH /configure_mock` | `PATCH /__admin/mocks` |
|
|
53
|
+
| `GET /storage` | `GET /__admin/mocks` |
|
|
54
|
+
| `DELETE /storage` | `DELETE /__admin/mocks` |
|
|
55
|
+
| `POST /storage/clean` | `DELETE /__admin/mocks` |
|
|
56
|
+
| `GET /traffic` | `GET /__admin/traffic` |
|
|
57
|
+
| `DELETE /traffic`, `POST /traffic/clean` | `DELETE /__admin/traffic` |
|
|
58
|
+
| `GET /traffic/settings` | `GET /__admin/settings` |
|
|
59
|
+
| `PATCH /traffic/settings`, `POST /traffic/settings` | `PATCH /__admin/settings` |
|
|
60
|
+
| `GET /storage/snapshot` | `GET /__admin/snapshot` |
|
|
61
|
+
| `POST /storage/snapshot` | `POST /__admin/snapshot` |
|
|
62
|
+
| `POST /cache/clean` | No replacement; removed with caching in 3.0 |
|
|
63
|
+
|
|
64
|
+
In 2.13 aliases keep the existing JSON/msgpack bodies, query parameters and status codes.
|
|
65
|
+
`?path=...`, traffic filters and snapshot `?mode=merge|replace` work unchanged. The full RESTful
|
|
66
|
+
review for 3.0 may refine contracts; check the final 3.0 migration notes before upgrading.
|
|
67
|
+
Old paths remain available even when aliases are enabled. A 3.0 server will default to
|
|
68
|
+
`/__admin` and remove the legacy paths.
|
|
69
|
+
|
|
70
|
+
`clean_storage(path=...)` still calls its legacy endpoint, even with `admin_prefix`, because
|
|
71
|
+
it returns `200` with `success: false` for a missing mock. Switch to `delete_mock(path)`, which
|
|
72
|
+
returns `404` when the mock is missing. `clean_cache()` also stays on its legacy path in 2.13.
|
|
73
|
+
|
|
74
|
+
## Understand the warnings
|
|
75
|
+
|
|
76
|
+
Every matched legacy service operation returns `Deprecation: @1789603200` (the announcement
|
|
77
|
+
date, 2026-09-17 UTC) and a `Link` with `rel="deprecation"` pointing to this guide. The date
|
|
78
|
+
syntax follows [RFC 9745](https://www.rfc-editor.org/rfc/rfc9745.html); it replaces the old
|
|
79
|
+
non-standard `Deprecation: true` value. When aliases are enabled, another link with
|
|
80
|
+
`rel="successor-version"` points to the replacement resource. Consult the table for the HTTP
|
|
81
|
+
method: a URI alone does not tell you to change `POST` to `DELETE` or `PATCH`.
|
|
82
|
+
The same notices accompany handled error responses. User mock responses are not marked.
|
|
83
|
+
Aliases themselves have no route-deprecation header. Server warnings are logged once per
|
|
84
|
+
operation per process, rather than for every request. No `Sunset` date is promised yet.
|
|
85
|
+
|
|
86
|
+
Python emits `DeprecationWarning` for the synchronous client's upcoming transport change,
|
|
87
|
+
`cache_time`, `clean_cache()` and `clean_storage(path=...)`. Python's standard warning filters
|
|
88
|
+
control repetition and visibility; use `python -W default::DeprecationWarning -m pytest` to
|
|
89
|
+
review them. Warnings do not change successful responses or retries. Projects treating warnings
|
|
90
|
+
as errors should review these notices before enabling that policy for a dependency upgrade.
|
|
91
|
+
|
|
92
|
+
## Synchronous client transport in 3.0
|
|
93
|
+
|
|
94
|
+
In 2.13, `ProxyMock` still uses `requests.Session` and returns `requests.Response` from
|
|
95
|
+
`execute_request()`. Async transport remains `httpx2`. In 3.0 both clients will use `httpx2`.
|
|
96
|
+
Before migrating, audit code that relies on:
|
|
97
|
+
|
|
98
|
+
- The concrete `requests.Response` type, `.ok`, response truth testing, or `isinstance` checks.
|
|
99
|
+
Prefer explicit `status_code` checks now, with the exact success range your test requires.
|
|
100
|
+
- `requests` exceptions. The current sync transport wraps connection/timeouts in
|
|
101
|
+
`ProxyMockRequestError`; other transport exceptions can still come from `requests`.
|
|
102
|
+
The 3.0 exception contract must be documented and tested before release.
|
|
103
|
+
- Request keyword arguments, body encoding, redirects, timeouts, streaming, cookies and
|
|
104
|
+
session customisation (including adapters and direct `.session` access). Do not assume
|
|
105
|
+
requests-specific arguments or defaults transfer unchanged to `httpx2`.
|
|
106
|
+
|
|
107
|
+
Exact 3.0 transport defaults and exception mappings are not implemented by 2.13. They must be
|
|
108
|
+
specified in this guide when 3.0 ships, with executable before/after examples.
|
|
109
|
+
|
|
110
|
+
## Installation and startup in 3.0
|
|
111
|
+
|
|
112
|
+
The planned base install, `proxy_mock`, contains only clients; use `proxy_mock[server]` when
|
|
113
|
+
running the server, CLI or the pytest fixture that starts a local server. **Do not use this
|
|
114
|
+
extra as a migration step in 2.13: that release still installs the server by default.**
|
|
115
|
+
Switch existing uvicorn entry points from `proxy_mock.any_catcher:app` to the supported factory
|
|
116
|
+
`uvicorn proxy_mock.app:create_app --factory`, or use the `proxy-mock` console command already
|
|
117
|
+
available in 2.x. Only one worker is supported because storage is in process memory.
|
|
118
|
+
|
|
119
|
+
## Response caching and new 3.0 features
|
|
120
|
+
|
|
121
|
+
`cache_time`, `clean_cache()` and `POST /cache/clean` will be removed. Remove caching from static
|
|
122
|
+
mocks now; their configured response already remains available until the mock is changed or
|
|
123
|
+
removed. For cached upstream responses, stay on 2.x until record & replay is available, then
|
|
124
|
+
explicitly record a response as a mock and replay it. There is no record & replay API in 2.13.
|
|
125
|
+
|
|
126
|
+
Record & replay and ordered response sequences are required for 3.0. Their concrete API,
|
|
127
|
+
sequence exhaustion/reset behaviour and examples will be documented with implementation.
|
|
128
|
+
gRPC and an authentication token are outside the 3.0 scope.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: proxy_mock
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.13.0
|
|
4
4
|
Summary: Tool for proxying and mocking HTTP traffic
|
|
5
5
|
Project-URL: Homepage, https://github.com/ivi-ru/proxy_mock
|
|
6
6
|
Project-URL: Repository, https://github.com/ivi-ru/proxy_mock
|
|
@@ -40,7 +40,11 @@ Description-Content-Type: text/markdown
|
|
|
40
40
|
**Proxy Mock** is a tool that combines a proxy server and a mock server.
|
|
41
41
|
It suits automated tests, integration scenarios and local debugging of service-to-service calls.
|
|
42
42
|
|
|
43
|
-
[Roadmap](ROADMAP.md) · [Contributing](CONTRIBUTING.md) · [Changelog](CHANGELOG.md) · [Security policy](SECURITY.md)
|
|
43
|
+
[Migration to 3.0](MIGRATING.md) · [Roadmap](ROADMAP.md) · [Contributing](CONTRIBUTING.md) · [Changelog](CHANGELOG.md) · [Security policy](SECURITY.md)
|
|
44
|
+
|
|
45
|
+
> **Preparing for 3.0:** 2.13 preserves the 2.x API and adds migration warnings and opt-in
|
|
46
|
+
> administrative aliases. Pin `proxy_mock>=2.13,<3` to retain compatibility while upgrading.
|
|
47
|
+
> See [Migrating from 2.x to 3.0](MIGRATING.md) for routes, client transport and installation.
|
|
44
48
|
|
|
45
49
|
## Quick start
|
|
46
50
|
|
|
@@ -288,6 +292,7 @@ Binary bodies cannot be written as JSON, so they travel base64-encoded in `body_
|
|
|
288
292
|
|
|
289
293
|
| Variable | Values | Description |
|
|
290
294
|
|----------|--------|-------------|
|
|
295
|
+
| `PROXY_MOCK_ADMIN_PREFIX` | unset (aliases disabled in 2.13); e.g. `/__admin` | Opt-in administrative resource paths; old paths keep working. See [migration guide](MIGRATING.md) |
|
|
291
296
|
| `PROXY_MOCK_LOG_REQUESTS` | `full` (default), `minimal`, `off` | Logging level for incoming requests |
|
|
292
297
|
| `PROXY_MOCK_TRAFFIC_MAX` | integer > 0 (default `1000`) | Maximum number of records in the in-memory traffic store. Changeable at runtime via `PATCH /traffic/settings` |
|
|
293
298
|
| `PROXY_MOCK_PROXY_TIMEOUT` | float, seconds (default `30`) | Timeout for outgoing proxied requests |
|
|
@@ -323,8 +328,10 @@ A short reference and the key examples follow.
|
|
|
323
328
|
| `GET` | `/traffic/settings` | Current traffic recording settings | `200` — `{"success": true, "data": {"record_unknown_traffic": true, "max_items": 1000}}` |
|
|
324
329
|
| `PATCH` | `/traffic/settings` | Change traffic settings. Body: `{"record_unknown_traffic": bool}` and/or `{"max_items": int > 0}`; the update is partial | `200` — same shape as GET; `400` on malformed JSON, `422` on an invalid or empty body |
|
|
325
330
|
|
|
326
|
-
**
|
|
327
|
-
`Deprecation
|
|
331
|
+
**All legacy administrative paths move in 3.0.** In 2.13 they keep their response bodies and
|
|
332
|
+
status codes and return a `Deprecation` date header and a migration `Link`. The following
|
|
333
|
+
action-style operations also have replacements already available in 2.x. See
|
|
334
|
+
[the migration guide](MIGRATING.md) for opt-in `/__admin` aliases:
|
|
328
335
|
|
|
329
336
|
| Method | URL | Replacement |
|
|
330
337
|
|--------|-----|-------------|
|
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
**Proxy Mock** is a tool that combines a proxy server and a mock server.
|
|
9
9
|
It suits automated tests, integration scenarios and local debugging of service-to-service calls.
|
|
10
10
|
|
|
11
|
-
[Roadmap](ROADMAP.md) · [Contributing](CONTRIBUTING.md) · [Changelog](CHANGELOG.md) · [Security policy](SECURITY.md)
|
|
11
|
+
[Migration to 3.0](MIGRATING.md) · [Roadmap](ROADMAP.md) · [Contributing](CONTRIBUTING.md) · [Changelog](CHANGELOG.md) · [Security policy](SECURITY.md)
|
|
12
|
+
|
|
13
|
+
> **Preparing for 3.0:** 2.13 preserves the 2.x API and adds migration warnings and opt-in
|
|
14
|
+
> administrative aliases. Pin `proxy_mock>=2.13,<3` to retain compatibility while upgrading.
|
|
15
|
+
> See [Migrating from 2.x to 3.0](MIGRATING.md) for routes, client transport and installation.
|
|
12
16
|
|
|
13
17
|
## Quick start
|
|
14
18
|
|
|
@@ -256,6 +260,7 @@ Binary bodies cannot be written as JSON, so they travel base64-encoded in `body_
|
|
|
256
260
|
|
|
257
261
|
| Variable | Values | Description |
|
|
258
262
|
|----------|--------|-------------|
|
|
263
|
+
| `PROXY_MOCK_ADMIN_PREFIX` | unset (aliases disabled in 2.13); e.g. `/__admin` | Opt-in administrative resource paths; old paths keep working. See [migration guide](MIGRATING.md) |
|
|
259
264
|
| `PROXY_MOCK_LOG_REQUESTS` | `full` (default), `minimal`, `off` | Logging level for incoming requests |
|
|
260
265
|
| `PROXY_MOCK_TRAFFIC_MAX` | integer > 0 (default `1000`) | Maximum number of records in the in-memory traffic store. Changeable at runtime via `PATCH /traffic/settings` |
|
|
261
266
|
| `PROXY_MOCK_PROXY_TIMEOUT` | float, seconds (default `30`) | Timeout for outgoing proxied requests |
|
|
@@ -291,8 +296,10 @@ A short reference and the key examples follow.
|
|
|
291
296
|
| `GET` | `/traffic/settings` | Current traffic recording settings | `200` — `{"success": true, "data": {"record_unknown_traffic": true, "max_items": 1000}}` |
|
|
292
297
|
| `PATCH` | `/traffic/settings` | Change traffic settings. Body: `{"record_unknown_traffic": bool}` and/or `{"max_items": int > 0}`; the update is partial | `200` — same shape as GET; `400` on malformed JSON, `422` on an invalid or empty body |
|
|
293
298
|
|
|
294
|
-
**
|
|
295
|
-
`Deprecation
|
|
299
|
+
**All legacy administrative paths move in 3.0.** In 2.13 they keep their response bodies and
|
|
300
|
+
status codes and return a `Deprecation` date header and a migration `Link`. The following
|
|
301
|
+
action-style operations also have replacements already available in 2.x. See
|
|
302
|
+
[the migration guide](MIGRATING.md) for opt-in `/__admin` aliases:
|
|
296
303
|
|
|
297
304
|
| Method | URL | Replacement |
|
|
298
305
|
|--------|-----|-------------|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Opt-in 3.0 resource paths, retaining the existing 2.x request/response contracts."""
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter
|
|
4
|
+
|
|
5
|
+
from proxy_mock.api.routes import configure, service, storage, traffic
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def create_admin_router(prefix: str) -> APIRouter:
|
|
9
|
+
router = APIRouter()
|
|
10
|
+
# Keep the full paths on the original router: runtime mock cleanup must never remove them.
|
|
11
|
+
for suffix, method, endpoint in [
|
|
12
|
+
("", "GET", service.get_proxy_mock),
|
|
13
|
+
("/mocks", "POST", configure.configure_mock),
|
|
14
|
+
("/mocks", "PATCH", configure.patch_mock_configuration),
|
|
15
|
+
("/mocks", "GET", storage.get_storage),
|
|
16
|
+
("/mocks", "DELETE", storage.delete_storage),
|
|
17
|
+
("/traffic", "GET", traffic.get_traffic),
|
|
18
|
+
("/traffic", "DELETE", traffic.delete_traffic),
|
|
19
|
+
("/settings", "GET", traffic.get_traffic_settings),
|
|
20
|
+
("/settings", "PATCH", traffic.patch_traffic_settings),
|
|
21
|
+
("/snapshot", "GET", storage.get_storage_snapshot),
|
|
22
|
+
("/snapshot", "POST", storage.post_storage_snapshot),
|
|
23
|
+
]:
|
|
24
|
+
router.add_api_route(prefix + suffix, endpoint, methods=[method], name="admin_" + endpoint.__name__)
|
|
25
|
+
return router
|
|
@@ -5,12 +5,13 @@ from fastapi import APIRouter, Request
|
|
|
5
5
|
from fastapi.responses import JSONResponse
|
|
6
6
|
from pydantic import ValidationError
|
|
7
7
|
|
|
8
|
+
from proxy_mock.core.deprecation import LegacyAdminRoute
|
|
8
9
|
from proxy_mock.domain.constants import ParseError
|
|
9
10
|
from proxy_mock.domain.models import ConfigureMockRequestSchema
|
|
10
11
|
from proxy_mock.services.mock_service import mock_initialization, patch_mock_data, return_mock_data
|
|
11
12
|
from proxy_mock.services.request_parser import parse_configure
|
|
12
13
|
|
|
13
|
-
router = APIRouter()
|
|
14
|
+
router = APIRouter(route_class=LegacyAdminRoute, deprecated=True)
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
@router.post("/configure_mock")
|
|
@@ -3,9 +3,10 @@ import platform
|
|
|
3
3
|
from fastapi import APIRouter, Request
|
|
4
4
|
from fastapi.responses import JSONResponse
|
|
5
5
|
|
|
6
|
+
from proxy_mock.core.deprecation import LegacyAdminRoute
|
|
6
7
|
from proxy_mock.services.mock_service import count_mocks
|
|
7
8
|
|
|
8
|
-
router = APIRouter()
|
|
9
|
+
router = APIRouter(route_class=LegacyAdminRoute, deprecated=True)
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
@router.get("/proxy_mock")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from fastapi import APIRouter, Request
|
|
2
2
|
from fastapi.responses import JSONResponse
|
|
3
3
|
|
|
4
|
-
from proxy_mock.core.deprecation import
|
|
4
|
+
from proxy_mock.core.deprecation import LegacyAdminRoute
|
|
5
5
|
from proxy_mock.core.serializers import convert_bytes_to_str
|
|
6
6
|
from proxy_mock.services.mock_service import (
|
|
7
7
|
cleanup_storage,
|
|
@@ -12,7 +12,7 @@ from proxy_mock.services.mock_service import (
|
|
|
12
12
|
)
|
|
13
13
|
from proxy_mock.services.snapshot import SnapshotError, export_snapshot, import_snapshot
|
|
14
14
|
|
|
15
|
-
router = APIRouter()
|
|
15
|
+
router = APIRouter(route_class=LegacyAdminRoute, deprecated=True)
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
@router.get("/storage")
|
|
@@ -85,4 +85,4 @@ async def clean_storage(request: Request):
|
|
|
85
85
|
result = await cleanup_storage(request.app)
|
|
86
86
|
|
|
87
87
|
response = JSONResponse({"success": result, "data": convert_bytes_to_str(await return_storage())}, 200)
|
|
88
|
-
return
|
|
88
|
+
return response
|
|
@@ -4,10 +4,10 @@ from fastapi import APIRouter, Request
|
|
|
4
4
|
from fastapi.responses import JSONResponse
|
|
5
5
|
from pydantic import ValidationError
|
|
6
6
|
|
|
7
|
-
from proxy_mock.core.deprecation import
|
|
7
|
+
from proxy_mock.core.deprecation import LegacyAdminRoute
|
|
8
8
|
from proxy_mock.domain.models import TrafficSettingsSchema
|
|
9
9
|
|
|
10
|
-
router = APIRouter()
|
|
10
|
+
router = APIRouter(route_class=LegacyAdminRoute, deprecated=True)
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def _traffic_settings(request: Request) -> dict:
|
|
@@ -39,7 +39,7 @@ async def clean_traffic(request: Request):
|
|
|
39
39
|
"""Deprecated alias for clearing traffic. The RESTful variant is `DELETE /traffic`."""
|
|
40
40
|
await request.app.state.traffic_store.clear()
|
|
41
41
|
response = JSONResponse({"success": True, "data": []}, 200)
|
|
42
|
-
return
|
|
42
|
+
return response
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
@router.get("/traffic/settings")
|
|
@@ -60,7 +60,7 @@ async def patch_traffic_settings(request: Request):
|
|
|
60
60
|
async def set_traffic_settings(request: Request):
|
|
61
61
|
"""Deprecated alias for updating the settings. The RESTful variant is `PATCH /traffic/settings`."""
|
|
62
62
|
response = await _update_traffic_settings(request)
|
|
63
|
-
return
|
|
63
|
+
return response
|
|
64
64
|
|
|
65
65
|
|
|
66
66
|
async def _update_traffic_settings(request: Request) -> JSONResponse:
|
|
@@ -95,4 +95,4 @@ async def _update_traffic_settings(request: Request) -> JSONResponse:
|
|
|
95
95
|
async def clean_cache(request: Request):
|
|
96
96
|
"""Deprecated: response caching is removed in 3.0 together with this endpoint."""
|
|
97
97
|
await request.app.state.cache.clear()
|
|
98
|
-
return
|
|
98
|
+
return JSONResponse({"success": True}, 200)
|
|
@@ -5,10 +5,12 @@ import httpx2
|
|
|
5
5
|
from fastapi import FastAPI, Request
|
|
6
6
|
from fastapi.responses import JSONResponse
|
|
7
7
|
|
|
8
|
+
from proxy_mock.api.routes.admin import create_admin_router
|
|
8
9
|
from proxy_mock.api.routes.configure import router as configure_router
|
|
9
10
|
from proxy_mock.api.routes.service import router as service_router
|
|
10
11
|
from proxy_mock.api.routes.storage import router as storage_router
|
|
11
12
|
from proxy_mock.api.routes.traffic import router as traffic_router
|
|
13
|
+
from proxy_mock.client.migration import validate_admin_prefix
|
|
12
14
|
from proxy_mock.core.logging import app_logger
|
|
13
15
|
from proxy_mock.core.settings import get_version_from_pyproject, record_unknown_traffic_default
|
|
14
16
|
from proxy_mock.repositories.traffic_store import TrafficStore
|
|
@@ -70,6 +72,10 @@ def create_app() -> ProxyMockApp:
|
|
|
70
72
|
app = ProxyMockApp(title="Proxy Mock")
|
|
71
73
|
app.state.version = get_version_from_pyproject(app.logger)
|
|
72
74
|
|
|
75
|
+
app.state.admin_prefix = validate_admin_prefix(os.getenv("PROXY_MOCK_ADMIN_PREFIX"))
|
|
76
|
+
if app.state.admin_prefix is not None:
|
|
77
|
+
app.include_router(create_admin_router(app.state.admin_prefix))
|
|
78
|
+
|
|
73
79
|
app.include_router(service_router)
|
|
74
80
|
app.include_router(configure_router)
|
|
75
81
|
app.include_router(storage_router)
|
|
@@ -6,6 +6,7 @@ from urllib.parse import urlencode
|
|
|
6
6
|
import httpx2
|
|
7
7
|
import msgpack
|
|
8
8
|
|
|
9
|
+
from proxy_mock.client.migration import MIGRATION_URL, service_endpoint, validate_admin_prefix
|
|
9
10
|
from proxy_mock.client.service_endpoints import Endpoints
|
|
10
11
|
|
|
11
12
|
CONFIGURE_CONTENT_TYPE = "application/octet-stream"
|
|
@@ -30,11 +31,15 @@ class AsyncProxyMockResponseError(Exception):
|
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
class AsyncProxyMock:
|
|
33
|
-
def __init__(self, host: str, timeout: float = 10.0) -> None:
|
|
34
|
+
def __init__(self, host: str, timeout: float = 10.0, *, admin_prefix: str | None = None) -> None:
|
|
35
|
+
self.admin_prefix = validate_admin_prefix(admin_prefix)
|
|
34
36
|
self.host = host.rstrip("/")
|
|
35
37
|
self.timeout = timeout
|
|
36
38
|
self._client = httpx2.AsyncClient(base_url=self.host, timeout=timeout)
|
|
37
39
|
|
|
40
|
+
def _service_endpoint(self, endpoint: str) -> str:
|
|
41
|
+
return service_endpoint(endpoint, self.admin_prefix)
|
|
42
|
+
|
|
38
43
|
async def aclose(self) -> None:
|
|
39
44
|
await self._client.aclose()
|
|
40
45
|
|
|
@@ -122,6 +127,11 @@ class AsyncProxyMock:
|
|
|
122
127
|
if rules is not None:
|
|
123
128
|
request_data["rules"] = rules
|
|
124
129
|
if cache_time is not None:
|
|
130
|
+
warnings.warn(
|
|
131
|
+
f"cache_time is deprecated and will be removed in 3.0; see {MIGRATION_URL}",
|
|
132
|
+
DeprecationWarning,
|
|
133
|
+
stacklevel=3,
|
|
134
|
+
)
|
|
125
135
|
request_data["cache_time"] = cache_time
|
|
126
136
|
|
|
127
137
|
request_data.update(kwargs)
|
|
@@ -130,13 +140,15 @@ class AsyncProxyMock:
|
|
|
130
140
|
async def _send_configure(self, method: HTTPMethod, payload: dict):
|
|
131
141
|
return await self.execute_request_and_get_response_body(
|
|
132
142
|
method=method,
|
|
133
|
-
route=Endpoints.CONFIGURE_MOCK,
|
|
143
|
+
route=self._service_endpoint(Endpoints.CONFIGURE_MOCK),
|
|
134
144
|
content=msgpack.packb(payload),
|
|
135
145
|
headers={"Content-Type": CONFIGURE_CONTENT_TYPE},
|
|
136
146
|
)
|
|
137
147
|
|
|
138
148
|
async def get_proxy_mock(self):
|
|
139
|
-
return await self.execute_request_and_get_response_body(
|
|
149
|
+
return await self.execute_request_and_get_response_body(
|
|
150
|
+
HTTPMethod.GET, self._service_endpoint(Endpoints.PROXY_MOCK)
|
|
151
|
+
)
|
|
140
152
|
|
|
141
153
|
async def configure_mock(
|
|
142
154
|
self,
|
|
@@ -204,12 +216,12 @@ class AsyncProxyMock:
|
|
|
204
216
|
**({"method": method} if method else {}),
|
|
205
217
|
**({"limit": limit} if limit is not None else {}),
|
|
206
218
|
}
|
|
207
|
-
full_path = f"{Endpoints.TRAFFIC}?{urlencode(query_params)}"
|
|
219
|
+
full_path = f"{self._service_endpoint(Endpoints.TRAFFIC)}?{urlencode(query_params)}"
|
|
208
220
|
return await self.execute_request_and_get_response_body(HTTPMethod.GET, full_path)
|
|
209
221
|
|
|
210
222
|
async def get_storage(self, path: str | None = None):
|
|
211
223
|
query_params = {**({"path": path} if path else {})}
|
|
212
|
-
full_path = f"{Endpoints.STORAGE}?{urlencode(query_params)}"
|
|
224
|
+
full_path = f"{self._service_endpoint(Endpoints.STORAGE)}?{urlencode(query_params)}"
|
|
213
225
|
return await self.execute_request_and_get_response_body(HTTPMethod.GET, full_path)
|
|
214
226
|
|
|
215
227
|
async def clean_storage(self, path: str | None = None):
|
|
@@ -224,34 +236,42 @@ class AsyncProxyMock:
|
|
|
224
236
|
DeprecationWarning,
|
|
225
237
|
stacklevel=2,
|
|
226
238
|
)
|
|
227
|
-
full_path = f"{Endpoints.STORAGE_CLEAN}?{urlencode({'path': path})}"
|
|
239
|
+
full_path = f"{self._service_endpoint(Endpoints.STORAGE_CLEAN)}?{urlencode({'path': path})}"
|
|
228
240
|
return await self.execute_request_and_get_response_body(HTTPMethod.POST, full_path)
|
|
229
241
|
|
|
230
|
-
return await self.execute_request_and_get_response_body(
|
|
242
|
+
return await self.execute_request_and_get_response_body(
|
|
243
|
+
HTTPMethod.DELETE, self._service_endpoint(Endpoints.STORAGE)
|
|
244
|
+
)
|
|
231
245
|
|
|
232
246
|
async def export_mocks(self):
|
|
233
247
|
"""Export every configured mock as a snapshot document."""
|
|
234
|
-
return await self.execute_request_and_get_response_body(
|
|
248
|
+
return await self.execute_request_and_get_response_body(
|
|
249
|
+
HTTPMethod.GET, self._service_endpoint(Endpoints.STORAGE_SNAPSHOT)
|
|
250
|
+
)
|
|
235
251
|
|
|
236
252
|
async def import_mocks(self, snapshot: dict, mode: str = "merge"):
|
|
237
253
|
"""Load a snapshot: `merge` keeps the configured mocks, `replace` clears them first."""
|
|
238
|
-
full_path = f"{Endpoints.STORAGE_SNAPSHOT}?{urlencode({'mode': mode})}"
|
|
254
|
+
full_path = f"{self._service_endpoint(Endpoints.STORAGE_SNAPSHOT)}?{urlencode({'mode': mode})}"
|
|
239
255
|
return await self.execute_request_and_get_response_body(HTTPMethod.POST, full_path, json=snapshot)
|
|
240
256
|
|
|
241
257
|
async def delete_mock(self, path: str):
|
|
242
|
-
full_path = f"{Endpoints.STORAGE}?{urlencode({'path': path})}"
|
|
258
|
+
full_path = f"{self._service_endpoint(Endpoints.STORAGE)}?{urlencode({'path': path})}"
|
|
243
259
|
return await self.execute_request_and_get_response_body(HTTPMethod.DELETE, full_path)
|
|
244
260
|
|
|
245
261
|
async def clean_traffic(self):
|
|
246
|
-
return await self.execute_request_and_get_response_body(
|
|
262
|
+
return await self.execute_request_and_get_response_body(
|
|
263
|
+
HTTPMethod.DELETE, self._service_endpoint(Endpoints.TRAFFIC)
|
|
264
|
+
)
|
|
247
265
|
|
|
248
266
|
async def get_traffic_settings(self):
|
|
249
|
-
return await self.execute_request_and_get_response_body(
|
|
267
|
+
return await self.execute_request_and_get_response_body(
|
|
268
|
+
HTTPMethod.GET, self._service_endpoint(Endpoints.TRAFFIC_SETTINGS)
|
|
269
|
+
)
|
|
250
270
|
|
|
251
271
|
async def set_traffic_settings(self, record_unknown_traffic: bool | None = None, max_items: int | None = None):
|
|
252
272
|
return await self.execute_request_and_get_response_body(
|
|
253
273
|
HTTPMethod.PATCH,
|
|
254
|
-
Endpoints.TRAFFIC_SETTINGS,
|
|
274
|
+
self._service_endpoint(Endpoints.TRAFFIC_SETTINGS),
|
|
255
275
|
json=_build_traffic_settings_payload(record_unknown_traffic, max_items),
|
|
256
276
|
)
|
|
257
277
|
|
|
@@ -262,4 +282,6 @@ class AsyncProxyMock:
|
|
|
262
282
|
DeprecationWarning,
|
|
263
283
|
stacklevel=2,
|
|
264
284
|
)
|
|
265
|
-
return await self.execute_request_and_get_response_body(
|
|
285
|
+
return await self.execute_request_and_get_response_body(
|
|
286
|
+
HTTPMethod.POST, self._service_endpoint(Endpoints.CACHE_CLEAN)
|
|
287
|
+
)
|
|
@@ -5,6 +5,7 @@ from urllib.parse import urlencode
|
|
|
5
5
|
|
|
6
6
|
import msgpack
|
|
7
7
|
|
|
8
|
+
from proxy_mock.client.migration import MIGRATION_URL
|
|
8
9
|
from proxy_mock.client.route import Route
|
|
9
10
|
from proxy_mock.client.service_endpoints import Endpoints
|
|
10
11
|
|
|
@@ -23,7 +24,9 @@ def _build_traffic_settings_payload(record_unknown_traffic: bool | None, max_ite
|
|
|
23
24
|
|
|
24
25
|
class ProxyMock(Route):
|
|
25
26
|
def get_proxy_mock(self):
|
|
26
|
-
return super().execute_request_and_get_response_body(
|
|
27
|
+
return super().execute_request_and_get_response_body(
|
|
28
|
+
HTTPMethod.GET, self._service_endpoint(Endpoints.PROXY_MOCK)
|
|
29
|
+
)
|
|
27
30
|
|
|
28
31
|
def _build_mock_request_data(
|
|
29
32
|
self,
|
|
@@ -62,6 +65,11 @@ class ProxyMock(Route):
|
|
|
62
65
|
if rules is not None:
|
|
63
66
|
request_data["rules"] = rules
|
|
64
67
|
if cache_time is not None:
|
|
68
|
+
warnings.warn(
|
|
69
|
+
f"cache_time is deprecated and will be removed in 3.0; see {MIGRATION_URL}",
|
|
70
|
+
DeprecationWarning,
|
|
71
|
+
stacklevel=3,
|
|
72
|
+
)
|
|
65
73
|
request_data["cache_time"] = cache_time
|
|
66
74
|
|
|
67
75
|
request_data.update(kwargs)
|
|
@@ -70,7 +78,7 @@ class ProxyMock(Route):
|
|
|
70
78
|
def _send_configure(self, method: HTTPMethod, payload: dict):
|
|
71
79
|
return super().execute_request_and_get_response_body(
|
|
72
80
|
method=method,
|
|
73
|
-
route=Endpoints.CONFIGURE_MOCK,
|
|
81
|
+
route=self._service_endpoint(Endpoints.CONFIGURE_MOCK),
|
|
74
82
|
data=msgpack.packb(payload),
|
|
75
83
|
headers={"Content-Type": CONFIGURE_CONTENT_TYPE},
|
|
76
84
|
)
|
|
@@ -141,12 +149,12 @@ class ProxyMock(Route):
|
|
|
141
149
|
**({"method": method} if method else {}),
|
|
142
150
|
**({"limit": limit} if limit is not None else {}),
|
|
143
151
|
}
|
|
144
|
-
full_path = f"{Endpoints.TRAFFIC}?{urlencode(query_params)}"
|
|
152
|
+
full_path = f"{self._service_endpoint(Endpoints.TRAFFIC)}?{urlencode(query_params)}"
|
|
145
153
|
return super().execute_request_and_get_response_body(HTTPMethod.GET, full_path)
|
|
146
154
|
|
|
147
155
|
def get_storage(self, path: str | None = None):
|
|
148
156
|
query_params = {**({"path": path} if path else {})}
|
|
149
|
-
full_path = f"{Endpoints.STORAGE}?{urlencode(query_params)}"
|
|
157
|
+
full_path = f"{self._service_endpoint(Endpoints.STORAGE)}?{urlencode(query_params)}"
|
|
150
158
|
return super().execute_request_and_get_response_body(HTTPMethod.GET, full_path)
|
|
151
159
|
|
|
152
160
|
def clean_storage(self, path: str | None = None):
|
|
@@ -161,34 +169,42 @@ class ProxyMock(Route):
|
|
|
161
169
|
DeprecationWarning,
|
|
162
170
|
stacklevel=2,
|
|
163
171
|
)
|
|
164
|
-
full_path = f"{Endpoints.STORAGE_CLEAN}?{urlencode({'path': path})}"
|
|
172
|
+
full_path = f"{self._service_endpoint(Endpoints.STORAGE_CLEAN)}?{urlencode({'path': path})}"
|
|
165
173
|
return super().execute_request_and_get_response_body(HTTPMethod.POST, full_path)
|
|
166
174
|
|
|
167
|
-
return super().execute_request_and_get_response_body(
|
|
175
|
+
return super().execute_request_and_get_response_body(
|
|
176
|
+
HTTPMethod.DELETE, self._service_endpoint(Endpoints.STORAGE)
|
|
177
|
+
)
|
|
168
178
|
|
|
169
179
|
def export_mocks(self):
|
|
170
180
|
"""Export every configured mock as a snapshot document."""
|
|
171
|
-
return super().execute_request_and_get_response_body(
|
|
181
|
+
return super().execute_request_and_get_response_body(
|
|
182
|
+
HTTPMethod.GET, self._service_endpoint(Endpoints.STORAGE_SNAPSHOT)
|
|
183
|
+
)
|
|
172
184
|
|
|
173
185
|
def import_mocks(self, snapshot: dict, mode: str = "merge"):
|
|
174
186
|
"""Load a snapshot: `merge` keeps the configured mocks, `replace` clears them first."""
|
|
175
|
-
full_path = f"{Endpoints.STORAGE_SNAPSHOT}?{urlencode({'mode': mode})}"
|
|
187
|
+
full_path = f"{self._service_endpoint(Endpoints.STORAGE_SNAPSHOT)}?{urlencode({'mode': mode})}"
|
|
176
188
|
return super().execute_request_and_get_response_body(HTTPMethod.POST, full_path, json=snapshot)
|
|
177
189
|
|
|
178
190
|
def delete_mock(self, path: str):
|
|
179
|
-
full_path = f"{Endpoints.STORAGE}?{urlencode({'path': path})}"
|
|
191
|
+
full_path = f"{self._service_endpoint(Endpoints.STORAGE)}?{urlencode({'path': path})}"
|
|
180
192
|
return super().execute_request_and_get_response_body(HTTPMethod.DELETE, full_path)
|
|
181
193
|
|
|
182
194
|
def clean_traffic(self):
|
|
183
|
-
return super().execute_request_and_get_response_body(
|
|
195
|
+
return super().execute_request_and_get_response_body(
|
|
196
|
+
HTTPMethod.DELETE, self._service_endpoint(Endpoints.TRAFFIC)
|
|
197
|
+
)
|
|
184
198
|
|
|
185
199
|
def get_traffic_settings(self):
|
|
186
|
-
return super().execute_request_and_get_response_body(
|
|
200
|
+
return super().execute_request_and_get_response_body(
|
|
201
|
+
HTTPMethod.GET, self._service_endpoint(Endpoints.TRAFFIC_SETTINGS)
|
|
202
|
+
)
|
|
187
203
|
|
|
188
204
|
def set_traffic_settings(self, record_unknown_traffic: bool | None = None, max_items: int | None = None):
|
|
189
205
|
return super().execute_request_and_get_response_body(
|
|
190
206
|
HTTPMethod.PATCH,
|
|
191
|
-
Endpoints.TRAFFIC_SETTINGS,
|
|
207
|
+
self._service_endpoint(Endpoints.TRAFFIC_SETTINGS),
|
|
192
208
|
json=_build_traffic_settings_payload(record_unknown_traffic, max_items),
|
|
193
209
|
)
|
|
194
210
|
|
|
@@ -199,4 +215,6 @@ class ProxyMock(Route):
|
|
|
199
215
|
DeprecationWarning,
|
|
200
216
|
stacklevel=2,
|
|
201
217
|
)
|
|
202
|
-
return super().execute_request_and_get_response_body(
|
|
218
|
+
return super().execute_request_and_get_response_body(
|
|
219
|
+
HTTPMethod.POST, self._service_endpoint(Endpoints.CACHE_CLEAN)
|
|
220
|
+
)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Migration helpers shared by the clients without importing server dependencies."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
MIGRATION_URL = "https://github.com/ivi-ru/proxy_mock/blob/main/MIGRATING.md"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def validate_admin_prefix(prefix: str | None) -> str | None:
|
|
9
|
+
if prefix is None:
|
|
10
|
+
return None
|
|
11
|
+
if not re.fullmatch(r"/[A-Za-z0-9_-]+(?:/[A-Za-z0-9_-]+)*", prefix):
|
|
12
|
+
raise ValueError("admin_prefix must be an absolute path without trailing slash, query, or URL escapes")
|
|
13
|
+
# An alias must not replace a legacy service route or the built-in API documentation.
|
|
14
|
+
if prefix.split("/")[1] in {
|
|
15
|
+
"proxy_mock",
|
|
16
|
+
"configure_mock",
|
|
17
|
+
"storage",
|
|
18
|
+
"traffic",
|
|
19
|
+
"cache",
|
|
20
|
+
"docs",
|
|
21
|
+
"redoc",
|
|
22
|
+
"openapi.json",
|
|
23
|
+
}:
|
|
24
|
+
raise ValueError("admin_prefix overlaps an existing service path")
|
|
25
|
+
return prefix
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
ADMIN_SUFFIXES = {
|
|
29
|
+
"/proxy_mock": "",
|
|
30
|
+
"/configure_mock": "/mocks",
|
|
31
|
+
"/storage": "/mocks",
|
|
32
|
+
"/traffic": "/traffic",
|
|
33
|
+
"/traffic/settings": "/settings",
|
|
34
|
+
"/storage/snapshot": "/snapshot",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def service_endpoint(endpoint: str, prefix: str | None) -> str:
|
|
39
|
+
# Removed operations keep their legacy path and semantics throughout 2.x.
|
|
40
|
+
if prefix is not None and endpoint in ADMIN_SUFFIXES:
|
|
41
|
+
return prefix + ADMIN_SUFFIXES[endpoint]
|
|
42
|
+
return endpoint
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import warnings
|
|
2
3
|
from typing import Any
|
|
3
4
|
|
|
4
5
|
import requests
|
|
5
6
|
|
|
7
|
+
from proxy_mock.client.migration import MIGRATION_URL, service_endpoint, validate_admin_prefix
|
|
8
|
+
|
|
6
9
|
|
|
7
10
|
class ProxyMockRequestError(Exception):
|
|
8
11
|
"""Error while performing a request to the proxy-mock server."""
|
|
@@ -13,11 +16,21 @@ class ProxyMockResponseError(Exception):
|
|
|
13
16
|
|
|
14
17
|
|
|
15
18
|
class Route:
|
|
16
|
-
def __init__(self, host: str, timeout: float = 10.0) -> None:
|
|
19
|
+
def __init__(self, host: str, timeout: float = 10.0, *, admin_prefix: str | None = None) -> None:
|
|
20
|
+
self.admin_prefix = validate_admin_prefix(admin_prefix)
|
|
21
|
+
warnings.warn(
|
|
22
|
+
"ProxyMock will use httpx2 instead of requests in 3.0; response types, exceptions, "
|
|
23
|
+
f"and transport options will change. See {MIGRATION_URL}",
|
|
24
|
+
DeprecationWarning,
|
|
25
|
+
stacklevel=2,
|
|
26
|
+
)
|
|
17
27
|
self.host = host.rstrip("/")
|
|
18
28
|
self.timeout = timeout
|
|
19
29
|
self.session = requests.Session()
|
|
20
30
|
|
|
31
|
+
def _service_endpoint(self, endpoint: str) -> str:
|
|
32
|
+
return service_endpoint(endpoint, self.admin_prefix)
|
|
33
|
+
|
|
21
34
|
def _build_url(self, command: str) -> str:
|
|
22
35
|
command = command.strip("/")
|
|
23
36
|
return f"{self.host}/{command}"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Deprecation notices for the 3.0 migration (RFC 9745)."""
|
|
2
|
+
|
|
3
|
+
from fastapi import Request, Response
|
|
4
|
+
from fastapi.routing import APIRoute
|
|
5
|
+
|
|
6
|
+
from proxy_mock.client.migration import ADMIN_SUFFIXES, MIGRATION_URL
|
|
7
|
+
from proxy_mock.core.logging import app_logger
|
|
8
|
+
|
|
9
|
+
REMOVED_IN = "3.0"
|
|
10
|
+
# Announcement date: 2026-09-17 UTC. No removal date is promised by a Sunset header.
|
|
11
|
+
DEPRECATION_DATE = "@1789603200"
|
|
12
|
+
_warned: set[str] = set()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def mark_deprecated(response: Response, endpoint: str, replacement: str | None = None) -> Response:
|
|
16
|
+
response.headers["Deprecation"] = DEPRECATION_DATE
|
|
17
|
+
links = [f'<{MIGRATION_URL}>; rel="deprecation"']
|
|
18
|
+
if replacement:
|
|
19
|
+
links.append(f'<{replacement}>; rel="successor-version"')
|
|
20
|
+
existing = response.headers.get("Link")
|
|
21
|
+
response.headers["Link"] = ", ".join(([existing] if existing else []) + links)
|
|
22
|
+
if endpoint not in _warned:
|
|
23
|
+
_warned.add(endpoint)
|
|
24
|
+
successor = f"; replacement resource: {replacement}" if replacement else ""
|
|
25
|
+
app_logger.warning(f"{endpoint} is deprecated and will be removed in {REMOVED_IN}{successor}; {MIGRATION_URL}")
|
|
26
|
+
return response
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class LegacyAdminRoute(APIRoute):
|
|
30
|
+
"""Mark only a matched legacy service operation, never a user mock at the same path."""
|
|
31
|
+
|
|
32
|
+
def get_route_handler(self):
|
|
33
|
+
handler = super().get_route_handler()
|
|
34
|
+
|
|
35
|
+
async def deprecated_handler(request: Request) -> Response:
|
|
36
|
+
response = await handler(request)
|
|
37
|
+
prefix = request.app.state.admin_prefix
|
|
38
|
+
legacy_path = {"/storage/clean": "/storage", "/traffic/clean": "/traffic"}.get(self.path, self.path)
|
|
39
|
+
suffix = ADMIN_SUFFIXES.get(legacy_path)
|
|
40
|
+
replacement = prefix + suffix if prefix is not None and suffix is not None else None
|
|
41
|
+
return mark_deprecated(response, f"{request.method} {self.path}", replacement)
|
|
42
|
+
|
|
43
|
+
return deprecated_handler
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def warn_deprecated_field(field: str, detail: str) -> None:
|
|
47
|
+
"""Warn once per process about a deprecated field in the configuration payload."""
|
|
48
|
+
if field in _warned:
|
|
49
|
+
return
|
|
50
|
+
_warned.add(field)
|
|
51
|
+
app_logger.warning(f"Field '{field}' is deprecated and will be removed in {REMOVED_IN}: {detail}; {MIGRATION_URL}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "proxy_mock"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.13.0"
|
|
4
4
|
description = "Tool for proxying and mocking HTTP traffic"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Andrey Ryabokon", email = "aryabokon@ivi.ru" }
|
|
@@ -61,7 +61,7 @@ build-backend = "hatchling.build"
|
|
|
61
61
|
packages = ["proxy_mock"]
|
|
62
62
|
|
|
63
63
|
[tool.hatch.build.targets.sdist]
|
|
64
|
-
include = ["proxy_mock", "README.md", "CHANGELOG.md", "LICENSE", "pyproject.toml"]
|
|
64
|
+
include = ["proxy_mock", "README.md", "CHANGELOG.md", "MIGRATING.md", "LICENSE", "pyproject.toml"]
|
|
65
65
|
|
|
66
66
|
[tool.ruff]
|
|
67
67
|
line-length = 120
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
"""Marking of endpoints that 3.0 removes.
|
|
2
|
-
|
|
3
|
-
A deprecated endpoint keeps working unchanged; it only gains the RFC 8594 ``Deprecation``
|
|
4
|
-
header, a ``Link`` to its replacement and a one-off warning in the log, so that the removal in
|
|
5
|
-
3.0 does not come as a surprise.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from fastapi import Response
|
|
9
|
-
|
|
10
|
-
from proxy_mock.core.logging import app_logger
|
|
11
|
-
|
|
12
|
-
REMOVED_IN = "3.0"
|
|
13
|
-
|
|
14
|
-
_warned: set[str] = set()
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def mark_deprecated(response: Response, endpoint: str, replacement: str | None = None) -> Response:
|
|
18
|
-
response.headers["Deprecation"] = "true"
|
|
19
|
-
if replacement:
|
|
20
|
-
response.headers["Link"] = f'<{replacement}>; rel="successor-version"'
|
|
21
|
-
|
|
22
|
-
if endpoint not in _warned:
|
|
23
|
-
_warned.add(endpoint)
|
|
24
|
-
successor = f"; use {replacement} instead" if replacement else ""
|
|
25
|
-
app_logger.warning(f"{endpoint} is deprecated and will be removed in {REMOVED_IN}{successor}")
|
|
26
|
-
|
|
27
|
-
return response
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def warn_deprecated_field(field: str, detail: str) -> None:
|
|
31
|
-
"""Warn once per process about a deprecated field in the configuration payload."""
|
|
32
|
-
if field in _warned:
|
|
33
|
-
return
|
|
34
|
-
|
|
35
|
-
_warned.add(field)
|
|
36
|
-
app_logger.warning(f"Field '{field}' is deprecated and will be removed in {REMOVED_IN}: {detail}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|