mockstack 0.4.0__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.
Files changed (61) hide show
  1. {mockstack-0.4.0/mockstack.egg-info → mockstack-0.4.2}/PKG-INFO +5 -2
  2. {mockstack-0.4.0 → mockstack-0.4.2}/README.md +3 -1
  3. {mockstack-0.4.0 → mockstack-0.4.2}/mockstack/config.py +2 -4
  4. mockstack-0.4.2/mockstack/display.py +27 -0
  5. {mockstack-0.4.0 → mockstack-0.4.2}/mockstack/intent.py +15 -0
  6. mockstack-0.4.2/mockstack/middleware.py +55 -0
  7. mockstack-0.4.2/mockstack/routers/catchall.py +14 -0
  8. {mockstack-0.4.0 → mockstack-0.4.2}/mockstack/strategies/filefixtures.py +3 -6
  9. {mockstack-0.4.0 → mockstack-0.4.2}/mockstack/strategies/proxyrules.py +14 -13
  10. {mockstack-0.4.0 → mockstack-0.4.2}/mockstack/telemetry.py +19 -0
  11. {mockstack-0.4.0 → mockstack-0.4.2}/mockstack/tests/test_display.py +1 -14
  12. {mockstack-0.4.0 → mockstack-0.4.2/mockstack.egg-info}/PKG-INFO +5 -2
  13. {mockstack-0.4.0 → mockstack-0.4.2}/mockstack.egg-info/requires.txt +1 -0
  14. {mockstack-0.4.0 → mockstack-0.4.2}/pyproject.toml +1 -0
  15. {mockstack-0.4.0 → mockstack-0.4.2}/uv.lock +592 -590
  16. mockstack-0.4.0/mockstack/display.py +0 -38
  17. mockstack-0.4.0/mockstack/middleware.py +0 -19
  18. mockstack-0.4.0/mockstack/routers/catchall.py +0 -33
  19. {mockstack-0.4.0 → mockstack-0.4.2}/.env.example +0 -0
  20. {mockstack-0.4.0 → mockstack-0.4.2}/.github/workflows/ci.yml +0 -0
  21. {mockstack-0.4.0 → mockstack-0.4.2}/.github/workflows/publish-docs.yml +0 -0
  22. {mockstack-0.4.0 → mockstack-0.4.2}/.github/workflows/publish-to-pypi.yml +0 -0
  23. {mockstack-0.4.0 → mockstack-0.4.2}/.gitignore +0 -0
  24. {mockstack-0.4.0 → mockstack-0.4.2}/.pre-commit-config.yaml +0 -0
  25. {mockstack-0.4.0 → mockstack-0.4.2}/CODE_OF_CONDUCT.md +0 -0
  26. {mockstack-0.4.0 → mockstack-0.4.2}/LICENSE +0 -0
  27. {mockstack-0.4.0 → mockstack-0.4.2}/SECURITY.md +0 -0
  28. {mockstack-0.4.0 → mockstack-0.4.2}/docs/assets/favicon.ico +0 -0
  29. {mockstack-0.4.0 → mockstack-0.4.2}/docs/assets/logo.png +0 -0
  30. {mockstack-0.4.0 → mockstack-0.4.2}/docs/assets/mockstack.png +0 -0
  31. {mockstack-0.4.0 → mockstack-0.4.2}/mkdocs.yml +0 -0
  32. {mockstack-0.4.0 → mockstack-0.4.2}/mockstack/__init__.py +0 -0
  33. {mockstack-0.4.0 → mockstack-0.4.2}/mockstack/constants.py +0 -0
  34. {mockstack-0.4.0 → mockstack-0.4.2}/mockstack/identifiers.py +0 -0
  35. {mockstack-0.4.0 → mockstack-0.4.2}/mockstack/lifespan.py +0 -0
  36. {mockstack-0.4.0 → mockstack-0.4.2}/mockstack/main.py +0 -0
  37. {mockstack-0.4.0 → mockstack-0.4.2}/mockstack/routers/__init__.py +0 -0
  38. {mockstack-0.4.0 → mockstack-0.4.2}/mockstack/routers/homepage.py +0 -0
  39. {mockstack-0.4.0 → mockstack-0.4.2}/mockstack/strategies/__init__.py +0 -0
  40. {mockstack-0.4.0 → mockstack-0.4.2}/mockstack/strategies/base.py +0 -0
  41. {mockstack-0.4.0 → mockstack-0.4.2}/mockstack/strategies/create_mixin.py +0 -0
  42. {mockstack-0.4.0 → mockstack-0.4.2}/mockstack/strategies/factory.py +0 -0
  43. {mockstack-0.4.0 → mockstack-0.4.2}/mockstack/templating.py +0 -0
  44. {mockstack-0.4.0 → mockstack-0.4.2}/mockstack/tests/__init__.py +0 -0
  45. {mockstack-0.4.0 → mockstack-0.4.2}/mockstack/tests/conftest.py +0 -0
  46. {mockstack-0.4.0 → mockstack-0.4.2}/mockstack/tests/fixtures/proxyrules.yml +0 -0
  47. {mockstack-0.4.0 → mockstack-0.4.2}/mockstack/tests/fixtures/templates/__init__.py +0 -0
  48. {mockstack-0.4.0 → mockstack-0.4.2}/mockstack/tests/fixtures/templates/example-template.j2 +0 -0
  49. {mockstack-0.4.0 → mockstack-0.4.2}/mockstack/tests/routers/__init__.py +0 -0
  50. {mockstack-0.4.0 → mockstack-0.4.2}/mockstack/tests/routers/test_catchall.py +0 -0
  51. {mockstack-0.4.0 → mockstack-0.4.2}/mockstack/tests/routers/test_homepage.py +0 -0
  52. {mockstack-0.4.0 → mockstack-0.4.2}/mockstack/tests/strategies/test_filefixtures.py +0 -0
  53. {mockstack-0.4.0 → mockstack-0.4.2}/mockstack/tests/strategies/test_proxyrules.py +0 -0
  54. {mockstack-0.4.0 → mockstack-0.4.2}/mockstack/tests/test_identifiers.py +0 -0
  55. {mockstack-0.4.0 → mockstack-0.4.2}/mockstack/tests/test_middleware.py +0 -0
  56. {mockstack-0.4.0 → mockstack-0.4.2}/mockstack/tests/test_templating.py +0 -0
  57. {mockstack-0.4.0 → mockstack-0.4.2}/mockstack.egg-info/SOURCES.txt +0 -0
  58. {mockstack-0.4.0 → mockstack-0.4.2}/mockstack.egg-info/dependency_links.txt +0 -0
  59. {mockstack-0.4.0 → mockstack-0.4.2}/mockstack.egg-info/entry_points.txt +0 -0
  60. {mockstack-0.4.0 → mockstack-0.4.2}/mockstack.egg-info/top_level.txt +0 -0
  61. {mockstack-0.4.0 → mockstack-0.4.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mockstack
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: An API mocking workhorse
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/adamhadani/mockstack
@@ -18,6 +18,7 @@ Requires-Dist: opentelemetry-instrumentation-fastapi>=0.53b1
18
18
  Requires-Dist: pydantic>=2.11.3
19
19
  Requires-Dist: pydantic-settings>=2.9.1
20
20
  Requires-Dist: pyyaml>=6.0.2
21
+ Requires-Dist: rich>=14.0.0
21
22
  Requires-Dist: typer>=0.15.2
22
23
  Requires-Dist: uvicorn>=0.34.2
23
24
  Dynamic: license-file
@@ -104,4 +105,6 @@ If you are contributing to development, you will want to clone this project, and
104
105
 
105
106
  Run in development mode (for live-reload of changes when developing):
106
107
 
107
- uv run -- mockstack --debug
108
+ uv run uvicorn --factory mockstack.main:create_app --reload
109
+
110
+ Note that when you run using the uvicorn CLI, you will need to set any configuration via `.env` file or environment variables.
@@ -80,4 +80,6 @@ If you are contributing to development, you will want to clone this project, and
80
80
 
81
81
  Run in development mode (for live-reload of changes when developing):
82
82
 
83
- uv run -- mockstack --debug
83
+ uv run uvicorn --factory mockstack.main:create_app --reload
84
+
85
+ Note that when you run using the uvicorn CLI, you will need to set any configuration via `.env` file or environment variables.
@@ -103,10 +103,8 @@ class Settings(BaseSettings):
103
103
  },
