interposition-http-adapter 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Osoekawa IT Laboratory
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,104 @@
1
+ Metadata-Version: 2.4
2
+ Name: interposition_http_adapter
3
+ Version: 0.1.0
4
+ Summary: HTTP adapter for Interposition.
5
+ Author: osoken
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/osoekawaitlab/interposition-http-adapter
8
+ Project-URL: Repository, https://github.com/osoekawaitlab/interposition-http-adapter
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: POSIX
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: interposition>=0.3.0
22
+ Requires-Dist: starlette>=0.52.1
23
+ Requires-Dist: uvicorn>=0.40.0
24
+ Dynamic: license-file
25
+
26
+ # interposition-http-adapter
27
+
28
+ HTTP adapter for [Interposition](https://github.com/osoekawaitlab/interposition).
29
+ Serves recorded HTTP interactions from an Interposition `Cassette` as a real HTTP server, allowing you to replay captured API responses for testing and development.
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install interposition-http-adapter
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ `InterpositionHttpAdapter` is an ASGI application (based on Starlette) that replays HTTP interactions through an Interposition `Broker`.
40
+
41
+ ```python
42
+ from interposition import (
43
+ Broker,
44
+ Cassette,
45
+ Interaction,
46
+ InteractionRequest,
47
+ ResponseChunk,
48
+ )
49
+ import uvicorn
50
+
51
+ from interposition_http_adapter import InterpositionHttpAdapter
52
+
53
+ # 1. Build an InteractionRequest describing the HTTP request to match
54
+ request = InteractionRequest(
55
+ protocol="http",
56
+ action="GET",
57
+ target="/api/data",
58
+ headers=(),
59
+ body=b"",
60
+ )
61
+
62
+ # 2. Build ResponseChunks with status_code in the first chunk's metadata
63
+ response_chunks = (
64
+ ResponseChunk(
65
+ data=b'{"message": "hello"}',
66
+ sequence=0,
67
+ metadata=(("status_code", "200"),),
68
+ ),
69
+ )
70
+
71
+ # 3. Create a Cassette containing the interaction
72
+ interaction = Interaction(
73
+ request=request,
74
+ fingerprint=request.fingerprint(),
75
+ response_chunks=response_chunks,
76
+ )
77
+ cassette = Cassette(interactions=(interaction,))
78
+
79
+ # 4. Create a Broker in replay mode
80
+ broker = Broker(cassette=cassette, mode="replay")
81
+
82
+ # 5. Create the adapter and serve it
83
+ app = InterpositionHttpAdapter(broker=broker)
84
+ uvicorn.run(app, host="127.0.0.1", port=8000)
85
+ ```
86
+
87
+ Once the server is running, a `GET` request to `http://127.0.0.1:8000/api/data` returns the recorded response with status code 200 and body `{"message": "hello"}`.
88
+ Requests that do not match any recorded interaction return a `500 Internal Server Error` response.
89
+
90
+ ## API Reference
91
+
92
+ Detailed documentation is available in MkDocs: <https://osoekawaitlab.github.io/interposition-http-adapter/>.
93
+
94
+ ## CLI
95
+
96
+ The package provides a command-line interface:
97
+
98
+ ```bash
99
+ interposition_http_adapter --version
100
+ ```
101
+
102
+ ## License
103
+
104
+ MIT
@@ -0,0 +1,79 @@
1
+ # interposition-http-adapter
2
+
3
+ HTTP adapter for [Interposition](https://github.com/osoekawaitlab/interposition).
4
+ Serves recorded HTTP interactions from an Interposition `Cassette` as a real HTTP server, allowing you to replay captured API responses for testing and development.
5
+
6
+ ## Installation
7
+
8
+ ```bash
9
+ pip install interposition-http-adapter
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ `InterpositionHttpAdapter` is an ASGI application (based on Starlette) that replays HTTP interactions through an Interposition `Broker`.
15
+
16
+ ```python
17
+ from interposition import (
18
+ Broker,
19
+ Cassette,
20
+ Interaction,
21
+ InteractionRequest,
22
+ ResponseChunk,
23
+ )
24
+ import uvicorn
25
+
26
+ from interposition_http_adapter import InterpositionHttpAdapter
27
+
28
+ # 1. Build an InteractionRequest describing the HTTP request to match
29
+ request = InteractionRequest(
30
+ protocol="http",
31
+ action="GET",
32
+ target="/api/data",
33
+ headers=(),
34
+ body=b"",
35
+ )
36
+
37
+ # 2. Build ResponseChunks with status_code in the first chunk's metadata
38
+ response_chunks = (
39
+ ResponseChunk(
40
+ data=b'{"message": "hello"}',
41
+ sequence=0,
42
+ metadata=(("status_code", "200"),),
43
+ ),
44
+ )
45
+
46
+ # 3. Create a Cassette containing the interaction
47
+ interaction = Interaction(
48
+ request=request,
49
+ fingerprint=request.fingerprint(),
50
+ response_chunks=response_chunks,
51
+ )
52
+ cassette = Cassette(interactions=(interaction,))
53
+
54
+ # 4. Create a Broker in replay mode
55
+ broker = Broker(cassette=cassette, mode="replay")
56
+
57
+ # 5. Create the adapter and serve it
58
+ app = InterpositionHttpAdapter(broker=broker)
59
+ uvicorn.run(app, host="127.0.0.1", port=8000)
60
+ ```
61
+
62
+ Once the server is running, a `GET` request to `http://127.0.0.1:8000/api/data` returns the recorded response with status code 200 and body `{"message": "hello"}`.
63
+ Requests that do not match any recorded interaction return a `500 Internal Server Error` response.
64
+
65
+ ## API Reference
66
+
67
+ Detailed documentation is available in MkDocs: <https://osoekawaitlab.github.io/interposition-http-adapter/>.
68
+
69
+ ## CLI
70
+
71
+ The package provides a command-line interface:
72
+
73
+ ```bash
74
+ interposition_http_adapter --version
75
+ ```
76
+
77
+ ## License
78
+
79
+ MIT
@@ -0,0 +1,124 @@
1
+ [project]
2
+ name = "interposition_http_adapter"
3
+ dynamic = ["version"]
4
+ description = "HTTP adapter for Interposition."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ authors = [
8
+ { name = "osoken" }
9
+ ]
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Intended Audience :: Developers",
13
+ "Operating System :: POSIX",
14
+ "Programming Language :: Python :: 3 :: Only",
15
+ "Programming Language :: Python :: 3.10",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Typing :: Typed",
20
+ ]
21
+ requires-python = ">=3.10"
22
+ dependencies = [
23
+ "interposition>=0.3.0",
24
+ "starlette>=0.52.1",
25
+ "uvicorn>=0.40.0",
26
+ ]
27
+
28
+ [project.urls]
29
+ Homepage = "https://github.com/osoekawaitlab/interposition-http-adapter"
30
+ Repository = "https://github.com/osoekawaitlab/interposition-http-adapter"
31
+
32
+ [build-system]
33
+ requires = ["setuptools>=70.1"]
34
+ build-backend = "setuptools.build_meta"
35
+
36
+ [tool.setuptools.dynamic]
37
+ version = {attr = "interposition_http_adapter._version.__version__"}
38
+
39
+
40
+ [project.scripts]
41
+ interposition_http_adapter = "interposition_http_adapter.cli:main"
42
+
43
+ [dependency-groups]
44
+ dev = [
45
+ "getgauge>=0.5.0",
46
+ "httpx>=0.28.1",
47
+ "mypy>=1.18.2",
48
+ "nox>=2025.11.12",
49
+ "pytest>=9.0.1",
50
+ "pytest-cov>=7.0.0",
51
+ "pytest-mock>=3.15.1",
52
+ "pytest-randomly>=4.0.1",
53
+ "ruff>=0.14.5",
54
+ ]
55
+ docs = [
56
+ "mkdocs>=1.6.1",
57
+ "mkdocs-awesome-nav>=3.2.0",
58
+ "mkdocs-material>=9.7.0",
59
+ "mkdocstrings>=0.30.1",
60
+ "mkdocstrings-python>=1.19.0",
61
+ ]
62
+
63
+ [tool.ruff]
64
+ indent-width = 4
65
+ target-version = "py312"
66
+ line-length = 88
67
+
68
+ [tool.ruff.lint]
69
+ select = ["ALL"]
70
+ ignore = [
71
+ "COM812", # trailing-comma-missing (conflicts with formatter)
72
+ "ISC001", # single-line-implicit-string-concatenation (conflicts with formatter)
73
+ "D203", # one-blank-line-before-class (conflicts with D211)
74
+ "D213", # multi-line-summary-second-line (conflicts with D212)
75
+ ]
76
+
77
+ [tool.ruff.lint.per-file-ignores]
78
+ "tests/**/*.py" = [
79
+ "S101", # assert-used (pytest uses asserts)
80
+ ]
81
+ "e2e/**/*.py" = [
82
+ "S101", # assert-used (test uses asserts)
83
+ ]
84
+
85
+ [tool.ruff.lint.pydocstyle]
86
+ convention = "google"
87
+
88
+ [tool.ruff.format]
89
+ quote-style = "double"
90
+ indent-style = "space"
91
+ skip-magic-trailing-comma = false
92
+ line-ending = "auto"
93
+
94
+ [tool.mypy]
95
+ python_version = "3.12"
96
+ strict = true
97
+ check_untyped_defs = true
98
+ disallow_any_explicit = true
99
+ disallow_any_generics = true
100
+ disallow_incomplete_defs = true
101
+ disallow_untyped_decorators = true
102
+ disallow_untyped_defs = true
103
+ no_implicit_optional = true
104
+ show_error_codes = true
105
+ strict_equality = true
106
+ warn_no_return = true
107
+ warn_redundant_casts = true
108
+ warn_return_any = true
109
+ warn_unreachable = true
110
+ warn_unused_configs = true
111
+ warn_unused_ignores = true
112
+ mypy_path = ["stubs"]
113
+
114
+ [tool.pytest.ini_options]
115
+ testpaths = ["tests", "src"]
116
+ python_files = ["test_*.py", "*_test.py"]
117
+ python_functions = ["test_*"]
118
+ addopts = [
119
+ "--strict-markers",
120
+ "--strict-config",
121
+ "--doctest-modules",
122
+ ]
123
+ markers = [
124
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ """HTTP adapter for Interposition."""
2
+
3
+ from interposition_http_adapter._version import __version__
4
+ from interposition_http_adapter.app import InterpositionHttpAdapter
5
+
6
+ __all__ = ["InterpositionHttpAdapter", "__version__"]
@@ -0,0 +1,3 @@
1
+ """Version information for interposition_http_adapter."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,143 @@
1
+ """HTTP adapter application for Interposition."""
2
+
3
+ from collections.abc import Awaitable, Callable
4
+
5
+ from interposition import (
6
+ Broker,
7
+ Interaction,
8
+ InteractionNotFoundError,
9
+ InteractionRequest,
10
+ ResponseChunk,
11
+ )
12
+ from starlette.applications import Starlette
13
+ from starlette.datastructures import Headers
14
+ from starlette.requests import Request
15
+ from starlette.responses import Response
16
+ from starlette.routing import Route
17
+
18
+
19
+ def _create_get_handler(
20
+ broker: Broker,
21
+ ) -> Callable[[Request], Awaitable[Response]]:
22
+ """Create a GET request handler bound to the given broker."""
23
+
24
+ async def handle_get(request: Request) -> Response:
25
+ target = request.url.path
26
+ if request.url.query:
27
+ target = f"{target}?{request.url.query}"
28
+
29
+ body = await request.body()
30
+ chunks = _replay_matching_get(
31
+ broker=broker,
32
+ request_headers=request.headers,
33
+ target=target,
34
+ body=body,
35
+ )
36
+ if chunks is None:
37
+ return Response(status_code=500, content=b"Interaction Not Found")
38
+
39
+ status_code = 200
40
+ if chunks:
41
+ for key, value in chunks[0].metadata:
42
+ if key == "status_code":
43
+ status_code = int(value)
44
+
45
+ response_body = b"".join(chunk.data for chunk in chunks)
46
+ return Response(status_code=status_code, content=response_body)
47
+
48
+ return handle_get
49
+
50
+
51
+ def _replay_matching_get(
52
+ broker: Broker,
53
+ request_headers: Headers,
54
+ target: str,
55
+ body: bytes,
56
+ ) -> tuple[ResponseChunk, ...] | None:
57
+ """Try replay candidates built from stored interaction header schemas."""
58
+ candidates = _build_get_replay_candidates(
59
+ interactions=broker.cassette.interactions,
60
+ request_headers=request_headers,
61
+ target=target,
62
+ body=body,
63
+ )
64
+ for candidate in candidates:
65
+ try:
66
+ return tuple(broker.replay(candidate))
67
+ except InteractionNotFoundError:
68
+ continue
69
+ return None
70
+
71
+
72
+ def _build_get_replay_candidates(
73
+ interactions: tuple[Interaction, ...],
74
+ request_headers: Headers,
75
+ target: str,
76
+ body: bytes,
77
+ ) -> tuple[InteractionRequest, ...]:
78
+ """Create candidate requests using the stored header fields per interaction."""
79
+ candidates: list[InteractionRequest] = []
80
+ seen_fingerprints: set[str] = set()
81
+
82
+ for interaction in interactions:
83
+ recorded_request = interaction.request
84
+ if recorded_request.protocol != "http":
85
+ continue
86
+ if recorded_request.action != "GET":
87
+ continue
88
+ if recorded_request.target != target:
89
+ continue
90
+
91
+ candidate_headers: list[tuple[str, str]] = []
92
+ missing_header = False
93
+ for key, _ in recorded_request.headers:
94
+ value = request_headers.get(key)
95
+ if value is None:
96
+ missing_header = True
97
+ break
98
+ candidate_headers.append((key, value))
99
+
100
+ if missing_header:
101
+ continue
102
+
103
+ candidate = InteractionRequest(
104
+ protocol="http",
105
+ action="GET",
106
+ target=target,
107
+ headers=tuple(candidate_headers),
108
+ body=body,
109
+ )
110
+ candidate_fingerprint = candidate.fingerprint().value
111
+ if candidate_fingerprint in seen_fingerprints:
112
+ continue
113
+ seen_fingerprints.add(candidate_fingerprint)
114
+ candidates.append(candidate)
115
+
116
+ if not candidates:
117
+ candidates.append(
118
+ InteractionRequest(
119
+ protocol="http",
120
+ action="GET",
121
+ target=target,
122
+ headers=(),
123
+ body=body,
124
+ )
125
+ )
126
+
127
+ return tuple(candidates)
128
+
129
+
130
+ class InterpositionHttpAdapter(Starlette):
131
+ """ASGI application that replays HTTP interactions via an Interposition Broker."""
132
+
133
+ def __init__(self, broker: Broker) -> None:
134
+ """Initialize the adapter with a Broker.
135
+
136
+ Args:
137
+ broker: The Interposition Broker to use for replaying interactions.
138
+ """
139
+ self._broker = broker
140
+ routes = [
141
+ Route("/{path:path}", _create_get_handler(broker), methods=["GET"]),
142
+ ]
143
+ super().__init__(routes=routes)
@@ -0,0 +1,18 @@
1
+ """CLI module for interposition_http_adapter."""
2
+
3
+ from argparse import ArgumentParser
4
+
5
+ from interposition_http_adapter._version import __version__
6
+
7
+
8
+ def generate_cli_parser() -> ArgumentParser:
9
+ """Generate the argument parser for the interposition_http_adapter CLI."""
10
+ parser = ArgumentParser(description="HTTP adapter for Interposition.")
11
+ parser.add_argument("--version", action="version", version=__version__)
12
+ return parser
13
+
14
+
15
+ def main() -> None:
16
+ """Entry point for the interposition_http_adapter command-line interface."""
17
+ parser = generate_cli_parser()
18
+ _ = parser.parse_args()
@@ -0,0 +1,104 @@
1
+ Metadata-Version: 2.4
2
+ Name: interposition_http_adapter
3
+ Version: 0.1.0
4
+ Summary: HTTP adapter for Interposition.
5
+ Author: osoken
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/osoekawaitlab/interposition-http-adapter
8
+ Project-URL: Repository, https://github.com/osoekawaitlab/interposition-http-adapter
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: POSIX
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: interposition>=0.3.0
22
+ Requires-Dist: starlette>=0.52.1
23
+ Requires-Dist: uvicorn>=0.40.0
24
+ Dynamic: license-file
25
+
26
+ # interposition-http-adapter
27
+
28
+ HTTP adapter for [Interposition](https://github.com/osoekawaitlab/interposition).
29
+ Serves recorded HTTP interactions from an Interposition `Cassette` as a real HTTP server, allowing you to replay captured API responses for testing and development.
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install interposition-http-adapter
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ `InterpositionHttpAdapter` is an ASGI application (based on Starlette) that replays HTTP interactions through an Interposition `Broker`.
40
+
41
+ ```python
42
+ from interposition import (
43
+ Broker,
44
+ Cassette,
45
+ Interaction,
46
+ InteractionRequest,
47
+ ResponseChunk,
48
+ )
49
+ import uvicorn
50
+
51
+ from interposition_http_adapter import InterpositionHttpAdapter
52
+
53
+ # 1. Build an InteractionRequest describing the HTTP request to match
54
+ request = InteractionRequest(
55
+ protocol="http",
56
+ action="GET",
57
+ target="/api/data",
58
+ headers=(),
59
+ body=b"",
60
+ )
61
+
62
+ # 2. Build ResponseChunks with status_code in the first chunk's metadata
63
+ response_chunks = (
64
+ ResponseChunk(
65
+ data=b'{"message": "hello"}',
66
+ sequence=0,
67
+ metadata=(("status_code", "200"),),
68
+ ),
69
+ )
70
+
71
+ # 3. Create a Cassette containing the interaction
72
+ interaction = Interaction(
73
+ request=request,
74
+ fingerprint=request.fingerprint(),
75
+ response_chunks=response_chunks,
76
+ )
77
+ cassette = Cassette(interactions=(interaction,))
78
+
79
+ # 4. Create a Broker in replay mode
80
+ broker = Broker(cassette=cassette, mode="replay")
81
+
82
+ # 5. Create the adapter and serve it
83
+ app = InterpositionHttpAdapter(broker=broker)
84
+ uvicorn.run(app, host="127.0.0.1", port=8000)
85
+ ```
86
+
87
+ Once the server is running, a `GET` request to `http://127.0.0.1:8000/api/data` returns the recorded response with status code 200 and body `{"message": "hello"}`.
88
+ Requests that do not match any recorded interaction return a `500 Internal Server Error` response.
89
+
90
+ ## API Reference
91
+
92
+ Detailed documentation is available in MkDocs: <https://osoekawaitlab.github.io/interposition-http-adapter/>.
93
+
94
+ ## CLI
95
+
96
+ The package provides a command-line interface:
97
+
98
+ ```bash
99
+ interposition_http_adapter --version
100
+ ```
101
+
102
+ ## License
103
+
104
+ MIT
@@ -0,0 +1,14 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/interposition_http_adapter/__init__.py
5
+ src/interposition_http_adapter/_version.py
6
+ src/interposition_http_adapter/app.py
7
+ src/interposition_http_adapter/cli.py
8
+ src/interposition_http_adapter/py.typed
9
+ src/interposition_http_adapter.egg-info/PKG-INFO
10
+ src/interposition_http_adapter.egg-info/SOURCES.txt
11
+ src/interposition_http_adapter.egg-info/dependency_links.txt
12
+ src/interposition_http_adapter.egg-info/entry_points.txt
13
+ src/interposition_http_adapter.egg-info/requires.txt
14
+ src/interposition_http_adapter.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ interposition_http_adapter = interposition_http_adapter.cli:main
@@ -0,0 +1,3 @@
1
+ interposition>=0.3.0
2
+ starlette>=0.52.1
3
+ uvicorn>=0.40.0