mockstack 0.2.0__tar.gz → 0.4.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.4.0/.env.example +20 -0
- {mockstack-0.2.0/mockstack.egg-info → mockstack-0.4.0}/PKG-INFO +10 -7
- {mockstack-0.2.0 → mockstack-0.4.0}/README.md +9 -6
- {mockstack-0.2.0 → mockstack-0.4.0}/mockstack/config.py +69 -11
- {mockstack-0.2.0 → mockstack-0.4.0}/mockstack/constants.py +4 -1
- {mockstack-0.2.0 → mockstack-0.4.0}/mockstack/display.py +2 -1
- mockstack-0.4.0/mockstack/intent.py +53 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/mockstack/lifespan.py +6 -1
- {mockstack-0.2.0 → mockstack-0.4.0}/mockstack/main.py +16 -18
- mockstack-0.4.0/mockstack/strategies/create_mixin.py +81 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/mockstack/strategies/filefixtures.py +11 -101
- {mockstack-0.2.0 → mockstack-0.4.0}/mockstack/strategies/proxyrules.py +26 -5
- {mockstack-0.2.0 → mockstack-0.4.0}/mockstack/telemetry.py +0 -1
- {mockstack-0.2.0 → mockstack-0.4.0}/mockstack/tests/conftest.py +1 -0
- {mockstack-0.2.0 → mockstack-0.4.0/mockstack.egg-info}/PKG-INFO +10 -7
- {mockstack-0.2.0 → mockstack-0.4.0}/mockstack.egg-info/SOURCES.txt +2 -0
- mockstack-0.4.0/mockstack.egg-info/entry_points.txt +2 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/pyproject.toml +2 -2
- mockstack-0.2.0/.env.example +0 -7
- mockstack-0.2.0/mockstack.egg-info/entry_points.txt +0 -2
- {mockstack-0.2.0 → mockstack-0.4.0}/.github/workflows/ci.yml +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/.github/workflows/publish-docs.yml +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/.github/workflows/publish-to-pypi.yml +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/.gitignore +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/.pre-commit-config.yaml +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/CODE_OF_CONDUCT.md +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/LICENSE +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/SECURITY.md +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/docs/assets/favicon.ico +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/docs/assets/logo.png +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/docs/assets/mockstack.png +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/mkdocs.yml +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/mockstack/__init__.py +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/mockstack/identifiers.py +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/mockstack/middleware.py +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/mockstack/routers/__init__.py +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/mockstack/routers/catchall.py +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/mockstack/routers/homepage.py +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/mockstack/strategies/__init__.py +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/mockstack/strategies/base.py +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/mockstack/strategies/factory.py +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/mockstack/templating.py +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/mockstack/tests/__init__.py +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/mockstack/tests/fixtures/proxyrules.yml +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/mockstack/tests/fixtures/templates/__init__.py +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/mockstack/tests/fixtures/templates/example-template.j2 +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/mockstack/tests/routers/__init__.py +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/mockstack/tests/routers/test_catchall.py +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/mockstack/tests/routers/test_homepage.py +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/mockstack/tests/strategies/test_filefixtures.py +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/mockstack/tests/strategies/test_proxyrules.py +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/mockstack/tests/test_display.py +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/mockstack/tests/test_identifiers.py +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/mockstack/tests/test_middleware.py +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/mockstack/tests/test_templating.py +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/mockstack.egg-info/dependency_links.txt +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/mockstack.egg-info/requires.txt +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/mockstack.egg-info/top_level.txt +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/setup.cfg +0 -0
- {mockstack-0.2.0 → mockstack-0.4.0}/uv.lock +0 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Example environment variables for mockstack
|
|
2
|
+
|
|
3
|
+
# the strategy to use
|
|
4
|
+
MOCKSTACK__STRATEGY = filefixtures
|
|
5
|
+
|
|
6
|
+
# strategy-specific settings
|
|
7
|
+
|
|
8
|
+
# base dir for all templates. Used by 'filefixtures' strategy
|
|
9
|
+
# and other hybrid strategies utilizing templates.
|
|
10
|
+
MOCKSTACK__TEMPLATES_DIR = "/some/path/templates/"
|
|
11
|
+
|
|
12
|
+
# rules table for the proxyrules strategy.
|
|
13
|
+
# see .mockstack/tests/fixtures/ for examples.
|
|
14
|
+
MOCKSTACK__PROXYRULES_RULES_FILENAME="/some/path/proxyrules.yml"
|
|
15
|
+
|
|
16
|
+
MOCKSTACK__PROXYRULES_SIMULATE_CREATE_ON_MISSING = true
|
|
17
|
+
|
|
18
|
+
# OpenTelemetry settings
|
|
19
|
+
MOCKSTACK__OPENTELEMETRY__ENABLED = true
|
|
20
|
+
MOCKSTACK__OPENTELEMETRY__CAPTURE_RESPONSE_BODY = true
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mockstack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: An API mocking workhorse
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/adamhadani/mockstack
|
|
@@ -47,7 +47,7 @@ Highlights include:
|
|
|
47
47
|
|
|
48
48
|
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
49
|
|
|
50
|
-
uvx mockstack
|
|
50
|
+
uvx mockstack --help
|
|
51
51
|
|
|
52
52
|
or install into a persistent environment and add it to the PATH with:
|
|
53
53
|
|
|
@@ -56,15 +56,20 @@ or install into a persistent environment and add it to the PATH with:
|
|
|
56
56
|
|
|
57
57
|
## Usage
|
|
58
58
|
|
|
59
|
-
Available configuration options are [here](https://github.com/adamhadani/mockstack/blob/main/mockstack/config.py).
|
|
59
|
+
Available configuration options are [here](https://github.com/adamhadani/mockstack/blob/main/mockstack/config.py).
|
|
60
|
+
|
|
61
|
+
Setting individual options can be done either through an `.env` file, individual environment variables, or command-line arguments. For example:
|
|
60
62
|
|
|
61
63
|
```shell
|
|
64
|
+
export MOCKSTACK__STRATEGY=filefixtures
|
|
62
65
|
export MOCKSTACK__TEMPLATES_DIR=~/mockstack-templates/
|
|
63
66
|
export MOCKSTACK__OPENTELEMETRY__ENABLED=true
|
|
64
67
|
export MOCKSTACK__OPENTELEMETRY__CAPTURE_RESPONSE_BODY=true
|
|
65
|
-
|
|
68
|
+
uvx mockstack
|
|
66
69
|
```
|
|
67
70
|
|
|
71
|
+
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.
|
|
72
|
+
|
|
68
73
|
Out of the box, you get the following behavior when using the default `filefixtures` strategy:
|
|
69
74
|
|
|
70
75
|
- 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 +102,6 @@ If you are contributing to development, you will want to clone this project, and
|
|
|
97
102
|
uv sync
|
|
98
103
|
uv pip install -e .
|
|
99
104
|
|
|
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
105
|
Run in development mode (for live-reload of changes when developing):
|
|
103
106
|
|
|
104
|
-
uv run
|
|
107
|
+
uv run -- mockstack --debug
|
|
@@ -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,6 @@ 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
|
-
uv run
|
|
83
|
+
uv run -- mockstack --debug
|
|
@@ -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
|
|
|
@@ -49,9 +68,13 @@ class Settings(BaseSettings):
|
|
|
49
68
|
# or reverse proxy the request to the target URL "silently".
|
|
50
69
|
proxyrules_redirect_via: ProxyRulesRedirectVia = ProxyRulesRedirectVia.REVERSE_PROXY
|
|
51
70
|
|
|
71
|
+
# controls behavior of proxying. Whether to simulate creation of resources
|
|
72
|
+
# when a POST request is made to a resource that doesn't match any rules..
|
|
73
|
+
proxyrules_simulate_create_on_missing: CliImplicitFlag[bool] = False
|
|
74
|
+
|
|
52
75
|
# metadata fields to inject into created resources.
|
|
53
76
|
# A few template fields are available. See documentation for more details.
|
|
54
|
-
created_resource_metadata: dict[str, Any] = {
|
|
77
|
+
created_resource_metadata: CliSuppress[dict[str, Any]] = {
|
|
55
78
|
"id": "{{ uuid4() }}",
|
|
56
79
|
"createdAt": "{{ utcnow().isoformat() }}",
|
|
57
80
|
"updatedAt": "{{ utcnow().isoformat() }}",
|
|
@@ -61,7 +84,7 @@ class Settings(BaseSettings):
|
|
|
61
84
|
|
|
62
85
|
# fields to inject into missing resources response json.
|
|
63
86
|
# some services may require such additional fields to be present in the response.
|
|
64
|
-
missing_resource_fields: dict[str, Any] = dict(
|
|
87
|
+
missing_resource_fields: CliSuppress[dict[str, Any]] = dict(
|
|
65
88
|
code=404,
|
|
66
89
|
message="mockstack: resource not found",
|
|
67
90
|
retryable=False,
|
|
@@ -69,28 +92,45 @@ class Settings(BaseSettings):
|
|
|
69
92
|
|
|
70
93
|
# logging configuration. schema is based on the logging configuration schema:
|
|
71
94
|
# https://docs.python.org/3/library/logging.config.html#logging-config-dictschema
|
|
72
|
-
logging: dict[str, Any] = {
|
|
95
|
+
logging: CliSuppress[dict[str, Any]] = {
|
|
73
96
|
"version": 1,
|
|
74
97
|
"disable_existing_loggers": False,
|
|
75
98
|
"formatters": {
|
|
76
99
|
"standard": {
|
|
77
|
-
"
|
|
100
|
+
"()": "uvicorn.logging.DefaultFormatter",
|
|
101
|
+
"fmt": "%(levelprefix)s %(message)s",
|
|
78
102
|
},
|
|
79
103
|
},
|
|
80
104
|
"handlers": {
|
|
81
105
|
"console": {
|
|
82
106
|
"class": "logging.StreamHandler",
|
|
83
|
-
"level": "
|
|
107
|
+
"level": "INFO",
|
|
84
108
|
"formatter": "standard",
|
|
85
109
|
"stream": "ext://sys.stdout",
|
|
86
110
|
},
|
|
87
111
|
},
|
|
88
112
|
"loggers": {
|
|
113
|
+
"uvicorn": {
|
|
114
|
+
"handlers": ["console"],
|
|
115
|
+
"level": "DEBUG",
|
|
116
|
+
"propagate": False,
|
|
117
|
+
},
|
|
89
118
|
"FileFixturesStrategy": {
|
|
90
119
|
"handlers": ["console"],
|
|
91
|
-
"level": "
|
|
120
|
+
"level": "DEBUG",
|
|
121
|
+
"propagate": False,
|
|
122
|
+
},
|
|
123
|
+
"ProxyRulesStrategy": {
|
|
124
|
+
"handlers": ["console"],
|
|
125
|
+
"level": "DEBUG",
|
|
126
|
+
"propagate": False,
|
|
92
127
|
},
|
|
93
128
|
},
|
|
129
|
+
"root": {
|
|
130
|
+
"handlers": ["console"],
|
|
131
|
+
"level": "DEBUG",
|
|
132
|
+
"propagate": False,
|
|
133
|
+
},
|
|
94
134
|
}
|
|
95
135
|
|
|
96
136
|
@model_validator(mode="after")
|
|
@@ -114,6 +154,24 @@ class Settings(BaseSettings):
|
|
|
114
154
|
return self
|
|
115
155
|
|
|
116
156
|
|
|
157
|
+
# Nb. We separate the Cli-specific parameters since currently breaks pytest
|
|
158
|
+
# when running via pre-commit hooks. Can remove once fixed by pytest / pre-commit.
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class CliSettings(Settings):
|
|
162
|
+
"""Settings for mockstack CLI."""
|
|
163
|
+
|
|
164
|
+
model_config = SettingsConfigDict(
|
|
165
|
+
env_prefix=ENV_PREFIX,
|
|
166
|
+
env_file=ENV_FILE,
|
|
167
|
+
env_nested_delimiter=ENV_NESTED_DELIMITER,
|
|
168
|
+
cli_parse_args=True,
|
|
169
|
+
cli_kebab_case=True,
|
|
170
|
+
cli_hide_none_type=True,
|
|
171
|
+
cli_avoid_json=True,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
117
175
|
@lru_cache
|
|
118
176
|
def settings_provider() -> Settings:
|
|
119
177
|
"""Provide the settings for the application."""
|
|
@@ -27,7 +27,8 @@ def announce(app: FastAPI, settings: Settings):
|
|
|
27
27
|
logger = logging.getLogger("uvicorn")
|
|
28
28
|
logger.info(
|
|
29
29
|
f"{HIGHLIGHT}mockstack{ENDC} ready to roll. "
|
|
30
|
-
f"
|
|
30
|
+
f"debug: {HIGHLIGHT}{settings.debug}{ENDC}. "
|
|
31
|
+
f"strategy: {HIGHLIGHT}{settings.strategy}{ENDC}. "
|
|
31
32
|
)
|
|
32
33
|
logger.info(str(app.state.strategy))
|
|
33
34
|
logger.info(
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Helpers for deducing user intent from a request."""
|
|
2
|
+
|
|
3
|
+
from fastapi import Request
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def wants_json(request: Request) -> bool:
|
|
7
|
+
"""Check if the request wants JSON response."""
|
|
8
|
+
content_type = request.headers.get("Content-Type", "")
|
|
9
|
+
return any(
|
|
10
|
+
(
|
|
11
|
+
content_type.startswith("application/json"),
|
|
12
|
+
content_type.startswith("text/json"),
|
|
13
|
+
request.url.path.endswith(".json"),
|
|
14
|
+
)
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def looks_like_a_search(request: Request) -> bool:
|
|
19
|
+
"""Check if the request looks like a search.
|
|
20
|
+
|
|
21
|
+
This is a heuristic to try and identify cases where a POST
|
|
22
|
+
request is used for issuing a search rather than for creating
|
|
23
|
+
a new resource.
|
|
24
|
+
|
|
25
|
+
"""
|
|
26
|
+
return any(
|
|
27
|
+
(
|
|
28
|
+
request.url.path.endswith("_search"),
|
|
29
|
+
request.url.path.endswith("/search"),
|
|
30
|
+
request.url.path.endswith("_query"),
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def looks_like_a_command(request: Request) -> bool:
|
|
36
|
+
"""Check if the request looks like a command.
|
|
37
|
+
|
|
38
|
+
This is a heuristic to try and identify cases where a POST
|
|
39
|
+
request is used for issuing a command rather than for creating
|
|
40
|
+
a new resource.
|
|
41
|
+
"""
|
|
42
|
+
return any(
|
|
43
|
+
(
|
|
44
|
+
request.url.path.endswith("_command"),
|
|
45
|
+
request.url.path.endswith("/command"),
|
|
46
|
+
request.url.path.endswith("_request"),
|
|
47
|
+
request.url.path.endswith("/request"),
|
|
48
|
+
request.url.path.endswith("_run"),
|
|
49
|
+
request.url.path.endswith("/run"),
|
|
50
|
+
request.url.path.endswith("_execute"),
|
|
51
|
+
request.url.path.endswith("/execute"),
|
|
52
|
+
)
|
|
53
|
+
)
|
|
@@ -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()
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Create mixin class."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from uuid import uuid4
|
|
5
|
+
|
|
6
|
+
from fastapi import Request, Response, status
|
|
7
|
+
from fastapi.responses import JSONResponse
|
|
8
|
+
from jinja2 import Environment
|
|
9
|
+
|
|
10
|
+
from mockstack.intent import wants_json
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CreateMixin:
|
|
14
|
+
"""A mixin for strategies that need to simulate creation of resources."""
|
|
15
|
+
|
|
16
|
+
async def _create(
|
|
17
|
+
self, request: Request, *, env: Environment, created_resource_metadata: dict
|
|
18
|
+
) -> Response:
|
|
19
|
+
if wants_json(request):
|
|
20
|
+
# We return a 201 CREATED response with the resource as the body,
|
|
21
|
+
# potentially injecting the resource ID into the response.
|
|
22
|
+
resource = await request.json()
|
|
23
|
+
|
|
24
|
+
return JSONResponse(
|
|
25
|
+
status_code=status.HTTP_201_CREATED,
|
|
26
|
+
content=self._content(
|
|
27
|
+
resource,
|
|
28
|
+
request=request,
|
|
29
|
+
env=env,
|
|
30
|
+
created_resource_metadata=created_resource_metadata,
|
|
31
|
+
),
|
|
32
|
+
)
|
|
33
|
+
else:
|
|
34
|
+
# We return a 201 CREATED response with an empty body.
|
|
35
|
+
return Response(
|
|
36
|
+
status_code=status.HTTP_201_CREATED,
|
|
37
|
+
content=None,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def _content(
|
|
41
|
+
self,
|
|
42
|
+
resource: dict,
|
|
43
|
+
*,
|
|
44
|
+
env: Environment,
|
|
45
|
+
request: Request,
|
|
46
|
+
created_resource_metadata: dict,
|
|
47
|
+
) -> dict:
|
|
48
|
+
"""Create a new resource given a request resource.
|
|
49
|
+
|
|
50
|
+
We use the request resource as the basis for the new resource.
|
|
51
|
+
We then inject an identifier into the resource if it doesn't already have one,
|
|
52
|
+
as well as any other metadata fields that are configured for the strategy.
|
|
53
|
+
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def with_metadata(resource: dict, copy=True) -> dict:
|
|
57
|
+
"""Inject metadata fields into the resource."""
|
|
58
|
+
_resource = resource.copy() if copy else resource
|
|
59
|
+
for key, value in created_resource_metadata.items():
|
|
60
|
+
if isinstance(value, str):
|
|
61
|
+
_resource[key] = env.from_string(value).render(
|
|
62
|
+
self._metadata_context(request)
|
|
63
|
+
)
|
|
64
|
+
else:
|
|
65
|
+
_resource[key] = value
|
|
66
|
+
return _resource
|
|
67
|
+
|
|
68
|
+
return with_metadata(resource)
|
|
69
|
+
|
|
70
|
+
def _metadata_context(self, request: Request) -> dict:
|
|
71
|
+
"""Context for injecting metadata fields into resources.
|
|
72
|
+
|
|
73
|
+
Some care is needed to ensure that we only expose the minimum amount
|
|
74
|
+
of information here since templates are user-defined.
|
|
75
|
+
|
|
76
|
+
"""
|
|
77
|
+
return {
|
|
78
|
+
"utcnow": lambda: datetime.now(timezone.utc),
|
|
79
|
+
"uuid4": uuid4,
|
|
80
|
+
"request": request,
|
|
81
|
+
}
|
|
@@ -2,67 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
|
-
from datetime import datetime, timezone
|
|
6
5
|
from pathlib import Path
|
|
7
|
-
from uuid import uuid4
|
|
8
6
|
|
|
9
7
|
from fastapi import HTTPException, Request, Response, status
|
|
10
|
-
from fastapi.responses import JSONResponse
|
|
11
8
|
from jinja2 import Environment, FileSystemLoader
|
|
12
9
|
|
|
13
10
|
from mockstack.config import Settings
|
|
14
11
|
from mockstack.display import ANSIColors
|
|
12
|
+
from mockstack.intent import (
|
|
13
|
+
looks_like_a_command,
|
|
14
|
+
looks_like_a_search,
|
|
15
|
+
)
|
|
15
16
|
from mockstack.strategies.base import BaseStrategy
|
|
17
|
+
from mockstack.strategies.create_mixin import CreateMixin
|
|
16
18
|
from mockstack.templating import (
|
|
17
19
|
iter_possible_template_arguments,
|
|
18
20
|
missing_template_detail,
|
|
19
21
|
)
|
|
20
22
|
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
"""Check if the media type is JSON."""
|
|
24
|
-
return media_type in ("application/json", "text/json")
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def looks_like_a_search(request: Request) -> bool:
|
|
28
|
-
"""Check if the request looks like a search.
|
|
29
|
-
|
|
30
|
-
This is a heuristic to try and identify cases where a POST
|
|
31
|
-
request is used for issuing a search rather than for creating
|
|
32
|
-
a new resource.
|
|
33
|
-
|
|
34
|
-
"""
|
|
35
|
-
return any(
|
|
36
|
-
(
|
|
37
|
-
request.url.path.endswith("_search"),
|
|
38
|
-
request.url.path.endswith("/search"),
|
|
39
|
-
request.url.path.endswith("_query"),
|
|
40
|
-
)
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def looks_like_a_command(request: Request) -> bool:
|
|
45
|
-
"""Check if the request looks like a command.
|
|
46
|
-
|
|
47
|
-
This is a heuristic to try and identify cases where a POST
|
|
48
|
-
request is used for issuing a command rather than for creating
|
|
49
|
-
a new resource.
|
|
50
|
-
"""
|
|
51
|
-
return any(
|
|
52
|
-
(
|
|
53
|
-
request.url.path.endswith("_command"),
|
|
54
|
-
request.url.path.endswith("/command"),
|
|
55
|
-
request.url.path.endswith("_request"),
|
|
56
|
-
request.url.path.endswith("/request"),
|
|
57
|
-
request.url.path.endswith("_run"),
|
|
58
|
-
request.url.path.endswith("/run"),
|
|
59
|
-
request.url.path.endswith("_execute"),
|
|
60
|
-
request.url.path.endswith("/execute"),
|
|
61
|
-
)
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
class FileFixturesStrategy(BaseStrategy):
|
|
24
|
+
class FileFixturesStrategy(BaseStrategy, CreateMixin):
|
|
66
25
|
"""Strategy for using file-based fixtures."""
|
|
67
26
|
|
|
68
27
|
logger = logging.getLogger("FileFixturesStrategy")
|
|
@@ -126,24 +85,11 @@ class FileFixturesStrategy(BaseStrategy):
|
|
|
126
85
|
request, status_code=status.HTTP_201_CREATED
|
|
127
86
|
)
|
|
128
87
|
else:
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
# potentially injecting the resource ID into the response.
|
|
135
|
-
resource = await request.json()
|
|
136
|
-
|
|
137
|
-
return JSONResponse(
|
|
138
|
-
status_code=status.HTTP_201_CREATED,
|
|
139
|
-
content=self._created(resource, request=request),
|
|
140
|
-
)
|
|
141
|
-
else:
|
|
142
|
-
# We return a 201 CREATEDresponse with an empty body.
|
|
143
|
-
return Response(
|
|
144
|
-
status_code=status.HTTP_201_CREATED,
|
|
145
|
-
content=None,
|
|
146
|
-
)
|
|
88
|
+
return await self._create(
|
|
89
|
+
request,
|
|
90
|
+
env=self.env,
|
|
91
|
+
created_resource_metadata=self.created_resource_metadata,
|
|
92
|
+
)
|
|
147
93
|
|
|
148
94
|
async def _get(self, request: Request) -> Response:
|
|
149
95
|
"""Apply the strategy for GET requests.
|
|
@@ -176,42 +122,6 @@ class FileFixturesStrategy(BaseStrategy):
|
|
|
176
122
|
"""Apply the strategy for PUT requests."""
|
|
177
123
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
178
124
|
|
|
179
|
-
def _created(self, resource: dict, *, request: Request) -> dict:
|
|
180
|
-
"""Create a new resource given a request resource.
|
|
181
|
-
|
|
182
|
-
We use the request resource as the basis for the new resource.
|
|
183
|
-
We then inject an identifier into the resource if it doesn't already have one,
|
|
184
|
-
as well as any other metadata fields that are configured for the strategy.
|
|
185
|
-
|
|
186
|
-
"""
|
|
187
|
-
|
|
188
|
-
def with_metadata(resource: dict, copy=True) -> dict:
|
|
189
|
-
"""Inject metadata fields into the resource."""
|
|
190
|
-
_resource = resource.copy() if copy else resource
|
|
191
|
-
for key, value in self.created_resource_metadata.items():
|
|
192
|
-
if isinstance(value, str):
|
|
193
|
-
_resource[key] = self.env.from_string(value).render(
|
|
194
|
-
self._metadata_context(request)
|
|
195
|
-
)
|
|
196
|
-
else:
|
|
197
|
-
_resource[key] = value
|
|
198
|
-
return _resource
|
|
199
|
-
|
|
200
|
-
return with_metadata(resource)
|
|
201
|
-
|
|
202
|
-
def _metadata_context(self, request: Request) -> dict:
|
|
203
|
-
"""Context for injecting metadata fields into resources.
|
|
204
|
-
|
|
205
|
-
Some care is needed to ensure that we only expose the minimum amount
|
|
206
|
-
of information here since templates are user-defined.
|
|
207
|
-
|
|
208
|
-
"""
|
|
209
|
-
return {
|
|
210
|
-
"utcnow": lambda: datetime.now(timezone.utc),
|
|
211
|
-
"uuid4": uuid4,
|
|
212
|
-
"request": request,
|
|
213
|
-
}
|
|
214
|
-
|
|
215
125
|
def _response_from_template(
|
|
216
126
|
self, request: Request, status_code: int = status.HTTP_200_OK
|
|
217
127
|
) -> Response:
|
|
@@ -10,12 +10,14 @@ import httpx
|
|
|
10
10
|
import yaml
|
|
11
11
|
from fastapi import Request, Response, status
|
|
12
12
|
from fastapi.responses import RedirectResponse
|
|
13
|
+
from jinja2 import Environment
|
|
13
14
|
from starlette.datastructures import Headers
|
|
14
15
|
|
|
15
16
|
from mockstack.config import Settings
|
|
16
17
|
from mockstack.constants import ProxyRulesRedirectVia
|
|
17
18
|
from mockstack.display import ANSIColors
|
|
18
19
|
from mockstack.strategies.base import BaseStrategy
|
|
20
|
+
from mockstack.strategies.create_mixin import CreateMixin
|
|
19
21
|
|
|
20
22
|
|
|
21
23
|
class Rule:
|
|
@@ -50,7 +52,7 @@ class Rule:
|
|
|
50
52
|
return re.sub(self.pattern, self.replacement, path)
|
|
51
53
|
|
|
52
54
|
|
|
53
|
-
class ProxyRulesStrategy(BaseStrategy):
|
|
55
|
+
class ProxyRulesStrategy(BaseStrategy, CreateMixin):
|
|
54
56
|
"""Strategy for using proxy rules."""
|
|
55
57
|
|
|
56
58
|
logger = logging.getLogger("ProxyRulesStrategy")
|
|
@@ -59,6 +61,12 @@ class ProxyRulesStrategy(BaseStrategy):
|
|
|
59
61
|
super().__init__(settings, *args, **kwargs)
|
|
60
62
|
self.rules_filename = settings.proxyrules_rules_filename
|
|
61
63
|
self.proxyrules_redirect_via = settings.proxyrules_redirect_via
|
|
64
|
+
self.proxyrules_simulate_create_on_missing = (
|
|
65
|
+
settings.proxyrules_simulate_create_on_missing
|
|
66
|
+
)
|
|
67
|
+
self.created_resource_metadata = settings.created_resource_metadata
|
|
68
|
+
|
|
69
|
+
self.env = Environment()
|
|
62
70
|
|
|
63
71
|
def __str__(self) -> str:
|
|
64
72
|
HIGHLIGHT = ANSIColors.HEADER
|
|
@@ -67,7 +75,8 @@ class ProxyRulesStrategy(BaseStrategy):
|
|
|
67
75
|
return (
|
|
68
76
|
f"{HIGHLIGHT}[proxyrules]{ENDC} "
|
|
69
77
|
f"rules_filename: {HIGHLIGHT}{self.rules_filename}{ENDC}. "
|
|
70
|
-
f"redirect_via: {HIGHLIGHT}{self.proxyrules_redirect_via}{ENDC}"
|
|
78
|
+
f"redirect_via: {HIGHLIGHT}{self.proxyrules_redirect_via}{ENDC}. "
|
|
79
|
+
f"simulate_create_on_missing: {HIGHLIGHT}{self.proxyrules_simulate_create_on_missing}{ENDC}"
|
|
71
80
|
)
|
|
72
81
|
|
|
73
82
|
@cached_property
|
|
@@ -91,8 +100,20 @@ class ProxyRulesStrategy(BaseStrategy):
|
|
|
91
100
|
async def apply(self, request: Request) -> Response:
|
|
92
101
|
rule = self.rule_for(request)
|
|
93
102
|
if rule is None:
|
|
94
|
-
self.logger.warning(
|
|
95
|
-
|
|
103
|
+
self.logger.warning(
|
|
104
|
+
f"No rule found for request: {request.method} {request.url.path}"
|
|
105
|
+
)
|
|
106
|
+
if self.proxyrules_simulate_create_on_missing:
|
|
107
|
+
self.logger.info(
|
|
108
|
+
"Simulating resource creation for missing rule for {request.method} {request.url.path}"
|
|
109
|
+
)
|
|
110
|
+
return await self._create(
|
|
111
|
+
request,
|
|
112
|
+
env=self.env,
|
|
113
|
+
created_resource_metadata=self.created_resource_metadata,
|
|
114
|
+
)
|
|
115
|
+
else:
|
|
116
|
+
return Response(status_code=status.HTTP_404_NOT_FOUND)
|
|
96
117
|
|
|
97
118
|
url = rule.apply(request)
|
|
98
119
|
self.logger.info(f"Redirecting to: {url}")
|
|
@@ -114,7 +135,7 @@ class ProxyRulesStrategy(BaseStrategy):
|
|
|
114
135
|
|
|
115
136
|
case _:
|
|
116
137
|
raise ValueError(
|
|
117
|
-
f"Invalid redirect via: {self.proxyrules_redirect_via}"
|
|
138
|
+
f"Invalid redirect via value: {self.proxyrules_redirect_via=}"
|
|
118
139
|
)
|
|
119
140
|
|
|
120
141
|
async def reverse_proxy(self, request: Request, url: str) -> Response:
|
|
@@ -5,7 +5,6 @@ from importlib import metadata
|
|
|
5
5
|
from fastapi import FastAPI, Request
|
|
6
6
|
from opentelemetry import trace
|
|
7
7
|
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
|
8
|
-
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
|
9
8
|
from opentelemetry.sdk.resources import Resource
|
|
10
9
|
from opentelemetry.sdk.trace import TracerProvider
|
|
11
10
|
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
@@ -37,6 +37,7 @@ def settings(templates_dir, proxyrules_rules_filename):
|
|
|
37
37
|
templates_dir=templates_dir,
|
|
38
38
|
proxyrules_rules_filename=proxyrules_rules_filename,
|
|
39
39
|
proxyrules_redirect_via=ProxyRulesRedirectVia.HTTP_TEMPORARY_REDIRECT,
|
|
40
|
+
proxyrules_simulate_create_on_missing=False,
|
|
40
41
|
opentelemetry=OpenTelemetrySettings(enabled=False),
|
|
41
42
|
)
|
|
42
43
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mockstack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: An API mocking workhorse
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/adamhadani/mockstack
|
|
@@ -47,7 +47,7 @@ Highlights include:
|
|
|
47
47
|
|
|
48
48
|
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
49
|
|
|
50
|
-
uvx mockstack
|
|
50
|
+
uvx mockstack --help
|
|
51
51
|
|
|
52
52
|
or install into a persistent environment and add it to the PATH with:
|
|
53
53
|
|
|
@@ -56,15 +56,20 @@ or install into a persistent environment and add it to the PATH with:
|
|
|
56
56
|
|
|
57
57
|
## Usage
|
|
58
58
|
|
|
59
|
-
Available configuration options are [here](https://github.com/adamhadani/mockstack/blob/main/mockstack/config.py).
|
|
59
|
+
Available configuration options are [here](https://github.com/adamhadani/mockstack/blob/main/mockstack/config.py).
|
|
60
|
+
|
|
61
|
+
Setting individual options can be done either through an `.env` file, individual environment variables, or command-line arguments. For example:
|
|
60
62
|
|
|
61
63
|
```shell
|
|
64
|
+
export MOCKSTACK__STRATEGY=filefixtures
|
|
62
65
|
export MOCKSTACK__TEMPLATES_DIR=~/mockstack-templates/
|
|
63
66
|
export MOCKSTACK__OPENTELEMETRY__ENABLED=true
|
|
64
67
|
export MOCKSTACK__OPENTELEMETRY__CAPTURE_RESPONSE_BODY=true
|
|
65
|
-
|
|
68
|
+
uvx mockstack
|
|
66
69
|
```
|
|
67
70
|
|
|
71
|
+
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.
|
|
72
|
+
|
|
68
73
|
Out of the box, you get the following behavior when using the default `filefixtures` strategy:
|
|
69
74
|
|
|
70
75
|
- 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 +102,6 @@ If you are contributing to development, you will want to clone this project, and
|
|
|
97
102
|
uv sync
|
|
98
103
|
uv pip install -e .
|
|
99
104
|
|
|
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
105
|
Run in development mode (for live-reload of changes when developing):
|
|
103
106
|
|
|
104
|
-
uv run
|
|
107
|
+
uv run -- mockstack --debug
|
|
@@ -19,6 +19,7 @@ mockstack/config.py
|
|
|
19
19
|
mockstack/constants.py
|
|
20
20
|
mockstack/display.py
|
|
21
21
|
mockstack/identifiers.py
|
|
22
|
+
mockstack/intent.py
|
|
22
23
|
mockstack/lifespan.py
|
|
23
24
|
mockstack/main.py
|
|
24
25
|
mockstack/middleware.py
|
|
@@ -35,6 +36,7 @@ mockstack/routers/catchall.py
|
|
|
35
36
|
mockstack/routers/homepage.py
|
|
36
37
|
mockstack/strategies/__init__.py
|
|
37
38
|
mockstack/strategies/base.py
|
|
39
|
+
mockstack/strategies/create_mixin.py
|
|
38
40
|
mockstack/strategies/factory.py
|
|
39
41
|
mockstack/strategies/filefixtures.py
|
|
40
42
|
mockstack/strategies/proxyrules.py
|
|
@@ -30,7 +30,7 @@ Homepage = "https://github.com/adamhadani/mockstack"
|
|
|
30
30
|
Issues = "https://github.com/adamhadani/mockstack/issues"
|
|
31
31
|
|
|
32
32
|
[project.scripts]
|
|
33
|
-
mockstack = "mockstack.main:
|
|
33
|
+
mockstack = "mockstack.main:run"
|
|
34
34
|
|
|
35
35
|
[dependency-groups]
|
|
36
36
|
dev = [
|
|
@@ -47,9 +47,9 @@ dev = [
|
|
|
47
47
|
|
|
48
48
|
[tool.mypy]
|
|
49
49
|
namespace_packages = true
|
|
50
|
+
ignore_missing_imports = true
|
|
50
51
|
|
|
51
52
|
[tool.pytest.ini_options]
|
|
52
53
|
asyncio_default_fixture_loop_scope = "function"
|
|
53
54
|
|
|
54
55
|
[tool.ruff.lint]
|
|
55
|
-
select = ["I"]
|
mockstack-0.2.0/.env.example
DELETED
|
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
|
|
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
|
|
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
|
|
File without changes
|