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.
Files changed (86) hide show
  1. {mockstack-0.7.3 → mockstack-0.8.0}/PKG-INFO +4 -2
  2. {mockstack-0.7.3 → mockstack-0.8.0}/README.md +1 -1
  3. mockstack-0.8.0/docs/ollama.md +86 -0
  4. {mockstack-0.7.3 → mockstack-0.8.0}/examples/llm/mockstack-langchain-example.ipynb +73 -8
  5. mockstack-0.8.0/examples/llm/templates/ollama-openai-v1-chat-completions.j2 +35 -0
  6. {mockstack-0.7.3 → mockstack-0.8.0}/mkdocs.yml +8 -0
  7. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/config.py +2 -2
  8. mockstack-0.8.0/mockstack/exceptions.py +6 -0
  9. mockstack-0.8.0/mockstack/llm/ollama.py +50 -0
  10. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/strategies/filefixtures.py +21 -8
  11. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/strategies/proxyrules.py +6 -2
  12. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/templating.py +44 -1
  13. mockstack-0.8.0/mockstack/tests/llm/__init__.py +0 -0
  14. mockstack-0.8.0/mockstack/tests/llm/test_ollama.py +95 -0
  15. mockstack-0.8.0/mockstack/tests/routers/__init__.py +0 -0
  16. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/tests/test_templating.py +29 -6
  17. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack.egg-info/PKG-INFO +4 -2
  18. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack.egg-info/SOURCES.txt +7 -0
  19. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack.egg-info/requires.txt +3 -0
  20. {mockstack-0.7.3 → mockstack-0.8.0}/pyproject.toml +5 -0
  21. {mockstack-0.7.3 → mockstack-0.8.0}/uv.lock +20 -0
  22. {mockstack-0.7.3 → mockstack-0.8.0}/.dockerignore +0 -0
  23. {mockstack-0.7.3 → mockstack-0.8.0}/.env.example +0 -0
  24. {mockstack-0.7.3 → mockstack-0.8.0}/.github/workflows/ci.yml +0 -0
  25. {mockstack-0.7.3 → mockstack-0.8.0}/.github/workflows/publish-docs.yml +0 -0
  26. {mockstack-0.7.3 → mockstack-0.8.0}/.github/workflows/publish-to-dockerhub.yml +0 -0
  27. {mockstack-0.7.3 → mockstack-0.8.0}/.github/workflows/publish-to-pypi.yml +0 -0
  28. {mockstack-0.7.3 → mockstack-0.8.0}/.gitignore +0 -0
  29. {mockstack-0.7.3 → mockstack-0.8.0}/.pre-commit-config.yaml +0 -0
  30. {mockstack-0.7.3 → mockstack-0.8.0}/CODE_OF_CONDUCT.md +0 -0
  31. {mockstack-0.7.3 → mockstack-0.8.0}/Dockerfile +0 -0
  32. {mockstack-0.7.3 → mockstack-0.8.0}/LICENSE +0 -0
  33. {mockstack-0.7.3 → mockstack-0.8.0}/SECURITY.md +0 -0
  34. {mockstack-0.7.3 → mockstack-0.8.0}/docker-entrypoint.sh +0 -0
  35. {mockstack-0.7.3 → mockstack-0.8.0}/docs/assets/favicon.ico +0 -0
  36. {mockstack-0.7.3 → mockstack-0.8.0}/docs/assets/logo.png +0 -0
  37. {mockstack-0.7.3 → mockstack-0.8.0}/docs/assets/mockstack.png +0 -0
  38. {mockstack-0.7.3 → mockstack-0.8.0}/docs/configuration.md +0 -0
  39. {mockstack-0.7.3 → mockstack-0.8.0}/docs/strategies/base.md +0 -0
  40. {mockstack-0.7.3 → mockstack-0.8.0}/docs/strategies/filefixtures.md +0 -0
  41. {mockstack-0.7.3 → mockstack-0.8.0}/docs/strategies/proxyrules.md +0 -0
  42. {mockstack-0.7.3 → mockstack-0.8.0}/examples/filefixtures-with-templates/.env.example +0 -0
  43. {mockstack-0.7.3 → mockstack-0.8.0}/examples/filefixtures-with-templates/README.md +0 -0
  44. {mockstack-0.7.3 → mockstack-0.8.0}/examples/filefixtures-with-templates/templates/servicename-api-v1-items.j2 +0 -0
  45. {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
  46. {mockstack-0.7.3 → mockstack-0.8.0}/examples/llm/README.md +0 -0
  47. {mockstack-0.7.3 → mockstack-0.8.0}/examples/llm/templates/openai-v1-chat-completions.j2 +0 -0
  48. {mockstack-0.7.3 → mockstack-0.8.0}/examples/llm/templates/openai-v1-deployments-gpt-4o-chat-completions.j2 +0 -0
  49. {mockstack-0.7.3 → mockstack-0.8.0}/examples/proxyrules-with-rules-file/.env.example +0 -0
  50. {mockstack-0.7.3 → mockstack-0.8.0}/examples/proxyrules-with-rules-file/README.md +0 -0
  51. {mockstack-0.7.3 → mockstack-0.8.0}/examples/proxyrules-with-rules-file/rules.yml +0 -0
  52. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/__init__.py +0 -0
  53. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/constants.py +0 -0
  54. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/display.py +0 -0
  55. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/identifiers.py +0 -0
  56. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/intent.py +0 -0
  57. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/lifespan.py +0 -0
  58. {mockstack-0.7.3/mockstack/routers → mockstack-0.8.0/mockstack/llm}/__init__.py +0 -0
  59. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/main.py +0 -0
  60. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/middleware.py +0 -0
  61. {mockstack-0.7.3/mockstack/tests → mockstack-0.8.0/mockstack/routers}/__init__.py +0 -0
  62. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/routers/catchall.py +0 -0
  63. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/routers/homepage.py +0 -0
  64. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/strategies/__init__.py +0 -0
  65. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/strategies/base.py +0 -0
  66. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/strategies/create_mixin.py +0 -0
  67. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/strategies/factory.py +0 -0
  68. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/telemetry.py +0 -0
  69. {mockstack-0.7.3/mockstack/tests/fixtures/templates → mockstack-0.8.0/mockstack/tests}/__init__.py +0 -0
  70. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/tests/conftest.py +0 -0
  71. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/tests/fixtures/proxyrules.yml +0 -0
  72. {mockstack-0.7.3/mockstack/tests/routers → mockstack-0.8.0/mockstack/tests/fixtures/templates}/__init__.py +0 -0
  73. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/tests/fixtures/templates/example-template.j2 +0 -0
  74. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/tests/routers/test_catchall.py +0 -0
  75. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/tests/routers/test_homepage.py +0 -0
  76. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/tests/strategies/test_create_mixin.py +0 -0
  77. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/tests/strategies/test_filefixtures.py +0 -0
  78. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/tests/strategies/test_proxyrules.py +0 -0
  79. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/tests/test_display.py +0 -0
  80. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/tests/test_identifiers.py +0 -0
  81. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/tests/test_middleware.py +0 -0
  82. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack/tests/test_telemetry.py +0 -0
  83. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack.egg-info/dependency_links.txt +0 -0
  84. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack.egg-info/entry_points.txt +0 -0
  85. {mockstack-0.7.3 → mockstack-0.8.0}/mockstack.egg-info/top_level.txt +0 -0
  86. {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.7.3
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
  ![mockstack logo](https://github.com/adamhadani/mockstack/raw/main/docs/assets/mockstack.png)
@@ -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 often want to have a consistent response from a tool while you're tuning prompts or debugging other aspects of a particular trace. **mockstack** can give you a solid foundation for this.
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 often want to have a consistent response from a tool while you're tuning prompts or debugging other aspects of a particular trace. **mockstack** can give you a solid foundation for this.
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": 26,
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: Template-based mocking (`filefixtures` strategy)\n",
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": 38,
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": 40,
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: `llm` strategy\n",
143
+ "## Example #2: Dynamic template-based mocking with ollama integration\n",
124
144
  "\n",
125
- "** COMING SOON **"
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
- "source": []
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,6 @@
1
+ """Custom exceptions for mockstack."""
2
+
3
+
4
+ def raise_for_missing(message: str, *args, **kwargs):
5
+ """Raise an exception for a missing dependency."""
6
+ raise RuntimeError(message)
@@ -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, FileSystemLoader
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, request: Request, status_code: int = status.HTTP_200_OK
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(request):
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": {"projects": "1234", "query": {}, "headers": {}},
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": {"projects": "1234", "query": {}, "headers": {}},
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": {"projects": "1234", "query": {}, "headers": {}},
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": {"id": "1234", "query": {}, "headers": {}},
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.7.3
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
  ![mockstack logo](https://github.com/adamhadani/mockstack/raw/main/docs/assets/mockstack.png)
@@ -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 often want to have a consistent response from a tool while you're tuning prompts or debugging other aspects of a particular trace. **mockstack** can give you a solid foundation for this.
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
@@ -10,3 +10,6 @@ pyyaml>=6.0.2
10
10
  rich>=14.0.0
11
11
  typer>=0.15.2
12
12
  uvicorn>=0.34.2
13
+
14
+ [llm]
15
+ ollama>=0.4.8
@@ -41,6 +41,11 @@ Issues = "https://github.com/adamhadani/mockstack/issues"
41
41
  [project.scripts]
42
42
  mockstack = "mockstack.main:run"
43
43
 
44
+ [project.optional-dependencies]
45
+ llm = [
46
+ "ollama>=0.4.8",
47
+ ]
48
+
44
49
  [dependency-groups]
45
50
  dev = [
46
51
  "mypy>=1.15.0",
@@ -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