104
104
  "handlers": {
105
105
  "console": {
106
- "class": "logging.StreamHandler",
106
+ "class": "rich.logging.RichHandler",
107
107
  "level": "INFO",
108
- "formatter": "standard",
109
- "stream": "ext://sys.stdout",
110
108
  },
111
109
  },
112
110
  "loggers": {
@@ -128,7 +126,7 @@ class Settings(BaseSettings):
128
126
  },
129
127
  "root": {
130
128
  "handlers": ["console"],
131
- "level": "DEBUG",
129
+ "level": "NOTSET",
132
130
  "propagate": False,
133
131
  },
134
132
  }
@@ -0,0 +1,27 @@
1
+ """Display and logging functionality."""
2
+
3
+ import logging
4
+
5
+ from fastapi import FastAPI
6
+
7
+ from mockstack.config import Settings
8
+
9
+
10
+ def announce(app: FastAPI, settings: Settings):
11
+ """Log the startup message with the active settings."""
12
+ logger = logging.getLogger("uvicorn")
13
+ extra = {"markup": True}
14
+
15
+ logger.info(
16
+ f"[bold medium_purple]mockstack[/bold medium_purple] ready to roll. "
17
+ f"debug: [medium_purple]{settings.debug}[/medium_purple]. "
18
+ f"strategy: [medium_purple]{settings.strategy}[/medium_purple]. ",
19
+ extra=extra,
20
+ )
21
+ logger.info(str(app.state.strategy), extra=extra)
22
+ logger.info(
23
+ f"OpenTelemetry enabled: [medium_purple]{settings.opentelemetry.enabled}[/medium_purple], "
24
+ f"endpoint: [medium_purple]{settings.opentelemetry.endpoint}[/medium_purple], "
25
+ f"capture_response_body: [medium_purple]{settings.opentelemetry.capture_response_body}[/medium_purple]",
26
+ extra=extra,
27
+ )
@@ -51,3 +51,18 @@ def looks_like_a_command(request: Request) -> bool:
51
51
  request.url.path.endswith("/execute"),
