mockstack 0.11.2__tar.gz → 0.12.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 (95) hide show
  1. {mockstack-0.11.2 → mockstack-0.12.1}/.github/workflows/ci.yml +3 -7
  2. {mockstack-0.11.2 → mockstack-0.12.1}/.github/workflows/publish-docs.yml +3 -3
  3. {mockstack-0.11.2 → mockstack-0.12.1}/.github/workflows/publish-to-pypi.yml +6 -6
  4. mockstack-0.12.1/.github/workflows/smoke-test-pypi.yml +83 -0
  5. {mockstack-0.11.2 → mockstack-0.12.1}/PKG-INFO +1 -1
  6. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/routers/catchall.py +3 -1
  7. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/tests/strategies/test_proxyrules.py +22 -1
  8. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/tests/test_rules.py +41 -11
  9. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack.egg-info/PKG-INFO +1 -1
  10. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack.egg-info/SOURCES.txt +1 -0
  11. mockstack-0.12.1/uv.lock +2030 -0
  12. mockstack-0.11.2/uv.lock +0 -1535
  13. {mockstack-0.11.2 → mockstack-0.12.1}/.dockerignore +0 -0
  14. {mockstack-0.11.2 → mockstack-0.12.1}/.env.example +0 -0
  15. {mockstack-0.11.2 → mockstack-0.12.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  16. {mockstack-0.11.2 → mockstack-0.12.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  17. {mockstack-0.11.2 → mockstack-0.12.1}/.gitignore +0 -0
  18. {mockstack-0.11.2 → mockstack-0.12.1}/.pre-commit-config.yaml +0 -0
  19. {mockstack-0.11.2 → mockstack-0.12.1}/CODE_OF_CONDUCT.md +0 -0
  20. {mockstack-0.11.2 → mockstack-0.12.1}/CONTRIBUTING.md +0 -0
  21. {mockstack-0.11.2 → mockstack-0.12.1}/Dockerfile +0 -0
  22. {mockstack-0.11.2 → mockstack-0.12.1}/LICENSE +0 -0
  23. {mockstack-0.11.2 → mockstack-0.12.1}/README.md +0 -0
  24. {mockstack-0.11.2 → mockstack-0.12.1}/SECURITY.md +0 -0
  25. {mockstack-0.11.2 → mockstack-0.12.1}/docker-entrypoint.sh +0 -0
  26. {mockstack-0.11.2 → mockstack-0.12.1}/docs/assets/favicon.ico +0 -0
  27. {mockstack-0.11.2 → mockstack-0.12.1}/docs/assets/logo.png +0 -0
  28. {mockstack-0.11.2 → mockstack-0.12.1}/docs/assets/mockstack.png +0 -0
  29. {mockstack-0.11.2 → mockstack-0.12.1}/docs/configuration.md +0 -0
  30. {mockstack-0.11.2 → mockstack-0.12.1}/docs/ollama.md +0 -0
  31. {mockstack-0.11.2 → mockstack-0.12.1}/docs/strategies/base.md +0 -0
  32. {mockstack-0.11.2 → mockstack-0.12.1}/docs/strategies/filefixtures.md +0 -0
  33. {mockstack-0.11.2 → mockstack-0.12.1}/docs/strategies/proxyrules.md +0 -0
  34. {mockstack-0.11.2 → mockstack-0.12.1}/examples/filefixtures-with-templates/.env.example +0 -0
  35. {mockstack-0.11.2 → mockstack-0.12.1}/examples/filefixtures-with-templates/README.md +0 -0
  36. {mockstack-0.11.2 → mockstack-0.12.1}/examples/filefixtures-with-templates/templates/servicename-api-v1-items.j2 +0 -0
  37. {mockstack-0.11.2 → mockstack-0.12.1}/examples/filefixtures-with-templates/templates/servicename2-api-v2-user.533ec889-7c68-45c8-b21e-4a7e455d1234.j2 +0 -0
  38. {mockstack-0.11.2 → mockstack-0.12.1}/examples/llm/README.md +0 -0
  39. {mockstack-0.11.2 → mockstack-0.12.1}/examples/llm/mockstack-langchain-example.ipynb +0 -0
  40. {mockstack-0.11.2 → mockstack-0.12.1}/examples/llm/templates/ollama-openai-v1-chat-completions.j2 +0 -0
  41. {mockstack-0.11.2 → mockstack-0.12.1}/examples/llm/templates/openai-v1-chat-completions.j2 +0 -0
  42. {mockstack-0.11.2 → mockstack-0.12.1}/examples/llm/templates/openai-v1-deployments-gpt-4o-chat-completions.j2 +0 -0
  43. {mockstack-0.11.2 → mockstack-0.12.1}/examples/proxyrules-with-rules-file/.env.example +0 -0
  44. {mockstack-0.11.2 → mockstack-0.12.1}/examples/proxyrules-with-rules-file/README.md +0 -0
  45. {mockstack-0.11.2 → mockstack-0.12.1}/examples/proxyrules-with-rules-file/rules.yml +0 -0
  46. {mockstack-0.11.2 → mockstack-0.12.1}/mkdocs.yml +0 -0
  47. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/__init__.py +0 -0
  48. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/config.py +0 -0
  49. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/constants.py +0 -0
  50. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/display.py +0 -0
  51. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/exceptions.py +0 -0
  52. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/identifiers.py +0 -0
  53. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/intent.py +0 -0
  54. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/lifespan.py +0 -0
  55. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/llm/__init__.py +0 -0
  56. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/llm/ollama.py +0 -0
  57. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/main.py +0 -0
  58. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/middleware.py +0 -0
  59. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/routers/__init__.py +0 -0
  60. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/routers/homepage.py +0 -0
  61. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/rules.py +0 -0
  62. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/strategies/__init__.py +0 -0
  63. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/strategies/base.py +0 -0
  64. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/strategies/create_mixin.py +0 -0
  65. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/strategies/factory.py +0 -0
  66. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/strategies/filefixtures.py +0 -0
  67. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/strategies/proxyrules.py +0 -0
  68. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/telemetry.py +0 -0
  69. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/templating.py +0 -0
  70. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/tests/__init__.py +0 -0
  71. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/tests/conftest.py +0 -0
  72. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/tests/fixtures/__init__.py +0 -0
  73. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/tests/fixtures/proxyrules.yml +0 -0
  74. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/tests/fixtures/templates/__init__.py +0 -0
  75. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/tests/fixtures/templates/example-template.j2 +0 -0
  76. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/tests/llm/__init__.py +0 -0
  77. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/tests/llm/test_ollama.py +0 -0
  78. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/tests/routers/__init__.py +0 -0
  79. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/tests/routers/test_catchall.py +0 -0
  80. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/tests/routers/test_homepage.py +0 -0
  81. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/tests/strategies/__init__.py +0 -0
  82. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/tests/strategies/test_create_mixin.py +0 -0
  83. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/tests/strategies/test_filefixtures.py +0 -0
  84. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/tests/test_display.py +0 -0
  85. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/tests/test_identifiers.py +0 -0
  86. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/tests/test_intent.py +0 -0
  87. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/tests/test_middleware.py +0 -0
  88. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/tests/test_telemetry.py +0 -0
  89. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack/tests/test_templating.py +0 -0
  90. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack.egg-info/dependency_links.txt +0 -0
  91. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack.egg-info/entry_points.txt +0 -0
  92. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack.egg-info/requires.txt +0 -0
  93. {mockstack-0.11.2 → mockstack-0.12.1}/mockstack.egg-info/top_level.txt +0 -0
  94. {mockstack-0.11.2 → mockstack-0.12.1}/pyproject.toml +0 -0
  95. {mockstack-0.11.2 → mockstack-0.12.1}/setup.cfg +0 -0
@@ -23,20 +23,16 @@ jobs:
23
23
  python-version: ["3.11", "3.12", "3.13"]
24
24
 
25
25
  steps:
26
- - uses: actions/checkout@v4
26
+ - uses: actions/checkout@v6
27
27
  - name: Install uv and set the python version
28
- uses: astral-sh/setup-uv@v5
28
+ uses: astral-sh/setup-uv@v8.1.0
29
29
  with:
30
30
  python-version: ${{ matrix.python-version }}
31
31
  version: "latest"
32
32
  - name: Install ruff
33
- uses: astral-sh/ruff-action@v3
33
+ uses: astral-sh/ruff-action@v4.0.0
34
34
  with:
35
35
  version: "latest"
36
- - name: Set up Python ${{ matrix.python-version }}
37
- uses: actions/setup-python@v3
38
- with:
39
- python-version: ${{ matrix.python-version }}
40
36
  - name: Install the project
41
37
  run: uv sync --all-extras --dev
42
38
  - name: Run ruff lint and formatting checks
@@ -13,16 +13,16 @@ jobs:
13
13
  deploy:
14
14
  runs-on: ubuntu-latest
15
15
  steps:
16
- - uses: actions/checkout@v4
16
+ - uses: actions/checkout@v6
17
17
  - name: Configure Git Credentials
18
18
  run: |
19
19
  git config --global user.name "${GITHUB_ACTOR}"
20
20
  git config --global user.email "${GITHUB_ACTOR_ID}+${GITHUB_ACTOR}@users.noreply.github.com"
21
- - uses: actions/setup-python@v5
21
+ - uses: actions/setup-python@v6
22
22
  with:
23
23
  python-version: 3.x
24
24
  - run: echo "cache_id=$(date --utc '+%V')" >> "${GITHUB_ENV}"
25
- - uses: actions/cache@v4
25
+ - uses: actions/cache@v5
26
26
  with:
27
27
  key: mkdocs-material-${{ env.cache_id }}
28
28
  path: .cache
@@ -10,11 +10,11 @@ jobs:
10
10
  build:
11
11
  runs-on: ubuntu-latest
12
12
  steps:
13
- - uses: actions/checkout@v4
13
+ - uses: actions/checkout@v6
14
14
  with:
15
15
  persist-credentials: false
16
16
  - name: Set up Python
17
- uses: actions/setup-python@v5
17
+ uses: actions/setup-python@v6
18
18
  with:
19
19
  python-version: "3.x"
20
20
  - name: Install pypa/build
@@ -23,7 +23,7 @@ jobs:
23
23
  - name: Build a binary wheel and a source tarball
24
24
  run: python3 -m build
25
25
  - name: Store the distribution packages
26
- uses: actions/upload-artifact@v4
26
+ uses: actions/upload-artifact@v7
27
27
  with:
28
28
  name: python-package-distributions
29
29
  path: dist/
@@ -42,7 +42,7 @@ jobs:
42
42
  id-token: write # IMPORTANT: mandatory for trusted publishing
43
43
  steps:
44
44
  - name: Download all the dists
45
- uses: actions/download-artifact@v4
45
+ uses: actions/download-artifact@v8
46
46
  with:
47
47
  name: python-package-distributions
48
48
  path: dist/
@@ -62,7 +62,7 @@ jobs:
62
62
  id-token: write # IMPORTANT: mandatory for sigstore
63
63
  steps:
64
64
  - name: Download all the dists
65
- uses: actions/download-artifact@v4
65
+ uses: actions/download-artifact@v8
66
66
  with:
67
67
  name: python-package-distributions
68
68
  path: dist/
@@ -98,7 +98,7 @@ jobs:
98
98
  id-token: write # IMPORTANT: mandatory for trusted publishing
99
99
  steps:
100
100
  - name: Download all the dists
101
- uses: actions/download-artifact@v4
101
+ uses: actions/download-artifact@v8
102
102
  with:
103
103
  name: python-package-distributions
104
104
  path: dist/
@@ -0,0 +1,83 @@
1
+ # Weekly smoke test that installs mockstack from PyPI without the lock file
2
+ # and boots the server. Catches breaking changes in transitive dependencies
3
+ # (fastapi, starlette, uvicorn, pydantic, ...) that would otherwise only
4
+ # surface when end-users run `uvx mockstack`.
5
+
6
+ name: Smoke Test (PyPI, latest deps)
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ on:
12
+ schedule:
13
+ # Mondays at 06:17 UTC. Off-the-hour to avoid GH-Actions cron congestion.
14
+ - cron: '17 6 * * 1'
15
+ workflow_dispatch:
16
+
17
+ jobs:
18
+ smoke-test:
19
+ runs-on: ubuntu-latest
20
+ strategy:
21
+ fail-fast: false
22
+ matrix:
23
+ python-version: ["3.11", "3.12", "3.13"]
24
+ steps:
25
+ - name: Install uv with Python ${{ matrix.python-version }}
26
+ uses: astral-sh/setup-uv@v8.1.0
27
+ with:
28
+ python-version: ${{ matrix.python-version }}
29
+ version: "latest"
30
+ # We don't `actions/checkout` (the smoke test installs from PyPI),
31
+ # so silence setup-uv's empty-workdir + no-lock-to-cache warnings.
32
+ ignore-empty-workdir: true
33
+ enable-cache: false
34
+
35
+ - name: Show resolved dependency versions
36
+ run: |
37
+ set -euo pipefail
38
+ uvx --from mockstack python -c \
39
+ "import fastapi, starlette, uvicorn, pydantic; \
40
+ print(f'fastapi={fastapi.__version__}'); \
41
+ print(f'starlette={starlette.__version__}'); \
42
+ print(f'uvicorn={uvicorn.__version__}'); \
43
+ print(f'pydantic={pydantic.__version__}')"
44
+
45
+ - name: Boot mockstack and probe HTTP
46
+ run: |
47
+ set -euo pipefail
48
+ mkdir -p /tmp/mockstack-templates
49
+ uvx mockstack --templates-dir /tmp/mockstack-templates --port 18999 \
50
+ > server.log 2>&1 &
51
+ SERVER_PID=$!
52
+ echo "started mockstack pid=$SERVER_PID"
53
+
54
+ # Poll for readiness for up to 30s. /openapi.json is served by FastAPI
55
+ # itself, so a 200 here proves import + app construction + uvicorn boot
56
+ # + HTTP serving are all healthy end-to-end.
57
+ for i in $(seq 1 30); do
58
+ if curl -fsS http://127.0.0.1:18999/openapi.json -o /dev/null; then
59
+ echo "ready after ${i}s"
60
+ READY=1
61
+ break
62
+ fi
63
+ if ! kill -0 "$SERVER_PID" 2>/dev/null; then
64
+ echo "server died before becoming ready; logs:"
65
+ cat server.log
66
+ exit 1
67
+ fi
68
+ sleep 1
69
+ done
70
+
71
+ if [ "${READY:-0}" != "1" ]; then
72
+ echo "server never became ready; logs:"
73
+ cat server.log
74
+ kill "$SERVER_PID" 2>/dev/null || true
75
+ exit 1
76
+ fi
77
+
78
+ # Final assertion + capture status
79
+ curl -fsS http://127.0.0.1:18999/openapi.json -o /dev/null
80
+ echo "smoke test passed"
81
+
82
+ kill "$SERVER_PID" 2>/dev/null || true
83
+ wait "$SERVER_PID" 2>/dev/null || true
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mockstack
3
- Version: 0.11.2
3
+ Version: 0.12.1
4
4
  Summary: An API mocking workhorse
5
5
  Author-email: Adam Ever-Hadani <mockstack.contact@gmail.com>
6
6
  License-Expression: MIT
@@ -8,7 +8,9 @@ from mockstack.config import Settings
8
8
  def catchall_router_provider(app: FastAPI, settings: Settings) -> None:
9
9
  """Create the catch-all routes for the mockstack app."""
10
10
 
11
- @app.route("/{full_path:path}", methods=["GET", "PATCH", "POST", "PUT", "DELETE"])
11
+ @app.api_route(
12
+ "/{full_path:path}", methods=["GET", "PATCH", "POST", "PUT", "DELETE"]
13
+ )
12
14
  async def catch_all(request: Request):
13
15
  """Catch all requests and delegate to the strategy."""
14
16
  return await app.state.strategy.apply(request)
@@ -7,7 +7,7 @@ import httpx
7
7
  import pytest
8
8
  from fastapi import Request, status
9
9
  from fastapi.responses import RedirectResponse
10
- from starlette.datastructures import Headers
10
+ from starlette.datastructures import Headers, URL
11
11
 
12
12
  from mockstack.constants import ProxyRulesRedirectVia
13
13
  from mockstack.strategies.proxyrules import (
@@ -79,6 +79,27 @@ async def test_proxy_rules_strategy_apply(settings, span):
79
79
  assert response.headers["location"] == "/projects/123"
80
80
 
81
81
 
82
+ @pytest.mark.asyncio
83
+ async def test_proxy_rules_strategy_apply_with_fragment(settings, span):
84
+ """Test applying a rule to a request with URL fragment."""
85
+ strategy = ProxyRulesStrategy(settings)
86
+ request = Request(
87
+ scope={
88
+ "type": "http",
89
+ "method": "GET",
90
+ "path": "/api/v1/projects/123",
91
+ "query_string": b"",
92
+ "headers": [],
93
+ }
94
+ )
95
+ # Set URL with fragment (fragments are client-side only in HTTP, but we test the logic)
96
+ request._url = URL("http://testserver/api/v1/projects/123#section")
97
+ request.state.span = span
98
+ response = await strategy.apply(request)
99
+ assert isinstance(response, RedirectResponse)
100
+ assert response.headers["location"] == "/projects/123%23section"
101
+
102
+
82
103
  @pytest.mark.asyncio
83
104
  async def test_proxy_rules_strategy_apply_no_match(settings, span):
84
105
  """Test applying strategy when no rule matches."""
@@ -2,6 +2,7 @@
2
2
 
3
3
  import pytest
4
4
  from fastapi import Request
5
+ from starlette.datastructures import URL
5
6
 
6
7
  from mockstack.rules import Rule, TemplateRuleResult, URLRuleResult
7
8
 
@@ -79,34 +80,63 @@ def test_rule_matches_with_method(pattern, path, method, expected):
79
80
 
80
81
 
81
82
  @pytest.mark.parametrize(
82
- "pattern,replacement,path,expected_url",
83
+ "pattern,replacement,path,fragment,expected_url",
83
84
  [
84
85
  (
85
86
  r"/api/v1/projects/(\d+)",
86
87
  r"/projects/\1",
87
88
  "/api/v1/projects/123",
89
+ None,
88
90
  "/projects/123",
89
91
  ),
90
92
  (
91
93
  r"/api/v1/users/([^/]+)",
92
94
  r"/users/\1",
93
95
  "/api/v1/users/john",
96
+ None,
94
97
  "/users/john",
95
98
  ),
99
+ (
100
+ r"/api/v1/projects/(\d+)",
101
+ r"/projects/\1",
102
+ "/api/v1/projects/123",
103
+ "section",
104
+ "/projects/123%23section",
105
+ ),
106
+ (
107
+ r"/api/v1/users/([^/]+)",
108
+ r"/users/\1",
109
+ "/api/v1/users/john",
110
+ "profile",
111
+ "/users/john%23profile",
112
+ ),
113
+ (
114
+ r"/api/v1/projects/(\d+)",
115
+ r"/projects/\1",
116
+ "/api/v1/projects/456",
117
+ "top",
118
+ "/projects/456%23top",
119
+ ),
96
120
  ],
97
121
  )
98
- def test_rule_apply(pattern, replacement, path, expected_url):
122
+ def test_rule_apply(pattern, replacement, path, fragment, expected_url):
99
123
  """Test the rule application logic."""
100
124
  rule = Rule(pattern=pattern, replacement=replacement)
101
- request = Request(
102
- scope={
103
- "type": "http",
104
- "method": "GET",
105
- "path": path,
106
- "query_string": b"",
107
- "headers": [],
108
- }
109
- )
125
+ scope = {
126
+ "type": "http",
127
+ "method": "GET",
128
+ "path": path,
129
+ "query_string": b"",
130
+ "headers": [],
131
+ }
132
+ request = Request(scope=scope)
133
+
134
+ # If fragment is specified, we need to manually set the URL with the fragment
135
+ # since fragments are not part of standard HTTP requests (they're client-side)
136
+ if fragment:
137
+ url_with_fragment = f"http://testserver{path}#{fragment}"
138
+ request._url = URL(url_with_fragment)
139
+
110
140
  result = rule.apply(request)
111
141
  assert isinstance(result, URLRuleResult)
112
142
  assert result.get_result_type() == "url"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mockstack
3
- Version: 0.11.2
3
+ Version: 0.12.1
4
4
  Summary: An API mocking workhorse
5
5
  Author-email: Adam Ever-Hadani <mockstack.contact@gmail.com>
6
6
  License-Expression: MIT
@@ -17,6 +17,7 @@ uv.lock
17
17
  .github/workflows/ci.yml
18
18
  .github/workflows/publish-docs.yml
19
19
  .github/workflows/publish-to-pypi.yml
20
+ .github/workflows/smoke-test-pypi.yml
20
21
  docs/configuration.md
21
22
  docs/ollama.md
22
23
  docs/assets/favicon.ico