vortelio 0.3.49__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.
@@ -0,0 +1,17 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ Copyright 2024 Metiu
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
@@ -0,0 +1,354 @@
1
+ Metadata-Version: 2.4
2
+ Name: vortelio
3
+ Version: 0.3.49
4
+ Summary: Python SDK for Vortelio — run LLMs, images, audio, video & 3D locally. OpenAI & Ollama API compatible.
5
+ Author: Vortelio Contributors
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/metiu1/Vortelio
8
+ Project-URL: Repository, https://github.com/metiu1/Vortelio-python_libraries
9
+ Project-URL: Bug Tracker, https://github.com/metiu1/Vortelio/issues
10
+ Project-URL: Changelog, https://github.com/metiu1/Vortelio/blob/main/CHANGELOG.md
11
+ Keywords: ai,llm,local-ai,local-llm,offline-ai,ollama,ollama-alternative,openai-compatible,stable-diffusion,flux,sdxl,image-generation,whisper,tts,stt,audio,video-generation,3d,llama-cpp,gguf,huggingface,rag,embeddings,multimodal,ai-agent,privacy,self-hosted,cuda,rocm,metal
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Requires-Python: >=3.8
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Provides-Extra: async
28
+ Requires-Dist: aiohttp>=3.8; extra == "async"
29
+ Dynamic: license-file
30
+
31
+ # Vortelio Python SDK
32
+
33
+ [![PyPI version](https://img.shields.io/pypi/v/vortelio.svg)](https://pypi.org/project/vortelio/)
34
+ [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/)
35
+ [![License](https://img.shields.io/badge/license-Apache%202.0-green.svg)](LICENSE)
36
+
37
+ Official Python client for [Vortelio](https://github.com/metiu1/Vortelio) — run LLMs, generate images, audio, video, and 3D models locally.
38
+
39
+ Zero external dependencies. Fully OpenAI API and Ollama API compatible.
40
+
41
+ ```bash
42
+ pip install vortelio
43
+ ```
44
+
45
+ For async support:
46
+ ```bash
47
+ pip install "vortelio[async]" # adds aiohttp
48
+ ```
49
+
50
+ ---
51
+
52
+ ## Prerequisites
53
+
54
+ Start the Vortelio server first:
55
+
56
+ ```bash
57
+ vortelio serve # default port 11500
58
+ ```
59
+
60
+ Or let the SDK auto-start it:
61
+
62
+ ```python
63
+ from vortelio import ensure_server
64
+ ensure_server() # finds and starts vortelio if installed
65
+ ```
66
+
67
+ ---
68
+
69
+ ## Quick Start
70
+
71
+ ```python
72
+ from vortelio import Vortelio
73
+
74
+ ai = Vortelio() # connects to http://localhost:11500
75
+
76
+ # Download a model
77
+ ai.pull("llm/mistral:7b")
78
+
79
+ # Chat — streams tokens to stdout, returns full reply
80
+ reply = ai.chat("llm/mistral:7b", "What is quantum computing?")
81
+
82
+ # Generator streaming
83
+ for token in ai.chat_stream("llm/mistral:7b", "Tell me a story"):
84
+ print(token, end="", flush=True)
85
+ print()
86
+ ```
87
+
88
+ ---
89
+
90
+ ## Chat & Conversations
91
+
92
+ ```python
93
+ # Simple chat
94
+ reply = ai.chat("llm/mistral:7b", "Hello!")
95
+
96
+ # With messages list (Ollama/OpenAI format)
97
+ reply = ai.chat("llm/mistral:7b", [
98
+ {"role": "system", "content": "You are a helpful assistant."},
99
+ {"role": "user", "content": "What is 2 + 2?"},
100
+ ])
101
+
102
+ # Stateful multi-turn conversation
103
+ conv = ai.conversation("llm/mistral:7b", system="You are a pirate.")
104
+ conv.say("What is your name?")
105
+ reply = conv.say("Where do you sail?")
106
+
107
+ # Streaming from a conversation
108
+ for tok in conv.stream("Tell me about treasure"):
109
+ print(tok, end="", flush=True)
110
+ ```
111
+
112
+ ---
113
+
114
+ ## Generate (Ollama-style)
115
+
116
+ ```python
117
+ # Non-streaming
118
+ result = ai.generate("llm/mistral:7b", "The capital of France is")
119
+ print(result["response"])
120
+
121
+ # Streaming generator
122
+ for tok in ai.generate_stream("llm/mistral:7b", "Count to 10"):
123
+ print(tok, end="", flush=True)
124
+
125
+ # With options
126
+ result = ai.generate(
127
+ "llm/mistral:7b",
128
+ "Explain photosynthesis",
129
+ system="You are a biology teacher.",
130
+ options={"temperature": 0.7, "num_ctx": 4096},
131
+ think=True, # chain-of-thought with <think> models
132
+ )
133
+ print(result.get("thinking", ""))
134
+ print(result["response"])
135
+ ```
136
+
137
+ ---
138
+
139
+ ## Embeddings
140
+
141
+ ```python
142
+ # Batch embeddings
143
+ vecs = ai.embed("llm/nomic-embed-text:latest", ["Hello", "World"])
144
+ # → [[0.1, 0.2, ...], [0.3, 0.4, ...]]
145
+
146
+ # Legacy single-prompt
147
+ vec = ai.embeddings("llm/nomic-embed-text:latest", "Hello world")
148
+ ```
149
+
150
+ ---
151
+
152
+ ## RAG (Retrieval-Augmented Generation)
153
+
154
+ ```python
155
+ # Ingest documents
156
+ ai.rag_ingest(
157
+ "llm/nomic-embed-text:latest",
158
+ [
159
+ {"text": "Paris is the capital of France.", "meta": {"source": "facts"}},
160
+ {"text": "Berlin is the capital of Germany.", "meta": {"source": "facts"}},
161
+ ],
162
+ collection="my-docs",
163
+ )
164
+
165
+ # Query
166
+ hits = ai.rag_query("llm/nomic-embed-text:latest", "capital of France", collection="my-docs")
167
+ for h in hits["results"]:
168
+ print(f"[{h['score']:.3f}] {h['text']}")
169
+ ```
170
+
171
+ ---
172
+
173
+ ## Model Management
174
+
175
+ ```python
176
+ ai.models() # list all downloaded models
177
+ ai.pull("llm/llama3:8b") # download from HuggingFace
178
+ ai.show("llm/mistral:7b") # model details, template, capabilities
179
+ ai.delete("llm/old-model:latest") # remove a model
180
+ ai.copy("llm/mistral:7b", "llm/my-mistral:latest") # duplicate
181
+ ai.quantize("llm/mistral:7b", "q4_k_m") # quantize
182
+ ai.create("llm/my-model:latest", from_model="llm/mistral:7b",
183
+ system="You are a helpful assistant.")
184
+ ai.ps() # currently loaded models
185
+ ai.version() # server version
186
+ ```
187
+
188
+ ---
189
+
190
+ ## Media Generation
191
+
192
+ ```python
193
+ # Image
194
+ ai.image("image/sdxl:latest", "a red panda on the moon", "panda.png")
195
+
196
+ # Or get bytes directly
197
+ png_bytes = ai.generate_image("image/sdxl:latest", "sunset over mountains")
198
+
199
+ # Audio (TTS / music)
200
+ wav_bytes = ai.generate_audio("audio/kokoro:latest", "Hello, this is a test.")
201
+
202
+ # Video
203
+ mp4_bytes = ai.generate_video("video/wan2-1:latest", "a cat playing piano")
204
+
205
+ # 3D
206
+ obj_bytes = ai.generate_3d("3d/triposr:latest", "a wooden chair")
207
+ ```
208
+
209
+ ---
210
+
211
+ ## Advanced API
212
+
213
+ ```python
214
+ # A/B compare models
215
+ result = ai.compare(
216
+ ["llm/mistral:7b", "llm/llama3:8b"],
217
+ "Explain gravity in one sentence.",
218
+ )
219
+ for r in result["results"]:
220
+ print(f"{r['model']}: {r['response']}")
221
+
222
+ # Structured JSON output
223
+ result = ai.structured(
224
+ "llm/mistral:7b",
225
+ "List 3 programming languages",
226
+ schema={"type": "array", "items": {"type": "string"}},
227
+ )
228
+ print(result["parsed"])
229
+
230
+ # Long-text summarization (map-reduce)
231
+ summary = ai.summarize("llm/mistral:7b", very_long_text, style="bullets")
232
+ print(summary["summary"])
233
+
234
+ # Chain-of-thought
235
+ result = ai.think("llm/qwq:32b", "Is 97 a prime number?")
236
+ print("Reasoning:", result["thinking"])
237
+ print("Answer:", result["answer"])
238
+
239
+ # Smart model router
240
+ best = ai.route("code", prompt="Write a sorting algorithm")
241
+ print("Best model:", best["model"])
242
+ ```
243
+
244
+ ---
245
+
246
+ ## OpenAI-Compatible API
247
+
248
+ ```python
249
+ # Drop-in OpenAI replacement
250
+ response = ai.openai_chat(
251
+ "mistral:7b",
252
+ [{"role": "user", "content": "Hello!"}],
253
+ temperature=0.7,
254
+ )
255
+ print(response["choices"][0]["message"]["content"])
256
+
257
+ # Streaming
258
+ for tok in ai.openai_chat_stream("mistral:7b", [{"role":"user","content":"Hi"}]):
259
+ print(tok, end="", flush=True)
260
+
261
+ # Embeddings (OpenAI format)
262
+ result = ai.openai_embeddings("nomic-embed-text:latest", "Hello world")
263
+ ```
264
+
265
+ ---
266
+
267
+ ## Async Client
268
+
269
+ ```python
270
+ import asyncio
271
+ from vortelio import AsyncVortelio
272
+
273
+ async def main():
274
+ ai = AsyncVortelio()
275
+
276
+ # All methods are async
277
+ reply = await ai.chat("llm/mistral:7b", "Hello!")
278
+
279
+ # Async streaming
280
+ async for tok in ai.chat_stream("llm/mistral:7b", "Tell me a joke"):
281
+ print(tok, end="", flush=True)
282
+
283
+ # Async conversation
284
+ conv = ai.conversation("llm/mistral:7b", system="You are helpful.")
285
+ reply = await conv.say("My name is Alice.")
286
+
287
+ asyncio.run(main())
288
+ ```
289
+
290
+ ---
291
+
292
+ ## Agents
293
+
294
+ ```python
295
+ # List available agents (Open WebUI, OpenClaw, CrewAI, AnythingLLM, ...)
296
+ catalog = ai.agents_catalog()
297
+
298
+ # Install and start an agent
299
+ ai.agents_install("open-webui")
300
+ ai.agents_start("open-webui")
301
+
302
+ # Stop an agent
303
+ ai.agents_stop("open-webui")
304
+ ```
305
+
306
+ ---
307
+
308
+ ## Webhooks & Audit
309
+
310
+ ```python
311
+ # Register a webhook
312
+ ai.hooks_create("https://my-server.com/webhook", event="generate")
313
+
314
+ # List webhooks
315
+ ai.hooks_list()
316
+
317
+ # Audit log
318
+ entries = ai.audit(limit=50)
319
+ ```
320
+
321
+ ---
322
+
323
+ ## GGUF Inspect & Ollama Import
324
+
325
+ ```python
326
+ # Inspect a local GGUF file
327
+ info = ai.gguf_inspect("/path/to/model.gguf")
328
+
329
+ # Import models from a local Ollama installation
330
+ ai.import_ollama() # imports all
331
+ ai.import_ollama(["mistral:7b", "llama3:8b"]) # selective
332
+ ```
333
+
334
+ ---
335
+
336
+ ## Custom Port / Remote Server
337
+
338
+ ```python
339
+ ai = Vortelio(host="http://192.168.1.100", port=11500)
340
+ ai = Vortelio(port=8080) # local custom port
341
+ ai = Vortelio(timeout=600) # longer timeout for large models
342
+ ```
343
+
344
+ ---
345
+
346
+ ## Server Version Compatibility
347
+
348
+ This SDK version **0.3.49** requires Vortelio server **≥ 0.3.38**.
349
+
350
+ ---
351
+
352
+ ## License
353
+
354
+ Apache 2.0 — see [LICENSE](LICENSE).
@@ -0,0 +1,324 @@
1
+ # Vortelio Python SDK
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/vortelio.svg)](https://pypi.org/project/vortelio/)
4
+ [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/)
5
+ [![License](https://img.shields.io/badge/license-Apache%202.0-green.svg)](LICENSE)
6
+
7
+ Official Python client for [Vortelio](https://github.com/metiu1/Vortelio) — run LLMs, generate images, audio, video, and 3D models locally.
8
+
9
+ Zero external dependencies. Fully OpenAI API and Ollama API compatible.
10
+
11
+ ```bash
12
+ pip install vortelio
13
+ ```
14
+
15
+ For async support:
16
+ ```bash
17
+ pip install "vortelio[async]" # adds aiohttp
18
+ ```
19
+
20
+ ---
21
+
22
+ ## Prerequisites
23
+
24
+ Start the Vortelio server first:
25
+
26
+ ```bash
27
+ vortelio serve # default port 11500
28
+ ```
29
+
30
+ Or let the SDK auto-start it:
31
+
32
+ ```python
33
+ from vortelio import ensure_server
34
+ ensure_server() # finds and starts vortelio if installed
35
+ ```
36
+
37
+ ---
38
+
39
+ ## Quick Start
40
+
41
+ ```python
42
+ from vortelio import Vortelio
43
+
44
+ ai = Vortelio() # connects to http://localhost:11500
45
+
46
+ # Download a model
47
+ ai.pull("llm/mistral:7b")
48
+
49
+ # Chat — streams tokens to stdout, returns full reply
50
+ reply = ai.chat("llm/mistral:7b", "What is quantum computing?")
51
+
52
+ # Generator streaming
53
+ for token in ai.chat_stream("llm/mistral:7b", "Tell me a story"):
54
+ print(token, end="", flush=True)
55
+ print()
56
+ ```
57
+
58
+ ---
59
+
60
+ ## Chat & Conversations
61
+
62
+ ```python
63
+ # Simple chat
64
+ reply = ai.chat("llm/mistral:7b", "Hello!")
65
+
66
+ # With messages list (Ollama/OpenAI format)
67
+ reply = ai.chat("llm/mistral:7b", [
68
+ {"role": "system", "content": "You are a helpful assistant."},
69
+ {"role": "user", "content": "What is 2 + 2?"},
70
+ ])
71
+
72
+ # Stateful multi-turn conversation
73
+ conv = ai.conversation("llm/mistral:7b", system="You are a pirate.")
74
+ conv.say("What is your name?")
75
+ reply = conv.say("Where do you sail?")
76
+
77
+ # Streaming from a conversation
78
+ for tok in conv.stream("Tell me about treasure"):
79
+ print(tok, end="", flush=True)
80
+ ```
81
+
82
+ ---
83
+
84
+ ## Generate (Ollama-style)
85
+
86
+ ```python
87
+ # Non-streaming
88
+ result = ai.generate("llm/mistral:7b", "The capital of France is")
89
+ print(result["response"])
90
+
91
+ # Streaming generator
92
+ for tok in ai.generate_stream("llm/mistral:7b", "Count to 10"):
93
+ print(tok, end="", flush=True)
94
+
95
+ # With options
96
+ result = ai.generate(
97
+ "llm/mistral:7b",
98
+ "Explain photosynthesis",
99
+ system="You are a biology teacher.",
100
+ options={"temperature": 0.7, "num_ctx": 4096},
101
+ think=True, # chain-of-thought with <think> models
102
+ )
103
+ print(result.get("thinking", ""))
104
+ print(result["response"])
105
+ ```
106
+
107
+ ---
108
+
109
+ ## Embeddings
110
+
111
+ ```python
112
+ # Batch embeddings
113
+ vecs = ai.embed("llm/nomic-embed-text:latest", ["Hello", "World"])
114
+ # → [[0.1, 0.2, ...], [0.3, 0.4, ...]]
115
+
116
+ # Legacy single-prompt
117
+ vec = ai.embeddings("llm/nomic-embed-text:latest", "Hello world")
118
+ ```
119
+
120
+ ---
121
+
122
+ ## RAG (Retrieval-Augmented Generation)
123
+
124
+ ```python
125
+ # Ingest documents
126
+ ai.rag_ingest(
127
+ "llm/nomic-embed-text:latest",
128
+ [
129
+ {"text": "Paris is the capital of France.", "meta": {"source": "facts"}},
130
+ {"text": "Berlin is the capital of Germany.", "meta": {"source": "facts"}},
131
+ ],
132
+ collection="my-docs",
133
+ )
134
+
135
+ # Query
136
+ hits = ai.rag_query("llm/nomic-embed-text:latest", "capital of France", collection="my-docs")
137
+ for h in hits["results"]:
138
+ print(f"[{h['score']:.3f}] {h['text']}")
139
+ ```
140
+
141
+ ---
142
+
143
+ ## Model Management
144
+
145
+ ```python
146
+ ai.models() # list all downloaded models
147
+ ai.pull("llm/llama3:8b") # download from HuggingFace
148
+ ai.show("llm/mistral:7b") # model details, template, capabilities
149
+ ai.delete("llm/old-model:latest") # remove a model
150
+ ai.copy("llm/mistral:7b", "llm/my-mistral:latest") # duplicate
151
+ ai.quantize("llm/mistral:7b", "q4_k_m") # quantize
152
+ ai.create("llm/my-model:latest", from_model="llm/mistral:7b",
153
+ system="You are a helpful assistant.")
154
+ ai.ps() # currently loaded models
155
+ ai.version() # server version
156
+ ```
157
+
158
+ ---
159
+
160
+ ## Media Generation
161
+
162
+ ```python
163
+ # Image
164
+ ai.image("image/sdxl:latest", "a red panda on the moon", "panda.png")
165
+
166
+ # Or get bytes directly
167
+ png_bytes = ai.generate_image("image/sdxl:latest", "sunset over mountains")
168
+
169
+ # Audio (TTS / music)
170
+ wav_bytes = ai.generate_audio("audio/kokoro:latest", "Hello, this is a test.")
171
+
172
+ # Video
173
+ mp4_bytes = ai.generate_video("video/wan2-1:latest", "a cat playing piano")
174
+
175
+ # 3D
176
+ obj_bytes = ai.generate_3d("3d/triposr:latest", "a wooden chair")
177
+ ```
178
+
179
+ ---
180
+
181
+ ## Advanced API
182
+
183
+ ```python
184
+ # A/B compare models
185
+ result = ai.compare(
186
+ ["llm/mistral:7b", "llm/llama3:8b"],
187
+ "Explain gravity in one sentence.",
188
+ )
189
+ for r in result["results"]:
190
+ print(f"{r['model']}: {r['response']}")
191
+
192
+ # Structured JSON output
193
+ result = ai.structured(
194
+ "llm/mistral:7b",
195
+ "List 3 programming languages",
196
+ schema={"type": "array", "items": {"type": "string"}},
197
+ )
198
+ print(result["parsed"])
199
+
200
+ # Long-text summarization (map-reduce)
201
+ summary = ai.summarize("llm/mistral:7b", very_long_text, style="bullets")
202
+ print(summary["summary"])
203
+
204
+ # Chain-of-thought
205
+ result = ai.think("llm/qwq:32b", "Is 97 a prime number?")
206
+ print("Reasoning:", result["thinking"])
207
+ print("Answer:", result["answer"])
208
+
209
+ # Smart model router
210
+ best = ai.route("code", prompt="Write a sorting algorithm")
211
+ print("Best model:", best["model"])
212
+ ```
213
+
214
+ ---
215
+
216
+ ## OpenAI-Compatible API
217
+
218
+ ```python
219
+ # Drop-in OpenAI replacement
220
+ response = ai.openai_chat(
221
+ "mistral:7b",
222
+ [{"role": "user", "content": "Hello!"}],
223
+ temperature=0.7,
224
+ )
225
+ print(response["choices"][0]["message"]["content"])
226
+
227
+ # Streaming
228
+ for tok in ai.openai_chat_stream("mistral:7b", [{"role":"user","content":"Hi"}]):
229
+ print(tok, end="", flush=True)
230
+
231
+ # Embeddings (OpenAI format)
232
+ result = ai.openai_embeddings("nomic-embed-text:latest", "Hello world")
233
+ ```
234
+
235
+ ---
236
+
237
+ ## Async Client
238
+
239
+ ```python
240
+ import asyncio
241
+ from vortelio import AsyncVortelio
242
+
243
+ async def main():
244
+ ai = AsyncVortelio()
245
+
246
+ # All methods are async
247
+ reply = await ai.chat("llm/mistral:7b", "Hello!")
248
+
249
+ # Async streaming
250
+ async for tok in ai.chat_stream("llm/mistral:7b", "Tell me a joke"):
251
+ print(tok, end="", flush=True)
252
+
253
+ # Async conversation
254
+ conv = ai.conversation("llm/mistral:7b", system="You are helpful.")
255
+ reply = await conv.say("My name is Alice.")
256
+
257
+ asyncio.run(main())
258
+ ```
259
+
260
+ ---
261
+
262
+ ## Agents
263
+
264
+ ```python
265
+ # List available agents (Open WebUI, OpenClaw, CrewAI, AnythingLLM, ...)
266
+ catalog = ai.agents_catalog()
267
+
268
+ # Install and start an agent
269
+ ai.agents_install("open-webui")
270
+ ai.agents_start("open-webui")
271
+
272
+ # Stop an agent
273
+ ai.agents_stop("open-webui")
274
+ ```
275
+
276
+ ---
277
+
278
+ ## Webhooks & Audit
279
+
280
+ ```python
281
+ # Register a webhook
282
+ ai.hooks_create("https://my-server.com/webhook", event="generate")
283
+
284
+ # List webhooks
285
+ ai.hooks_list()
286
+
287
+ # Audit log
288
+ entries = ai.audit(limit=50)
289
+ ```
290
+
291
+ ---
292
+
293
+ ## GGUF Inspect & Ollama Import
294
+
295
+ ```python
296
+ # Inspect a local GGUF file
297
+ info = ai.gguf_inspect("/path/to/model.gguf")
298
+
299
+ # Import models from a local Ollama installation
300
+ ai.import_ollama() # imports all
301
+ ai.import_ollama(["mistral:7b", "llama3:8b"]) # selective
302
+ ```
303
+
304
+ ---
305
+
306
+ ## Custom Port / Remote Server
307
+
308
+ ```python
309
+ ai = Vortelio(host="http://192.168.1.100", port=11500)
310
+ ai = Vortelio(port=8080) # local custom port
311
+ ai = Vortelio(timeout=600) # longer timeout for large models
312
+ ```
313
+
314
+ ---
315
+
316
+ ## Server Version Compatibility
317
+
318
+ This SDK version **0.3.49** requires Vortelio server **≥ 0.3.38**.
319
+
320
+ ---
321
+
322
+ ## License
323
+
324
+ Apache 2.0 — see [LICENSE](LICENSE).