mockstack 0.5.2__tar.gz → 0.6.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.
- {mockstack-0.5.2 → mockstack-0.6.0}/.env.example +4 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/.pre-commit-config.yaml +1 -1
- {mockstack-0.5.2 → mockstack-0.6.0}/PKG-INFO +1 -1
- mockstack-0.6.0/docs/configuration.md +103 -0
- mockstack-0.6.0/docs/strategies/base.md +55 -0
- mockstack-0.6.0/docs/strategies/filefixtures.md +109 -0
- mockstack-0.6.0/docs/strategies/proxyrules.md +123 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/examples/filefixtures-with-templates/.env.example +4 -0
- mockstack-0.6.0/examples/filefixtures-with-templates/README.md +17 -0
- mockstack-0.6.0/examples/llm/README.md +11 -0
- mockstack-0.6.0/examples/llm/mockstack-langchain-example.ipynb +128 -0
- mockstack-0.6.0/examples/llm/templates/openai-v1-chat-completions.j2 +35 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack/config.py +7 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack/strategies/filefixtures.py +14 -1
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack/tests/conftest.py +15 -0
- mockstack-0.6.0/mockstack/tests/strategies/test_create_mixin.py +200 -0
- mockstack-0.6.0/mockstack/tests/strategies/test_filefixtures.py +294 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack/tests/strategies/test_proxyrules.py +175 -1
- mockstack-0.6.0/mockstack/tests/test_telemetry.py +218 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack.egg-info/PKG-INFO +1 -1
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack.egg-info/SOURCES.txt +9 -0
- mockstack-0.5.2/examples/filefixtures-with-templates/README.md +0 -10
- mockstack-0.5.2/mockstack/tests/strategies/test_filefixtures.py +0 -100
- {mockstack-0.5.2 → mockstack-0.6.0}/.github/workflows/ci.yml +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/.github/workflows/publish-docs.yml +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/.github/workflows/publish-to-pypi.yml +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/.gitignore +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/CODE_OF_CONDUCT.md +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/LICENSE +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/README.md +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/SECURITY.md +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/docs/assets/favicon.ico +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/docs/assets/logo.png +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/docs/assets/mockstack.png +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/examples/filefixtures-with-templates/templates/servicename-api-v1-items.j2 +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/examples/filefixtures-with-templates/templates/servicename2-api-v2-user.533ec889-7c68-45c8-b21e-4a7e455d1234.j2 +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/examples/proxyrules-with-rules-file/.env.example +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/examples/proxyrules-with-rules-file/README.md +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/examples/proxyrules-with-rules-file/rules.yml +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mkdocs.yml +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack/__init__.py +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack/constants.py +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack/display.py +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack/identifiers.py +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack/intent.py +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack/lifespan.py +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack/main.py +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack/middleware.py +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack/routers/__init__.py +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack/routers/catchall.py +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack/routers/homepage.py +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack/strategies/__init__.py +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack/strategies/base.py +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack/strategies/create_mixin.py +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack/strategies/factory.py +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack/strategies/proxyrules.py +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack/telemetry.py +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack/templating.py +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack/tests/__init__.py +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack/tests/fixtures/proxyrules.yml +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack/tests/fixtures/templates/__init__.py +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack/tests/fixtures/templates/example-template.j2 +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack/tests/routers/__init__.py +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack/tests/routers/test_catchall.py +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack/tests/routers/test_homepage.py +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack/tests/test_display.py +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack/tests/test_identifiers.py +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack/tests/test_middleware.py +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack/tests/test_templating.py +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack.egg-info/dependency_links.txt +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack.egg-info/entry_points.txt +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack.egg-info/requires.txt +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/mockstack.egg-info/top_level.txt +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/pyproject.toml +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/setup.cfg +0 -0
- {mockstack-0.5.2 → mockstack-0.6.0}/uv.lock +0 -0
|
@@ -9,6 +9,10 @@ MOCKSTACK__STRATEGY = filefixtures
|
|
|
9
9
|
# and other hybrid strategies utilizing templates.
|
|
10
10
|
MOCKSTACK__TEMPLATES_DIR = "/some/path/templates/"
|
|
11
11
|
|
|
12
|
+
# When using filefixtures strategy, controls whether to enable
|
|
13
|
+
# using templates for POST requests or go directly to simulate a create.
|
|
14
|
+
FILEFIXTURES_ENABLE_TEMPLATES_FOR_POST = false
|
|
15
|
+
|
|
12
16
|
# rules table for the proxyrules strategy.
|
|
13
17
|
# see .mockstack/tests/fixtures/ for examples.
|
|
14
18
|
MOCKSTACK__PROXYRULES_RULES_FILENAME="/some/path/proxyrules.yml"
|
|
@@ -39,7 +39,7 @@ repos:
|
|
|
39
39
|
hooks:
|
|
40
40
|
- id: pytest
|
|
41
41
|
name: pytest [with coverage, without slow]
|
|
42
|
-
entry: bash -ec "uv run python -m pytest --cov=mockstack mockstack/tests --cov-fail-under='
|
|
42
|
+
entry: bash -ec "uv run python -m pytest --cov=mockstack mockstack/tests --cov-fail-under='85' -m 'not slow'"
|
|
43
43
|
language: system
|
|
44
44
|
types: [python]
|
|
45
45
|
pass_filenames: false
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
mockstack can be configured through multiple methods, in order of priority:
|
|
4
|
+
|
|
5
|
+
- Command-line arguments
|
|
6
|
+
- Environment variables
|
|
7
|
+
- `.env` file
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
All configuration options are prefixed with `MOCKSTACK__` when using environment variables or the `.env` file.
|
|
11
|
+
|
|
12
|
+
## General Settings
|
|
13
|
+
|
|
14
|
+
| Option | Type | Default | Description |
|
|
15
|
+
|--------|------|---------|-------------|
|
|
16
|
+
| `debug` | boolean | `false` | Whether to run in debug mode |
|
|
17
|
+
| `host` | string | `0.0.0.0` | Host to run the server on |
|
|
18
|
+
| `port` | integer | `8000` | Port to run the server on |
|
|
19
|
+
| `strategy` | string | `filefixtures` | Strategy to use for handling requests. Options: `filefixtures`, `proxyrules` |
|
|
20
|
+
|
|
21
|
+
## OpenTelemetry Settings
|
|
22
|
+
|
|
23
|
+
| Option | Type | Default | Description |
|
|
24
|
+
|--------|------|---------|-------------|
|
|
25
|
+
| `opentelemetry.enabled` | boolean | `false` | Whether to enable OpenTelemetry integration |
|
|
26
|
+
| `opentelemetry.endpoint` | string | `http://localhost:4317/` | OpenTelemetry endpoint |
|
|
27
|
+
| `opentelemetry.capture_response_body` | boolean | `false` | Whether to capture response body in traces |
|
|
28
|
+
|
|
29
|
+
## Strategy-Specific Settings
|
|
30
|
+
|
|
31
|
+
### FileFixtures Strategy
|
|
32
|
+
|
|
33
|
+
| Option | Type | Default | Description |
|
|
34
|
+
|--------|------|---------|-------------|
|
|
35
|
+
| `templates_dir` | string | - | Base directory for templates used by the strategy |
|
|
36
|
+
| `filefixtures_enable_templates_for_post` | boolean | `false` | Whether to enable template-based responses for POST requests |
|
|
37
|
+
|
|
38
|
+
### ProxyRules Strategy
|
|
39
|
+
|
|
40
|
+
| Option | Type | Default | Description |
|
|
41
|
+
|--------|------|---------|-------------|
|
|
42
|
+
| `proxyrules_rules_filename` | string | - | Rules filename for proxyrules strategy |
|
|
43
|
+
| `proxyrules_redirect_via` | string | `reverse_proxy` | Controls behavior of proxying. Options: `reverse_proxy`, `http_307_temporary`, `http_301_permanent` |
|
|
44
|
+
| `proxyrules_reverse_proxy_timeout` | float | `10.0` | Default timeout for reverse proxy requests in seconds |
|
|
45
|
+
| `proxyrules_simulate_create_on_missing` | boolean | `false` | Whether to simulate creation of resources when a POST request is made to a resource that doesn't match any rules |
|
|
46
|
+
|
|
47
|
+
## Resource Creation Settings
|
|
48
|
+
|
|
49
|
+
| Option | Type | Default | Description |
|
|
50
|
+
|--------|------|---------|-------------|
|
|
51
|
+
| `created_resource_metadata` | object | See below | Metadata fields to inject into created resources |
|
|
52
|
+
| `missing_resource_fields` | object | See below | Fields to inject into missing resources response JSON |
|
|
53
|
+
|
|
54
|
+
### Default created_resource_metadata
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"id": "{{ uuid4() }}",
|
|
58
|
+
"createdAt": "{{ utcnow().isoformat() }}",
|
|
59
|
+
"updatedAt": "{{ utcnow().isoformat() }}",
|
|
60
|
+
"createdBy": "{{ request.headers.get('X-User-Id', uuid4()) }}",
|
|
61
|
+
"status": {
|
|
62
|
+
"code": "OK",
|
|
63
|
+
"error_code": null
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Default missing_resource_fields
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"code": 404,
|
|
72
|
+
"message": "mockstack: resource not found",
|
|
73
|
+
"retryable": false
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Logging Configuration
|
|
78
|
+
|
|
79
|
+
The logging configuration follows the Python logging configuration schema. By default, it includes:
|
|
80
|
+
|
|
81
|
+
- Rich console handler
|
|
82
|
+
- Uvicorn formatter
|
|
83
|
+
- Separate loggers for different components
|
|
84
|
+
- Debug level logging for strategy-specific loggers
|
|
85
|
+
|
|
86
|
+
## Example Configuration
|
|
87
|
+
|
|
88
|
+
Here's an example `.env` file:
|
|
89
|
+
|
|
90
|
+
```env
|
|
91
|
+
MOCKSTACK__STRATEGY=filefixtures
|
|
92
|
+
MOCKSTACK__TEMPLATES_DIR=~/mockstack-templates/
|
|
93
|
+
MOCKSTACK__OPENTELEMETRY__ENABLED=true
|
|
94
|
+
MOCKSTACK__OPENTELEMETRY__CAPTURE_RESPONSE_BODY=true
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Command Line Usage
|
|
98
|
+
|
|
99
|
+
You can also set configuration options via command line arguments:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
uvx mockstack --strategy filefixtures --templates-dir ~/mockstack-templates/
|
|
103
|
+
```
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Base Strategy
|
|
2
|
+
|
|
3
|
+
The `BaseStrategy` class serves as the foundation for all mockstack strategies. It defines the core interface that all strategies must implement.
|
|
4
|
+
|
|
5
|
+
## Interface
|
|
6
|
+
|
|
7
|
+
### Constructor
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
def __init__(self, settings: Settings, *args, **kwargs)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Initializes the strategy with the provided settings.
|
|
14
|
+
|
|
15
|
+
### Methods
|
|
16
|
+
|
|
17
|
+
#### `apply`
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
async def apply(self, request: Request) -> Response
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
This is the main method that all strategies must implement. It takes a FastAPI `Request` object and returns a FastAPI `Response` object. This is where the strategy's core logic for handling requests is implemented.
|
|
24
|
+
|
|
25
|
+
#### `update_opentelemetry`
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
def update_opentelemetry(self, request: Request, *args, **kwargs) -> None
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This method allows strategies to add strategy-specific attributes to the OpenTelemetry span. The span is available on `request.state.span`. When OpenTelemetry is not enabled, this span will exist but will not be reported.
|
|
32
|
+
|
|
33
|
+
## Creating Custom Strategies
|
|
34
|
+
|
|
35
|
+
To create a custom strategy, you should:
|
|
36
|
+
|
|
37
|
+
1. Inherit from `BaseStrategy`
|
|
38
|
+
2. Implement the `apply` method
|
|
39
|
+
3. Optionally override `update_opentelemetry` to add strategy-specific telemetry
|
|
40
|
+
|
|
41
|
+
Example:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from mockstack.strategies.base import BaseStrategy
|
|
45
|
+
from fastapi import Request, Response
|
|
46
|
+
|
|
47
|
+
class CustomStrategy(BaseStrategy):
|
|
48
|
+
async def apply(self, request: Request) -> Response:
|
|
49
|
+
# Implement your custom logic here
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
def update_opentelemetry(self, request: Request, *args, **kwargs) -> None:
|
|
53
|
+
# Add custom telemetry attributes
|
|
54
|
+
request.state.span.set_attribute("custom.attribute", "value")
|
|
55
|
+
```
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# FileFixtures Strategy
|
|
2
|
+
|
|
3
|
+
The `FileFixturesStrategy` is a template-based strategy that uses Jinja2 templates to generate responses. It's particularly useful for creating consistent mock responses for your API endpoints.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This strategy:
|
|
8
|
+
|
|
9
|
+
- Uses Jinja2 templates stored in a specified directory
|
|
10
|
+
- Intelligently matches requests to templates based on the request path
|
|
11
|
+
- Supports all HTTP methods (GET, POST, PUT, PATCH, DELETE)
|
|
12
|
+
- Simulates resource creation for POST requests which are not handled by proxying rules (behavior controlled via configuration)
|
|
13
|
+
- Provides OpenTelemetry integration for observability
|
|
14
|
+
|
|
15
|
+
## Template Resolution
|
|
16
|
+
|
|
17
|
+
For a given request path like `/api/v1/projects/1234`, the strategy will look for templates in the following order:
|
|
18
|
+
|
|
19
|
+
1. `api-v1-projects.1234.j2` (specific to the resource ID)
|
|
20
|
+
2. `api-v1-projects.j2` (generic for the resource type)
|
|
21
|
+
3. `index.j2` (fallback template)
|
|
22
|
+
|
|
23
|
+
## HTTP Method Handling
|
|
24
|
+
|
|
25
|
+
### GET Requests
|
|
26
|
+
- Attempts to find and render a matching template
|
|
27
|
+
- Returns 404 if no matching template is found
|
|
28
|
+
|
|
29
|
+
### POST Requests
|
|
30
|
+
The strategy intelligently handles POST requests based on the request context:
|
|
31
|
+
|
|
32
|
+
1. **Search Requests**: If the request looks like a search (based on URL and body), returns a template response
|
|
33
|
+
2. **Command Requests**: If the request looks like a command, returns a 201 CREATED status with template response
|
|
34
|
+
3. **Resource Creation**: Otherwise, simulates resource creation with injected metadata
|
|
35
|
+
|
|
36
|
+
### DELETE/PUT/PATCH Requests
|
|
37
|
+
- Returns 204 NO CONTENT by default
|
|
38
|
+
- These are no-op operations in the default implementation
|
|
39
|
+
|
|
40
|
+
## Template Context
|
|
41
|
+
|
|
42
|
+
Templates have access to the following context variables:
|
|
43
|
+
|
|
44
|
+
- `request`: The FastAPI Request object
|
|
45
|
+
- `request.body`: The parsed request body
|
|
46
|
+
- `request.headers`: The request headers
|
|
47
|
+
- `request.query_params`: The query parameters
|
|
48
|
+
- `request.path_params`: The path parameters
|
|
49
|
+
|
|
50
|
+
## Resource Creation
|
|
51
|
+
|
|
52
|
+
When simulating resource creation (POST requests), the strategy injects the following metadata fields by default:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"id": "{{ uuid4() }}",
|
|
57
|
+
"createdAt": "{{ utcnow().isoformat() }}",
|
|
58
|
+
"updatedAt": "{{ utcnow().isoformat() }}",
|
|
59
|
+
"createdBy": "{{ request.headers.get('X-User-Id', uuid4()) }}",
|
|
60
|
+
"status": {
|
|
61
|
+
"code": "OK",
|
|
62
|
+
"error_code": null
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Configuration
|
|
68
|
+
|
|
69
|
+
The strategy requires the following configuration:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
settings = Settings(
|
|
73
|
+
strategy="filefixtures",
|
|
74
|
+
templates_dir="/path/to/templates",
|
|
75
|
+
filefixtures_enable_templates_for_post=True # Optional: Enables template-based responses for POST requests
|
|
76
|
+
)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Example Template
|
|
80
|
+
|
|
81
|
+
Here's an example template for a user resource:
|
|
82
|
+
|
|
83
|
+
```jinja2
|
|
84
|
+
{
|
|
85
|
+
"id": "{{ uuid4() }}",
|
|
86
|
+
"name": "{{ request.body.name }}",
|
|
87
|
+
"email": "{{ request.body.email }}",
|
|
88
|
+
"createdAt": "{{ utcnow().isoformat() }}",
|
|
89
|
+
"updatedAt": "{{ utcnow().isoformat() }}"
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## OpenTelemetry Integration
|
|
94
|
+
|
|
95
|
+
The strategy automatically adds the following OpenTelemetry attributes:
|
|
96
|
+
|
|
97
|
+
- `mockstack.filefixtures.template_name`: The name of the template being rendered
|
|
98
|
+
|
|
99
|
+
## Error Handling
|
|
100
|
+
|
|
101
|
+
When no matching template is found, the strategy returns a 404 response with the following structure:
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"code": 404,
|
|
106
|
+
"message": "mockstack: resource not found",
|
|
107
|
+
"retryable": false
|
|
108
|
+
}
|
|
109
|
+
```
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# ProxyRules Strategy
|
|
2
|
+
|
|
3
|
+
The `ProxyRulesStrategy` is a powerful strategy that allows you to define rules for redirecting or proxying requests to other services. It's particularly useful when you need to mix mock responses with real service calls.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This strategy:
|
|
8
|
+
|
|
9
|
+
- Uses a YAML configuration file to define routing rules
|
|
10
|
+
- Supports multiple redirection methods (HTTP redirects or reverse proxy)
|
|
11
|
+
- Can simulate resource creation for unmatched requests
|
|
12
|
+
- Provides OpenTelemetry integration for observability
|
|
13
|
+
|
|
14
|
+
## Configuration
|
|
15
|
+
|
|
16
|
+
The strategy requires the following configuration:
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
settings = Settings(
|
|
20
|
+
strategy="proxyrules",
|
|
21
|
+
proxyrules_rules_filename="/path/to/rules.yaml",
|
|
22
|
+
proxyrules_redirect_via="REVERSE_PROXY", # or "HTTP_TEMPORARY_REDIRECT" or "HTTP_PERMANENT_REDIRECT"
|
|
23
|
+
proxyrules_reverse_proxy_timeout=10.0,
|
|
24
|
+
proxyrules_simulate_create_on_missing=False
|
|
25
|
+
)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Rules Configuration
|
|
29
|
+
|
|
30
|
+
Rules are defined in a YAML file with the following structure:
|
|
31
|
+
|
|
32
|
+
```yaml
|
|
33
|
+
rules:
|
|
34
|
+
- name: "user-service"
|
|
35
|
+
pattern: "^/api/v1/users/(.*)"
|
|
36
|
+
replacement: "http://user-service/api/v1/users/\1"
|
|
37
|
+
method: "GET" # optional, if not specified matches all methods
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Rule Properties
|
|
41
|
+
|
|
42
|
+
- `name`: Optional identifier for the rule (used in telemetry)
|
|
43
|
+
- `pattern`: Regular expression pattern to match against the request path
|
|
44
|
+
- `replacement`: URL template to redirect to (can use capture groups from pattern)
|
|
45
|
+
- `method`: Optional HTTP method to match (if not specified, matches all methods)
|
|
46
|
+
|
|
47
|
+
## Redirection Methods
|
|
48
|
+
|
|
49
|
+
The strategy supports three redirection methods:
|
|
50
|
+
|
|
51
|
+
1. **HTTP Temporary Redirect (307)**
|
|
52
|
+
- Client makes a new request to the target URL
|
|
53
|
+
- Preserves the original HTTP method
|
|
54
|
+
|
|
55
|
+
2. **HTTP Permanent Redirect (301)**
|
|
56
|
+
- Client makes a new request to the target URL
|
|
57
|
+
- Browsers may cache the redirect
|
|
58
|
+
|
|
59
|
+
3. **Reverse Proxy**
|
|
60
|
+
- Server forwards the request to the target service
|
|
61
|
+
- Client is unaware of the redirection
|
|
62
|
+
- Useful when you need to work with clients that do not handle HTTP redirects gracefully.
|
|
63
|
+
|
|
64
|
+
## Resource Creation Simulation
|
|
65
|
+
|
|
66
|
+
When `proxyrules_simulate_create_on_missing` is enabled and a POST request doesn't match any rules, the strategy will simulate resource creation by:
|
|
67
|
+
|
|
68
|
+
1. Injecting metadata fields into the response
|
|
69
|
+
2. Returning a 201 CREATED status code
|
|
70
|
+
3. Echoing back the request body with added metadata
|
|
71
|
+
|
|
72
|
+
The default metadata fields are controlled via the configuration file and at the time of writing are as follows:
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"id": "{{ uuid4() }}",
|
|
77
|
+
"createdAt": "{{ utcnow().isoformat() }}",
|
|
78
|
+
"updatedAt": "{{ utcnow().isoformat() }}",
|
|
79
|
+
"createdBy": "{{ request.headers.get('X-User-Id', uuid4()) }}",
|
|
80
|
+
"status": {
|
|
81
|
+
"code": "OK",
|
|
82
|
+
"error_code": null
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## OpenTelemetry Integration
|
|
88
|
+
|
|
89
|
+
The strategy automatically adds the following OpenTelemetry attributes:
|
|
90
|
+
|
|
91
|
+
- `mockstack.proxyrules.rule_name`: The name of the matched rule (if specified)
|
|
92
|
+
- `mockstack.proxyrules.rule_method`: The HTTP method the rule matches (if specified)
|
|
93
|
+
- `mockstack.proxyrules.rule_pattern`: The pattern used to match the request
|
|
94
|
+
- `mockstack.proxyrules.rule_replacement`: The replacement URL template
|
|
95
|
+
- `mockstack.proxyrules.rewritten_url`: The final URL after applying the rule
|
|
96
|
+
|
|
97
|
+
## Example Rules
|
|
98
|
+
|
|
99
|
+
Here are some example rules:
|
|
100
|
+
|
|
101
|
+
```yaml
|
|
102
|
+
rules:
|
|
103
|
+
# Redirect all GET requests to /api/v1/users/* to the user service
|
|
104
|
+
- name: "user-service-get"
|
|
105
|
+
pattern: "^/api/v1/users/(.*)"
|
|
106
|
+
replacement: "http://user-service/api/v1/users/\1"
|
|
107
|
+
method: "GET"
|
|
108
|
+
|
|
109
|
+
# Proxy all POST requests to /api/v1/orders to the order service
|
|
110
|
+
- name: "order-service-post"
|
|
111
|
+
pattern: "^/api/v1/orders"
|
|
112
|
+
replacement: "http://order-service/api/v1/orders"
|
|
113
|
+
method: "POST"
|
|
114
|
+
|
|
115
|
+
# Redirect all requests to /api/v1/products to the product service
|
|
116
|
+
- name: "product-service"
|
|
117
|
+
pattern: "^/api/v1/products/(.*)"
|
|
118
|
+
replacement: "http://product-service/api/v1/products/\1"
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Error Handling
|
|
122
|
+
|
|
123
|
+
When no matching rule is found and resource creation simulation is disabled, the strategy returns a 404 NOT FOUND response.
|
|
@@ -9,5 +9,9 @@ MOCKSTACK__STRATEGY = filefixtures
|
|
|
9
9
|
# and other hybrid strategies utilizing templates.
|
|
10
10
|
MOCKSTACK__TEMPLATES_DIR = "./templates/"
|
|
11
11
|
|
|
12
|
+
# When using filefixtures strategy, controls whether to enable
|
|
13
|
+
# using templates for POST requests or go directly to simulate a create.
|
|
14
|
+
FILEFIXTURES_ENABLE_TEMPLATES_FOR_POST = false
|
|
15
|
+
|
|
12
16
|
# OpenTelemetry settings
|
|
13
17
|
MOCKSTACK__OPENTELEMETRY__ENABLED = false
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
|
|
10
|
+
A `templates/` dir, pointed to by a configurationkey in the env file, which contains our templates.
|
|
11
|
+
We have a few example templates showcasing some of the capabilities of template-based mocking:
|
|
12
|
+
|
|
13
|
+
* 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.
|
|
14
|
+
* Another template shows the naming convention for templates that depend on an identifier embedded in the request URL, e.g. `GET
|
|
15
|
+
/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`.
|
|
16
|
+
* 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.
|
|
17
|
+
* A configuration value in controllable via `.env` / environment variables lets you also decide whether to allow using templates for POST requests or try to simulate a createion. When templates are allowed, mockstaack will first try to find a suitable template for the request based on the URL, and if it fails will fallback to the create simulation behavior.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# LLMs
|
|
2
|
+
|
|
3
|
+
**mockstack** can be a great tool for the development of LLM-based flows.
|
|
4
|
+
|
|
5
|
+
A lot of current applications involve a sort of DAG of API calls to LLM APIs and various "tool" APIs, sometimes serially, sometimes in parallel, and sometimes in a "iterative" fashion (e.g. see [Agents](https://langchain-ai.github.io/langgraph/agents/overview/) documentation on LangGraph website).
|
|
6
|
+
|
|
7
|
+
you can mock any of these parts out to accelerate development, debugging and integration testing of such workflows.
|
|
8
|
+
|
|
9
|
+
Mocking out the LLMs themselves can be a particularly effective method to make sure no costs are incurred in early stages of development and in debugging scenarios that don't critically rely on the semantic content of the responses.
|
|
10
|
+
|
|
11
|
+
This example shows a few possible scenarios involving mockstack and (mostly LangChain-based) LLM workflows.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cells": [
|
|
3
|
+
{
|
|
4
|
+
"cell_type": "markdown",
|
|
5
|
+
"metadata": {},
|
|
6
|
+
"source": [
|
|
7
|
+
"# LLMs + mockstack\n",
|
|
8
|
+
"\n",
|
|
9
|
+
"A few simple examples for using `mockstack` to mock various components in typical LLM-driven use cases."
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"cell_type": "code",
|
|
14
|
+
"execution_count": 26,
|
|
15
|
+
"metadata": {},
|
|
16
|
+
"outputs": [],
|
|
17
|
+
"source": [
|
|
18
|
+
"# Install langchain dependencies with the following or using `uv` depending on your venv setup:\n",
|
|
19
|
+
"\n",
|
|
20
|
+
"#!pip install -q langchain langchain-openai\n",
|
|
21
|
+
"# or:\n",
|
|
22
|
+
"#!uv pip install langchain langchain-openai"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"cell_type": "markdown",
|
|
27
|
+
"metadata": {},
|
|
28
|
+
"source": [
|
|
29
|
+
"## Example #1: Template-based mocking (`filefixtures` strategy)\n",
|
|
30
|
+
"\n",
|
|
31
|
+
"Here we simply use the **filefixtures** strategy to route requesets coming in for a certain URL to a template file with the appropriate name.\n",
|
|
32
|
+
"\n",
|
|
33
|
+
"For the below example you'll want to make sure:\n",
|
|
34
|
+
"\n",
|
|
35
|
+
"- mockstack is running at http://localhost:8000 which are the default settings\n",
|
|
36
|
+
"- `MOCKSTACK__TEMPLATES_DIR` is pointing to a valid directory with a template file called `openai-v1-chat-completions.j2`. See the [included file](./templates/openai-v1-chat-completions.j2) with same name in the ./templates sub-directory of this example for a a template with a valid response based on [OpenAI API reference](https://platform.openai.com/docs/api-reference/chat/create).\n",
|
|
37
|
+
"- the `MOCKSTACK__FILEFIXTURES_ENABLE_TEMPLATES_FOR_POST` flag is set to true (which it should be by default)\n",
|
|
38
|
+
"\n",
|
|
39
|
+
"\n",
|
|
40
|
+
"If everything is setup correctly, you should get back the mocked response in the correct format from the template and the below assert should pass."
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"cell_type": "code",
|
|
45
|
+
"execution_count": 31,
|
|
46
|
+
"metadata": {},
|
|
47
|
+
"outputs": [],
|
|
48
|
+
"source": [
|
|
49
|
+
"from langchain_openai import ChatOpenAI\n",
|
|
50
|
+
"\n",
|
|
51
|
+
"\n",
|
|
52
|
+
"llm = ChatOpenAI(\n",
|
|
53
|
+
" model=\"gpt-4o\",\n",
|
|
54
|
+
" temperature=0,\n",
|
|
55
|
+
" max_tokens=None,\n",
|
|
56
|
+
" timeout=None,\n",
|
|
57
|
+
" max_retries=2,\n",
|
|
58
|
+
" base_url=\"http://localhost:8000/openai/v1\",\n",
|
|
59
|
+
" api_key=\"SOME_STRING_THAT_DOES_NOT_MATTER\",\n",
|
|
60
|
+
")\n",
|
|
61
|
+
"\n",
|
|
62
|
+
"messages = [\n",
|
|
63
|
+
" (\n",
|
|
64
|
+
" \"system\",\n",
|
|
65
|
+
" \"You are a helpful assistant that translates English to French. Translate the user sentence.\",\n",
|
|
66
|
+
" ),\n",
|
|
67
|
+
" (\"human\", \"I love programming.\"),\n",
|
|
68
|
+
"]\n",
|
|
69
|
+
"ai_msg = llm.invoke(messages)\n",
|
|
70
|
+
"\n",
|
|
71
|
+
"assert ai_msg.content == \"Hello! How can I assist you today?\""
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"cell_type": "markdown",
|
|
76
|
+
"metadata": {},
|
|
77
|
+
"source": [
|
|
78
|
+
"## Example #2: `llm` strategy\n",
|
|
79
|
+
"\n",
|
|
80
|
+
"** COMING SOON **"
|
|
81
|
+
]
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"cell_type": "code",
|
|
85
|
+
"execution_count": null,
|
|
86
|
+
"metadata": {},
|
|
87
|
+
"outputs": [],
|
|
88
|
+
"source": []
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"cell_type": "markdown",
|
|
92
|
+
"metadata": {},
|
|
93
|
+
"source": [
|
|
94
|
+
"## Example #3: mocking out a tool call\n",
|
|
95
|
+
"\n",
|
|
96
|
+
"** COMING SOON **"
|
|
97
|
+
]
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"cell_type": "code",
|
|
101
|
+
"execution_count": null,
|
|
102
|
+
"metadata": {},
|
|
103
|
+
"outputs": [],
|
|
104
|
+
"source": []
|
|
105
|
+
}
|
|
106
|
+
],
|
|
107
|
+
"metadata": {
|
|
108
|
+
"kernelspec": {
|
|
109
|
+
"display_name": ".venv",
|
|
110
|
+
"language": "python",
|
|
111
|
+
"name": "python3"
|
|
112
|
+
},
|
|
113
|
+
"language_info": {
|
|
114
|
+
"codemirror_mode": {
|
|
115
|
+
"name": "ipython",
|
|
116
|
+
"version": 3
|
|
117
|
+
},
|
|
118
|
+
"file_extension": ".py",
|
|
119
|
+
"mimetype": "text/x-python",
|
|
120
|
+
"name": "python",
|
|
121
|
+
"nbconvert_exporter": "python",
|
|
122
|
+
"pygments_lexer": "ipython3",
|
|
123
|
+
"version": "3.13.3"
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
"nbformat": 4,
|
|
127
|
+
"nbformat_minor": 2
|
|
128
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "chatcmpl-B9MBs8CjcvOU2jLn4n570S5qMJKcT",
|
|
3
|
+
"object": "chat.completion",
|
|
4
|
+
"created": 1741569952,
|
|
5
|
+
"model": "gpt-4.1-2025-04-14",
|
|
6
|
+
"choices": [
|
|
7
|
+
{
|
|
8
|
+
"index": 0,
|
|
9
|
+
"message": {
|
|
10
|
+
"role": "assistant",
|
|
11
|
+
"content": "Hello! How can I assist you today?",
|
|
12
|
+
"refusal": null,
|
|
13
|
+
"annotations": []
|
|
14
|
+
},
|
|
15
|
+
"logprobs": null,
|
|
16
|
+
"finish_reason": "stop"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"usage": {
|
|
20
|
+
"prompt_tokens": 19,
|
|
21
|
+
"completion_tokens": 10,
|
|
22
|
+
"total_tokens": 29,
|
|
23
|
+
"prompt_tokens_details": {
|
|
24
|
+
"cached_tokens": 0,
|
|
25
|
+
"audio_tokens": 0
|
|
26
|
+
},
|
|
27
|
+
"completion_tokens_details": {
|
|
28
|
+
"reasoning_tokens": 0,
|
|
29
|
+
"audio_tokens": 0,
|
|
30
|
+
"accepted_prediction_tokens": 0,
|
|
31
|
+
"rejected_prediction_tokens": 0
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"service_tier": "default"
|
|
35
|
+
}
|
|
@@ -61,6 +61,13 @@ class Settings(BaseSettings):
|
|
|
61
61
|
# base directory for templates used by strategies
|
|
62
62
|
templates_dir: DirectoryPath | None = None # type: ignore[assignment]
|
|
63
63
|
|
|
64
|
+
# whether to enable templates for POST requests.
|
|
65
|
+
# By default, templates are not used for POSTs, and instead we try to
|
|
66
|
+
# simulate a create (or search) operation. If turned on, we will first
|
|
67
|
+
# try to materialize a template for the response, and if that fails
|
|
68
|
+
# with a 404, we will then try to simulate creation of the resource.
|
|
69
|
+
filefixtures_enable_templates_for_post: CliImplicitFlag[bool] = True
|
|
70
|
+
|
|
64
71
|
# rules filename for proxyrules strategy
|
|
65
72
|
proxyrules_rules_filename: FilePath | None = None # type: ignore[assignment]
|
|
66
73
|
|
|
@@ -32,6 +32,8 @@ class FileFixturesStrategy(BaseStrategy, CreateMixin):
|
|
|
32
32
|
raise ValueError("templates_dir is not set")
|
|
33
33
|
|
|
34
34
|
self.templates_dir = Path(settings.templates_dir)
|
|
35
|
+
self.enable_templates_for_post = settings.filefixtures_enable_templates_for_post
|
|
36
|
+
|
|
35
37
|
self.created_resource_metadata = settings.created_resource_metadata
|
|
36
38
|
self.missing_resource_fields = settings.missing_resource_fields
|
|
37
39
|
|
|
@@ -40,7 +42,8 @@ class FileFixturesStrategy(BaseStrategy, CreateMixin):
|
|
|
40
42
|
def __str__(self) -> str:
|
|
41
43
|
return (
|
|
42
44
|
f"[medium_purple]filefixtures[/medium_purple]\n "
|
|
43
|
-
f"templates_dir: [medium_purple]{self.templates_dir}[/medium_purple]
|
|
45
|
+
f"templates_dir: [medium_purple]{self.templates_dir}[/medium_purple].\n "
|
|
46
|
+
f"enable_templates_for_post: [medium_purple]{self.enable_templates_for_post}[/medium_purple]. "
|
|
44
47
|
)
|
|
45
48
|
|
|
46
49
|
async def apply(self, request: Request) -> Response:
|
|
@@ -71,6 +74,16 @@ class FileFixturesStrategy(BaseStrategy, CreateMixin):
|
|
|
71
74
|
We also allow a configuration to specify a default intent.
|
|
72
75
|
|
|
73
76
|
"""
|
|
77
|
+
if self.enable_templates_for_post:
|
|
78
|
+
try:
|
|
79
|
+
return self._response_from_template(request)
|
|
80
|
+
except HTTPException as e:
|
|
81
|
+
if e.status_code == status.HTTP_404_NOT_FOUND:
|
|
82
|
+
# If the template is not found, we try to create the resource with logic below.
|
|
83
|
+
pass
|
|
84
|
+
else:
|
|
85
|
+
raise e
|
|
86
|
+
|
|
74
87
|
if looks_like_a_search(request):
|
|
75
88
|
# Searching for resources with a complex query that cannot be expressed in a URI.
|
|
76
89
|
return self._response_from_template(request)
|