mockstack 0.5.0__tar.gz → 0.5.1__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 (65) hide show
  1. {mockstack-0.5.0 → mockstack-0.5.1}/PKG-INFO +1 -1
  2. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack/middleware.py +5 -0
  3. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack/strategies/base.py +9 -0
  4. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack/strategies/filefixtures.py +9 -0
  5. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack/strategies/proxyrules.py +22 -1
  6. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack/tests/conftest.py +11 -1
  7. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack/tests/strategies/test_filefixtures.py +6 -3
  8. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack/tests/strategies/test_proxyrules.py +10 -5
  9. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack.egg-info/PKG-INFO +1 -1
  10. {mockstack-0.5.0 → mockstack-0.5.1}/.env.example +0 -0
  11. {mockstack-0.5.0 → mockstack-0.5.1}/.github/workflows/ci.yml +0 -0
  12. {mockstack-0.5.0 → mockstack-0.5.1}/.github/workflows/publish-docs.yml +0 -0
  13. {mockstack-0.5.0 → mockstack-0.5.1}/.github/workflows/publish-to-pypi.yml +0 -0
  14. {mockstack-0.5.0 → mockstack-0.5.1}/.gitignore +0 -0
  15. {mockstack-0.5.0 → mockstack-0.5.1}/.pre-commit-config.yaml +0 -0
  16. {mockstack-0.5.0 → mockstack-0.5.1}/CODE_OF_CONDUCT.md +0 -0
  17. {mockstack-0.5.0 → mockstack-0.5.1}/LICENSE +0 -0
  18. {mockstack-0.5.0 → mockstack-0.5.1}/README.md +0 -0
  19. {mockstack-0.5.0 → mockstack-0.5.1}/SECURITY.md +0 -0
  20. {mockstack-0.5.0 → mockstack-0.5.1}/docs/assets/favicon.ico +0 -0
  21. {mockstack-0.5.0 → mockstack-0.5.1}/docs/assets/logo.png +0 -0
  22. {mockstack-0.5.0 → mockstack-0.5.1}/docs/assets/mockstack.png +0 -0
  23. {mockstack-0.5.0 → mockstack-0.5.1}/examples/filefixtures-with-templates/.env.example +0 -0
  24. {mockstack-0.5.0 → mockstack-0.5.1}/examples/filefixtures-with-templates/README.md +0 -0
  25. {mockstack-0.5.0 → mockstack-0.5.1}/examples/filefixtures-with-templates/templates/servicename-api-v1-items.j2 +0 -0
  26. {mockstack-0.5.0 → mockstack-0.5.1}/examples/filefixtures-with-templates/templates/servicename2-api-v2-user.533ec889-7c68-45c8-b21e-4a7e455d1234.j2 +0 -0
  27. {mockstack-0.5.0 → mockstack-0.5.1}/examples/proxyrules-with-rules-file/.env.example +0 -0
  28. {mockstack-0.5.0 → mockstack-0.5.1}/examples/proxyrules-with-rules-file/README.md +0 -0
  29. {mockstack-0.5.0 → mockstack-0.5.1}/examples/proxyrules-with-rules-file/rules.yml +0 -0
  30. {mockstack-0.5.0 → mockstack-0.5.1}/mkdocs.yml +0 -0
  31. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack/__init__.py +0 -0
  32. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack/config.py +0 -0
  33. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack/constants.py +0 -0
  34. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack/display.py +0 -0
  35. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack/identifiers.py +0 -0
  36. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack/intent.py +0 -0
  37. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack/lifespan.py +0 -0
  38. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack/main.py +0 -0
  39. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack/routers/__init__.py +0 -0
  40. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack/routers/catchall.py +0 -0
  41. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack/routers/homepage.py +0 -0
  42. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack/strategies/__init__.py +0 -0
  43. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack/strategies/create_mixin.py +0 -0
  44. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack/strategies/factory.py +0 -0
  45. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack/telemetry.py +0 -0
  46. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack/templating.py +0 -0
  47. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack/tests/__init__.py +0 -0
  48. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack/tests/fixtures/proxyrules.yml +0 -0
  49. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack/tests/fixtures/templates/__init__.py +0 -0
  50. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack/tests/fixtures/templates/example-template.j2 +0 -0
  51. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack/tests/routers/__init__.py +0 -0
  52. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack/tests/routers/test_catchall.py +0 -0
  53. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack/tests/routers/test_homepage.py +0 -0
  54. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack/tests/test_display.py +0 -0
  55. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack/tests/test_identifiers.py +0 -0
  56. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack/tests/test_middleware.py +0 -0
  57. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack/tests/test_templating.py +0 -0
  58. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack.egg-info/SOURCES.txt +0 -0
  59. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack.egg-info/dependency_links.txt +0 -0
  60. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack.egg-info/entry_points.txt +0 -0
  61. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack.egg-info/requires.txt +0 -0
  62. {mockstack-0.5.0 → mockstack-0.5.1}/mockstack.egg-info/top_level.txt +0 -0
  63. {mockstack-0.5.0 → mockstack-0.5.1}/pyproject.toml +0 -0
  64. {mockstack-0.5.0 → mockstack-0.5.1}/setup.cfg +0 -0
  65. {mockstack-0.5.0 → mockstack-0.5.1}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mockstack