52
52
  )
53
53
  )
54
+
55
+
56
+ def looks_like_a_create(request: Request) -> bool:
57
+ """Check if the request looks like a create.
58
+
59
+ This is a heuristic to try and identify cases where a POST
60
+ request is used for creating a new resource.
61
+
62
+ """
63
+ return any(
64
+ (
65
+ request.method == "POST",
66
+ request.url.path.endswith("/create"),
67
+ ),
68
+ )
@@ -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,7 +8,6 @@ from fastapi import HTTPException, Request, Response, status
8
8
  from jinja2 import Environment, FileSystemLoader
9
9
 
10
10
  from mockstack.config import Settings
11
- from mockstack.display import ANSIColors
12
11
  from mockstack.intent import (
13
12
  looks_like_a_command,
14
13
  looks_like_a_search,
@@ -39,12 +38,9 @@ class FileFixturesStrategy(BaseStrategy, CreateMixin):
39
38
  self.env = Environment(loader=FileSystemLoader(self.templates_dir))
40
39
 
41
40
  def __str__(self) -> str:
42
- HIGHLIGHT = ANSIColors.HEADER
43
- ENDC = ANSIColors.ENDC
44
-
45
41
  return (
46
- f"{HIGHLIGHT}[filefixtures]{ENDC} "
47
- f"templates_dir: {HIGHLIGHT}{self.templates_dir}{ENDC}. "
42
+ f"[medium_purple]filefixtures[/medium_purple] "
43
+ f"templates_dir: [medium_purple]{self.templates_dir}[/medium_purple]. "
48
44
  )
49
45
 
50
46
  async def apply(self, request: Request) -> Response:
@@ -85,6 +81,7 @@ class FileFixturesStrategy(BaseStrategy, CreateMixin):
85
81
  request, status_code=status.HTTP_201_CREATED
86
82
  )
87
83
  else:
