voodoo-framework 1.0.0__py3-none-any.whl

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.
voodoo/queue.py ADDED
@@ -0,0 +1,61 @@
1
+ import asyncio
2
+ import inspect
3
+ import logging
4
+ from typing import Callable, Dict, Any
5
+
6
+ _queues: Dict[str, asyncio.Queue] = {}
7
+ _workers: Dict[str, Callable] = {}
8
+ _worker_tasks: list[asyncio.Task] = []
9
+
10
+ logger = logging.getLogger("voodoo.queue")
11
+
12
+ def queue(name: str):
13
+ def decorator(func: Callable):
14
+ if name not in _queues:
15
+ _queues[name] = asyncio.Queue()
16
+ _workers[name] = func
17
+ return func
18
+ return decorator
19
+
20
+ async def enqueue(name: str, payload: Any):
21
+ if name not in _queues:
22
+ _queues[name] = asyncio.Queue()
23
+ from voodoo.telemetry import trace_id_var
24
+ trace_id = trace_id_var.get()
25
+ await _queues[name].put({"payload": payload, "trace_id": trace_id})
26
+
27
+ async def _run_worker(name: str):
28
+ q = _queues[name]
29
+ func = _workers[name]
30
+ from voodoo.telemetry import trace_id_var
31
+ import uuid
32
+ while True:
33
+ try:
34
+ item = await q.get()
35
+ payload = item.get("payload")
36
+ trace_id = item.get("trace_id") or str(uuid.uuid4())
37
+ token = trace_id_var.set(trace_id)
38
+ try:
39
+ if inspect.iscoroutinefunction(func):
40
+ await func(payload)
41
+ else:
42
+ func(payload)
43
+ except Exception as e:
44
+ logger.error(f"Error in worker {name}: {e}")
45
+ finally:
46
+ trace_id_var.reset(token)
47
+ q.task_done()
48
+ except asyncio.CancelledError:
49
+ break
50
+
51
+ async def start_workers():
52
+ for name in _workers.keys():
53
+ task = asyncio.create_task(_run_worker(name))
54
+ _worker_tasks.append(task)
55
+
56
+ async def stop_workers():
57
+ for task in _worker_tasks:
58
+ task.cancel()
59
+ if _worker_tasks:
60
+ await asyncio.gather(*_worker_tasks, return_exceptions=True)
61
+ _worker_tasks.clear()
voodoo/status.py ADDED
@@ -0,0 +1,131 @@
1
+ import asyncio
2
+ from typing import Dict, Any
3
+ from voodoo.api import api
4
+ from voodoo.data import get_db
5
+ from voodoo.queue import _worker_tasks, _queues
6
+ from voodoo.storage import storage
7
+ from voodoo.components import Div, Card, Heading
8
+
9
+ from voodoo.i18n import _
10
+
11
+ async def check_database() -> Dict[str, Any]:
12
+ try:
13
+ db = await get_db()
14
+ await db.execute("SELECT 1")
15
+ return {"status": "Operational", "latency": "ok"}
16
+ except Exception as e:
17
+ return {"status": "Down", "error": str(e)}
18
+
19
+ async def check_queue() -> Dict[str, Any]:
20
+ try:
21
+ total_workers = len(_worker_tasks)
22
+ active_queues = len(_queues)
23
+ return {
24
+ "status": "Operational" if total_workers > 0 else "Degraded",
25
+ "workers": total_workers,
26
+ "queues": active_queues
27
+ }
28
+ except Exception as e:
29
+ return {"status": "Down", "error": str(e)}
30
+
31
+ async def check_storage() -> Dict[str, Any]:
32
+ try:
33
+ if storage.use_s3 and storage.s3_client:
34
+ return {"status": "Operational", "type": "S3"}
35
+ else:
36
+ import os
37
+ if os.path.exists(storage.base_dir):
38
+ return {"status": "Operational", "type": "Local"}
39
+ return {"status": "Degraded", "type": "Local", "error": "Dir missing"}
40
+ except Exception as e:
41
+ return {"status": "Down", "error": str(e)}
42
+
43
+ async def check_agent() -> Dict[str, Any]:
44
+ # Placeholder for Agent connectivity
45
+ return {"status": "Operational", "provider": "Simulated"}
46
+
47
+ @api.get("/status")
48
+ async def get_status():
49
+ db_health = await check_database()
50
+ queue_health = await check_queue()
51
+ storage_health = await check_storage()
52
+ agent_health = await check_agent()
53
+
54
+ status_code = 200
55
+ if any(s["status"] == "Down" for s in [db_health, queue_health, storage_health, agent_health]):
56
+ status_code = 503
57
+
58
+ return {
59
+ "services": {
60
+ "database": db_health,
61
+ "queue": queue_health,
62
+ "storage": storage_health,
63
+ "agent": agent_health
64
+ },
65
+ "overall": "Operational" if status_code == 200 else "Down"
66
+ }
67
+
68
+ @api.get("/status/locales")
69
+ async def get_status_locales():
70
+ return {
71
+ "database": _("status.database"),
72
+ "queue": _("status.queue"),
73
+ "storage": _("status.storage"),
74
+ "agent": _("status.agent"),
75
+ "Operational": _("status.operational"),
76
+ "Degraded": _("status.degraded"),
77
+ "Down": _("status.down")
78
+ }
79
+
80
+ class ServiceStatus(Div):
81
+ def __init__(self, **kwargs):
82
+ # Provide default glass aesthetics for the card
83
+ classes = kwargs.pop("className", "")
84
+ card_classes = f"bg-[var(--color-surface)] border-[var(--color-border)] backdrop-blur-xl {classes}".strip()
85
+ super().__init__(
86
+ Card(
87
+ Heading(_("status.title"), level=2, className="text-xl font-semibold mb-6 text-[var(--color-text)] tracking-tight"),
88
+ Div(id="service-status-container", className="space-y-3"),
89
+ Div(
90
+ """
91
+ <script>
92
+ async function fetchStatus() {
93
+ try {
94
+ const [resStatus, resLocales] = await Promise.all([
95
+ fetch('/status'),
96
+ fetch('/status/locales')
97
+ ]);
98
+ const data = await resStatus.json();
99
+ const locales = await resLocales.json();
100
+
101
+ const container = document.getElementById('service-status-container');
102
+ let html = '';
103
+ for (const [service, info] of Object.entries(data.services)) {
104
+ let color = info.status === 'Operational' ? 'text-green-400' : (info.status === 'Degraded' ? 'text-yellow-400' : 'text-red-400');
105
+ let dot = info.status === 'Operational' ? 'bg-green-400' : (info.status === 'Degraded' ? 'bg-yellow-400' : 'bg-red-400');
106
+ let serviceName = locales[service] || service;
107
+ let statusName = locales[info.status] || info.status;
108
+ html += `
109
+ <div class="flex items-center justify-between p-3 bg-[var(--color-surface)] rounded-lg border border-[var(--color-border)] backdrop-blur-sm">
110
+ <div class="flex items-center space-x-3">
111
+ <div class="w-2.5 h-2.5 rounded-full ${dot} shadow-[0_0_8px_rgba(0,0,0,0.5)] shadow-${dot.replace('bg-', '')}"></div>
112
+ <span class="capitalize text-[var(--color-text)] font-medium tracking-wide">${serviceName}</span>
113
+ </div>
114
+ <span class="${color} font-semibold text-sm tracking-wide">${statusName}</span>
115
+ </div>
116
+ `;
117
+ }
118
+ container.innerHTML = html;
119
+ } catch (e) {
120
+ console.error('Failed to fetch status', e);
121
+ }
122
+ }
123
+ fetchStatus();
124
+ setInterval(fetchStatus, 10000);
125
+ </script>
126
+ """
127
+ ),
128
+ className=card_classes,
129
+ **kwargs
130
+ )
131
+ )
voodoo/storage.py ADDED
@@ -0,0 +1,85 @@
1
+ import os
2
+ import aiofiles
3
+ import asyncio
4
+ from typing import Union
5
+
6
+ try:
7
+ import boto3
8
+ import botocore.config
9
+ except ImportError:
10
+ boto3 = None
11
+
12
+ class StorageManager:
13
+ def __init__(self):
14
+ self.s3_bucket = os.getenv("VOODOO_S3_BUCKET")
15
+ self.key = os.getenv("VOODOO_S3_KEY")
16
+ self.secret = os.getenv("VOODOO_S3_SECRET")
17
+ self.endpoint = os.getenv("VOODOO_S3_ENDPOINT")
18
+
19
+ self.use_s3 = all([self.s3_bucket, self.key, self.secret, self.endpoint])
20
+ self.base_dir = os.path.join(os.getcwd(), os.getenv("VOODOO_STORAGE_DIR", "storage"))
21
+
22
+ if self.use_s3 and boto3:
23
+ self.s3_client = boto3.client(
24
+ 's3',
25
+ aws_access_key_id=self.key,
26
+ aws_secret_access_key=self.secret,
27
+ endpoint_url=self.endpoint,
28
+ config=botocore.config.Config(signature_version='s3v4')
29
+ )
30
+ else:
31
+ self.s3_client = None
32
+
33
+ def _get_local_path(self, bucket: str, path: str) -> str:
34
+ """Helper to resolve the local file path for a specific bucket."""
35
+ return os.path.join(self.base_dir, bucket, path)
36
+
37
+ async def upload(self, file_content: Union[bytes, str], path: str, bucket: str = "public") -> str:
38
+ """Uploads a file to a specific bucket and returns its path/url"""
39
+ if isinstance(file_content, str):
40
+ file_content = file_content.encode('utf-8')
41
+
42
+ if self.use_s3 and self.s3_client:
43
+ s3_key = f"{bucket}/{path}"
44
+ await asyncio.to_thread(
45
+ self.s3_client.put_object,
46
+ Bucket=self.s3_bucket,
47
+ Key=s3_key,
48
+ Body=file_content
49
+ )
50
+ return self.url(path, bucket)
51
+ else:
52
+ local_path = self._get_local_path(bucket, path)
53
+ os.makedirs(os.path.dirname(local_path), exist_ok=True)
54
+ async with aiofiles.open(local_path, 'wb') as f:
55
+ await f.write(file_content)
56
+ return self.url(path, bucket)
57
+
58
+ async def delete(self, path: str, bucket: str = "public") -> bool:
59
+ """Deletes a file from a specific bucket"""
60
+ if self.use_s3 and self.s3_client:
61
+ s3_key = f"{bucket}/{path}"
62
+ await asyncio.to_thread(
63
+ self.s3_client.delete_object,
64
+ Bucket=self.s3_bucket,
65
+ Key=s3_key
66
+ )
67
+ return True
68
+ else:
69
+ local_path = self._get_local_path(bucket, path)
70
+ if os.path.exists(local_path):
71
+ os.remove(local_path)
72
+ return True
73
+ return False
74
+
75
+ def url(self, path: str, bucket: str = "public") -> str:
76
+ """Returns the URL for a file in a specific bucket"""
77
+ if self.use_s3 and self.s3_client:
78
+ s3_key = f"{bucket}/{path}"
79
+ if "amazonaws.com" in self.endpoint:
80
+ return f"https://{self.s3_bucket}.s3.amazonaws.com/{s3_key}"
81
+ return f"{self.endpoint}/{self.s3_bucket}/{s3_key}"
82
+ else:
83
+ return f"/storage/{bucket}/{path}"
84
+
85
+ storage = StorageManager()
voodoo/telemetry.py ADDED
@@ -0,0 +1,157 @@
1
+ import time
2
+ import uuid
3
+ import logging
4
+ import contextvars
5
+ import functools
6
+ import traceback
7
+ import inspect
8
+ from typing import Any, Callable, Dict, List
9
+ from starlette.types import ASGIApp, Receive, Scope, Send
10
+ from starlette.responses import JSONResponse
11
+ from voodoo.api import api
12
+
13
+ # Context variable for tracing
14
+ trace_id_var = contextvars.ContextVar("trace_id", default=None)
15
+
16
+ class TelemetryStore:
17
+ def __init__(self):
18
+ self.metrics: Dict[str, Any] = {
19
+ "requests_total": 0,
20
+ "errors_total": 0,
21
+ "latencies_ms": [],
22
+ "custom_traces": [],
23
+ "db_queries": 0,
24
+ "agent_tokens": 0
25
+ }
26
+
27
+ def record_request(self, latency_ms: float, error: bool = False):
28
+ self.metrics["requests_total"] += 1
29
+ if error:
30
+ self.metrics["errors_total"] += 1
31
+ self.metrics["latencies_ms"].append(latency_ms)
32
+ if len(self.metrics["latencies_ms"]) > 1000:
33
+ self.metrics["latencies_ms"].pop(0)
34
+
35
+ def record_trace(self, name: str, latency_ms: float, error: bool = False):
36
+ self.metrics["custom_traces"].append({
37
+ "name": name,
38
+ "latency_ms": latency_ms,
39
+ "error": error,
40
+ "timestamp": time.time(),
41
+ "trace_id": trace_id_var.get()
42
+ })
43
+ if len(self.metrics["custom_traces"]) > 1000:
44
+ self.metrics["custom_traces"].pop(0)
45
+
46
+ def record_db_query(self):
47
+ self.metrics["db_queries"] += 1
48
+
49
+ def record_agent_tokens(self, count: int):
50
+ self.metrics["agent_tokens"] += count
51
+
52
+ def get_summary(self) -> Dict[str, Any]:
53
+ latencies = self.metrics["latencies_ms"]
54
+ avg_latency = sum(latencies) / len(latencies) if latencies else 0.0
55
+ return {
56
+ "requests_total": self.metrics["requests_total"],
57
+ "errors_total": self.metrics["errors_total"],
58
+ "average_latency_ms": round(avg_latency, 2),
59
+ "db_queries": self.metrics["db_queries"],
60
+ "agent_tokens": self.metrics["agent_tokens"],
61
+ "recent_traces": self.metrics["custom_traces"][-10:]
62
+ }
63
+
64
+ telemetry_store = TelemetryStore()
65
+
66
+ logger = logging.getLogger("voodoo.telemetry")
67
+ if not logger.handlers:
68
+ handler = logging.StreamHandler()
69
+ formatter = logging.Formatter('%(asctime)s - %(name)s - [%(trace_id)s] - %(levelname)s - %(message)s')
70
+ handler.setFormatter(formatter)
71
+ logger.addHandler(handler)
72
+ logger.setLevel(logging.INFO)
73
+
74
+ class TraceFilter(logging.Filter):
75
+ def filter(self, record):
76
+ record.trace_id = trace_id_var.get() or "no-trace"
77
+ return True
78
+
79
+ logger.addFilter(TraceFilter())
80
+
81
+ class TelemetryMiddleware:
82
+ def __init__(self, app: ASGIApp):
83
+ self.app = app
84
+
85
+ async def __call__(self, scope: Scope, receive: Receive, send: Send):
86
+ if scope["type"] not in ("http", "websocket"):
87
+ await self.app(scope, receive, send)
88
+ return
89
+
90
+ trace_id = str(uuid.uuid4())
91
+ token = trace_id_var.set(trace_id)
92
+
93
+ start_time = time.perf_counter()
94
+ error = False
95
+
96
+ try:
97
+ await self.app(scope, receive, send)
98
+ except Exception as e:
99
+ error = True
100
+ logger.error(f"Unhandled exception in {scope['type']}: {e}\n{traceback.format_exc()}")
101
+ raise
102
+ finally:
103
+ latency_ms = (time.perf_counter() - start_time) * 1000
104
+ telemetry_store.record_request(latency_ms, error)
105
+
106
+ if scope["type"] == "http":
107
+ method = scope.get("method", "")
108
+ path = scope.get("path", "")
109
+ logger.info(f"{method} {path} completed in {latency_ms:.2f}ms")
110
+ elif scope["type"] == "websocket":
111
+ logger.info(f"WebSocket session completed in {latency_ms:.2f}ms")
112
+
113
+ trace_id_var.reset(token)
114
+
115
+ def trace(name: str = None):
116
+ """Decorator to trace a specific function block."""
117
+ def decorator(func: Callable):
118
+ @functools.wraps(func)
119
+ async def async_wrapper(*args, **kwargs):
120
+ t_name = name or func.__name__
121
+ start = time.perf_counter()
122
+ error = False
123
+ try:
124
+ return await func(*args, **kwargs)
125
+ except Exception as e:
126
+ error = True
127
+ logger.error(f"Error in traced block '{t_name}': {e}\n{traceback.format_exc()}")
128
+ raise
129
+ finally:
130
+ latency = (time.perf_counter() - start) * 1000
131
+ telemetry_store.record_trace(t_name, latency, error)
132
+ logger.info(f"Trace '{t_name}' completed in {latency:.2f}ms")
133
+
134
+ @functools.wraps(func)
135
+ def sync_wrapper(*args, **kwargs):
136
+ t_name = name or func.__name__
137
+ start = time.perf_counter()
138
+ error = False
139
+ try:
140
+ return func(*args, **kwargs)
141
+ except Exception as e:
142
+ error = True
143
+ logger.error(f"Error in traced block '{t_name}': {e}\n{traceback.format_exc()}")
144
+ raise
145
+ finally:
146
+ latency = (time.perf_counter() - start) * 1000
147
+ telemetry_store.record_trace(t_name, latency, error)
148
+ logger.info(f"Trace '{t_name}' completed in {latency:.2f}ms")
149
+
150
+ if inspect.iscoroutinefunction(func):
151
+ return async_wrapper
152
+ return sync_wrapper
153
+ return decorator
154
+
155
+ @api.get("/voodoo/metrics")
156
+ async def get_metrics():
157
+ return telemetry_store.get_summary()
voodoo/theme.py ADDED
@@ -0,0 +1,112 @@
1
+ import json
2
+ from typing import Dict, Any, Optional
3
+ from pydantic import BaseModel, Field
4
+
5
+ class ThemeColors(BaseModel):
6
+ """Modern color palette for the theme."""
7
+ primary: str = "#007AFF" # Apple blue
8
+ secondary: str = "#5856D6" # Violet
9
+
10
+ # Dark mode
11
+ background: str = "#0A0A0A" # Charcoal
12
+ surface: str = "rgba(255, 255, 255, 0.05)" # Glass surface
13
+ text: str = "#F3F4F6"
14
+ text_muted: str = "#9CA3AF"
15
+ border: str = "rgba(255, 255, 255, 0.1)"
16
+
17
+ # Light mode
18
+ light_background: str = "#F9FAFB"
19
+ light_surface: str = "rgba(0, 0, 0, 0.05)"
20
+ light_text: str = "#111827"
21
+ light_text_muted: str = "#4B5563"
22
+ light_border: str = "rgba(0, 0, 0, 0.1)"
23
+
24
+ # Allow extra colors
25
+ extra: Dict[str, str] = Field(default_factory=dict)
26
+
27
+ class ThemeFonts(BaseModel):
28
+ """Typography settings."""
29
+ sans: str = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'
30
+ mono: str = 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
31
+
32
+ class Theme(BaseModel):
33
+ """
34
+ Core Theme configuration for Voodoo.
35
+ Designed for modern macOS 'glass' aesthetic.
36
+ """
37
+ mode: str = "dark" # dark, light, system
38
+ colors: ThemeColors = Field(default_factory=ThemeColors)
39
+ fonts: ThemeFonts = Field(default_factory=ThemeFonts)
40
+
41
+ def to_tailwind_config(self) -> str:
42
+ """Generates the Tailwind configuration script."""
43
+
44
+ # Build the colors dictionary for Tailwind
45
+ tw_colors = {
46
+ "primary": self.colors.primary,
47
+ "secondary": self.colors.secondary,
48
+ "background": self.colors.background,
49
+ "surface": self.colors.surface,
50
+ "text": self.colors.text,
51
+ "text-muted": self.colors.text_muted,
52
+ "border": self.colors.border,
53
+ }
54
+ tw_colors.update(self.colors.extra)
55
+
56
+ config = {
57
+ "darkMode": 'class',
58
+ "theme": {
59
+ "extend": {
60
+ "colors": {
61
+ "voodoo": tw_colors
62
+ },
63
+ "fontFamily": {
64
+ "sans": [self.fonts.sans],
65
+ "mono": [self.fonts.mono]
66
+ }
67
+ }
68
+ }
69
+ }
70
+ return json.dumps(config)
71
+
72
+ def to_css_variables(self) -> str:
73
+ """Generates global CSS variables for the theme."""
74
+ dark_vars = [
75
+ f"--color-primary: {self.colors.primary};",
76
+ f"--color-secondary: {self.colors.secondary};",
77
+ f"--color-background: {self.colors.background};",
78
+ f"--color-surface: {self.colors.surface};",
79
+ f"--color-text: {self.colors.text};",
80
+ f"--color-text-muted: {self.colors.text_muted};",
81
+ f"--color-border: {self.colors.border};",
82
+ f"--font-sans: {self.fonts.sans};",
83
+ f"--font-mono: {self.fonts.mono};",
84
+ ]
85
+ light_vars = [
86
+ f"--color-primary: {self.colors.primary};",
87
+ f"--color-secondary: {self.colors.secondary};",
88
+ f"--color-background: {self.colors.light_background};",
89
+ f"--color-surface: {self.colors.light_surface};",
90
+ f"--color-text: {self.colors.light_text};",
91
+ f"--color-text-muted: {self.colors.light_text_muted};",
92
+ f"--color-border: {self.colors.light_border};",
93
+ f"--font-sans: {self.fonts.sans};",
94
+ f"--font-mono: {self.fonts.mono};",
95
+ ]
96
+
97
+ for k, v in self.colors.extra.items():
98
+ dark_vars.append(f"--color-{k}: {v};")
99
+ light_vars.append(f"--color-{k}: {v};")
100
+
101
+ return (
102
+ ":root {\n " + "\n ".join(light_vars) + "\n}\n" +
103
+ ".dark {\n " + "\n ".join(dark_vars) + "\n}"
104
+ )
105
+
106
+ # Global default theme
107
+ default_theme = Theme()
108
+
109
+ def set_theme(theme: Theme):
110
+ """Sets the global default theme."""
111
+ global default_theme
112
+ default_theme = theme
@@ -0,0 +1,23 @@
1
+ Metadata-Version: 2.4
2
+ Name: voodoo-framework
3
+ Version: 1.0.0
4
+ Summary: Voodoo Framework
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: starlette
7
+ Requires-Dist: uvicorn
8
+ Requires-Dist: pydantic
9
+ Requires-Dist: aiosqlite
10
+ Requires-Dist: websockets
11
+ Requires-Dist: httpx
12
+ Requires-Dist: python-dotenv
13
+ Requires-Dist: pyyaml
14
+ Requires-Dist: typer
15
+ Requires-Dist: rich
16
+ Requires-Dist: openai
17
+ Requires-Dist: aiofiles
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest; extra == "dev"
20
+ Requires-Dist: pytest-asyncio; extra == "dev"
21
+ Requires-Dist: ruff; extra == "dev"
22
+ Requires-Dist: mypy; extra == "dev"
23
+ Requires-Dist: pre-commit; extra == "dev"
@@ -0,0 +1,20 @@
1
+ voodoo/__init__.py,sha256=3dz4ShWkS7Wg8g7QgmEeL3xmW7R88MCU053SPBFhZdE,1146
2
+ voodoo/agent.py,sha256=wqqyjHLVb8z9ngG0f59rfozrW7zNH4nMvz1M_GrmeBA,1497
3
+ voodoo/api.py,sha256=LVfpNt9hfB4r0U8mzasoLniOarr-JD6qFmN5Iu7AY_o,5154
4
+ voodoo/cli.py,sha256=BJszwEv5MS8X8bc4NYTSIQUR-cpoCosvbbW1Hv4clec,10465
5
+ voodoo/components.py,sha256=hNkUjk5RBHMs_V33fGsrsqTuVjEltSgVit7Hxv8YDgg,4037
6
+ voodoo/config.py,sha256=ACb3Vvhghm2ZJyHVXArkdGMU-isRWb0yduDr-C71HAw,2144
7
+ voodoo/core.py,sha256=gNi-Rddip3wByZ_ExvaT5io7Vib_mdlvweSfS_YsNcQ,9686
8
+ voodoo/data.py,sha256=9Up4cEF2pX5SeQoxDIDa8LDsJPQoCSViEkWk0suCpqc,5932
9
+ voodoo/i18n.py,sha256=fWjaDYHEKLnd82fujse_m3C_5nAlV0s4Yf-kVaZkyng,3937
10
+ voodoo/mcp.py,sha256=CbfSLYHGMhSYy9EB0QQzWWhmWtY0LQXui0ca5-F7LMc,5415
11
+ voodoo/queue.py,sha256=dZHw9WKIsThu8qDIbeBqKevDk060XH2DDZS3UGj8GVg,1833
12
+ voodoo/status.py,sha256=Esj6pd54-MocvVfq7Jwr9MIdKfGtuM7irfSvvxw2avo,5759
13
+ voodoo/storage.py,sha256=jJPPBoxMRgHPJETHe_SYWHMyoqiPSXmDS4yeRF3cnlQ,3157
14
+ voodoo/telemetry.py,sha256=k3YZRpw5tSIo0xOAoBfxczHf6EHpEI4BEXgJr4o_0DM,5582
15
+ voodoo/theme.py,sha256=40uDhCAX9pw9AfUps8SgFu7AjQ9LTobNxY23aawqMM8,4043
16
+ voodoo_framework-1.0.0.dist-info/METADATA,sha256=yIcnnMd7WrQw1eYsUN1TDbnJDJX2kNH4essiurK4BKI,610
17
+ voodoo_framework-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
18
+ voodoo_framework-1.0.0.dist-info/entry_points.txt,sha256=t57b2RrYkby0eg4utUoYjRYKOrHJ1Bzp7t5k_sNq_UQ,42
19
+ voodoo_framework-1.0.0.dist-info/top_level.txt,sha256=TUn9HhnGYiYJyWa6NkouunOjTGCZAGPasucG_BjBT1E,7
20
+ voodoo_framework-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ voodoo = voodoo.cli:app
@@ -0,0 +1 @@
1
+ voodoo