3
- Version: 0.5.0
3
+ Version: 0.5.1
4
4
  Summary: An API mocking workhorse
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/adamhadani/mockstack
@@ -36,6 +36,11 @@ def middleware_provider(app: FastAPI, settings: Settings) -> None:
36
36
  request, span, sensitive_headers=SENSITIVE_HEADERS
37
37
  )
38
38
 
39
+ # Make the current opentelemetry span available to the request.
40
+ # This is useful for strategies that need to add custom attributes
41
+ # to the span associated with the request.
42
+ request.state.span = span
43
+
39
44
  response = await call_next(request)
40
45
 
41
46
  span = with_response_attributes(
@@ -17,3 +17,12 @@ class BaseStrategy(ABC):
17
17
  async def apply(self, request: Request) -> Response:
18
18
  """Apply the strategy to the request and response."""
19
19
  pass
20
+
21
+ def update_opentelemetry(self, request: Request, *args, **kwargs) -> None:
22
+ """Update the opentelemetry span with strategy-specific attributes.
23
+
24
+ A span is made available on `request.state.span` to use.
25
+ When OpenTelemetry is not enabled, this span will exist but will not be reported.
26
+
27
+ """
28
+ pass
@@ -129,6 +129,7 @@ class FileFixturesStrategy(BaseStrategy, CreateMixin):
129
129
  continue
130
130
 
131
131
  self.logger.debug("Found template filename: %s", filename)
132
+ self.update_opentelemetry(request, template_args)
132
133
  template = self.env.get_template(template_args["name"])
133
134
 
134
135
  return Response(
@@ -149,3 +150,11 @@ class FileFixturesStrategy(BaseStrategy, CreateMixin):
149
150
  status_code=status.HTTP_404_NOT_FOUND,
150
151
  )
151
152
  """
153
+
154
+ def update_opentelemetry(self, request: Request, template_args: dict) -> None:
155
+ """Update the opentelemetry span with the file fixtures details."""
156
+ span = request.state.span
157
+
158
+ span.set_attribute(
159
+ "mockstack.filefixtures.template_name", template_args["name"]
160
+ )
@@ -23,10 +23,17 @@ from mockstack.strategies.create_mixin import CreateMixin
23
23
  class Rule:
24
24
  """A rule for the proxy rules strategy."""
25
25
 
26
- def __init__(self, pattern: str, replacement: str, method: str | None = None):
26
+ def __init__(
27
+ self,
28
+ pattern: str,
29
+ replacement: str,
30
+ method: str | None = None,
31
+ name: str | None = None,
32
+ ):
27
33
  self.pattern = pattern
28
34
  self.replacement = replacement
29
35
  self.method = method
36
+ self.name = name
30
37
 
31
38
  @classmethod
32
39
  def from_dict(cls, data: dict[str, str]) -> Self:
@@ -34,6 +41,7 @@ class Rule:
34
41
  pattern=data["pattern"],
35
42
  replacement=data["replacement"],
36
43
  method=data.get("method", None),
44
+ name=data.get("name", None),
37
45
  )
38
46
 
39
47
  def matches(self, request: Request) -> bool:
@@ -115,6 +123,7 @@ class ProxyRulesStrategy(BaseStrategy, CreateMixin):
115
123
 
116
124
  url = rule.apply(request)
117
125
  self.logger.info(f"Redirecting to: {url}")
126
+ self.update_opentelemetry(request, rule, url)
118
127
 
119
128
  match self.redirect_via:
120
129
  case ProxyRulesRedirectVia.HTTP_TEMPORARY_REDIRECT:
@@ -165,3 +174,15 @@ class ProxyRulesStrategy(BaseStrategy, CreateMixin):
165
174
  _headers["host"] = urlparse(url).netloc
166
175
 
167
176
  return _headers
177
+
178
+ def update_opentelemetry(self, request: Request, rule: Rule, url: str) -> None:
179
+ """Update the opentelemetry span with the proxy rules rule details."""
180
+ span = request.state.span
181
+ if rule.name is not None:
182
+ span.set_attribute("mockstack.proxyrules.rule_name", rule.name)
183
+ if rule.method is not None:
184
+ span.set_attribute("mockstack.proxyrules.rule_method", rule.method)
185
+
186
+ span.set_attribute("mockstack.proxyrules.rule_pattern", rule.pattern)
187
+ span.set_attribute("mockstack.proxyrules.rule_replacement", rule.replacement)
188
+ span.set_attribute("mockstack.proxyrules.rewritten_url", url)
@@ -1,6 +1,7 @@
1
1
  """Shared fixtures for the unit-tests."""
2
2
 
3
3
  import os
4
+ from unittest.mock import MagicMock
4
5
 
5
6
  import pytest
6
7
  from fastapi import FastAPI
@@ -11,10 +12,19 @@ from mockstack.strategies.filefixtures import FileFixturesStrategy
11
12
 
12
13
 
13
14
  @pytest.fixture
14
- def app(settings):
15
+ def span():
16
+ """Create a mock span object for testing."""
17
+ span = MagicMock()
18
+ span.set_attribute = MagicMock()
19
+ return span
20
+
21
+
22
+ @pytest.fixture
23
+ def app(settings, span):
15
24
  """Create a FastAPI app for testing."""
16
25
  app = FastAPI()
17
26
  app.state.strategy = FileFixturesStrategy(settings)
27
+ app.state.span = span
18
28
  return app
19
29
 
20
30
 
@@ -18,7 +18,7 @@ def test_filefixtures_strategy_init(settings):
18
18
 
19
19
 
20
20
  @pytest.mark.asyncio
21
- async def test_filefixtures_strategy_apply(settings):
21
+ async def test_filefixtures_strategy_apply(settings, span):
22
22
  """Test the FileFixturesStrategy apply method."""
23
23
  strategy = FileFixturesStrategy(settings)
24
24
  request = Request(
@@ -30,6 +30,7 @@ async def test_filefixtures_strategy_apply(settings):
30
30
  "headers": [],
31
31
  }
32
32
  )
33
+ request.state.span = span
33
34
 
34
35
  with pytest.raises(HTTPException) as exc_info:
35
36
  await strategy.apply(request)
@@ -38,7 +39,7 @@ async def test_filefixtures_strategy_apply(settings):
38
39
 
39
40
 
40
41
  @pytest.mark.asyncio
41
- async def test_file_fixtures_strategy_apply_success(settings):
42
+ async def test_file_fixtures_strategy_apply_success(settings, span):
42
43
  """Test the FileFixturesStrategy apply method when template exists."""
43
44
  # Setup
44
45
  strategy = FileFixturesStrategy(settings)
@@ -61,6 +62,7 @@ async def test_file_fixtures_strategy_apply_success(settings):
61
62
  "headers": [],
62
63
  }
63
64
  )
65
+ request.state.span = span
64
66
 
65
67
  # Execute
66
68
  response = await strategy.apply(request)
@@ -72,7 +74,7 @@ async def test_file_fixtures_strategy_apply_success(settings):
72
74
 
73
75
 
74
76
  @pytest.mark.asyncio
75
- async def test_file_fixtures_strategy_apply_template_not_found(settings):
77
+ async def test_file_fixtures_strategy_apply_template_not_found(settings, span):
76
78
  """Test the FileFixturesStrategy apply method when template doesn't exist."""
77
79
  # Setup
78
80
  strategy = FileFixturesStrategy(settings)
@@ -88,6 +90,7 @@ async def test_file_fixtures_strategy_apply_template_not_found(settings):
88
90
  "headers": [],
89
91
  }