84
+ # simulate resource creation:
88
85
  return await self._create(
89
86
  request,
90
87
  env=self.env,
@@ -15,7 +15,7 @@ from starlette.datastructures import Headers
15
15
 
16
16
  from mockstack.config import Settings
17
17
  from mockstack.constants import ProxyRulesRedirectVia
18
- from mockstack.display import ANSIColors
18
+ from mockstack.intent import looks_like_a_create
19
19
  from mockstack.strategies.base import BaseStrategy
20
20
  from mockstack.strategies.create_mixin import CreateMixin
21
21
 
@@ -69,14 +69,11 @@ class ProxyRulesStrategy(BaseStrategy, CreateMixin):
69
69
  self.env = Environment()
70
70
 
71
71
  def __str__(self) -> str:
72
- HIGHLIGHT = ANSIColors.HEADER
73
- ENDC = ANSIColors.ENDC
74
-
75
72
  return (
76
- f"{HIGHLIGHT}[proxyrules]{ENDC} "
77
- f"rules_filename: {HIGHLIGHT}{self.rules_filename}{ENDC}. "
78
- f"redirect_via: {HIGHLIGHT}{self.proxyrules_redirect_via}{ENDC}. "
79
- f"simulate_create_on_missing: {HIGHLIGHT}{self.proxyrules_simulate_create_on_missing}{ENDC}"
73
+ f"[medium_purple]proxyrules[/medium_purple] "
74
+ f"rules_filename: [medium_purple]{self.rules_filename}[/medium_purple]. "
75
+ f"redirect_via: [medium_purple]{self.proxyrules_redirect_via}[/medium_purple]. "
76
+ f"simulate_create_on_missing: [medium_purple]{self.proxyrules_simulate_create_on_missing}[/medium_purple]"
80
77
  )
81
78
 
82
79
  @cached_property
@@ -103,9 +100,12 @@ class ProxyRulesStrategy(BaseStrategy, CreateMixin):
103
100
  self.logger.warning(
104
101
  f"No rule found for request: {request.method} {request.url.path}"
105
102
  )
106
- if self.proxyrules_simulate_create_on_missing:
103
+
104
+ if self.proxyrules_simulate_create_on_missing and looks_like_a_create(
105
+ request
106
+ ):
107
107
  self.logger.info(
108
- "Simulating resource creation for missing rule for {request.method} {request.url.path}"
108
+ f"Simulating resource creation for missing rule for {request.method} {request.url.path}"
109
109
  )
110
110
  return await self._create(
111
111
  request,
@@ -113,6 +113,7 @@ class ProxyRulesStrategy(BaseStrategy, CreateMixin):
113
113
  created_resource_metadata=self.created_resource_metadata,
114
114
  )
115
115
  else:
116
+ # no rule found and we are not simulating resource creation
116
117
  return Response(status_code=status.HTTP_404_NOT_FOUND)
117
118
 
118
119
  url = rule.apply(request)
@@ -141,13 +142,13 @@ class ProxyRulesStrategy(BaseStrategy, CreateMixin):
141
142
  async def reverse_proxy(self, request: Request, url: str) -> Response:
142
143
  """Reverse proxy the request to the target URL."""
143
144
  async with httpx.AsyncClient() as client:
144
- method = request.method
145
145
  request_content = await request.body()
146
+ request_headers = self.reverse_proxy_headers(request.headers, url=url)
146
147
  req = client.build_request(
147
- method,
148
+ request.method,
148
149
  url,
149
150
  content=request_content,
150
- headers=self.reverse_proxy_headers(request.headers, url=url),
151
+ headers=request_headers,
151
152
  params=request.url.query,
152
153
  )
153
154
 
@@ -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:
@@ -2,20 +2,7 @@
2
2
 
3
3
  from unittest.mock import patch
4
4
 
5
- from mockstack.display import ANSIColors, announce
6
-
7
-
8
- def test_ansicolors_constants():
9
- """Test that ANSIColors constants are defined correctly."""
10
- assert ANSIColors.HEADER == "\033[95m"
11
- assert ANSIColors.OKBLUE == "\033[94m"
12
- assert ANSIColors.OKCYAN == "\033[96m"
13
- assert ANSIColors.OKGREEN == "\033[92m"
14
- assert ANSIColors.WARNING == "\033[93m"
15
- assert ANSIColors.FAIL == "\033[91m"
16
- assert ANSIColors.ENDC == "\033[0m"
17
- assert ANSIColors.BOLD == "\033[1m"
18
- assert ANSIColors.UNDERLINE == "\033[4m"
5
+ from mockstack.display import announce
19
6
 
20
7
 
21
8
  def test_announce(app, settings):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mockstack
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: An API mocking workhorse
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/adamhadani/mockstack
@@ -18,6 +18,7 @@ Requires-Dist: opentelemetry-instrumentation-fastapi>=0.53b1
18
18
  Requires-Dist: pydantic>=2.11.3
19
19
  Requires-Dist: pydantic-settings>=2.9.1
20
20
  Requires-Dist: pyyaml>=6.0.2
21
+ Requires-Dist: rich>=14.0.0
21
22
  Requires-Dist: typer>=0.15.2
22
23
  Requires-Dist: uvicorn>=0.34.2
23
24
  Dynamic: license-file
@@ -104,4 +105,6 @@ If you are contributing to development, you will want to clone this project, and
104
105
 
105
106
  Run in development mode (for live-reload of changes when developing):
106
107
 
107
- uv run -- mockstack --debug
108
+ uv run uvicorn --factory mockstack.main:create_app --reload
109
+
110
+ Note that when you run using the uvicorn CLI, you will need to set any configuration via `.env` file or environment variables.
@@ -7,5 +7,6 @@ opentelemetry-instrumentation-fastapi>=0.53b1
7
7
  pydantic>=2.11.3
8
8
  pydantic-settings>=2.9.1
9
9
  pyyaml>=6.0.2
10
+ rich>=14.0.0
10
11
  typer>=0.15.2
11
12
  uvicorn>=0.34.2
@@ -20,6 +20,7 @@ dependencies = [
20
20
  "pydantic>=2.11.3",
21
21
  "pydantic-settings>=2.9.1",
22
22
  "pyyaml>=6.0.2",
23
+ "rich>=14.0.0",
23
24
  "typer>=0.15.2",
24
25
  "uvicorn>=0.34.2",
25
26
  ]