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.
Files changed (57) hide show
  1. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/CHANGELOG.md +19 -0
  2. proxy_mock-2.13.0/MIGRATING.md +128 -0
  3. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/PKG-INFO +11 -4
  4. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/README.md +10 -3
  5. proxy_mock-2.13.0/proxy_mock/api/routes/admin.py +25 -0
  6. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/api/routes/configure.py +2 -1
  7. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/api/routes/service.py +2 -1
  8. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/api/routes/storage.py +3 -3
  9. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/api/routes/traffic.py +5 -5
  10. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/app/factory.py +6 -0
  11. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/client/async_client.py +36 -14
  12. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/client/client.py +31 -13
  13. proxy_mock-2.13.0/proxy_mock/client/migration.py +42 -0
  14. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/client/route.py +14 -1
  15. proxy_mock-2.13.0/proxy_mock/core/deprecation.py +51 -0
  16. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/pyproject.toml +2 -2
  17. proxy_mock-2.12.0/proxy_mock/core/deprecation.py +0 -36
  18. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/.gitignore +0 -0
  19. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/LICENSE +0 -0
  20. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/examples/README.md +0 -0
  21. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/examples/compose/README.md +0 -0
  22. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/__init__.py +0 -0
  23. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/__main__.py +0 -0
  24. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/any_catcher.py +0 -0
  25. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/api/__init__.py +0 -0
  26. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/api/deps.py +0 -0
  27. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/api/routes/__init__.py +0 -0
  28. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/api/schemas.py +0 -0
  29. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/app/__init__.py +0 -0
  30. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/app/state.py +0 -0
  31. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/cli.py +0 -0
  32. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/client/__init__.py +0 -0
  33. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/client/service_endpoints.py +0 -0
  34. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/core/__init__.py +0 -0
  35. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/core/logging.py +0 -0
  36. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/core/serializers.py +0 -0
  37. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/core/settings.py +0 -0
  38. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/core/time.py +0 -0
  39. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/core/urls.py +0 -0
  40. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/domain/__init__.py +0 -0
  41. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/domain/constants.py +0 -0
  42. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/domain/models.py +0 -0
  43. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/pytest_plugin.py +0 -0
  44. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/repositories/__init__.py +0 -0
  45. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/repositories/mock_storage.py +0 -0
  46. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/repositories/traffic_store.py +0 -0
  47. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/repositories/ttl_cache.py +0 -0
  48. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/services/__init__.py +0 -0
  49. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/services/cache_service.py +0 -0
  50. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/services/mock_service.py +0 -0
  51. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/services/proxy_service.py +0 -0
  52. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/services/request_parser.py +0 -0
  53. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/services/response_factory.py +0 -0
  54. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/services/rule_engine.py +0 -0
  55. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/services/snapshot.py +0 -0
  56. {proxy_mock-2.12.0 → proxy_mock-2.13.0}/proxy_mock/services/traffic_service.py +0 -0
  57. {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.12.0
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
- **Deprecated, removed in 3.0.** They still work and answer exactly as before, but send a
327
- `Deprecation: true` response header and a `Link` to the replacement:
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
- **Deprecated, removed in 3.0.** They still work and answer exactly as before, but send a
295
- `Deprecation: true` response header and a `Link` to the replacement:
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 mark_deprecated
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 mark_deprecated(response, "POST /storage/clean", "DELETE /storage")
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 mark_deprecated
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 mark_deprecated(response, "POST /traffic/clean", "DELETE /traffic")
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 mark_deprecated(response, "POST /traffic/settings", "PATCH /traffic/settings")
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 mark_deprecated(JSONResponse({"success": True}, 200), "POST /cache/clean")
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(HTTPMethod.GET, Endpoints.PROXY_MOCK)
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(HTTPMethod.DELETE, Endpoints.STORAGE)
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(HTTPMethod.GET, Endpoints.STORAGE_SNAPSHOT)
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(HTTPMethod.DELETE, Endpoints.TRAFFIC)
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(HTTPMethod.GET, Endpoints.TRAFFIC_SETTINGS)
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(HTTPMethod.POST, Endpoints.CACHE_CLEAN)
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(HTTPMethod.GET, Endpoints.PROXY_MOCK)
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(HTTPMethod.DELETE, Endpoints.STORAGE)
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(HTTPMethod.GET, Endpoints.STORAGE_SNAPSHOT)
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(HTTPMethod.DELETE, Endpoints.TRAFFIC)
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(HTTPMethod.GET, Endpoints.TRAFFIC_SETTINGS)
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(HTTPMethod.POST, Endpoints.CACHE_CLEAN)
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.12.0"
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