90
92
  )
93
+ request.state.span = span
91
94
 
92
95
  # Execute and Assert
93
96
  with pytest.raises(HTTPException) as exc_info:
@@ -124,7 +124,7 @@ def test_proxy_rules_strategy_load_rules(settings):
124
124
  assert all(isinstance(rule, Rule) for rule in rules)
125
125
 
126
126
 
127
- def test_proxy_rules_strategy_rule_for(settings):
127
+ def test_proxy_rules_strategy_rule_for(settings, span):
128
128
  """Test finding a matching rule for a request."""
129
129
  strategy = ProxyRulesStrategy(settings)
130
130
  request = Request(
@@ -136,12 +136,13 @@ def test_proxy_rules_strategy_rule_for(settings):
136
136
  "headers": [],
137
137
  }
138
138
  )
139
+ request.state.span = span
139
140
  rule = strategy.rule_for(request)
140
141
  assert rule is not None
141
142
  assert isinstance(rule, Rule)
142
143
 
143
144
 
144
- def test_proxy_rules_strategy_rule_for_no_match(settings):
145
+ def test_proxy_rules_strategy_rule_for_no_match(settings, span):
145
146
  """Test when no rule matches a request."""
146
147
  strategy = ProxyRulesStrategy(settings)
147
148
  request = Request(
@@ -153,12 +154,13 @@ def test_proxy_rules_strategy_rule_for_no_match(settings):
153
154
  "headers": [],
154
155
  }
