mockstack 0.4.1__tar.gz → 0.4.2__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.
- {mockstack-0.4.1/mockstack.egg-info → mockstack-0.4.2}/PKG-INFO +1 -1
- mockstack-0.4.2/mockstack/middleware.py +55 -0
- mockstack-0.4.2/mockstack/routers/catchall.py +14 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mockstack/telemetry.py +19 -0
- {mockstack-0.4.1 → mockstack-0.4.2/mockstack.egg-info}/PKG-INFO +1 -1
- mockstack-0.4.1/mockstack/middleware.py +0 -19
- mockstack-0.4.1/mockstack/routers/catchall.py +0 -33
- {mockstack-0.4.1 → mockstack-0.4.2}/.env.example +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/.github/workflows/ci.yml +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/.github/workflows/publish-docs.yml +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/.github/workflows/publish-to-pypi.yml +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/.gitignore +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/.pre-commit-config.yaml +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/CODE_OF_CONDUCT.md +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/LICENSE +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/README.md +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/SECURITY.md +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/docs/assets/favicon.ico +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/docs/assets/logo.png +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/docs/assets/mockstack.png +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mkdocs.yml +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mockstack/__init__.py +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mockstack/config.py +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mockstack/constants.py +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mockstack/display.py +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mockstack/identifiers.py +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mockstack/intent.py +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mockstack/lifespan.py +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mockstack/main.py +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mockstack/routers/__init__.py +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mockstack/routers/homepage.py +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mockstack/strategies/__init__.py +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mockstack/strategies/base.py +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mockstack/strategies/create_mixin.py +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mockstack/strategies/factory.py +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mockstack/strategies/filefixtures.py +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mockstack/strategies/proxyrules.py +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mockstack/templating.py +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mockstack/tests/__init__.py +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mockstack/tests/conftest.py +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mockstack/tests/fixtures/proxyrules.yml +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mockstack/tests/fixtures/templates/__init__.py +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mockstack/tests/fixtures/templates/example-template.j2 +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mockstack/tests/routers/__init__.py +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mockstack/tests/routers/test_catchall.py +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mockstack/tests/routers/test_homepage.py +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mockstack/tests/strategies/test_filefixtures.py +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mockstack/tests/strategies/test_proxyrules.py +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mockstack/tests/test_display.py +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mockstack/tests/test_identifiers.py +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mockstack/tests/test_middleware.py +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mockstack/tests/test_templating.py +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mockstack.egg-info/SOURCES.txt +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mockstack.egg-info/dependency_links.txt +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mockstack.egg-info/entry_points.txt +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mockstack.egg-info/requires.txt +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/mockstack.egg-info/top_level.txt +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/pyproject.toml +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/setup.cfg +0 -0
- {mockstack-0.4.1 → mockstack-0.4.2}/uv.lock +0 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Middleware definitionsfor the mockstack app."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
from fastapi import FastAPI, Request, Response
|
|
6
|
+
from opentelemetry import trace
|
|
7
|
+
from opentelemetry.propagate import extract
|
|
8
|
+
|
|
9
|
+
from mockstack.config import Settings
|
|
10
|
+
from mockstack.telemetry import extract_body, span_name_for
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def middleware_provider(app: FastAPI, settings: Settings) -> None:
|
|
14
|
+
"""Instrument the middlewares to the mockstack app."""
|
|
15
|
+
|
|
16
|
+
@app.middleware("http")
|
|
17
|
+
async def add_process_time_header(request: Request, call_next):
|
|
18
|
+
start_time = time.time()
|
|
19
|
+
response = await call_next(request)
|
|
20
|
+
process_time = time.time() - start_time
|
|
21
|
+
response.headers["X-Process-Time"] = str(process_time)
|
|
22
|
+
return response
|
|
23
|
+
|
|
24
|
+
@app.middleware("http")
|
|
25
|
+
async def instrument_opentelemetry(request: Request, call_next):
|
|
26
|
+
tracer = trace.get_tracer(__name__)
|
|
27
|
+
ctx = extract(request.headers)
|
|
28
|
+
with tracer.start_as_current_span(span_name_for(request), context=ctx) as span:
|
|
29
|
+
span.set_attribute("http.method", request.method)
|
|
30
|
+
span.set_attribute("http.url", str(request.url))
|
|
31
|
+
|
|
32
|
+
response = await call_next(request)
|
|
33
|
+
|
|
34
|
+
span.set_attribute("http.status_code", response.status_code)
|
|
35
|
+
|
|
36
|
+
# Nb. persisting response body can hamper performance,
|
|
37
|
+
# expose sensitive / PII data, and / or may not be needed.
|
|
38
|
+
# it is therefore an opt-in setting.
|
|
39
|
+
if settings.opentelemetry.capture_response_body:
|
|
40
|
+
body = await extract_body(response)
|
|
41
|
+
|
|
42
|
+
# for semantics of payload collection see:
|
|
43
|
+
# https://github.com/open-telemetry/oteps/pull/234
|
|
44
|
+
span.set_attribute("http.response.body", body)
|
|
45
|
+
|
|
46
|
+
# recreate response with the same body since when consuming it to log it above
|
|
47
|
+
# we effectively "deplete" the iterator.
|
|
48
|
+
response = Response(
|
|
49
|
+
content=body,
|
|
50
|
+
status_code=response.status_code,
|
|
51
|
+
headers=dict(response.headers),
|
|
52
|
+
media_type=response.media_type,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return response
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Routes for the mockstack app."""
|
|
2
|
+
|
|
3
|
+
from fastapi import FastAPI, Request
|
|
4
|
+
|
|
5
|
+
from mockstack.config import Settings
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def catchall_router_provider(app: FastAPI, settings: Settings) -> None:
|
|
9
|
+
"""Create the catch-all routes for the mockstack app."""
|
|
10
|
+
|
|
11
|
+
@app.route("/{full_path:path}", methods=["GET", "PATCH", "POST", "PUT", "DELETE"])
|
|
12
|
+
async def catch_all(request: Request):
|
|
13
|
+
"""Catch all requests and delegate to the strategy."""
|
|
14
|
+
return await app.state.strategy.apply(request)
|
|
@@ -8,6 +8,7 @@ from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExport
|
|
|
8
8
|
from opentelemetry.sdk.resources import Resource
|
|
9
9
|
from opentelemetry.sdk.trace import TracerProvider
|
|
10
10
|
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
11
|
+
from starlette.responses import StreamingResponse
|
|
11
12
|
|
|
12
13
|
from mockstack.config import Settings
|
|
13
14
|
|
|
@@ -17,6 +18,24 @@ def span_name_for(request: Request) -> str:
|
|
|
17
18
|
return f"{request.method.upper()} {request.url.path}"
|
|
18
19
|
|
|
19
20
|
|
|
21
|
+
async def extract_body(response: StreamingResponse) -> str:
|
|
22
|
+
"""Extract the body of a response."""
|
|
23
|
+
|
|
24
|
+
async def read_response_body(response: StreamingResponse) -> bytes:
|
|
25
|
+
"""Helper function to read response body asynchronously into memory."""
|
|
26
|
+
body = b""
|
|
27
|
+
async for chunk in response.body_iterator:
|
|
28
|
+
if isinstance(chunk, str):
|
|
29
|
+
body += chunk.encode()
|
|
30
|
+
else:
|
|
31
|
+
body += chunk
|
|
32
|
+
return body
|
|
33
|
+
|
|
34
|
+
body = await read_response_body(response)
|
|
35
|
+
|
|
36
|
+
return body.decode()
|
|
37
|
+
|
|
38
|
+
|
|
20
39
|
def opentelemetry_provider(app: FastAPI, settings: Settings) -> None:
|
|
21
40
|
"""Initialize OpenTelemetry for the mockstack app."""
|
|
22
41
|
if not settings.opentelemetry.enabled:
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
"""Middleware definitionsfor the mockstack app."""
|
|
2
|
-
|
|
3
|
-
import time
|
|
4
|
-
|
|
5
|
-
from fastapi import FastAPI, Request
|
|
6
|
-
|
|
7
|
-
from mockstack.config import Settings
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def middleware_provider(app: FastAPI, settings: Settings) -> None:
|
|
11
|
-
"""Instrument the middlewares to the mockstack app."""
|
|
12
|
-
|
|
13
|
-
@app.middleware("http")
|
|
14
|
-
async def add_process_time_header(request: Request, call_next):
|
|
15
|
-
start_time = time.time()
|
|
16
|
-
response = await call_next(request)
|
|
17
|
-
process_time = time.time() - start_time
|
|
18
|
-
response.headers["X-Process-Time"] = str(process_time)
|
|
19
|
-
return response
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
"""Routes for the mockstack app."""
|
|
2
|
-
|
|
3
|
-
from fastapi import FastAPI, Request
|
|
4
|
-
from opentelemetry import trace
|
|
5
|
-
from opentelemetry.propagate import extract
|
|
6
|
-
|
|
7
|
-
from mockstack.config import Settings
|
|
8
|
-
from mockstack.telemetry import span_name_for
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def catchall_router_provider(app: FastAPI, settings: Settings) -> None:
|
|
12
|
-
"""Create the catch-all routes for the mockstack app."""
|
|
13
|
-
|
|
14
|
-
@app.route("/{full_path:path}", methods=["GET", "PATCH", "POST", "PUT", "DELETE"])
|
|
15
|
-
async def catch_all(request: Request):
|
|
16
|
-
"""Catch all requests and delegate to the strategy."""
|
|
17
|
-
tracer = trace.get_tracer(__name__)
|
|
18
|
-
ctx = extract(request.headers)
|
|
19
|
-
with tracer.start_as_current_span(span_name_for(request), context=ctx) as span:
|
|
20
|
-
span.set_attribute("http.method", request.method)
|
|
21
|
-
span.set_attribute("http.url", str(request.url))
|
|
22
|
-
|
|
23
|
-
response = await app.state.strategy.apply(request)
|
|
24
|
-
|
|
25
|
-
span.set_attribute("http.status_code", response.status_code)
|
|
26
|
-
|
|
27
|
-
# Nb. persisting response body can hamper performance,
|
|
28
|
-
# expose sensitive / PII data, and / or may not be needed.
|
|
29
|
-
# it is therefore an opt-in setting.
|
|
30
|
-
if settings.opentelemetry.capture_response_body:
|
|
31
|
-
span.set_attribute("response", response.body)
|
|
32
|
-
|
|
33
|
-
return response
|
|
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
|
|
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
|