mockstack 0.7.3__tar.gz → 0.8.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.7.3 → mockstack-0.8.0}/PKG-INFO +4 -2
- {mockstack-0.7.3 → mockstack-0.8.0}/README.md +1 -1
- mockstack-0.8.0/docs/ollama.md +86 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/examples/llm/mockstack-langchain-example.ipynb +73 -8
- mockstack-0.8.0/examples/llm/templates/ollama-openai-v1-chat-completions.j2 +35 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/mkdocs.yml +8 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/config.py +2 -2
- mockstack-0.8.0/mockstack/exceptions.py +6 -0
- mockstack-0.8.0/mockstack/llm/ollama.py +50 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/strategies/filefixtures.py +21 -8
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/strategies/proxyrules.py +6 -2
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/templating.py +44 -1
- mockstack-0.8.0/mockstack/tests/llm/__init__.py +0 -0
- mockstack-0.8.0/mockstack/tests/llm/test_ollama.py +95 -0
- mockstack-0.8.0/mockstack/tests/routers/__init__.py +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/tests/test_templating.py +29 -6
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack.egg-info/PKG-INFO +4 -2
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack.egg-info/SOURCES.txt +7 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack.egg-info/requires.txt +3 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/pyproject.toml +5 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/uv.lock +20 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/.dockerignore +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/.env.example +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/.github/workflows/ci.yml +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/.github/workflows/publish-docs.yml +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/.github/workflows/publish-to-dockerhub.yml +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/.github/workflows/publish-to-pypi.yml +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/.gitignore +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/.pre-commit-config.yaml +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/CODE_OF_CONDUCT.md +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/Dockerfile +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/LICENSE +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/SECURITY.md +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/docker-entrypoint.sh +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/docs/assets/favicon.ico +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/docs/assets/logo.png +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/docs/assets/mockstack.png +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/docs/configuration.md +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/docs/strategies/base.md +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/docs/strategies/filefixtures.md +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/docs/strategies/proxyrules.md +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/examples/filefixtures-with-templates/.env.example +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/examples/filefixtures-with-templates/README.md +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/examples/filefixtures-with-templates/templates/servicename-api-v1-items.j2 +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/examples/filefixtures-with-templates/templates/servicename2-api-v2-user.533ec889-7c68-45c8-b21e-4a7e455d1234.j2 +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/examples/llm/README.md +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/examples/llm/templates/openai-v1-chat-completions.j2 +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/examples/llm/templates/openai-v1-deployments-gpt-4o-chat-completions.j2 +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/examples/proxyrules-with-rules-file/.env.example +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/examples/proxyrules-with-rules-file/README.md +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/examples/proxyrules-with-rules-file/rules.yml +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/__init__.py +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/constants.py +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/display.py +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/identifiers.py +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/intent.py +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/lifespan.py +0 -0
- {mockstack-0.7.3/mockstack/routers → mockstack-0.8.0/mockstack/llm}/__init__.py +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/main.py +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/middleware.py +0 -0
- {mockstack-0.7.3/mockstack/tests → mockstack-0.8.0/mockstack/routers}/__init__.py +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/routers/catchall.py +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/routers/homepage.py +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/strategies/__init__.py +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/strategies/base.py +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/strategies/create_mixin.py +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/strategies/factory.py +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/telemetry.py +0 -0
- {mockstack-0.7.3/mockstack/tests/fixtures/templates → mockstack-0.8.0/mockstack/tests}/__init__.py +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/tests/conftest.py +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/tests/fixtures/proxyrules.yml +0 -0
- {mockstack-0.7.3/mockstack/tests/routers → mockstack-0.8.0/mockstack/tests/fixtures/templates}/__init__.py +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/tests/fixtures/templates/example-template.j2 +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/tests/routers/test_catchall.py +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/tests/routers/test_homepage.py +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/tests/strategies/test_create_mixin.py +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/tests/strategies/test_filefixtures.py +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/tests/strategies/test_proxyrules.py +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/tests/test_display.py +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/tests/test_identifiers.py +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/tests/test_middleware.py +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/tests/test_telemetry.py +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack.egg-info/dependency_links.txt +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack.egg-info/entry_points.txt +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/mockstack.egg-info/top_level.txt +0 -0
- {mockstack-0.7.3 → mockstack-0.8.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mockstack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: An API mocking workhorse
|
|
5
5
|
Author-email: Adam Ever-Hadani <mockstack.contact@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -27,6 +27,8 @@ Requires-Dist: pyyaml>=6.0.2
|
|
|
27
27
|
Requires-Dist: rich>=14.0.0
|
|
28
28
|
Requires-Dist: typer>=0.15.2
|
|
29
29
|
Requires-Dist: uvicorn>=0.34.2
|
|
30
|
+
Provides-Extra: llm
|
|
31
|
+
Requires-Dist: ollama>=0.4.8; extra == "llm"
|
|
30
32
|
Dynamic: license-file
|
|
31
33
|
|
|
32
34
|

|
|
@@ -50,7 +52,7 @@ Use mockstack for:
|
|
|
50
52
|
|
|
51
53
|
* **Integration Testing** :ok_hand:. Creating a consistent environment for running integration tests on a single component, using fixture responses.
|
|
52
54
|
|
|
53
|
-
* **LLM-powered Workflows** :alien:. Speeding up development of LLM-based workflows and tools for use with frameworks such as [LangChain](https://python.langchain.com/docs/introduction/), [LangGraph](https://www.langchain.com/langgraph) and others. When developing LLM-driven execution graphs, you
|
|
55
|
+
* **LLM-powered Workflows** :alien:. Speeding up development and reducing per-token costs of LLM-based workflows and tools for use with frameworks such as [LangChain](https://python.langchain.com/docs/introduction/), [LangGraph](https://www.langchain.com/langgraph) and others. When developing LLM-driven execution graphs. Optional [Ollama](https://ollama.com/) integration allows for realistic mocking of 3rd party LLMs without changing a line of code in your project. In addition, when you want to have a consistent response from a tool while you're tuning prompts or debugging other aspects of a particular trace you can create various fixture responses with varying levels of dynamic content that is template-driven. **mockstack** can give you a solid foundation for this.
|
|
54
56
|
|
|
55
57
|
* **Chaos Engineering** :boom:. mockstack can simulate various real-world runtime error scenarios such as timeouts, http error codes, and invalid response payloads. This can be a great way to do some upfront [Chaos Monkey](https://github.com/Netflix/chaosmonkey) type of testing on software components.
|
|
56
58
|
|
|
@@ -19,7 +19,7 @@ Use mockstack for:
|
|
|
19
19
|
|
|
20
20
|
* **Integration Testing** :ok_hand:. Creating a consistent environment for running integration tests on a single component, using fixture responses.
|
|
21
21
|
|
|
22
|
-
* **LLM-powered Workflows** :alien:. Speeding up development of LLM-based workflows and tools for use with frameworks such as [LangChain](https://python.langchain.com/docs/introduction/), [LangGraph](https://www.langchain.com/langgraph) and others. When developing LLM-driven execution graphs, you
|
|
22
|
+
* **LLM-powered Workflows** :alien:. Speeding up development and reducing per-token costs of LLM-based workflows and tools for use with frameworks such as [LangChain](https://python.langchain.com/docs/introduction/), [LangGraph](https://www.langchain.com/langgraph) and others. When developing LLM-driven execution graphs. Optional [Ollama](https://ollama.com/) integration allows for realistic mocking of 3rd party LLMs without changing a line of code in your project. In addition, when you want to have a consistent response from a tool while you're tuning prompts or debugging other aspects of a particular trace you can create various fixture responses with varying levels of dynamic content that is template-driven. **mockstack** can give you a solid foundation for this.
|
|
23
23
|
|
|
24
24
|
* **Chaos Engineering** :boom:. mockstack can simulate various real-world runtime error scenarios such as timeouts, http error codes, and invalid response payloads. This can be a great way to do some upfront [Chaos Monkey](https://github.com/Netflix/chaosmonkey) type of testing on software components.
|
|
25
25
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Ollama Integration
|
|
2
|
+
|
|
3
|
+
Mockstack provides integration with [Ollama](https://ollama.com/), allowing you to use real LLM responses in your mock templates. This is particularly useful for development, debugging, and integration testing scenarios where you want to capture the non-deterministic nature of LLM responses.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
To use the Ollama integration, you'll need:
|
|
8
|
+
|
|
9
|
+
1. Mockstack installed with the optional `llm` dependencies:
|
|
10
|
+
```bash
|
|
11
|
+
uv pip install mockstack[llm]
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
2. [Ollama](https://ollama.com/) installed locally with at least one model (e.g., "llama3.2")
|
|
15
|
+
|
|
16
|
+
## Basic Usage
|
|
17
|
+
|
|
18
|
+
The Ollama integration works by routing requests to a template file that uses the special `ollama` template function.
|
|
19
|
+
|
|
20
|
+
1. Configure your LLM client to hit an endpoint that maps to a template filepath calling the `ollama` method, e.g.:
|
|
21
|
+
```python
|
|
22
|
+
from langchain_openai import ChatOpenAI
|
|
23
|
+
|
|
24
|
+
llm = ChatOpenAI(
|
|
25
|
+
model="gpt-4o",
|
|
26
|
+
base_url="http://localhost:8000/ollama/openai/v1",
|
|
27
|
+
api_key="SOME_STRING_THAT_DOES_NOT_MATTER",
|
|
28
|
+
)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
2. Make requests as you normally would:
|
|
32
|
+
```python
|
|
33
|
+
messages = [
|
|
34
|
+
(
|
|
35
|
+
"system",
|
|
36
|
+
"You are a helpful assistant that translates English to French. Translate the user sentence.",
|
|
37
|
+
),
|
|
38
|
+
("human", "mockstack is pretty cool. But LLMs are way cooler"),
|
|
39
|
+
]
|
|
40
|
+
ai_msg = llm.invoke(messages)
|
|
41
|
+
print(ai_msg.content)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Integration with Templates
|
|
45
|
+
|
|
46
|
+
You can also use Ollama responses within your Jinja templates. This allows you to:
|
|
47
|
+
|
|
48
|
+
1. Mix static and dynamic content
|
|
49
|
+
2. Apply transformations to the LLM responses
|
|
50
|
+
3. Create conditional logic based on the responses
|
|
51
|
+
|
|
52
|
+
Example template structure:
|
|
53
|
+
```jinja
|
|
54
|
+
{
|
|
55
|
+
"id": "chatcmpl-123",
|
|
56
|
+
"object": "chat.completion",
|
|
57
|
+
"created": 1677652288,
|
|
58
|
+
"model": "{{ model }}",
|
|
59
|
+
"choices": [{
|
|
60
|
+
"index": 0,
|
|
61
|
+
"message": {
|
|
62
|
+
"role": "assistant",
|
|
63
|
+
"content": "{{ ollama(request_json.messages, 'llama3.2') | json_escape }}"
|
|
64
|
+
},
|
|
65
|
+
"finish_reason": "stop"
|
|
66
|
+
}],
|
|
67
|
+
"usage": {
|
|
68
|
+
"prompt_tokens": 9,
|
|
69
|
+
"completion_tokens": 12,
|
|
70
|
+
"total_tokens": 21
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Best Practices
|
|
76
|
+
|
|
77
|
+
1. **Caching**: Consider implementing caching for frequently used responses to improve performance
|
|
78
|
+
2. **Model Selection**: Choose the appropriate model based on your testing needs
|
|
79
|
+
3. **Error Handling**: Implement proper error handling in your templates
|
|
80
|
+
4. **Performance**: Be mindful of response times when using real LLM responses
|
|
81
|
+
|
|
82
|
+
## Limitations
|
|
83
|
+
|
|
84
|
+
- The integration requires a local Ollama instance
|
|
85
|
+
- Response times will be slower than static templates
|
|
86
|
+
- Model availability depends on your local setup
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
{
|
|
13
13
|
"cell_type": "code",
|
|
14
|
-
"execution_count":
|
|
14
|
+
"execution_count": 7,
|
|
15
15
|
"metadata": {},
|
|
16
16
|
"outputs": [],
|
|
17
17
|
"source": [
|
|
@@ -22,11 +22,31 @@
|
|
|
22
22
|
"#!uv pip install langchain langchain-openai"
|
|
23
23
|
]
|
|
24
24
|
},
|
|
25
|
+
{
|
|
26
|
+
"cell_type": "code",
|
|
27
|
+
"execution_count": null,
|
|
28
|
+
"metadata": {},
|
|
29
|
+
"outputs": [],
|
|
30
|
+
"source": [
|
|
31
|
+
"from ollama import chat\n",
|
|
32
|
+
"from ollama import ChatResponse\n",
|
|
33
|
+
"\n",
|
|
34
|
+
"response: ChatResponse = chat(model='llama3.2', messages=[\n",
|
|
35
|
+
" {\n",
|
|
36
|
+
" 'role': 'user',\n",
|
|
37
|
+
" 'content': 'Why is the sky blue?',\n",
|
|
38
|
+
" },\n",
|
|
39
|
+
"])\n",
|
|
40
|
+
"print(response['message']['content'])\n",
|
|
41
|
+
"# or access fields directly from the response object\n",
|
|
42
|
+
"print(response.message.content)"
|
|
43
|
+
]
|
|
44
|
+
},
|
|
25
45
|
{
|
|
26
46
|
"cell_type": "markdown",
|
|
27
47
|
"metadata": {},
|
|
28
48
|
"source": [
|
|
29
|
-
"## Example #1:
|
|
49
|
+
"## Example #1: Static template-based mocking (`filefixtures` strategy)\n",
|
|
30
50
|
"\n",
|
|
31
51
|
"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
52
|
"\n",
|
|
@@ -42,7 +62,7 @@
|
|
|
42
62
|
},
|
|
43
63
|
{
|
|
44
64
|
"cell_type": "code",
|
|
45
|
-
"execution_count":
|
|
65
|
+
"execution_count": 2,
|
|
46
66
|
"metadata": {},
|
|
47
67
|
"outputs": [],
|
|
48
68
|
"source": [
|
|
@@ -86,7 +106,7 @@
|
|
|
86
106
|
},
|
|
87
107
|
{
|
|
88
108
|
"cell_type": "code",
|
|
89
|
-
"execution_count":
|
|
109
|
+
"execution_count": 3,
|
|
90
110
|
"metadata": {},
|
|
91
111
|
"outputs": [],
|
|
92
112
|
"source": [
|
|
@@ -120,17 +140,62 @@
|
|
|
120
140
|
"cell_type": "markdown",
|
|
121
141
|
"metadata": {},
|
|
122
142
|
"source": [
|
|
123
|
-
"## Example #2:
|
|
143
|
+
"## Example #2: Dynamic template-based mocking with ollama integration\n",
|
|
124
144
|
"\n",
|
|
125
|
-
"
|
|
145
|
+
"In this example we use the [ollama-python](https://github.com/ollama/ollama-python) integration to return mock responses that actually come from a real LLM running on your host! This is a great way to go a step further in development, debugging, and integration testing scenarios where LLMs and their non-determinism are important to capture.\n",
|
|
146
|
+
"\n",
|
|
147
|
+
"For this example you will need to make sure you have the following:\n",
|
|
148
|
+
"\n",
|
|
149
|
+
"* mockstack installed with the optional `llm` dependencies:\n",
|
|
150
|
+
"\n",
|
|
151
|
+
" ```bash\n",
|
|
152
|
+
" uv pip install mockstack[llm]\n",
|
|
153
|
+
" ```\n",
|
|
154
|
+
"\n",
|
|
155
|
+
"* [ollama](https://ollama.com/) installed locally with the \"llama3.2\" model (which is typically the default model installed)"
|
|
126
156
|
]
|
|
127
157
|
},
|
|
128
158
|
{
|
|
129
159
|
"cell_type": "code",
|
|
130
160
|
"execution_count": null,
|
|
131
161
|
"metadata": {},
|
|
132
|
-
"outputs": [
|
|
133
|
-
|
|
162
|
+
"outputs": [
|
|
163
|
+
{
|
|
164
|
+
"name": "stdout",
|
|
165
|
+
"output_type": "stream",
|
|
166
|
+
"text": [
|
|
167
|
+
"Le mockstack est très amusant, mais les LLMs sont beaucoup plus froids.\n",
|
|
168
|
+
"\n",
|
|
169
|
+
"(Note: I used \"froids\" instead of \"cool\" as it's a more common translation in French. If you want to keep the exact word order and meaning, I can suggest an alternative: Le mockstack est très amusant, mais les LLMs sont vraiment cool.)\n"
|
|
170
|
+
]
|
|
171
|
+
}
|
|
172
|
+
],
|
|
173
|
+
"source": [
|
|
174
|
+
"from langchain_openai import ChatOpenAI\n",
|
|
175
|
+
"\n",
|
|
176
|
+
"\n",
|
|
177
|
+
"llm = ChatOpenAI(\n",
|
|
178
|
+
" model=\"gpt-4o\",\n",
|
|
179
|
+
" temperature=0,\n",
|
|
180
|
+
" max_tokens=None,\n",
|
|
181
|
+
" timeout=None,\n",
|
|
182
|
+
" max_retries=2,\n",
|
|
183
|
+
" base_url=\"http://localhost:8000/ollama/openai/v1\",\n",
|
|
184
|
+
" api_key=\"SOME_STRING_THAT_DOES_NOT_MATTER\",\n",
|
|
185
|
+
")\n",
|
|
186
|
+
"\n",
|
|
187
|
+
"messages = [\n",
|
|
188
|
+
" (\n",
|
|
189
|
+
" \"system\",\n",
|
|
190
|
+
" \"You are a helpful assistant that translates English to French. Translate the user sentence.\",\n",
|
|
191
|
+
" ),\n",
|
|
192
|
+
" (\"human\", \"mockstack is pretty cool. But LLMs are way cooler\"),\n",
|
|
193
|
+
"]\n",
|
|
194
|
+
"ai_msg = llm.invoke(messages)\n",
|
|
195
|
+
"\n",
|
|
196
|
+
"# Output would not be deterministic, but should be a valid French translation.\n",
|
|
197
|
+
"print(ai_msg.content)"
|
|
198
|
+
]
|
|
134
199
|
},
|
|
135
200
|
{
|
|
136
201
|
"cell_type": "markdown",
|
|
@@ -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": "{{ ollama(request_json.messages, 'llama3.2') | json_escape }}",
|
|
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
|
+
}
|
|
@@ -49,6 +49,14 @@ markdown_extensions:
|
|
|
49
49
|
- pymdownx.tabbed:
|
|
50
50
|
alternate_style: true
|
|
51
51
|
|
|
52
|
+
nav:
|
|
53
|
+
- Home: README.md
|
|
54
|
+
- Configuration: configuration.md
|
|
55
|
+
- Strategies:
|
|
56
|
+
- Overview: strategies/index.md
|
|
57
|
+
- FileFixtures: strategies/filefixtures.md
|
|
58
|
+
- LLM Integrations:
|
|
59
|
+
- Ollama: ollama.md
|
|
52
60
|
|
|
53
61
|
extra:
|
|
54
62
|
social:
|
|
@@ -20,13 +20,13 @@ from mockstack.constants import (
|
|
|
20
20
|
class OpenTelemetrySettings(BaseSettings):
|
|
21
21
|
"""Settings for OpenTelemetry."""
|
|
22
22
|
|
|
23
|
-
enabled: bool = False
|
|
23
|
+
enabled: CliImplicitFlag[bool] = False
|
|
24
24
|
|
|
25
25
|
endpoint: str = "http://localhost:4317/"
|
|
26
26
|
|
|
27
27
|
# whether to capture the response body.
|
|
28
28
|
# this can be heavy, sensitive (PII) and/or not needed depending on the use case.
|
|
29
|
-
capture_response_body: bool = False
|
|
29
|
+
capture_response_body: CliImplicitFlag[bool] = False
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
class Settings(BaseSettings):
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Ollama integration"""
|
|
2
|
+
|
|
3
|
+
from typing import List, Dict
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from ollama import chat
|
|
7
|
+
from ollama import ChatResponse
|
|
8
|
+
|
|
9
|
+
IS_OLLAMA_AVAILABLE = True
|
|
10
|
+
except ImportError:
|
|
11
|
+
IS_OLLAMA_AVAILABLE = False
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
if IS_OLLAMA_AVAILABLE:
|
|
15
|
+
|
|
16
|
+
class OllamaLLM:
|
|
17
|
+
def __init__(self, model: str = "llama3.2"):
|
|
18
|
+
self.model = model
|
|
19
|
+
|
|
20
|
+
def __call__(
|
|
21
|
+
self,
|
|
22
|
+
messages: List[Dict[str, str]],
|
|
23
|
+
max_tokens: int = 4096,
|
|
24
|
+
temperature: float = 0.7,
|
|
25
|
+
) -> str:
|
|
26
|
+
response: ChatResponse = chat(
|
|
27
|
+
model=self.model,
|
|
28
|
+
messages=messages,
|
|
29
|
+
options={"num_ctx": max_tokens, "temperature": temperature},
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
return content(response)
|
|
33
|
+
|
|
34
|
+
def content(response: ChatResponse) -> str:
|
|
35
|
+
"""Extract the message content from the LLM response."""
|
|
36
|
+
return response["message"]["content"]
|
|
37
|
+
|
|
38
|
+
def ollama(
|
|
39
|
+
messages: List[Dict[str, str]],
|
|
40
|
+
model: str = "llama3.2",
|
|
41
|
+
*args,
|
|
42
|
+
**kwargs,
|
|
43
|
+
) -> str:
|
|
44
|
+
"""Fluent interface for Ollama to be used in templates."""
|
|
45
|
+
|
|
46
|
+
return OllamaLLM(model)(
|
|
47
|
+
messages,
|
|
48
|
+
*args,
|
|
49
|
+
**kwargs,
|
|
50
|
+
)
|
|
@@ -2,21 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
|
+
from functools import cached_property
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
|
|
7
8
|
from fastapi import HTTPException, Request, Response, status
|
|
8
|
-
from jinja2 import Environment
|
|
9
|
+
from jinja2 import Environment
|
|
9
10
|
|
|
10
11
|
from mockstack.config import Settings
|
|
11
12
|
from mockstack.intent import (
|
|
12
13
|
looks_like_a_command,
|
|
13
14
|
looks_like_a_search,
|
|
15
|
+
wants_json,
|
|
14
16
|
)
|
|
15
17
|
from mockstack.strategies.base import BaseStrategy
|
|
16
18
|
from mockstack.strategies.create_mixin import CreateMixin
|
|
17
19
|
from mockstack.templating import (
|
|
18
20
|
iter_possible_template_arguments,
|
|
19
21
|
missing_template_detail,
|
|
22
|
+
templates_env_provider,
|
|
20
23
|
)
|
|
21
24
|
|
|
22
25
|
|
|
@@ -37,8 +40,6 @@ class FileFixturesStrategy(BaseStrategy, CreateMixin):
|
|
|
37
40
|
self.created_resource_metadata = settings.created_resource_metadata
|
|
38
41
|
self.missing_resource_fields = settings.missing_resource_fields
|
|
39
42
|
|
|
40
|
-
self.env = Environment(loader=FileSystemLoader(self.templates_dir))
|
|
41
|
-
|
|
42
43
|
def __str__(self) -> str:
|
|
43
44
|
return (
|
|
44
45
|
f"[medium_purple]filefixtures[/medium_purple]\n "
|
|
@@ -46,6 +47,11 @@ class FileFixturesStrategy(BaseStrategy, CreateMixin):
|
|
|
46
47
|
f"enable_templates_for_post: [medium_purple]{self.enable_templates_for_post}[/medium_purple]. "
|
|
47
48
|
)
|
|
48
49
|
|
|
50
|
+
@cached_property
|
|
51
|
+
def env(self) -> Environment:
|
|
52
|
+
"""Jinja2 environment for the filefixtures strategy."""
|
|
53
|
+
return templates_env_provider(self.templates_dir)
|
|
54
|
+
|
|
49
55
|
async def apply(self, request: Request) -> Response:
|
|
50
56
|
match request.method:
|
|
51
57
|
case "GET":
|
|
@@ -74,9 +80,10 @@ class FileFixturesStrategy(BaseStrategy, CreateMixin):
|
|
|
74
80
|
We also allow a configuration to specify a default intent.
|
|
75
81
|
|
|
76
82
|
"""
|
|
83
|
+
request_json = (await request.json()) if wants_json(request) else None
|
|
77
84
|
if self.enable_templates_for_post:
|
|
78
85
|
try:
|
|
79
|
-
return self._response_from_template(request)
|
|
86
|
+
return self._response_from_template(request, request_json=request_json)
|
|
80
87
|
except HTTPException as e:
|
|
81
88
|
if e.status_code == status.HTTP_404_NOT_FOUND:
|
|
82
89
|
# If the template is not found, we try to create the resource with logic below.
|
|
@@ -86,12 +93,12 @@ class FileFixturesStrategy(BaseStrategy, CreateMixin):
|
|
|
86
93
|
|
|
87
94
|
if looks_like_a_search(request):
|
|
88
95
|
# Searching for resources with a complex query that cannot be expressed in a URI.
|
|
89
|
-
return self._response_from_template(request)
|
|
96
|
+
return self._response_from_template(request, request_json=request_json)
|
|
90
97
|
elif looks_like_a_command(request):
|
|
91
98
|
# Executing a 'command' of some sort, like a workflow or a batch job.
|
|
92
99
|
# We return a 201 CREATED status code with response from template.
|
|
93
100
|
return self._response_from_template(
|
|
94
|
-
request, status_code=status.HTTP_201_CREATED
|
|
101
|
+
request, request_json=request_json, status_code=status.HTTP_201_CREATED
|
|
95
102
|
)
|
|
96
103
|
else:
|
|
97
104
|
# simulate resource creation:
|
|
@@ -133,9 +140,15 @@ class FileFixturesStrategy(BaseStrategy, CreateMixin):
|
|
|
133
140
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
134
141
|
|
|
135
142
|
def _response_from_template(
|
|
136
|
-
self,
|
|
143
|
+
self,
|
|
144
|
+
request: Request,
|
|
145
|
+
*,
|
|
146
|
+
request_json: dict | None = None,
|
|
147
|
+
status_code: int = status.HTTP_200_OK,
|
|
137
148
|
) -> Response:
|
|
138
|
-
for template_args in iter_possible_template_arguments(
|
|
149
|
+
for template_args in iter_possible_template_arguments(
|
|
150
|
+
request, request_json=request_json
|
|
151
|
+
):
|
|
139
152
|
filename = self.templates_dir / template_args["name"]
|
|
140
153
|
self.logger.debug("Looking for template filename: %s", filename)
|
|
141
154
|
if not os.path.exists(filename):
|
|
@@ -18,6 +18,7 @@ from mockstack.constants import ProxyRulesRedirectVia
|
|
|
18
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
|
+
from mockstack.templating import templates_env_provider
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
class Rule:
|
|
@@ -73,8 +74,6 @@ class ProxyRulesStrategy(BaseStrategy, CreateMixin):
|
|
|
73
74
|
self.simulate_create_on_missing = settings.proxyrules_simulate_create_on_missing
|
|
74
75
|
self.created_resource_metadata = settings.created_resource_metadata
|
|
75
76
|
|
|
76
|
-
self.env = Environment()
|
|
77
|
-
|
|
78
77
|
def __str__(self) -> str:
|
|
79
78
|
return (
|
|
80
79
|
f"[medium_purple]proxyrules[/medium_purple]\n "
|
|
@@ -84,6 +83,11 @@ class ProxyRulesStrategy(BaseStrategy, CreateMixin):
|
|
|
84
83
|
f"reverse_proxy_timeout: {self.reverse_proxy_timeout}"
|
|
85
84
|
)
|
|
86
85
|
|
|
86
|
+
@cached_property
|
|
87
|
+
def env(self) -> Environment:
|
|
88
|
+
"""Jinja2 environment for the proxy rules strategy."""
|
|
89
|
+
return templates_env_provider()
|
|
90
|
+
|
|
87
91
|
@cached_property
|
|
88
92
|
def rules(self) -> list[Rule]:
|
|
89
93
|
return self.load_rules()
|
|
@@ -1,14 +1,39 @@
|
|
|
1
1
|
"""Templates related functionality."""
|
|
2
2
|
|
|
3
3
|
from collections import OrderedDict
|
|
4
|
+
from functools import partial
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from typing import Generator
|
|
6
7
|
|
|
7
8
|
from fastapi import Request
|
|
9
|
+
from jinja2 import Environment, FileSystemLoader
|
|
8
10
|
|
|
11
|
+
from mockstack.exceptions import raise_for_missing
|
|
9
12
|
from mockstack.identifiers import looks_like_id, prefixes
|
|
10
13
|
|
|
11
14
|
|
|
15
|
+
def templates_env_provider(templates_dir: Path | str | None = None) -> Environment:
|
|
16
|
+
"""Provide a Jinja2 environment for the templates."""
|
|
17
|
+
# TODO refactor a bit to be more generic for optional dependencies.
|
|
18
|
+
from mockstack.llm import ollama
|
|
19
|
+
|
|
20
|
+
loader = FileSystemLoader(templates_dir) if templates_dir else None
|
|
21
|
+
|
|
22
|
+
env = Environment(loader=loader)
|
|
23
|
+
|
|
24
|
+
env.filters["json_escape"] = json_escape
|
|
25
|
+
|
|
26
|
+
if ollama.IS_OLLAMA_AVAILABLE:
|
|
27
|
+
env.globals["ollama"] = ollama.ollama
|
|
28
|
+
else:
|
|
29
|
+
env.globals["ollama"] = partial(
|
|
30
|
+
raise_for_missing,
|
|
31
|
+
"Ollama is not available. Install with optional dependency mockstack[llm] to use it.",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
return env
|
|
35
|
+
|
|
36
|
+
|
|
12
37
|
def missing_template_detail(request: Request, *, templates_dir: Path) -> str:
|
|
13
38
|
"""Return a detailed message for a missing template."""
|
|
14
39
|
return (
|
|
@@ -21,6 +46,8 @@ def missing_template_detail(request: Request, *, templates_dir: Path) -> str:
|
|
|
21
46
|
|
|
22
47
|
def iter_possible_template_arguments(
|
|
23
48
|
request: Request,
|
|
49
|
+
*,
|
|
50
|
+
request_json: dict | None = None,
|
|
24
51
|
default_identifier_key: str = "id",
|
|
25
52
|
default_media_type: str = "application/json",
|
|
26
53
|
default_template_name: str = "index.j2",
|
|
@@ -47,9 +74,9 @@ def iter_possible_template_arguments(
|
|
|
47
74
|
)
|
|
48
75
|
media_type = request.headers.get("Content-Type", default_media_type)
|
|
49
76
|
|
|
50
|
-
# hydrate template context with additional request details such as query params.
|
|
51
77
|
context = dict(
|
|
52
78
|
query=dict(request.query_params),
|
|
79
|
+
request_json=request_json,
|
|
53
80
|
headers=dict(request.headers),
|
|
54
81
|
**identifiers,
|
|
55
82
|
)
|
|
@@ -119,3 +146,19 @@ def iter_possible_template_filenames(
|
|
|
119
146
|
yield template_file_separator.join(name_segments) + template_file_extension
|
|
120
147
|
|
|
121
148
|
yield default_template_name
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def json_escape(text: str) -> str:
|
|
152
|
+
"""Escape quotes, newlines, tabs, etc. in a string.
|
|
153
|
+
|
|
154
|
+
Used as a filter in templates to make the output JSON-compliant.
|
|
155
|
+
|
|
156
|
+
"""
|
|
157
|
+
return (
|
|
158
|
+
text.replace('"', '\\"')
|
|
159
|
+
.replace("\n", "\\n")
|
|
160
|
+
.replace("\r", "\\r")
|
|
161
|
+
.replace("\t", "\\t")
|
|
162
|
+
.replace("\f", "\\f")
|
|
163
|
+
.replace("\v", "\\v")
|
|
164
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Tests for the Ollama module."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from unittest.mock import patch, MagicMock
|
|
5
|
+
from typing import List, Dict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.fixture
|
|
9
|
+
def mock_messages() -> List[Dict[str, str]]:
|
|
10
|
+
"""Sample messages for testing."""
|
|
11
|
+
return [
|
|
12
|
+
{"role": "user", "content": "Hello"},
|
|
13
|
+
{"role": "assistant", "content": "Hi there!"},
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def mock_chat_response() -> Dict:
|
|
19
|
+
"""Mock chat response from Ollama."""
|
|
20
|
+
return {"message": {"content": "This is a test response"}}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def mock_ollama_module():
|
|
25
|
+
"""Mock the ollama module."""
|
|
26
|
+
# TODO: Should probably remove this in favor making sure ollama optional package
|
|
27
|
+
# is installed for running unit-tests, and instead mocking it as missing for the
|
|
28
|
+
# "not available" test cases.
|
|
29
|
+
with patch.dict("sys.modules", {"ollama": MagicMock()}) as mocked_dict:
|
|
30
|
+
yield mocked_dict
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TestOllamaAvailable:
|
|
34
|
+
"""Test cases when Ollama is available."""
|
|
35
|
+
|
|
36
|
+
@patch("mockstack.llm.ollama.IS_OLLAMA_AVAILABLE", True)
|
|
37
|
+
def test_ollama_llm_initialization(self, mock_ollama_module):
|
|
38
|
+
"""Test OllamaLLM initialization with default model."""
|
|
39
|
+
from mockstack.llm.ollama import OllamaLLM
|
|
40
|
+
|
|
41
|
+
llm = OllamaLLM()
|
|
42
|
+
assert llm.model == "llama3.2"
|
|
43
|
+
|
|
44
|
+
@patch("mockstack.llm.ollama.IS_OLLAMA_AVAILABLE", True)
|
|
45
|
+
def test_ollama_llm_initialization_custom_model(self, mock_ollama_module):
|
|
46
|
+
"""Test OllamaLLM initialization with custom model."""
|
|
47
|
+
from mockstack.llm.ollama import OllamaLLM
|
|
48
|
+
|
|
49
|
+
llm = OllamaLLM(model="custom-model")
|
|
50
|
+
assert llm.model == "custom-model"
|
|
51
|
+
|
|
52
|
+
@patch("mockstack.llm.ollama.IS_OLLAMA_AVAILABLE", True)
|
|
53
|
+
def test_ollama_llm_call(
|
|
54
|
+
self, mock_ollama_module, mock_messages, mock_chat_response
|
|
55
|
+
):
|
|
56
|
+
"""Test OllamaLLM.__call__ method."""
|
|
57
|
+
from mockstack.llm.ollama import OllamaLLM
|
|
58
|
+
|
|
59
|
+
mock_chat = MagicMock(return_value=mock_chat_response)
|
|
60
|
+
with patch("mockstack.llm.ollama.chat", mock_chat):
|
|
61
|
+
llm = OllamaLLM()
|
|
62
|
+
response = llm(mock_messages)
|
|
63
|
+
|
|
64
|
+
mock_chat.assert_called_once_with(
|
|
65
|
+
model="llama3.2",
|
|
66
|
+
messages=mock_messages,
|
|
67
|
+
options={"num_ctx": 4096, "temperature": 0.7},
|
|
68
|
+
)
|
|
69
|
+
assert response == "This is a test response"
|
|
70
|
+
|
|
71
|
+
@patch("mockstack.llm.ollama.IS_OLLAMA_AVAILABLE", True)
|
|
72
|
+
def test_content_function(self, mock_ollama_module, mock_chat_response):
|
|
73
|
+
"""Test content function extraction."""
|
|
74
|
+
from mockstack.llm.ollama import content
|
|
75
|
+
|
|
76
|
+
result = content(mock_chat_response)
|
|
77
|
+
assert result == "This is a test response"
|
|
78
|
+
|
|
79
|
+
@patch("mockstack.llm.ollama.IS_OLLAMA_AVAILABLE", True)
|
|
80
|
+
def test_ollama_function(
|
|
81
|
+
self, mock_ollama_module, mock_messages, mock_chat_response
|
|
82
|
+
):
|
|
83
|
+
"""Test ollama function."""
|
|
84
|
+
from mockstack.llm.ollama import ollama
|
|
85
|
+
|
|
86
|
+
mock_chat = MagicMock(return_value=mock_chat_response)
|
|
87
|
+
with patch("mockstack.llm.ollama.chat", mock_chat):
|
|
88
|
+
response = ollama(mock_messages)
|
|
89
|
+
|
|
90
|
+
mock_chat.assert_called_once_with(
|
|
91
|
+
model="llama3.2",
|
|
92
|
+
messages=mock_messages,
|
|
93
|
+
options={"num_ctx": 4096, "temperature": 0.7},
|
|
94
|
+
)
|
|
95
|
+
assert response == "This is a test response"
|
|
File without changes
|
|
@@ -18,17 +18,32 @@ from mockstack.templating import (
|
|
|
18
18
|
[
|
|
19
19
|
{
|
|
20
20
|
"name": "api-v1-projects.1234.j2",
|
|
21
|
-
"context": {
|
|
21
|
+
"context": {
|
|
22
|
+
"projects": "1234",
|
|
23
|
+
"query": {},
|
|
24
|
+
"headers": {},
|
|
25
|
+
"request_json": None,
|
|
26
|
+
},
|
|
22
27
|
"media_type": "application/json",
|
|
23
28
|
},
|
|
24
29
|
{
|
|
25
30
|
"name": "api-v1-projects.j2",
|
|
26
|
-
"context": {
|
|
31
|
+
"context": {
|
|
32
|
+
"projects": "1234",
|
|
33
|
+
"query": {},
|
|
34
|
+
"headers": {},
|
|
35
|
+
"request_json": None,
|
|
36
|
+
},
|
|
27
37
|
"media_type": "application/json",
|
|
28
38
|
},
|
|
29
39
|
{
|
|
30
40
|
"name": "index.j2",
|
|
31
|
-
"context": {
|
|
41
|
+
"context": {
|
|
42
|
+
"projects": "1234",
|
|
43
|
+
"query": {},
|
|
44
|
+
"headers": {},
|
|
45
|
+
"request_json": None,
|
|
46
|
+
},
|
|
32
47
|
"media_type": "application/json",
|
|
33
48
|
},
|
|
34
49
|
],
|
|
@@ -42,6 +57,7 @@ from mockstack.templating import (
|
|
|
42
57
|
"users": "3a4e5ad9-17ee-41af-972f-864dfccd4856",
|
|
43
58
|
"query": {},
|
|
44
59
|
"headers": {},
|
|
60
|
+
"request_json": None,
|
|
45
61
|
},
|
|
46
62
|
"media_type": "application/json",
|
|
47
63
|
},
|
|
@@ -51,6 +67,7 @@ from mockstack.templating import (
|
|
|
51
67
|
"users": "3a4e5ad9-17ee-41af-972f-864dfccd4856",
|
|
52
68
|
"query": {},
|
|
53
69
|
"headers": {},
|
|
70
|
+
"request_json": None,
|
|
54
71
|
},
|
|
55
72
|
"media_type": "application/json",
|
|
56
73
|
},
|
|
@@ -60,6 +77,7 @@ from mockstack.templating import (
|
|
|
60
77
|
"users": "3a4e5ad9-17ee-41af-972f-864dfccd4856",
|
|
61
78
|
"query": {},
|
|
62
79
|
"headers": {},
|
|
80
|
+
"request_json": None,
|
|
63
81
|
},
|
|
64
82
|
"media_type": "application/json",
|
|
65
83
|
},
|
|
@@ -70,12 +88,12 @@ from mockstack.templating import (
|
|
|
70
88
|
[
|
|
71
89
|
{
|
|
72
90
|
"name": "api-v1-projects.j2",
|
|
73
|
-
"context": {"query": {}, "headers": {}},
|
|
91
|
+
"context": {"query": {}, "headers": {}, "request_json": None},
|
|
74
92
|
"media_type": "application/json",
|
|
75
93
|
},
|
|
76
94
|
{
|
|
77
95
|
"name": "index.j2",
|
|
78
|
-
"context": {"query": {}, "headers": {}},
|
|
96
|
+
"context": {"query": {}, "headers": {}, "request_json": None},
|
|
79
97
|
"media_type": "application/json",
|
|
80
98
|
},
|
|
81
99
|
],
|
|
@@ -85,7 +103,12 @@ from mockstack.templating import (
|
|
|
85
103
|
[
|
|
86
104
|
{
|
|
87
105
|
"name": "index.j2",
|
|
88
|
-
"context": {
|
|
106
|
+
"context": {
|
|
107
|
+
"id": "1234",
|
|
108
|
+
"query": {},
|
|
109
|
+
"headers": {},
|
|
110
|
+
"request_json": None,
|
|
111
|
+
},
|
|
89
112
|
"media_type": "application/json",
|
|
90
113
|
},
|
|
91
114
|
],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mockstack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: An API mocking workhorse
|
|
5
5
|
Author-email: Adam Ever-Hadani <mockstack.contact@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -27,6 +27,8 @@ Requires-Dist: pyyaml>=6.0.2
|
|
|
27
27
|
Requires-Dist: rich>=14.0.0
|
|
28
28
|
Requires-Dist: typer>=0.15.2
|
|
29
29
|
Requires-Dist: uvicorn>=0.34.2
|
|
30
|
+
Provides-Extra: llm
|
|
31
|
+
Requires-Dist: ollama>=0.4.8; extra == "llm"
|
|
30
32
|
Dynamic: license-file
|
|
31
33
|
|
|
32
34
|

|
|
@@ -50,7 +52,7 @@ Use mockstack for:
|
|
|
50
52
|
|
|
51
53
|
* **Integration Testing** :ok_hand:. Creating a consistent environment for running integration tests on a single component, using fixture responses.
|
|
52
54
|
|
|
53
|
-
* **LLM-powered Workflows** :alien:. Speeding up development of LLM-based workflows and tools for use with frameworks such as [LangChain](https://python.langchain.com/docs/introduction/), [LangGraph](https://www.langchain.com/langgraph) and others. When developing LLM-driven execution graphs, you
|
|
55
|
+
* **LLM-powered Workflows** :alien:. Speeding up development and reducing per-token costs of LLM-based workflows and tools for use with frameworks such as [LangChain](https://python.langchain.com/docs/introduction/), [LangGraph](https://www.langchain.com/langgraph) and others. When developing LLM-driven execution graphs. Optional [Ollama](https://ollama.com/) integration allows for realistic mocking of 3rd party LLMs without changing a line of code in your project. In addition, when you want to have a consistent response from a tool while you're tuning prompts or debugging other aspects of a particular trace you can create various fixture responses with varying levels of dynamic content that is template-driven. **mockstack** can give you a solid foundation for this.
|
|
54
56
|
|
|
55
57
|
* **Chaos Engineering** :boom:. mockstack can simulate various real-world runtime error scenarios such as timeouts, http error codes, and invalid response payloads. This can be a great way to do some upfront [Chaos Monkey](https://github.com/Netflix/chaosmonkey) type of testing on software components.
|
|
56
58
|
|
|
@@ -16,6 +16,7 @@ uv.lock
|
|
|
16
16
|
.github/workflows/publish-to-dockerhub.yml
|
|
17
17
|
.github/workflows/publish-to-pypi.yml
|
|
18
18
|
docs/configuration.md
|
|
19
|
+
docs/ollama.md
|
|
19
20
|
docs/assets/favicon.ico
|
|
20
21
|
docs/assets/logo.png
|
|
21
22
|
docs/assets/mockstack.png
|
|
@@ -28,6 +29,7 @@ examples/filefixtures-with-templates/templates/servicename-api-v1-items.j2
|
|
|
28
29
|
examples/filefixtures-with-templates/templates/servicename2-api-v2-user.533ec889-7c68-45c8-b21e-4a7e455d1234.j2
|
|
29
30
|
examples/llm/README.md
|
|
30
31
|
examples/llm/mockstack-langchain-example.ipynb
|
|
32
|
+
examples/llm/templates/ollama-openai-v1-chat-completions.j2
|
|
31
33
|
examples/llm/templates/openai-v1-chat-completions.j2
|
|
32
34
|
examples/llm/templates/openai-v1-deployments-gpt-4o-chat-completions.j2
|
|
33
35
|
examples/proxyrules-with-rules-file/.env.example
|
|
@@ -37,6 +39,7 @@ mockstack/__init__.py
|
|
|
37
39
|
mockstack/config.py
|
|
38
40
|
mockstack/constants.py
|
|
39
41
|
mockstack/display.py
|
|
42
|
+
mockstack/exceptions.py
|
|
40
43
|
mockstack/identifiers.py
|
|
41
44
|
mockstack/intent.py
|
|
42
45
|
mockstack/lifespan.py
|
|
@@ -50,6 +53,8 @@ mockstack.egg-info/dependency_links.txt
|
|
|
50
53
|
mockstack.egg-info/entry_points.txt
|
|
51
54
|
mockstack.egg-info/requires.txt
|
|
52
55
|
mockstack.egg-info/top_level.txt
|
|
56
|
+
mockstack/llm/__init__.py
|
|
57
|
+
mockstack/llm/ollama.py
|
|
53
58
|
mockstack/routers/__init__.py
|
|
54
59
|
mockstack/routers/catchall.py
|
|
55
60
|
mockstack/routers/homepage.py
|
|
@@ -69,6 +74,8 @@ mockstack/tests/test_templating.py
|
|
|
69
74
|
mockstack/tests/fixtures/proxyrules.yml
|
|
70
75
|
mockstack/tests/fixtures/templates/__init__.py
|
|
71
76
|
mockstack/tests/fixtures/templates/example-template.j2
|
|
77
|
+
mockstack/tests/llm/__init__.py
|
|
78
|
+
mockstack/tests/llm/test_ollama.py
|
|
72
79
|
mockstack/tests/routers/__init__.py
|
|
73
80
|
mockstack/tests/routers/test_catchall.py
|
|
74
81
|
mockstack/tests/routers/test_homepage.py
|
|
@@ -493,6 +493,11 @@ dependencies = [
|
|
|
493
493
|
{ name = "uvicorn" },
|
|
494
494
|
]
|
|
495
495
|
|
|
496
|
+
[package.optional-dependencies]
|
|
497
|
+
llm = [
|
|
498
|
+
{ name = "ollama" },
|
|
499
|
+
]
|
|
500
|
+
|
|
496
501
|
[package.dev-dependencies]
|
|
497
502
|
dev = [
|
|
498
503
|
{ name = "mypy" },
|
|
@@ -509,6 +514,7 @@ requires-dist = [
|
|
|
509
514
|
{ name = "fastapi", extras = ["standard"], specifier = ">=0.115.12" },
|
|
510
515
|
{ name = "httpx", specifier = ">=0.28.1" },
|
|
511
516
|
{ name = "jinja2", specifier = ">=3.1.6" },
|
|
517
|
+
{ name = "ollama", marker = "extra == 'llm'", specifier = ">=0.4.8" },
|
|
512
518
|
{ name = "opentelemetry-distro", extras = ["otlp"], specifier = ">=0.53b1" },
|
|
513
519
|
{ name = "opentelemetry-instrumentation-fastapi", specifier = ">=0.53b1" },
|
|
514
520
|
{ name = "pydantic", specifier = ">=2.11.3" },
|
|
@@ -518,6 +524,7 @@ requires-dist = [
|
|
|
518
524
|
{ name = "typer", specifier = ">=0.15.2" },
|
|
519
525
|
{ name = "uvicorn", specifier = ">=0.34.2" },
|
|
520
526
|
]
|
|
527
|
+
provides-extras = ["llm"]
|
|
521
528
|
|
|
522
529
|
[package.metadata.requires-dev]
|
|
523
530
|
dev = [
|
|
@@ -569,6 +576,19 @@ wheels = [
|
|
|
569
576
|
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
|
|
570
577
|
]
|
|
571
578
|
|
|
579
|
+
[[package]]
|
|
580
|
+
name = "ollama"
|
|
581
|
+
version = "0.4.8"
|
|
582
|
+
source = { registry = "https://pypi.org/simple" }
|
|
583
|
+
dependencies = [
|
|
584
|
+
{ name = "httpx" },
|
|
585
|
+
{ name = "pydantic" },
|
|
586
|
+
]
|
|
587
|
+
sdist = { url = "https://files.pythonhosted.org/packages/e2/64/709dc99030f8f46ec552f0a7da73bbdcc2da58666abfec4742ccdb2e800e/ollama-0.4.8.tar.gz", hash = "sha256:1121439d49b96fa8339842965d0616eba5deb9f8c790786cdf4c0b3df4833802", size = 12972, upload-time = "2025-04-16T21:55:14.101Z" }
|
|
588
|
+
wheels = [
|
|
589
|
+
{ url = "https://files.pythonhosted.org/packages/33/3f/164de150e983b3a16e8bf3d4355625e51a357e7b3b1deebe9cc1f7cb9af8/ollama-0.4.8-py3-none-any.whl", hash = "sha256:04312af2c5e72449aaebac4a2776f52ef010877c554103419d3f36066fe8af4c", size = 13325, upload-time = "2025-04-16T21:55:12.779Z" },
|
|
590
|
+
]
|
|
591
|
+
|
|
572
592
|
[[package]]
|
|
573
593
|
name = "opentelemetry-api"
|
|
574
594
|
version = "1.32.1"
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mockstack-0.7.3/mockstack/tests/fixtures/templates → mockstack-0.8.0/mockstack/tests}/__init__.py
RENAMED
|
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
|