mockstack 0.4.2__tar.gz → 0.5.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 (67) hide show
  1. {mockstack-0.4.2 → mockstack-0.5.0}/PKG-INFO +14 -2
  2. {mockstack-0.4.2 → mockstack-0.5.0}/README.md +13 -1
  3. mockstack-0.5.0/examples/filefixtures-with-templates/.env.example +13 -0
  4. mockstack-0.5.0/examples/filefixtures-with-templates/README.md +10 -0
  5. mockstack-0.5.0/examples/filefixtures-with-templates/templates/servicename-api-v1-items.j2 +19 -0
  6. mockstack-0.5.0/examples/filefixtures-with-templates/templates/servicename2-api-v2-user.533ec889-7c68-45c8-b21e-4a7e455d1234.j2 +14 -0
  7. mockstack-0.5.0/examples/proxyrules-with-rules-file/.env.example +13 -0
  8. mockstack-0.5.0/examples/proxyrules-with-rules-file/README.md +15 -0
  9. mockstack-0.5.0/examples/proxyrules-with-rules-file/rules.yml +13 -0
  10. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack/config.py +3 -0
  11. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack/constants.py +3 -0
  12. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack/lifespan.py +15 -4
  13. mockstack-0.5.0/mockstack/middleware.py +48 -0
  14. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack/strategies/proxyrules.py +9 -14
  15. mockstack-0.5.0/mockstack/telemetry.py +141 -0
  16. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack.egg-info/PKG-INFO +14 -2
  17. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack.egg-info/SOURCES.txt +7 -0
  18. mockstack-0.4.2/mockstack/middleware.py +0 -55
  19. mockstack-0.4.2/mockstack/telemetry.py +0 -63
  20. {mockstack-0.4.2 → mockstack-0.5.0}/.env.example +0 -0
  21. {mockstack-0.4.2 → mockstack-0.5.0}/.github/workflows/ci.yml +0 -0
  22. {mockstack-0.4.2 → mockstack-0.5.0}/.github/workflows/publish-docs.yml +0 -0
  23. {mockstack-0.4.2 → mockstack-0.5.0}/.github/workflows/publish-to-pypi.yml +0 -0
  24. {mockstack-0.4.2 → mockstack-0.5.0}/.gitignore +0 -0
  25. {mockstack-0.4.2 → mockstack-0.5.0}/.pre-commit-config.yaml +0 -0
  26. {mockstack-0.4.2 → mockstack-0.5.0}/CODE_OF_CONDUCT.md +0 -0
  27. {mockstack-0.4.2 → mockstack-0.5.0}/LICENSE +0 -0
  28. {mockstack-0.4.2 → mockstack-0.5.0}/SECURITY.md +0 -0
  29. {mockstack-0.4.2 → mockstack-0.5.0}/docs/assets/favicon.ico +0 -0
  30. {mockstack-0.4.2 → mockstack-0.5.0}/docs/assets/logo.png +0 -0
  31. {mockstack-0.4.2 → mockstack-0.5.0}/docs/assets/mockstack.png +0 -0
  32. {mockstack-0.4.2 → mockstack-0.5.0}/mkdocs.yml +0 -0
  33. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack/__init__.py +0 -0
  34. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack/display.py +0 -0
  35. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack/identifiers.py +0 -0
  36. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack/intent.py +0 -0
  37. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack/main.py +0 -0
  38. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack/routers/__init__.py +0 -0
  39. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack/routers/catchall.py +0 -0
  40. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack/routers/homepage.py +0 -0
  41. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack/strategies/__init__.py +0 -0
  42. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack/strategies/base.py +0 -0
  43. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack/strategies/create_mixin.py +0 -0
  44. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack/strategies/factory.py +0 -0
  45. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack/strategies/filefixtures.py +0 -0
  46. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack/templating.py +0 -0
  47. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack/tests/__init__.py +0 -0
  48. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack/tests/conftest.py +0 -0
  49. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack/tests/fixtures/proxyrules.yml +0 -0
  50. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack/tests/fixtures/templates/__init__.py +0 -0
  51. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack/tests/fixtures/templates/example-template.j2 +0 -0
  52. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack/tests/routers/__init__.py +0 -0
  53. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack/tests/routers/test_catchall.py +0 -0
  54. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack/tests/routers/test_homepage.py +0 -0
  55. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack/tests/strategies/test_filefixtures.py +0 -0
  56. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack/tests/strategies/test_proxyrules.py +0 -0
  57. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack/tests/test_display.py +0 -0
  58. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack/tests/test_identifiers.py +0 -0
  59. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack/tests/test_middleware.py +0 -0
  60. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack/tests/test_templating.py +0 -0
  61. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack.egg-info/dependency_links.txt +0 -0
  62. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack.egg-info/entry_points.txt +0 -0
  63. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack.egg-info/requires.txt +0 -0
  64. {mockstack-0.4.2 → mockstack-0.5.0}/mockstack.egg-info/top_level.txt +0 -0
  65. {mockstack-0.4.2 → mockstack-0.5.0}/pyproject.toml +0 -0
  66. {mockstack-0.4.2 → mockstack-0.5.0}/setup.cfg +0 -0
  67. {mockstack-0.4.2 → mockstack-0.5.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mockstack
3
- Version: 0.4.2
3
+ Version: 0.5.0
4
4
  Summary: An API mocking workhorse
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/adamhadani/mockstack
@@ -34,7 +34,17 @@ Dynamic: license-file
34
34
 
35
35
  An API mocking workhorse :racehorse:
36
36
 
37
- Enabling a sane development lifecycle for microservice-oriented architectures.
37
+ Enabling a sane development lifecycle for microservice-oriented architectures and LLM-driven tool chains.
38
+
39
+ Use mockstack for:
40
+
41
+ * **Development** :pencil2:. Simulating HTTP-based interactions between a particular component you're developing or debugging locally and multiple other components it depends on during execution of a particular flow. You can create template-based mock responses, simulate creation of resources in a realistic way, as well as proxy to other services using a rich rules DSL. Full request and response metadata and payloads can be observed via OpenTelemetry integration.
42
+
43
+ * **Integration Testing** :ok_hand:. Creating a consistent environment for running integration tests on a single component, using fixture responses.
44
+
45
+ * **LLM-powered Workflows** :alien:. Speeding up development of LLM-based workflows and tools for use with frameworks such as [LangChain](https://python.langchain.com/docs/introduction/), [LangGraph](https://www.langchain.com/langgraph) and others. When developing LLM-driven execution graphs, you often want to have a consistent response from a tool while you're tuning prompts or debugging other aspects of a particular trace. `mockstack` can give you a solid foundation for this.
46
+
47
+ * **Chaos Engineering** :boom:. mockstack can simulate various real-world runtime error scenarios such as timeouts, http error codes, and invalid response payloads. This can be a great way to do some upfront "chaos monkey" type of testing on software components.
38
48
 
39
49
  Highlights include:
40
50
 
@@ -57,6 +67,8 @@ or install into a persistent environment and add it to the PATH with:
57
67
 
58
68
  ## Usage
59
69
 
70
+ See the [examples](https://github.com/adamhadani/mockstack/blob/main/examples/) directory for complete examples with documentation.
71
+
60
72
  Available configuration options are [here](https://github.com/adamhadani/mockstack/blob/main/mockstack/config.py).
61
73
 
62
74
  Setting individual options can be done either through an `.env` file, individual environment variables, or command-line arguments. For example:
@@ -9,7 +9,17 @@
9
9
 
10
10
  An API mocking workhorse :racehorse:
11
11
 
12
- Enabling a sane development lifecycle for microservice-oriented architectures.
12
+ Enabling a sane development lifecycle for microservice-oriented architectures and LLM-driven tool chains.
13
+
14
+ Use mockstack for:
15
+
16
+ * **Development** :pencil2:. Simulating HTTP-based interactions between a particular component you're developing or debugging locally and multiple other components it depends on during execution of a particular flow. You can create template-based mock responses, simulate creation of resources in a realistic way, as well as proxy to other services using a rich rules DSL. Full request and response metadata and payloads can be observed via OpenTelemetry integration.
17
+
18
+ * **Integration Testing** :ok_hand:. Creating a consistent environment for running integration tests on a single component, using fixture responses.
19
+
20
+ * **LLM-powered Workflows** :alien:. Speeding up development of LLM-based workflows and tools for use with frameworks such as [LangChain](https://python.langchain.com/docs/introduction/), [LangGraph](https://www.langchain.com/langgraph) and others. When developing LLM-driven execution graphs, you often want to have a consistent response from a tool while you're tuning prompts or debugging other aspects of a particular trace. `mockstack` can give you a solid foundation for this.
21
+
22
+ * **Chaos Engineering** :boom:. mockstack can simulate various real-world runtime error scenarios such as timeouts, http error codes, and invalid response payloads. This can be a great way to do some upfront "chaos monkey" type of testing on software components.
13
23
 
14
24
  Highlights include:
15
25
 
@@ -32,6 +42,8 @@ or install into a persistent environment and add it to the PATH with:
32
42
 
33
43
  ## Usage
34
44
 
45
+ See the [examples](https://github.com/adamhadani/mockstack/blob/main/examples/) directory for complete examples with documentation.
46
+
35
47
  Available configuration options are [here](https://github.com/adamhadani/mockstack/blob/main/mockstack/config.py).
36
48
 
37
49
  Setting individual options can be done either through an `.env` file, individual environment variables, or command-line arguments. For example:
@@ -0,0 +1,13 @@
1
+ # Example environment variables for using filefixtures strategy
2
+
3
+ # the strategy to use
4
+ MOCKSTACK__STRATEGY = filefixtures
5
+
6
+ # strategy-specific settings
7
+
8
+ # base dir for all templates. Used by 'filefixtures' strategy
9
+ # and other hybrid strategies utilizing templates.
10
+ MOCKSTACK__TEMPLATES_DIR = "./templates/"
11
+
12
+ # OpenTelemetry settings
13
+ MOCKSTACK__OPENTELEMETRY__ENABLED = false
@@ -0,0 +1,10 @@
1
+ # filefixtures example
2
+
3
+ This folder shows an example setup for using mockstack with the `filefixtures` strategy which lets you route
4
+ requests to template files.
5
+
6
+ The folder is comprised of the following:
7
+
8
+ * An `.env.example` file which you would rename `.env` and contains the relevant configuration options needed to run. When you have that file in the working directory when invoking mockstack it will automatically pick up the settings from this file.
9
+ * A `templates/` dir, pointed to be a configuration in the env file, which contains our templates. We have a few example templates showcasing some of the capabilities of template-based mocking. One template file shows how you can have Jinja2 templating conditionals in the template e.g. to handle different responses based on request query parameters. Another template shows the naming convention for templates that depend on an identifier embedded in the request URL, e.g. `GET
10
+ /someservice/api/v1/item/ae420979-33f3-4c99-bc42-9d7cdee5259e` would get routed to the template file `someservice-api-v1-item.ae420979-33f3-4c99-bc42-9d7cdee5259e.j2`. Multiple identifiers in the path are also supported and would appear in filenames separated by dots, according to the order in which they appear in the URL.
@@ -0,0 +1,19 @@
1
+ {% if query.item_id == "533ec889-7c68-45c8-b21e-4a7e455de123" %}
2
+ {
3
+ "items": [
4
+ "createdAt": "2023-05-02T11:48:16.708081+00:00",
5
+ "id": "533ec889-7c68-45c8-b21e-4a7e455de123",
6
+ },
7
+ ],
8
+ "count": 1,
9
+ "limit": 20,
10
+ "offset": 0
11
+ }
12
+ {% else %}
13
+ {
14
+ "items": [],
15
+ "count": 0,
16
+ "limit": 20,
17
+ "offset": 0
18
+ }
19
+ {% endif %}
@@ -0,0 +1,14 @@
1
+ {
2
+ "address": {
3
+ "city": "San Francisco",
4
+ "country": "United States",
5
+ "region": "California",
6
+ "streetAddress1": "123 market st.",
7
+ "streetAddress2": "null",
8
+ "zipCode": ""
9
+ },
10
+ "aliases": [],
11
+ "id": "533ec889-7c68-45c8-b21e-4a7e455d1234",
12
+ "name": "John Doe",
13
+ "updatedAt": "2025-04-15T18:34:17.067320+00:00"
14
+ }
@@ -0,0 +1,13 @@
1
+ # Example environment variables for using filefixtures strategy
2
+
3
+ # the strategy to use
4
+ MOCKSTACK__STRATEGY = proxyrules
5
+
6
+ # strategy-specific settings
7
+
8
+ MOCKSTACK__PROXYRULES_RULES_FILENAME = "./proxyrules.yml"
9
+ MOCKSTACK__PROXYRULES_SIMULATE_CREATE_ON_MISSING = true
10
+
11
+
12
+ # OpenTelemetry settings
13
+ MOCKSTACK__OPENTELEMETRY__ENABLED = false
@@ -0,0 +1,15 @@
1
+ # proxyrules
2
+
3
+ This example shows how to use the `proxyrules` strategy. This strategy lets you define rules in a YAML-based microformat that control how to proxy requests coming into mockstack to the other services.
4
+
5
+ * The included `.env.example` file should be renamed `.env` and contains the needed configuration to use this strategy and point to the rules file.
6
+ * The rules file `rules.yml` showcases some of the capabilities supported for rule-based proxying.
7
+
8
+
9
+ Some of the notable features for this strategy are:
10
+
11
+ * Can configure whether proxuying is done via Http redirects (Temporary Redirect, Permanent Redirect) or reverse proxying (e.g. silently re-routing request). Default is to reverse proxy.
12
+ * Can configure rules per URL mask and optionally limit a rule to specific HTTP methods.
13
+ * Can use regular expression capture groups to refer to matched groups in the "pattern" field when constructring the destination URL.
14
+ * All request metadata and content, including headers, query parameters, and request body when applicable (e.g. for POSTs) will be proxied through.
15
+ * Can simulate creation of resources for cases where we do not wish to proxy the request to a "real" service where creation might have undesirable sideeffects, and instead wish to simply simulate a realistic flow of creating a new resource. This is powered by the same mixin functionality thats used by the other strategies for simulation creation.
@@ -0,0 +1,13 @@
1
+ rules:
2
+ - name: someservice-get
3
+ method: GET
4
+ pattern: ^/someservice/(.*)
5
+ replacement: https://someservice.development.mycoolstartup.io/\1
6
+ - name: someservice-items-search
7
+ method: POST
8
+ pattern: ^/someservice/api/v1/items_search
9
+ replacement: https://someservice.development.mycoolstartup.io/api/v1/items_search
10
+ - name: someotherservice-get
11
+ method: GET
12
+ pattern: ^/someotherservice/(.*)
13
+ replacement: https://someotherservice.development.mycoolstartup.io/\1
@@ -68,6 +68,9 @@ class Settings(BaseSettings):
68
68
  # or reverse proxy the request to the target URL "silently".
69
69
  proxyrules_redirect_via: ProxyRulesRedirectVia = ProxyRulesRedirectVia.REVERSE_PROXY
70
70
 
71
+ # Default timeout for reverse proxy requests. given in seconds. None disables timeouts.
72
+ proxyrules_reverse_proxy_timeout: float | None = 10.0
73
+
71
74
  # controls behavior of proxying. Whether to simulate creation of resources
72
75
  # when a POST request is made to a resource that doesn't match any rules..
73
76
  proxyrules_simulate_create_on_missing: CliImplicitFlag[bool] = False
@@ -20,3 +20,6 @@ class ProxyRulesRedirectVia(StrEnum):
20
20
  HTTP_TEMPORARY_REDIRECT = "http_307_temporary"
21
21
  HTTP_PERMANENT_REDIRECT = "http_301_permanent"
22
22
  REVERSE_PROXY = "reverse_proxy"
23
+
24
+
25
+ SENSITIVE_HEADERS = ["authorization", "cookie", "set-cookie"]
@@ -10,6 +10,20 @@ from mockstack.config import Settings
10
10
  from mockstack.display import announce
11
11
 
12
12
 
13
+ def logging_dict_config_from(settings: Settings) -> dict:
14
+ """Get the logging config from the settings."""
15
+
16
+ def enable_debug_logging(settings: Settings):
17
+ """Enable verbose debug logging."""
18
+ settings.logging["handlers"]["console"]["level"] = DEBUG
19
+
20
+ if settings.debug:
21
+ # Enable verbose debug logging if debug mode is set.
22
+ enable_debug_logging(settings)
23
+
24
+ return settings.logging
25
+
26
+
13
27
  def lifespan_provider(
14
28
  settings: Settings,
15
29
  ) -> Callable:
@@ -21,11 +35,8 @@ def lifespan_provider(
21
35
 
22
36
  This is the context manager that FastAPI will use to manage the lifecycle of the application.
23
37
  """
24
- if settings.debug:
25
- # Enable verbose debug logging if debug mode is set.
26
- settings.logging["handlers"]["console"]["level"] = DEBUG
27
38
 
28
- config.dictConfig(settings.logging)
39
+ config.dictConfig(logging_dict_config_from(settings))
29
40
 
30
41
  announce(app, settings)
31
42
 
@@ -0,0 +1,48 @@
1
+ """Middleware definitionsfor the mockstack app."""
2
+
3
+ import time
4
+
5
+ from fastapi import FastAPI, Request
6
+ from opentelemetry import trace
7
+ from opentelemetry.propagate import extract
8
+
9
+ from mockstack.config import Settings
10
+ from mockstack.constants import SENSITIVE_HEADERS
11
+ from mockstack.telemetry import (
12
+ span_name_for,
13
+ with_request_attributes,
14
+ with_response_attributes,
15
+ with_response_body,
16
+ )
17
+
18
+
19
+ def middleware_provider(app: FastAPI, settings: Settings) -> None:
20
+ """Instrument the middlewares to the mockstack app."""
21
+
22
+ @app.middleware("http")
23
+ async def add_process_time_header(request: Request, call_next):
24
+ start_time = time.time()
25
+ response = await call_next(request)
26
+ process_time = time.time() - start_time
27
+ response.headers["X-Process-Time"] = str(process_time)
28
+ return response
29
+
30
+ @app.middleware("http")
31
+ async def instrument_opentelemetry(request: Request, call_next):
32
+ tracer = trace.get_tracer(__name__)
33
+ ctx = extract(request.headers)
34
+ with tracer.start_as_current_span(span_name_for(request), context=ctx) as span:
35
+ span = with_request_attributes(
36
+ request, span, sensitive_headers=SENSITIVE_HEADERS
37
+ )
38
+
39
+ response = await call_next(request)
40
+
41
+ span = with_response_attributes(
42
+ response, span, sensitive_headers=SENSITIVE_HEADERS
43
+ )
44
+
45
+ if settings.opentelemetry.capture_response_body:
46
+ response, span = await with_response_body(response, span)
47
+
48
+ return response
@@ -60,10 +60,9 @@ class ProxyRulesStrategy(BaseStrategy, CreateMixin):
60
60
  def __init__(self, settings: Settings, *args, **kwargs):
61
61
  super().__init__(settings, *args, **kwargs)
62
62
  self.rules_filename = settings.proxyrules_rules_filename
63
- self.proxyrules_redirect_via = settings.proxyrules_redirect_via
64
- self.proxyrules_simulate_create_on_missing = (
65
- settings.proxyrules_simulate_create_on_missing
66
- )
63
+ self.redirect_via = settings.proxyrules_redirect_via
64
+ self.reverse_proxy_timeout = settings.proxyrules_reverse_proxy_timeout
65
+ self.simulate_create_on_missing = settings.proxyrules_simulate_create_on_missing
67
66
  self.created_resource_metadata = settings.created_resource_metadata
68
67
 
69
68
  self.env = Environment()
@@ -72,8 +71,8 @@ class ProxyRulesStrategy(BaseStrategy, CreateMixin):
72
71
  return (
73
72
  f"[medium_purple]proxyrules[/medium_purple] "
74
73
  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]"
74
+ f"redirect_via: [medium_purple]{self.redirect_via}[/medium_purple]. "
75
+ f"simulate_create_on_missing: [medium_purple]{self.simulate_create_on_missing}[/medium_purple]"
77
76
  )
78
77
 
79
78
  @cached_property
@@ -101,9 +100,7 @@ class ProxyRulesStrategy(BaseStrategy, CreateMixin):
101
100
  f"No rule found for request: {request.method} {request.url.path}"
102
101
  )
103
102
 
104
- if self.proxyrules_simulate_create_on_missing and looks_like_a_create(
105
- request
106
- ):
103
+ if self.simulate_create_on_missing and looks_like_a_create(request):
107
104
  self.logger.info(
108
105
  f"Simulating resource creation for missing rule for {request.method} {request.url.path}"
109
106
  )
@@ -119,7 +116,7 @@ class ProxyRulesStrategy(BaseStrategy, CreateMixin):
119
116
  url = rule.apply(request)
120
117
  self.logger.info(f"Redirecting to: {url}")
121
118
 
122
- match self.proxyrules_redirect_via:
119
+ match self.redirect_via:
123
120
  case ProxyRulesRedirectVia.HTTP_TEMPORARY_REDIRECT:
124
121
  return RedirectResponse(
125
122
  url=url, status_code=status.HTTP_307_TEMPORARY_REDIRECT
@@ -135,13 +132,11 @@ class ProxyRulesStrategy(BaseStrategy, CreateMixin):
135
132
  return response
136
133
 
137
134
  case _:
138
- raise ValueError(
139
- f"Invalid redirect via value: {self.proxyrules_redirect_via=}"
140
- )
135
+ raise ValueError(f"Invalid redirect via value: {self.redirect_via=}")
141
136
 
142
137
  async def reverse_proxy(self, request: Request, url: str) -> Response:
143
138
  """Reverse proxy the request to the target URL."""
144
- async with httpx.AsyncClient() as client:
139
+ async with httpx.AsyncClient(timeout=self.reverse_proxy_timeout) as client:
145
140
  request_content = await request.body()
146
141
  request_headers = self.reverse_proxy_headers(request.headers, url=url)
147
142
  req = client.build_request(
@@ -0,0 +1,141 @@
1
+ """OpenTelemetry integration."""
2
+
3
+ from importlib import metadata
4
+ from typing import List, Tuple
5
+
6
+ from fastapi import FastAPI, Request
7
+ from opentelemetry import trace
8
+ from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
9
+ from opentelemetry.sdk.resources import Resource
10
+ from opentelemetry.sdk.trace import TracerProvider
11
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
12
+ from opentelemetry.trace import Span
13
+ from starlette.responses import StreamingResponse, Response
14
+
15
+ from mockstack.config import Settings
16
+
17
+
18
+ def span_name_for(request: Request) -> str:
19
+ """Get the span name for a request."""
20
+ return f"{request.method.upper()} {request.url.path}"
21
+
22
+
23
+ def with_request_attributes(
24
+ request: Request, span: Span, *, sensitive_headers: List[str] = []
25
+ ) -> Span:
26
+ """Add request attributes to the span."""
27
+ span.set_attribute("http.method", request.method)
28
+ span.set_attribute("http.url", str(request.url))
29
+ span.set_attribute("http.scheme", request.url.scheme)
30
+ if request.url.hostname:
31
+ span.set_attribute("http.host", request.url.hostname)
32
+ span.set_attribute("http.target", request.url.path)
33
+ if request.url.port:
34
+ span.set_attribute("http.server_port", request.url.port)
35
+
36
+ # Client information
37
+ if request.client:
38
+ span.set_attribute("net.peer.ip", request.client.host)
39
+ if request.client.port:
40
+ span.set_attribute("net.peer.port", request.client.port)
41
+
42
+ # Request headers (excluding sensitive headers)
43
+ for header_name, header_value in request.headers.items():
44
+ if header_name.lower() not in sensitive_headers:
45
+ span.set_attribute(
46
+ f"http.request.header.{header_name.lower()}", header_value
47
+ )
48
+
49
+ # Query parameters
50
+ if request.query_params:
51
+ for param_name, param_value in request.query_params.items():
52
+ span.set_attribute(f"http.request.query.{param_name}", param_value)
53
+
54
+ return span
55
+
56
+
57
+ def with_response_attributes(
58
+ response: Response, span: Span, *, sensitive_headers: List[str] = []
59
+ ) -> Span:
60
+ """Add response attributes to the span."""
61
+ # Response attributes
62
+ span.set_attribute("http.status_code", response.status_code)
63
+ span.set_attribute(
64
+ "http.response_content_length", response.headers.get("content-length", 0)
65
+ )
66
+
67
+ # Response headers
68
+ for header_name, header_value in response.headers.items():
69
+ if header_name.lower() not in sensitive_headers:
70
+ span.set_attribute(
71
+ f"http.response.header.{header_name.lower()}", header_value
72
+ )
73
+
74
+ return span
75
+
76
+
77
+ async def with_response_body(
78
+ response: StreamingResponse, span: Span
79
+ ) -> Tuple[Response, Span]:
80
+ """Add the response body to the span."""
81
+ body = await extract_body(response)
82
+
83
+ # for semantics of payload attribute naming see:
84
+ # https://github.com/open-telemetry/oteps/pull/234
85
+ span.set_attribute("http.response.body", body)
86
+
87
+ # recreate response with the same body since when consuming it to log it above
88
+ # we effectively "deplete" the iterator.
89
+ _response = Response(
90
+ content=body,
91
+ status_code=response.status_code,
92
+ headers=dict(response.headers),
93
+ media_type=response.media_type,
94
+ )
95
+
96
+ return _response, span
97
+
98
+
99
+ async def extract_body(response: StreamingResponse) -> str:
100
+ """Extract the body of a response."""
101
+
102
+ async def read_response_body(response: StreamingResponse) -> bytes:
103
+ """Helper function to read response body asynchronously into memory."""
104
+ body = b""
105
+ async for chunk in response.body_iterator:
106
+ if isinstance(chunk, str):
107
+ body += chunk.encode()
108
+ else:
109
+ body += chunk
110
+ return body
111
+
112
+ body = await read_response_body(response)
113
+
114
+ return body.decode()
115
+
116
+
117
+ def opentelemetry_provider(app: FastAPI, settings: Settings) -> None:
118
+ """Initialize OpenTelemetry for the mockstack app."""
119
+ if not settings.opentelemetry.enabled:
120
+ return
121
+
122
+ # Initialize OpenTelemetry
123
+ distribution = metadata.distribution("mockstack")
124
+ resource = Resource(
125
+ attributes={
126
+ "service.name": distribution.name,
127
+ "service.version": distribution.version,
128
+ }
129
+ )
130
+
131
+ tracer_provider = TracerProvider(resource=resource)
132
+ trace.set_tracer_provider(tracer_provider)
133
+
134
+ # Set up OTLP exporter
135
+ otlp_exporter = OTLPSpanExporter(endpoint=settings.opentelemetry.endpoint)
136
+ span_processor = BatchSpanProcessor(otlp_exporter)
137
+ tracer_provider.add_span_processor(span_processor)
138
+
139
+ # Nb. we do not actually use the default FastAPIInstrumentor here
140
+ # because we use custom tracing in various places.
141
+ # FastAPIInstrumentor.instrument_app(app)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mockstack
3
- Version: 0.4.2
3
+ Version: 0.5.0
4
4
  Summary: An API mocking workhorse
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/adamhadani/mockstack
@@ -34,7 +34,17 @@ Dynamic: license-file
34
34
 
35
35
  An API mocking workhorse :racehorse:
36
36
 
37
- Enabling a sane development lifecycle for microservice-oriented architectures.
37
+ Enabling a sane development lifecycle for microservice-oriented architectures and LLM-driven tool chains.
38
+
39
+ Use mockstack for:
40
+
41
+ * **Development** :pencil2:. Simulating HTTP-based interactions between a particular component you're developing or debugging locally and multiple other components it depends on during execution of a particular flow. You can create template-based mock responses, simulate creation of resources in a realistic way, as well as proxy to other services using a rich rules DSL. Full request and response metadata and payloads can be observed via OpenTelemetry integration.
42
+
43
+ * **Integration Testing** :ok_hand:. Creating a consistent environment for running integration tests on a single component, using fixture responses.
44
+
45
+ * **LLM-powered Workflows** :alien:. Speeding up development of LLM-based workflows and tools for use with frameworks such as [LangChain](https://python.langchain.com/docs/introduction/), [LangGraph](https://www.langchain.com/langgraph) and others. When developing LLM-driven execution graphs, you often want to have a consistent response from a tool while you're tuning prompts or debugging other aspects of a particular trace. `mockstack` can give you a solid foundation for this.
46
+
47
+ * **Chaos Engineering** :boom:. mockstack can simulate various real-world runtime error scenarios such as timeouts, http error codes, and invalid response payloads. This can be a great way to do some upfront "chaos monkey" type of testing on software components.
38
48
 
39
49
  Highlights include:
40
50
 
@@ -57,6 +67,8 @@ or install into a persistent environment and add it to the PATH with:
57
67
 
58
68
  ## Usage
59
69
 
70
+ See the [examples](https://github.com/adamhadani/mockstack/blob/main/examples/) directory for complete examples with documentation.
71
+
60
72
  Available configuration options are [here](https://github.com/adamhadani/mockstack/blob/main/mockstack/config.py).
61
73
 
62
74
  Setting individual options can be done either through an `.env` file, individual environment variables, or command-line arguments. For example:
@@ -14,6 +14,13 @@ uv.lock
14
14
  docs/assets/favicon.ico
15
15
  docs/assets/logo.png
16
16
  docs/assets/mockstack.png
17
+ examples/filefixtures-with-templates/.env.example
18
+ examples/filefixtures-with-templates/README.md
19
+ examples/filefixtures-with-templates/templates/servicename-api-v1-items.j2
20
+ examples/filefixtures-with-templates/templates/servicename2-api-v2-user.533ec889-7c68-45c8-b21e-4a7e455d1234.j2
21
+ examples/proxyrules-with-rules-file/.env.example
22
+ examples/proxyrules-with-rules-file/README.md
23
+ examples/proxyrules-with-rules-file/rules.yml
17
24
  mockstack/__init__.py
18
25
  mockstack/config.py
19
26
  mockstack/constants.py
@@ -1,55 +0,0 @@
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
@@ -1,63 +0,0 @@
1
- """OpenTelemetry integration."""
2
-
3
- from importlib import metadata
4
-
5
- from fastapi import FastAPI, Request
6
- from opentelemetry import trace
7
- from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
8
- from opentelemetry.sdk.resources import Resource
9
- from opentelemetry.sdk.trace import TracerProvider
10
- from opentelemetry.sdk.trace.export import BatchSpanProcessor
11
- from starlette.responses import StreamingResponse
12
-
13
- from mockstack.config import Settings
14
-
15
-
16
- def span_name_for(request: Request) -> str:
17
- """Get the span name for a request."""
18
- return f"{request.method.upper()} {request.url.path}"
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
-
39
- def opentelemetry_provider(app: FastAPI, settings: Settings) -> None:
40
- """Initialize OpenTelemetry for the mockstack app."""
41
- if not settings.opentelemetry.enabled:
42
- return
43
-
44
- # Initialize OpenTelemetry
45
- distribution = metadata.distribution("mockstack")
46
- resource = Resource(
47
- attributes={
48
- "service.name": distribution.name,
49
- "service.version": distribution.version,
50
- }
51
- )
52
-
53
- tracer_provider = TracerProvider(resource=resource)
54
- trace.set_tracer_provider(tracer_provider)
55
-
56
- # Set up OTLP exporter
57
- otlp_exporter = OTLPSpanExporter(endpoint=settings.opentelemetry.endpoint)
58
- span_processor = BatchSpanProcessor(otlp_exporter)
59
- tracer_provider.add_span_processor(span_processor)
60
-
61
- # Nb. we do not actually use the default FastAPIInstrumentor here
62
- # because we use custom tracing in various places.
63
- # FastAPIInstrumentor.instrument_app(app)
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