mockstack 0.3.0__tar.gz → 0.4.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.
- {mockstack-0.3.0/mockstack.egg-info → mockstack-0.4.1}/PKG-INFO +12 -6
- {mockstack-0.3.0 → mockstack-0.4.1}/README.md +10 -5
- {mockstack-0.3.0 → mockstack-0.4.1}/mockstack/config.py +52 -17
- {mockstack-0.3.0 → mockstack-0.4.1}/mockstack/constants.py +4 -1
- mockstack-0.4.1/mockstack/display.py +27 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/mockstack/intent.py +15 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/mockstack/lifespan.py +6 -1
- {mockstack-0.3.0 → mockstack-0.4.1}/mockstack/main.py +16 -18
- {mockstack-0.3.0 → mockstack-0.4.1}/mockstack/strategies/filefixtures.py +3 -6
- {mockstack-0.3.0 → mockstack-0.4.1}/mockstack/strategies/proxyrules.py +14 -13
- {mockstack-0.3.0 → mockstack-0.4.1}/mockstack/tests/test_display.py +1 -14
- {mockstack-0.3.0 → mockstack-0.4.1/mockstack.egg-info}/PKG-INFO +12 -6
- mockstack-0.4.1/mockstack.egg-info/entry_points.txt +2 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/mockstack.egg-info/requires.txt +1 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/pyproject.toml +2 -1
- {mockstack-0.3.0 → mockstack-0.4.1}/uv.lock +592 -590
- mockstack-0.3.0/mockstack/display.py +0 -37
- mockstack-0.3.0/mockstack.egg-info/entry_points.txt +0 -2
- {mockstack-0.3.0 → mockstack-0.4.1}/.env.example +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/.github/workflows/ci.yml +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/.github/workflows/publish-docs.yml +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/.github/workflows/publish-to-pypi.yml +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/.gitignore +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/.pre-commit-config.yaml +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/CODE_OF_CONDUCT.md +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/LICENSE +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/SECURITY.md +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/docs/assets/favicon.ico +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/docs/assets/logo.png +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/docs/assets/mockstack.png +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/mkdocs.yml +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/mockstack/__init__.py +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/mockstack/identifiers.py +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/mockstack/middleware.py +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/mockstack/routers/__init__.py +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/mockstack/routers/catchall.py +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/mockstack/routers/homepage.py +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/mockstack/strategies/__init__.py +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/mockstack/strategies/base.py +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/mockstack/strategies/create_mixin.py +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/mockstack/strategies/factory.py +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/mockstack/telemetry.py +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/mockstack/templating.py +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/mockstack/tests/__init__.py +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/mockstack/tests/conftest.py +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/mockstack/tests/fixtures/proxyrules.yml +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/mockstack/tests/fixtures/templates/__init__.py +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/mockstack/tests/fixtures/templates/example-template.j2 +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/mockstack/tests/routers/__init__.py +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/mockstack/tests/routers/test_catchall.py +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/mockstack/tests/routers/test_homepage.py +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/mockstack/tests/strategies/test_filefixtures.py +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/mockstack/tests/strategies/test_proxyrules.py +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/mockstack/tests/test_identifiers.py +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/mockstack/tests/test_middleware.py +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/mockstack/tests/test_templating.py +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/mockstack.egg-info/SOURCES.txt +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/mockstack.egg-info/dependency_links.txt +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/mockstack.egg-info/top_level.txt +0 -0
- {mockstack-0.3.0 → mockstack-0.4.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mockstack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: An API mocking workhorse
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/adamhadani/mockstack
|
|
@@ -18,6 +18,7 @@ Requires-Dist: opentelemetry-instrumentation-fastapi>=0.53b1
|
|
|
18
18
|
Requires-Dist: pydantic>=2.11.3
|
|
19
19
|
Requires-Dist: pydantic-settings>=2.9.1
|
|
20
20
|
Requires-Dist: pyyaml>=6.0.2
|
|
21
|
+
Requires-Dist: rich>=14.0.0
|
|
21
22
|
Requires-Dist: typer>=0.15.2
|
|
22
23
|
Requires-Dist: uvicorn>=0.34.2
|
|
23
24
|
Dynamic: license-file
|
|
@@ -47,7 +48,7 @@ Highlights include:
|
|
|
47
48
|
|
|
48
49
|
Install using [uv](https://docs.astral.sh/uv/). This package conforms the concept of a [tool](https://docs.astral.sh/uv/concepts/tools/) and hence can simply install / run with `uvx`:
|
|
49
50
|
|
|
50
|
-
uvx mockstack
|
|
51
|
+
uvx mockstack --help
|
|
51
52
|
|
|
52
53
|
or install into a persistent environment and add it to the PATH with:
|
|
53
54
|
|
|
@@ -56,15 +57,20 @@ or install into a persistent environment and add it to the PATH with:
|
|
|
56
57
|
|
|
57
58
|
## Usage
|
|
58
59
|
|
|
59
|
-
Available configuration options are [here](https://github.com/adamhadani/mockstack/blob/main/mockstack/config.py).
|
|
60
|
+
Available configuration options are [here](https://github.com/adamhadani/mockstack/blob/main/mockstack/config.py).
|
|
61
|
+
|
|
62
|
+
Setting individual options can be done either through an `.env` file, individual environment variables, or command-line arguments. For example:
|
|
60
63
|
|
|
61
64
|
```shell
|
|
65
|
+
export MOCKSTACK__STRATEGY=filefixtures
|
|
62
66
|
export MOCKSTACK__TEMPLATES_DIR=~/mockstack-templates/
|
|
63
67
|
export MOCKSTACK__OPENTELEMETRY__ENABLED=true
|
|
64
68
|
export MOCKSTACK__OPENTELEMETRY__CAPTURE_RESPONSE_BODY=true
|
|
65
|
-
|
|
69
|
+
uvx mockstack
|
|
66
70
|
```
|
|
67
71
|
|
|
72
|
+
See also the included [.env.example](https://github.com/adamhadani/mockstack/blob/main/.env.example) for more examples. You can copy that file to `.env` and fill in configuration as needed based on the given examples.
|
|
73
|
+
|
|
68
74
|
Out of the box, you get the following behavior when using the default `filefixtures` strategy:
|
|
69
75
|
|
|
70
76
|
- The HTTP request `GET /someservice/api/v1/user/c27f5b2b-6e81-420d-a4e4-6426e1c32db8` will try to find `<templates_dir>/someservice-api-v1-user.c27f5b2b-6e81-420d-a4e4-6426e1c32db8.j2`,
|
|
@@ -97,8 +103,8 @@ If you are contributing to development, you will want to clone this project, and
|
|
|
97
103
|
uv sync
|
|
98
104
|
uv pip install -e .
|
|
99
105
|
|
|
100
|
-
Copy the included [.env.example](https://github.com/adamhadani/mockstack/blob/main/.env.example) file to `.env` and fill in configuration as needed based on the given examples.
|
|
101
|
-
|
|
102
106
|
Run in development mode (for live-reload of changes when developing):
|
|
103
107
|
|
|
104
108
|
uv run uvicorn --factory mockstack.main:create_app --reload
|
|
109
|
+
|
|
110
|
+
Note that when you run using the uvicorn CLI, you will need to set any configuration via `.env` file or environment variables.
|
|
@@ -23,7 +23,7 @@ Highlights include:
|
|
|
23
23
|
|
|
24
24
|
Install using [uv](https://docs.astral.sh/uv/). This package conforms the concept of a [tool](https://docs.astral.sh/uv/concepts/tools/) and hence can simply install / run with `uvx`:
|
|
25
25
|
|
|
26
|
-
uvx mockstack
|
|
26
|
+
uvx mockstack --help
|
|
27
27
|
|
|
28
28
|
or install into a persistent environment and add it to the PATH with:
|
|
29
29
|
|
|
@@ -32,15 +32,20 @@ or install into a persistent environment and add it to the PATH with:
|
|
|
32
32
|
|
|
33
33
|
## Usage
|
|
34
34
|
|
|
35
|
-
Available configuration options are [here](https://github.com/adamhadani/mockstack/blob/main/mockstack/config.py).
|
|
35
|
+
Available configuration options are [here](https://github.com/adamhadani/mockstack/blob/main/mockstack/config.py).
|
|
36
|
+
|
|
37
|
+
Setting individual options can be done either through an `.env` file, individual environment variables, or command-line arguments. For example:
|
|
36
38
|
|
|
37
39
|
```shell
|
|
40
|
+
export MOCKSTACK__STRATEGY=filefixtures
|
|
38
41
|
export MOCKSTACK__TEMPLATES_DIR=~/mockstack-templates/
|
|
39
42
|
export MOCKSTACK__OPENTELEMETRY__ENABLED=true
|
|
40
43
|
export MOCKSTACK__OPENTELEMETRY__CAPTURE_RESPONSE_BODY=true
|
|
41
|
-
|
|
44
|
+
uvx mockstack
|
|
42
45
|
```
|
|
43
46
|
|
|
47
|
+
See also the included [.env.example](https://github.com/adamhadani/mockstack/blob/main/.env.example) for more examples. You can copy that file to `.env` and fill in configuration as needed based on the given examples.
|
|
48
|
+
|
|
44
49
|
Out of the box, you get the following behavior when using the default `filefixtures` strategy:
|
|
45
50
|
|
|
46
51
|
- The HTTP request `GET /someservice/api/v1/user/c27f5b2b-6e81-420d-a4e4-6426e1c32db8` will try to find `<templates_dir>/someservice-api-v1-user.c27f5b2b-6e81-420d-a4e4-6426e1c32db8.j2`,
|
|
@@ -73,8 +78,8 @@ If you are contributing to development, you will want to clone this project, and
|
|
|
73
78
|
uv sync
|
|
74
79
|
uv pip install -e .
|
|
75
80
|
|
|
76
|
-
Copy the included [.env.example](https://github.com/adamhadani/mockstack/blob/main/.env.example) file to `.env` and fill in configuration as needed based on the given examples.
|
|
77
|
-
|
|
78
81
|
Run in development mode (for live-reload of changes when developing):
|
|
79
82
|
|
|
80
83
|
uv run uvicorn --factory mockstack.main:create_app --reload
|
|
84
|
+
|
|
85
|
+
Note that when you run using the uvicorn CLI, you will need to set any configuration via `.env` file or environment variables.
|
|
@@ -2,9 +2,19 @@ from functools import lru_cache
|
|
|
2
2
|
from typing import Any, Literal, Self
|
|
3
3
|
|
|
4
4
|
from pydantic import DirectoryPath, FilePath, model_validator
|
|
5
|
-
from pydantic_settings import
|
|
5
|
+
from pydantic_settings import (
|
|
6
|
+
BaseSettings,
|
|
7
|
+
CliImplicitFlag,
|
|
8
|
+
CliSuppress,
|
|
9
|
+
SettingsConfigDict,
|
|
10
|
+
)
|
|
6
11
|
|
|
7
|
-
from mockstack.constants import
|
|
12
|
+
from mockstack.constants import (
|
|
13
|
+
ENV_FILE,
|
|
14
|
+
ENV_NESTED_DELIMITER,
|
|
15
|
+
ENV_PREFIX,
|
|
16
|
+
ProxyRulesRedirectVia,
|
|
17
|
+
)
|
|
8
18
|
|
|
9
19
|
|
|
10
20
|
class OpenTelemetrySettings(BaseSettings):
|
|
@@ -28,11 +38,20 @@ class Settings(BaseSettings):
|
|
|
28
38
|
"""
|
|
29
39
|
|
|
30
40
|
model_config = SettingsConfigDict(
|
|
31
|
-
env_prefix=
|
|
32
|
-
env_file=
|
|
33
|
-
env_nested_delimiter=
|
|
41
|
+
env_prefix=ENV_PREFIX,
|
|
42
|
+
env_file=ENV_FILE,
|
|
43
|
+
env_nested_delimiter=ENV_NESTED_DELIMITER,
|
|
34
44
|
)
|
|
35
45
|
|
|
46
|
+
# whether to run in debug mode
|
|
47
|
+
debug: CliImplicitFlag[bool] = False
|
|
48
|
+
|
|
49
|
+
# host to run the server on
|
|
50
|
+
host: str = "0.0.0.0"
|
|
51
|
+
|
|
52
|
+
# port to run the server on
|
|
53
|
+
port: int = 8000
|
|
54
|
+
|
|
36
55
|
# OpenTelemetry configuration
|
|
37
56
|
opentelemetry: OpenTelemetrySettings = OpenTelemetrySettings()
|
|
38
57
|
|
|
@@ -51,11 +70,11 @@ class Settings(BaseSettings):
|
|
|
51
70
|
|
|
52
71
|
# controls behavior of proxying. Whether to simulate creation of resources
|
|
53
72
|
# when a POST request is made to a resource that doesn't match any rules..
|
|
54
|
-
proxyrules_simulate_create_on_missing: bool = False
|
|
73
|
+
proxyrules_simulate_create_on_missing: CliImplicitFlag[bool] = False
|
|
55
74
|
|
|
56
75
|
# metadata fields to inject into created resources.
|
|
57
76
|
# A few template fields are available. See documentation for more details.
|
|
58
|
-
created_resource_metadata: dict[str, Any] = {
|
|
77
|
+
created_resource_metadata: CliSuppress[dict[str, Any]] = {
|
|
59
78
|
"id": "{{ uuid4() }}",
|
|
60
79
|
"createdAt": "{{ utcnow().isoformat() }}",
|
|
61
80
|
"updatedAt": "{{ utcnow().isoformat() }}",
|
|
@@ -65,7 +84,7 @@ class Settings(BaseSettings):
|
|
|
65
84
|
|
|
66
85
|
# fields to inject into missing resources response json.
|
|
67
86
|
# some services may require such additional fields to be present in the response.
|
|
68
|
-
missing_resource_fields: dict[str, Any] = dict(
|
|
87
|
+
missing_resource_fields: CliSuppress[dict[str, Any]] = dict(
|
|
69
88
|
code=404,
|
|
70
89
|
message="mockstack: resource not found",
|
|
71
90
|
retryable=False,
|
|
@@ -73,7 +92,7 @@ class Settings(BaseSettings):
|
|
|
73
92
|
|
|
74
93
|
# logging configuration. schema is based on the logging configuration schema:
|
|
75
94
|
# https://docs.python.org/3/library/logging.config.html#logging-config-dictschema
|
|
76
|
-
logging: dict[str, Any] = {
|
|
95
|
+
logging: CliSuppress[dict[str, Any]] = {
|
|
77
96
|
"version": 1,
|
|
78
97
|
"disable_existing_loggers": False,
|
|
79
98
|
"formatters": {
|
|
@@ -84,32 +103,30 @@ class Settings(BaseSettings):
|
|
|
84
103
|
},
|
|
85
104
|
"handlers": {
|
|
86
105
|
"console": {
|
|
87
|
-
"class": "logging.
|
|
88
|
-
"level": "
|
|
89
|
-
"formatter": "standard",
|
|
90
|
-
"stream": "ext://sys.stdout",
|
|
106
|
+
"class": "rich.logging.RichHandler",
|
|
107
|
+
"level": "INFO",
|
|
91
108
|
},
|
|
92
109
|
},
|
|
93
110
|
"loggers": {
|
|
94
111
|
"uvicorn": {
|
|
95
112
|
"handlers": ["console"],
|
|
96
|
-
"level": "
|
|
113
|
+
"level": "DEBUG",
|
|
97
114
|
"propagate": False,
|
|
98
115
|
},
|
|
99
116
|
"FileFixturesStrategy": {
|
|
100
117
|
"handlers": ["console"],
|
|
101
|
-
"level": "
|
|
118
|
+
"level": "DEBUG",
|
|
102
119
|
"propagate": False,
|
|
103
120
|
},
|
|
104
121
|
"ProxyRulesStrategy": {
|
|
105
122
|
"handlers": ["console"],
|
|
106
|
-
"level": "
|
|
123
|
+
"level": "DEBUG",
|
|
107
124
|
"propagate": False,
|
|
108
125
|
},
|
|
109
126
|
},
|
|
110
127
|
"root": {
|
|
111
128
|
"handlers": ["console"],
|
|
112
|
-
"level": "
|
|
129
|
+
"level": "NOTSET",
|
|
113
130
|
"propagate": False,
|
|
114
131
|
},
|
|
115
132
|
}
|
|
@@ -135,6 +152,24 @@ class Settings(BaseSettings):
|
|
|
135
152
|
return self
|
|
136
153
|
|
|
137
154
|
|
|
155
|
+
# Nb. We separate the Cli-specific parameters since currently breaks pytest
|
|
156
|
+
# when running via pre-commit hooks. Can remove once fixed by pytest / pre-commit.
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class CliSettings(Settings):
|
|
160
|
+
"""Settings for mockstack CLI."""
|
|
161
|
+
|
|
162
|
+
model_config = SettingsConfigDict(
|
|
163
|
+
env_prefix=ENV_PREFIX,
|
|
164
|
+
env_file=ENV_FILE,
|
|
165
|
+
env_nested_delimiter=ENV_NESTED_DELIMITER,
|
|
166
|
+
cli_parse_args=True,
|
|
167
|
+
cli_kebab_case=True,
|
|
168
|
+
cli_hide_none_type=True,
|
|
169
|
+
cli_avoid_json=True,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
138
173
|
@lru_cache
|
|
139
174
|
def settings_provider() -> Settings:
|
|
140
175
|
"""Provide the settings for the application."""
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Display and logging functionality."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from fastapi import FastAPI
|
|
6
|
+
|
|
7
|
+
from mockstack.config import Settings
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def announce(app: FastAPI, settings: Settings):
|
|
11
|
+
"""Log the startup message with the active settings."""
|
|
12
|
+
logger = logging.getLogger("uvicorn")
|
|
13
|
+
extra = {"markup": True}
|
|
14
|
+
|
|
15
|
+
logger.info(
|
|
16
|
+
f"[bold medium_purple]mockstack[/bold medium_purple] ready to roll. "
|
|
17
|
+
f"debug: [medium_purple]{settings.debug}[/medium_purple]. "
|
|
18
|
+
f"strategy: [medium_purple]{settings.strategy}[/medium_purple]. ",
|
|
19
|
+
extra=extra,
|
|
20
|
+
)
|
|
21
|
+
logger.info(str(app.state.strategy), extra=extra)
|
|
22
|
+
logger.info(
|
|
23
|
+
f"OpenTelemetry enabled: [medium_purple]{settings.opentelemetry.enabled}[/medium_purple], "
|
|
24
|
+
f"endpoint: [medium_purple]{settings.opentelemetry.endpoint}[/medium_purple], "
|
|
25
|
+
f"capture_response_body: [medium_purple]{settings.opentelemetry.capture_response_body}[/medium_purple]",
|
|
26
|
+
extra=extra,
|
|
27
|
+
)
|
|
@@ -51,3 +51,18 @@ def looks_like_a_command(request: Request) -> bool:
|
|
|
51
51
|
request.url.path.endswith("/execute"),
|
|
52
52
|
)
|
|
53
53
|
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def looks_like_a_create(request: Request) -> bool:
|
|
57
|
+
"""Check if the request looks like a create.
|
|
58
|
+
|
|
59
|
+
This is a heuristic to try and identify cases where a POST
|
|
60
|
+
request is used for creating a new resource.
|
|
61
|
+
|
|
62
|
+
"""
|
|
63
|
+
return any(
|
|
64
|
+
(
|
|
65
|
+
request.method == "POST",
|
|
66
|
+
request.url.path.endswith("/create"),
|
|
67
|
+
),
|
|
68
|
+
)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""FastAPI application lifecycle management."""
|
|
2
2
|
|
|
3
3
|
from contextlib import asynccontextmanager
|
|
4
|
-
from logging import config
|
|
4
|
+
from logging import DEBUG, config
|
|
5
5
|
from typing import Callable
|
|
6
6
|
|
|
7
7
|
from fastapi import FastAPI
|
|
@@ -21,7 +21,12 @@ def lifespan_provider(
|
|
|
21
21
|
|
|
22
22
|
This is the context manager that FastAPI will use to manage the lifecycle of the application.
|
|
23
23
|
"""
|
|
24
|
+
if settings.debug:
|
|
25
|
+
# Enable verbose debug logging if debug mode is set.
|
|
26
|
+
settings.logging["handlers"]["console"]["level"] = DEBUG
|
|
27
|
+
|
|
24
28
|
config.dictConfig(settings.logging)
|
|
29
|
+
|
|
25
30
|
announce(app, settings)
|
|
26
31
|
|
|
27
32
|
yield
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""Application entrypoints."""
|
|
2
2
|
|
|
3
3
|
from fastapi import FastAPI
|
|
4
|
-
from
|
|
4
|
+
from pydantic_settings import CliApp, CliSettingsSource
|
|
5
5
|
|
|
6
|
-
from mockstack.config import settings_provider
|
|
6
|
+
from mockstack.config import CliSettings, Settings, settings_provider
|
|
7
7
|
from mockstack.lifespan import lifespan_provider
|
|
8
8
|
from mockstack.middleware import middleware_provider
|
|
9
9
|
from mockstack.routers.catchall import catchall_router_provider
|
|
@@ -11,12 +11,10 @@ from mockstack.routers.homepage import homepage_router_provider
|
|
|
11
11
|
from mockstack.strategies.factory import strategy_provider
|
|
12
12
|
from mockstack.telemetry import opentelemetry_provider
|
|
13
13
|
|
|
14
|
-
cli = Typer()
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
settings = settings_provider()
|
|
15
|
+
def create_app(settings: Settings | None = None) -> FastAPI:
|
|
16
|
+
"""Create the fastapi app and bootstrap all dependencies."""
|
|
17
|
+
settings = settings or settings_provider()
|
|
20
18
|
|
|
21
19
|
app = FastAPI(lifespan=lifespan_provider(settings))
|
|
22
20
|
|
|
@@ -30,23 +28,23 @@ def create_app() -> FastAPI:
|
|
|
30
28
|
return app
|
|
31
29
|
|
|
32
30
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
def run():
|
|
32
|
+
"""run the mockstack server."""
|
|
33
|
+
import argparse
|
|
36
34
|
import uvicorn
|
|
37
35
|
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
parser = argparse.ArgumentParser()
|
|
37
|
+
cli_settings = CliSettingsSource(CliSettings, root_parser=parser)
|
|
38
|
+
settings = CliApp.run(CliSettings, cli_settings_source=cli_settings)
|
|
39
|
+
|
|
40
|
+
app = create_app(settings=settings)
|
|
41
|
+
|
|
42
|
+
uvicorn.run(app, host=settings.host, port=settings.port)
|
|
40
43
|
|
|
41
44
|
|
|
42
|
-
@cli.command()
|
|
43
45
|
def version():
|
|
44
|
-
"""mockstack version
|
|
46
|
+
"""display mockstack version."""
|
|
45
47
|
from importlib.metadata import version
|
|
46
48
|
|
|
47
49
|
pkg_version = version("mockstack")
|
|
48
50
|
print(f"mockstack {pkg_version}")
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if __name__ == "__main__":
|
|
52
|
-
cli()
|
|
@@ -8,7 +8,6 @@ from fastapi import HTTPException, Request, Response, status
|
|
|
8
8
|
from jinja2 import Environment, FileSystemLoader
|
|
9
9
|
|
|
10
10
|
from mockstack.config import Settings
|
|
11
|
-
from mockstack.display import ANSIColors
|
|
12
11
|
from mockstack.intent import (
|
|
13
12
|
looks_like_a_command,
|
|
14
13
|
looks_like_a_search,
|
|
@@ -39,12 +38,9 @@ class FileFixturesStrategy(BaseStrategy, CreateMixin):
|
|
|
39
38
|
self.env = Environment(loader=FileSystemLoader(self.templates_dir))
|
|
40
39
|
|
|
41
40
|
def __str__(self) -> str:
|
|
42
|
-
HIGHLIGHT = ANSIColors.HEADER
|
|
43
|
-
ENDC = ANSIColors.ENDC
|
|
44
|
-
|
|
45
41
|
return (
|
|
46
|
-
f"
|
|
47
|
-
f"templates_dir: {
|
|
42
|
+
f"[medium_purple]filefixtures[/medium_purple] "
|
|
43
|
+
f"templates_dir: [medium_purple]{self.templates_dir}[/medium_purple]. "
|
|
48
44
|
)
|
|
49
45
|
|
|
50
46
|
async def apply(self, request: Request) -> Response:
|
|
@@ -85,6 +81,7 @@ class FileFixturesStrategy(BaseStrategy, CreateMixin):
|
|
|
85
81
|
request, status_code=status.HTTP_201_CREATED
|
|
86
82
|
)
|
|
87
83
|
else:
|
|
84
|
+
# simulate resource creation:
|
|
88
85
|
return await self._create(
|
|
89
86
|
request,
|
|
90
87
|
env=self.env,
|
|
@@ -15,7 +15,7 @@ from starlette.datastructures import Headers
|
|
|
15
15
|
|
|
16
16
|
from mockstack.config import Settings
|
|
17
17
|
from mockstack.constants import ProxyRulesRedirectVia
|
|
18
|
-
from mockstack.
|
|
18
|
+
from mockstack.intent import looks_like_a_create
|
|
19
19
|
from mockstack.strategies.base import BaseStrategy
|
|
20
20
|
from mockstack.strategies.create_mixin import CreateMixin
|
|
21
21
|
|
|
@@ -69,14 +69,11 @@ class ProxyRulesStrategy(BaseStrategy, CreateMixin):
|
|
|
69
69
|
self.env = Environment()
|
|
70
70
|
|
|
71
71
|
def __str__(self) -> str:
|
|
72
|
-
HIGHLIGHT = ANSIColors.HEADER
|
|
73
|
-
ENDC = ANSIColors.ENDC
|
|
74
|
-
|
|
75
72
|
return (
|
|
76
|
-
f"
|
|
77
|
-
f"rules_filename: {
|
|
78
|
-
f"redirect_via: {
|
|
79
|
-
f"simulate_create_on_missing: {
|
|
73
|
+
f"[medium_purple]proxyrules[/medium_purple] "
|
|
74
|
+
f"rules_filename: [medium_purple]{self.rules_filename}[/medium_purple]. "
|
|
75
|
+
f"redirect_via: [medium_purple]{self.proxyrules_redirect_via}[/medium_purple]. "
|
|
76
|
+
f"simulate_create_on_missing: [medium_purple]{self.proxyrules_simulate_create_on_missing}[/medium_purple]"
|
|
80
77
|
)
|
|
81
78
|
|
|
82
79
|
@cached_property
|
|
@@ -103,9 +100,12 @@ class ProxyRulesStrategy(BaseStrategy, CreateMixin):
|
|
|
103
100
|
self.logger.warning(
|
|
104
101
|
f"No rule found for request: {request.method} {request.url.path}"
|
|
105
102
|
)
|
|
106
|
-
|
|
103
|
+
|
|
104
|
+
if self.proxyrules_simulate_create_on_missing and looks_like_a_create(
|
|
105
|
+
request
|
|
106
|
+
):
|
|
107
107
|
self.logger.info(
|
|
108
|
-
"Simulating resource creation for missing rule for {request.method} {request.url.path}"
|
|
108
|
+
f"Simulating resource creation for missing rule for {request.method} {request.url.path}"
|
|
109
109
|
)
|
|
110
110
|
return await self._create(
|
|
111
111
|
request,
|
|
@@ -113,6 +113,7 @@ class ProxyRulesStrategy(BaseStrategy, CreateMixin):
|
|
|
113
113
|
created_resource_metadata=self.created_resource_metadata,
|
|
114
114
|
)
|
|
115
115
|
else:
|
|
116
|
+
# no rule found and we are not simulating resource creation
|
|
116
117
|
return Response(status_code=status.HTTP_404_NOT_FOUND)
|
|
117
118
|
|
|
118
119
|
url = rule.apply(request)
|
|
@@ -141,13 +142,13 @@ class ProxyRulesStrategy(BaseStrategy, CreateMixin):
|
|
|
141
142
|
async def reverse_proxy(self, request: Request, url: str) -> Response:
|
|
142
143
|
"""Reverse proxy the request to the target URL."""
|
|
143
144
|
async with httpx.AsyncClient() as client:
|
|
144
|
-
method = request.method
|
|
145
145
|
request_content = await request.body()
|
|
146
|
+
request_headers = self.reverse_proxy_headers(request.headers, url=url)
|
|
146
147
|
req = client.build_request(
|
|
147
|
-
method,
|
|
148
|
+
request.method,
|
|
148
149
|
url,
|
|
149
150
|
content=request_content,
|
|
150
|
-
headers=
|
|
151
|
+
headers=request_headers,
|
|
151
152
|
params=request.url.query,
|
|
152
153
|
)
|
|
153
154
|
|
|
@@ -2,20 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from unittest.mock import patch
|
|
4
4
|
|
|
5
|
-
from mockstack.display import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def test_ansicolors_constants():
|
|
9
|
-
"""Test that ANSIColors constants are defined correctly."""
|
|
10
|
-
assert ANSIColors.HEADER == "\033[95m"
|
|
11
|
-
assert ANSIColors.OKBLUE == "\033[94m"
|
|
12
|
-
assert ANSIColors.OKCYAN == "\033[96m"
|
|
13
|
-
assert ANSIColors.OKGREEN == "\033[92m"
|
|
14
|
-
assert ANSIColors.WARNING == "\033[93m"
|
|
15
|
-
assert ANSIColors.FAIL == "\033[91m"
|
|
16
|
-
assert ANSIColors.ENDC == "\033[0m"
|
|
17
|
-
assert ANSIColors.BOLD == "\033[1m"
|
|
18
|
-
assert ANSIColors.UNDERLINE == "\033[4m"
|
|
5
|
+
from mockstack.display import announce
|
|
19
6
|
|
|
20
7
|
|
|
21
8
|
def test_announce(app, settings):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mockstack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: An API mocking workhorse
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/adamhadani/mockstack
|
|
@@ -18,6 +18,7 @@ Requires-Dist: opentelemetry-instrumentation-fastapi>=0.53b1
|
|
|
18
18
|
Requires-Dist: pydantic>=2.11.3
|
|
19
19
|
Requires-Dist: pydantic-settings>=2.9.1
|
|
20
20
|
Requires-Dist: pyyaml>=6.0.2
|
|
21
|
+
Requires-Dist: rich>=14.0.0
|
|
21
22
|
Requires-Dist: typer>=0.15.2
|
|
22
23
|
Requires-Dist: uvicorn>=0.34.2
|
|
23
24
|
Dynamic: license-file
|
|
@@ -47,7 +48,7 @@ Highlights include:
|
|
|
47
48
|
|
|
48
49
|
Install using [uv](https://docs.astral.sh/uv/). This package conforms the concept of a [tool](https://docs.astral.sh/uv/concepts/tools/) and hence can simply install / run with `uvx`:
|
|
49
50
|
|
|
50
|
-
uvx mockstack
|
|
51
|
+
uvx mockstack --help
|
|
51
52
|
|
|
52
53
|
or install into a persistent environment and add it to the PATH with:
|
|
53
54
|
|
|
@@ -56,15 +57,20 @@ or install into a persistent environment and add it to the PATH with:
|
|
|
56
57
|
|
|
57
58
|
## Usage
|
|
58
59
|
|
|
59
|
-
Available configuration options are [here](https://github.com/adamhadani/mockstack/blob/main/mockstack/config.py).
|
|
60
|
+
Available configuration options are [here](https://github.com/adamhadani/mockstack/blob/main/mockstack/config.py).
|
|
61
|
+
|
|
62
|
+
Setting individual options can be done either through an `.env` file, individual environment variables, or command-line arguments. For example:
|
|
60
63
|
|
|
61
64
|
```shell
|
|
65
|
+
export MOCKSTACK__STRATEGY=filefixtures
|
|
62
66
|
export MOCKSTACK__TEMPLATES_DIR=~/mockstack-templates/
|
|
63
67
|
export MOCKSTACK__OPENTELEMETRY__ENABLED=true
|
|
64
68
|
export MOCKSTACK__OPENTELEMETRY__CAPTURE_RESPONSE_BODY=true
|
|
65
|
-
|
|
69
|
+
uvx mockstack
|
|
66
70
|
```
|
|
67
71
|
|
|
72
|
+
See also the included [.env.example](https://github.com/adamhadani/mockstack/blob/main/.env.example) for more examples. You can copy that file to `.env` and fill in configuration as needed based on the given examples.
|
|
73
|
+
|
|
68
74
|
Out of the box, you get the following behavior when using the default `filefixtures` strategy:
|
|
69
75
|
|
|
70
76
|
- The HTTP request `GET /someservice/api/v1/user/c27f5b2b-6e81-420d-a4e4-6426e1c32db8` will try to find `<templates_dir>/someservice-api-v1-user.c27f5b2b-6e81-420d-a4e4-6426e1c32db8.j2`,
|
|
@@ -97,8 +103,8 @@ If you are contributing to development, you will want to clone this project, and
|
|
|
97
103
|
uv sync
|
|
98
104
|
uv pip install -e .
|
|
99
105
|
|
|
100
|
-
Copy the included [.env.example](https://github.com/adamhadani/mockstack/blob/main/.env.example) file to `.env` and fill in configuration as needed based on the given examples.
|
|
101
|
-
|
|
102
106
|
Run in development mode (for live-reload of changes when developing):
|
|
103
107
|
|
|
104
108
|
uv run uvicorn --factory mockstack.main:create_app --reload
|
|
109
|
+
|
|
110
|
+
Note that when you run using the uvicorn CLI, you will need to set any configuration via `.env` file or environment variables.
|
|
@@ -20,6 +20,7 @@ dependencies = [
|
|
|
20
20
|
"pydantic>=2.11.3",
|
|
21
21
|
"pydantic-settings>=2.9.1",
|
|
22
22
|
"pyyaml>=6.0.2",
|
|
23
|
+
"rich>=14.0.0",
|
|
23
24
|
"typer>=0.15.2",
|
|
24
25
|
"uvicorn>=0.34.2",
|
|
25
26
|
]
|
|
@@ -30,7 +31,7 @@ Homepage = "https://github.com/adamhadani/mockstack"
|
|
|
30
31
|
Issues = "https://github.com/adamhadani/mockstack/issues"
|
|
31
32
|
|
|
32
33
|
[project.scripts]
|
|
33
|
-
mockstack = "mockstack.main:
|
|
34
|
+
mockstack = "mockstack.main:run"
|
|
34
35
|
|
|
35
36
|
[dependency-groups]
|
|
36
37
|
dev = [
|