155
156
  )
157
+ request.state.span = span
156
158
  rule = strategy.rule_for(request)
157
159
  assert rule is None
158
160
 
159
161
 
160
162
  @pytest.mark.asyncio
161
- async def test_proxy_rules_strategy_apply(settings):
163
+ async def test_proxy_rules_strategy_apply(settings, span):
162
164
  """Test applying a rule to a request."""
163
165
  strategy = ProxyRulesStrategy(settings)
164
166
  request = Request(
@@ -170,13 +172,14 @@ async def test_proxy_rules_strategy_apply(settings):
170
172
  "headers": [],
171
173
  }
172
174
  )
175
+ request.state.span = span
173
176
  response = await strategy.apply(request)
174
177
  assert isinstance(response, RedirectResponse)
175
178
  assert response.headers["location"] == "/projects/123"
176
179
 
177
180
 
178
181
  @pytest.mark.asyncio
179
- async def test_proxy_rules_strategy_apply_no_match(settings):
182
+ async def test_proxy_rules_strategy_apply_no_match(settings, span):
180
183
  """Test applying strategy when no rule matches."""
181
184
  strategy = ProxyRulesStrategy(settings)
182
185
  request = Request(
@@ -188,13 +191,14 @@ async def test_proxy_rules_strategy_apply_no_match(settings):
188
191
  "headers": [],
189
192
  }
190
193
  )
194
+ request.state.span = span
191
195
  response = await strategy.apply(request)
192
196
  assert response.status_code == 404
193
197
 
194
198
 
195
199
  @pytest.mark.asyncio
196
200
  @pytest.mark.skip(reason="TODO: Fix this test")
197
- async def test_proxy_rules_strategy_apply_reverse_proxy(settings_reverse_proxy):
201
+ async def test_proxy_rules_strategy_apply_reverse_proxy(settings_reverse_proxy, span):
198
202
  """Test applying a rule to a request with reverse proxy enabled."""
199
203
  # Mock the httpx.AsyncClient to avoid making real HTTP requests
200
204
  mock_response = MagicMock() # Use MagicMock for response to avoid async attributes
@@ -218,6 +222,7 @@ async def test_proxy_rules_strategy_apply_reverse_proxy(settings_reverse_proxy):
218
222
  "headers": [("host", "example.com")],
219
223
  }
220
224
  )
225
+ request.state.span = span
221
226
  response = await strategy.apply(request)
222
227
 
223
228
  # Verify the response
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mockstack
3
- Version: 0.5.0
3
+ Version: 0.5.1
4
4
  Summary: An API mocking workhorse
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/adamhadani/mockstack
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