python-quantumflow 2.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.
- python_quantumflow-2.0.0.dist-info/METADATA +399 -0
- python_quantumflow-2.0.0.dist-info/RECORD +11 -0
- python_quantumflow-2.0.0.dist-info/WHEEL +5 -0
- python_quantumflow-2.0.0.dist-info/top_level.txt +1 -0
- quantumflow/__init__.py +47 -0
- quantumflow/cli.py +401 -0
- quantumflow/config.py +189 -0
- quantumflow/core.py +368 -0
- quantumflow/execution.py +116 -0
- quantumflow/metrics.py +168 -0
- quantumflow/validation.py +77 -0
quantumflow/core.py
ADDED
@@ -0,0 +1,368 @@
|
|
1
|
+
"""
|
2
|
+
Python QuantumFlow Core Module
|
3
|
+
|
4
|
+
This module provides the core functionality of Python QuantumFlow,
|
5
|
+
including type conversion and flow control.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import functools
|
9
|
+
import logging
|
10
|
+
import time
|
11
|
+
import random
|
12
|
+
import asyncio
|
13
|
+
import atexit
|
14
|
+
import sys
|
15
|
+
import inspect
|
16
|
+
from typing import Any, Callable, Type, TypeVar, Union, get_type_hints
|
17
|
+
|
18
|
+
# ANSI color codes for terminal output
|
19
|
+
RESET = "\033[0m"
|
20
|
+
BOLD = "\033[1m"
|
21
|
+
MAGENTA = "\033[35m"
|
22
|
+
BLUE = "\033[34m"
|
23
|
+
CYAN = "\033[36m"
|
24
|
+
|
25
|
+
# Signature string
|
26
|
+
SIGNATURE = f"\n{BOLD}{MAGENTA}────────────────────────────────────────{RESET}\n{CYAN}Created with Python QuantumFlow by Magi Sharma{RESET}"
|
27
|
+
|
28
|
+
from .config import config, EffectType, success, error, warning, info, highlight, code
|
29
|
+
|
30
|
+
T = TypeVar('T')
|
31
|
+
|
32
|
+
def flow(target_type: Type[T]) -> Callable[[Any], T]:
|
33
|
+
"""
|
34
|
+
Create a flow function that converts the input to the specified target type.
|
35
|
+
|
36
|
+
Args:
|
37
|
+
target_type: The type to convert to.
|
38
|
+
|
39
|
+
Returns:
|
40
|
+
A function that takes an input and returns it converted to the target type.
|
41
|
+
"""
|
42
|
+
def converter(value: Any) -> T:
|
43
|
+
# Apply colorization if enabled
|
44
|
+
if config.get_effect(EffectType.COLORIZE):
|
45
|
+
print(info(f"Converting {highlight(str(type(value).__name__))} to {highlight(str(target_type.__name__))}"))
|
46
|
+
|
47
|
+
# Apply conversion logic
|
48
|
+
try:
|
49
|
+
# Simple case: already the correct type
|
50
|
+
if isinstance(value, target_type):
|
51
|
+
return value
|
52
|
+
|
53
|
+
# Try to convert using the target type constructor
|
54
|
+
result = target_type(value)
|
55
|
+
|
56
|
+
# Show success message if colorize is enabled
|
57
|
+
if config.get_effect(EffectType.COLORIZE):
|
58
|
+
print(success(f"Successfully converted to {highlight(str(target_type.__name__))}"))
|
59
|
+
|
60
|
+
return result
|
61
|
+
except Exception as e:
|
62
|
+
# Apply error handling if enabled
|
63
|
+
if config.get_effect(EffectType.ERROR_HANDLING):
|
64
|
+
print(error(f"Conversion error: {str(e)}"))
|
65
|
+
# Could implement retry logic or fallback here
|
66
|
+
raise
|
67
|
+
|
68
|
+
return converter
|
69
|
+
|
70
|
+
def qflow(func: Callable) -> Callable:
|
71
|
+
"""
|
72
|
+
Decorator that applies Python QuantumFlow effects to a function.
|
73
|
+
|
74
|
+
This decorator enables automatic type conversion based on type hints,
|
75
|
+
error handling, and colorized output.
|
76
|
+
|
77
|
+
Args:
|
78
|
+
func: The function to decorate.
|
79
|
+
|
80
|
+
Returns:
|
81
|
+
The decorated function.
|
82
|
+
"""
|
83
|
+
@functools.wraps(func)
|
84
|
+
def wrapper(*args, **kwargs):
|
85
|
+
# Apply auto-conversion effect if enabled
|
86
|
+
if config.get_effect(EffectType.AUTO_CONVERSION):
|
87
|
+
# Get type hints for the function
|
88
|
+
hints = get_type_hints(func)
|
89
|
+
|
90
|
+
# Get the parameter names
|
91
|
+
sig = inspect.signature(func)
|
92
|
+
param_names = list(sig.parameters.keys())
|
93
|
+
|
94
|
+
# Convert positional arguments based on type hints
|
95
|
+
converted_args = list(args)
|
96
|
+
for i, arg in enumerate(args):
|
97
|
+
if i < len(param_names):
|
98
|
+
param_name = param_names[i]
|
99
|
+
if param_name in hints:
|
100
|
+
target_type = hints[param_name]
|
101
|
+
# Apply the flow conversion
|
102
|
+
try:
|
103
|
+
converted_args[i] = flow(target_type)(arg)
|
104
|
+
except:
|
105
|
+
# If conversion fails, keep original value
|
106
|
+
pass
|
107
|
+
|
108
|
+
# Convert keyword arguments based on type hints
|
109
|
+
converted_kwargs = {}
|
110
|
+
for key, value in kwargs.items():
|
111
|
+
if key in hints:
|
112
|
+
target_type = hints[key]
|
113
|
+
try:
|
114
|
+
converted_kwargs[key] = flow(target_type)(value)
|
115
|
+
except:
|
116
|
+
# If conversion fails, keep original value
|
117
|
+
converted_kwargs[key] = value
|
118
|
+
else:
|
119
|
+
converted_kwargs[key] = value
|
120
|
+
|
121
|
+
# Call the function with converted arguments
|
122
|
+
result = func(*converted_args, **converted_kwargs)
|
123
|
+
else:
|
124
|
+
# If auto-conversion is disabled, just call the function normally
|
125
|
+
result = func(*args, **kwargs)
|
126
|
+
|
127
|
+
# Show signature if colorize effect is enabled
|
128
|
+
if config.get_effect(EffectType.COLORIZE):
|
129
|
+
print(SIGNATURE)
|
130
|
+
|
131
|
+
return result
|
132
|
+
|
133
|
+
return wrapper
|
134
|
+
|
135
|
+
def with_typeflow():
|
136
|
+
"""
|
137
|
+
Context manager for QuantumFlow operations.
|
138
|
+
|
139
|
+
This enables automatic type conversion within the context block.
|
140
|
+
|
141
|
+
Returns:
|
142
|
+
A context manager object.
|
143
|
+
"""
|
144
|
+
class TypeFlowContext:
|
145
|
+
def __enter__(self):
|
146
|
+
# Enable all effects when entering the context
|
147
|
+
for effect in EffectType:
|
148
|
+
config.set_effect(effect, True)
|
149
|
+
return self
|
150
|
+
|
151
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
152
|
+
# Keep effects enabled when exiting
|
153
|
+
return False
|
154
|
+
|
155
|
+
return TypeFlowContext()
|
156
|
+
|
157
|
+
def async_flow(func):
|
158
|
+
"""
|
159
|
+
Decorator for async functions to make them part of a flow.
|
160
|
+
"""
|
161
|
+
@functools.wraps(func)
|
162
|
+
async def wrapper(*args, **kwargs):
|
163
|
+
return await func(*args, **kwargs)
|
164
|
+
return wrapper
|
165
|
+
|
166
|
+
def retry(max_attempts=3, backoff_factor=0.5, exceptions=(Exception,)):
|
167
|
+
"""
|
168
|
+
Decorator to retry a function if it raises specified exceptions.
|
169
|
+
|
170
|
+
Args:
|
171
|
+
max_attempts: Maximum number of retry attempts
|
172
|
+
backoff_factor: Exponential backoff factor
|
173
|
+
exceptions: Tuple of exceptions to catch and retry
|
174
|
+
"""
|
175
|
+
def decorator(func):
|
176
|
+
@functools.wraps(func)
|
177
|
+
async def async_wrapper(*args, **kwargs):
|
178
|
+
last_exception = None
|
179
|
+
for attempt in range(max_attempts):
|
180
|
+
try:
|
181
|
+
return await func(*args, **kwargs)
|
182
|
+
except exceptions as e:
|
183
|
+
last_exception = e
|
184
|
+
if attempt < max_attempts - 1:
|
185
|
+
sleep_time = backoff_factor * (2 ** attempt) + random.uniform(0, 0.1)
|
186
|
+
await asyncio.sleep(sleep_time)
|
187
|
+
else:
|
188
|
+
raise last_exception
|
189
|
+
|
190
|
+
@functools.wraps(func)
|
191
|
+
def sync_wrapper(*args, **kwargs):
|
192
|
+
last_exception = None
|
193
|
+
for attempt in range(max_attempts):
|
194
|
+
try:
|
195
|
+
return func(*args, **kwargs)
|
196
|
+
except exceptions as e:
|
197
|
+
last_exception = e
|
198
|
+
if attempt < max_attempts - 1:
|
199
|
+
sleep_time = backoff_factor * (2 ** attempt) + random.uniform(0, 0.1)
|
200
|
+
time.sleep(sleep_time)
|
201
|
+
else:
|
202
|
+
raise last_exception
|
203
|
+
|
204
|
+
# Choose the appropriate wrapper based on whether the function is async
|
205
|
+
if inspect.iscoroutinefunction(func):
|
206
|
+
return async_wrapper
|
207
|
+
return sync_wrapper
|
208
|
+
|
209
|
+
return decorator
|
210
|
+
|
211
|
+
def configure_logging(level="INFO", fancy=False):
|
212
|
+
"""Configure basic logging."""
|
213
|
+
numeric_level = getattr(logging, level.upper(), None)
|
214
|
+
if not isinstance(numeric_level, int):
|
215
|
+
raise ValueError(f"Invalid log level: {level}")
|
216
|
+
logging.basicConfig(level=numeric_level)
|
217
|
+
|
218
|
+
def fancy_print(message, style=None, end="\n"):
|
219
|
+
"""Print with styling using ANSI color codes for terminal output.
|
220
|
+
|
221
|
+
If style is not provided, will attempt to apply default styling based on message content.
|
222
|
+
"""
|
223
|
+
# Define ANSI color codes
|
224
|
+
COLORS = {
|
225
|
+
"black": "\033[30m",
|
226
|
+
"red": "\033[31m",
|
227
|
+
"green": "\033[32m",
|
228
|
+
"yellow": "\033[33m",
|
229
|
+
"blue": "\033[34m",
|
230
|
+
"magenta": "\033[35m",
|
231
|
+
"cyan": "\033[36m",
|
232
|
+
"white": "\033[37m",
|
233
|
+
"bold": "\033[1m",
|
234
|
+
"dim": "\033[2m",
|
235
|
+
"italic": "\033[3m",
|
236
|
+
"underline": "\033[4m",
|
237
|
+
"reset": "\033[0m"
|
238
|
+
}
|
239
|
+
|
240
|
+
# Auto-detect style if none provided
|
241
|
+
if style is None:
|
242
|
+
message_lower = message.lower()
|
243
|
+
|
244
|
+
# Apply default styling based on message content
|
245
|
+
if any(kw in message_lower for kw in ["error", "failed", "exception", "crash"]):
|
246
|
+
style = "bold red"
|
247
|
+
elif any(kw in message_lower for kw in ["warning", "caution", "attention"]):
|
248
|
+
style = "bold yellow"
|
249
|
+
elif any(kw in message_lower for kw in ["success", "completed", "done", "finished"]):
|
250
|
+
style = "bold green"
|
251
|
+
elif any(kw in message_lower for kw in ["info", "note", "notice"]):
|
252
|
+
style = "cyan"
|
253
|
+
elif any(kw in message_lower for kw in ["debug", "trace"]):
|
254
|
+
style = "dim"
|
255
|
+
elif any(kw in message_lower for kw in ["important", "critical"]):
|
256
|
+
style = "bold magenta"
|
257
|
+
elif message.isupper(): # ALL CAPS text gets bold styling
|
258
|
+
style = "bold"
|
259
|
+
elif message.startswith(("=>", "->", ">>", "-->")): # Arrow indicators
|
260
|
+
style = "bold blue"
|
261
|
+
elif message.startswith(("# ", "## ", "### ")): # Heading-like text
|
262
|
+
heading_level = message.count('#')
|
263
|
+
if heading_level == 1:
|
264
|
+
style = "bold magenta"
|
265
|
+
elif heading_level == 2:
|
266
|
+
style = "bold blue"
|
267
|
+
else:
|
268
|
+
style = "bold cyan"
|
269
|
+
else:
|
270
|
+
# Default effects for normal text - subtle gradient effect across the text
|
271
|
+
return _gradient_print(message, end=end)
|
272
|
+
|
273
|
+
if style:
|
274
|
+
# Parse multiple styles (e.g., "bold red")
|
275
|
+
style_parts = style.split()
|
276
|
+
codes = ""
|
277
|
+
for part in style_parts:
|
278
|
+
if part in COLORS:
|
279
|
+
codes += COLORS[part]
|
280
|
+
|
281
|
+
# Apply style and reset after the message
|
282
|
+
print(f"{codes}{message}{COLORS['reset']}", end=end)
|
283
|
+
else:
|
284
|
+
print(message, end=end)
|
285
|
+
|
286
|
+
def _gradient_print(text, start_color=(70, 130, 180), end_color=(138, 43, 226), end="\n"):
|
287
|
+
"""Print text with a color gradient using ANSI RGB color codes."""
|
288
|
+
start_r, start_g, start_b = start_color
|
289
|
+
end_r, end_g, end_b = end_color
|
290
|
+
result = ""
|
291
|
+
|
292
|
+
for i, char in enumerate(text):
|
293
|
+
# Calculate the gradient position
|
294
|
+
ratio = i / (len(text) - 1) if len(text) > 1 else 0
|
295
|
+
|
296
|
+
# Interpolate between start and end colors
|
297
|
+
r = int(start_r + (end_r - start_r) * ratio)
|
298
|
+
g = int(start_g + (end_g - start_g) * ratio)
|
299
|
+
b = int(start_b + (end_b - start_b) * ratio)
|
300
|
+
|
301
|
+
# Add the colored character
|
302
|
+
result += f"\033[38;2;{r};{g};{b}m{char}"
|
303
|
+
|
304
|
+
# Reset color at the end
|
305
|
+
print(f"{result}\033[0m", end=end)
|
306
|
+
return True
|
307
|
+
|
308
|
+
def print_author_credit(small=True):
|
309
|
+
"""Print a small credit line with the author's name.
|
310
|
+
|
311
|
+
This function can be called at the end of scripts to provide attribution.
|
312
|
+
"""
|
313
|
+
if small:
|
314
|
+
# Subtle, small attribution
|
315
|
+
print("\n\033[38;2;180;180;180m" + "─" * 40 + "\033[0m")
|
316
|
+
print("\033[38;2;180;180;180mCreated with QuantumFlow by Magi Sharma\033[0m")
|
317
|
+
else:
|
318
|
+
# More elaborate attribution with gradient
|
319
|
+
author_text = "Created by Magi Sharma"
|
320
|
+
framework_text = "QUANTUMFLOW FRAMEWORK"
|
321
|
+
version_text = "v0.6.1"
|
322
|
+
|
323
|
+
print("\n" + "─" * 50)
|
324
|
+
|
325
|
+
# Print author with gradient
|
326
|
+
result = ""
|
327
|
+
start_r, start_g, start_b = 255, 215, 0 # Gold
|
328
|
+
end_r, end_g, end_b = 255, 140, 0 # Dark orange
|
329
|
+
|
330
|
+
for i, char in enumerate(author_text):
|
331
|
+
ratio = i / (len(author_text) - 1) if len(author_text) > 1 else 0
|
332
|
+
r = int(start_r + (end_r - start_r) * ratio)
|
333
|
+
g = int(start_g + (end_g - start_g) * ratio)
|
334
|
+
b = int(start_b + (end_b - start_b) * ratio)
|
335
|
+
result += f"\033[38;2;{r};{g};{b}m{char}"
|
336
|
+
|
337
|
+
print(f"{result}\033[0m")
|
338
|
+
|
339
|
+
# Print framework name with different gradient
|
340
|
+
result = ""
|
341
|
+
start_r, start_g, start_b = 0, 191, 255 # Deep sky blue
|
342
|
+
end_r, end_g, end_b = 138, 43, 226 # Purple
|
343
|
+
|
344
|
+
for i, char in enumerate(framework_text):
|
345
|
+
ratio = i / (len(framework_text) - 1) if len(framework_text) > 1 else 0
|
346
|
+
r = int(start_r + (end_r - start_r) * ratio)
|
347
|
+
g = int(start_g + (end_g - start_g) * ratio)
|
348
|
+
b = int(start_b + (end_b - start_b) * ratio)
|
349
|
+
result += f"\033[38;2;{r};{g};{b}m{char}"
|
350
|
+
|
351
|
+
print(f"{result} \033[38;2;150;150;150m{version_text}\033[0m")
|
352
|
+
print("─" * 50)
|
353
|
+
|
354
|
+
def _show_credits_on_exit():
|
355
|
+
"""Show small author credits when the program exits."""
|
356
|
+
# Only show on regular exits, not errors or interrupts
|
357
|
+
if sys.exc_info()[0] is None:
|
358
|
+
print_author_credit(small=True)
|
359
|
+
|
360
|
+
# Register the exit handler
|
361
|
+
atexit.register(_show_credits_on_exit)
|
362
|
+
|
363
|
+
def create_progress(description=None):
|
364
|
+
"""Create a simple progress indicator."""
|
365
|
+
return None # Fallback to simple printing
|
366
|
+
|
367
|
+
# Add console object for compatibility
|
368
|
+
console = None
|
quantumflow/execution.py
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
"""
|
2
|
+
Execution backends for QuantumFlow.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import asyncio
|
6
|
+
import concurrent.futures
|
7
|
+
import logging
|
8
|
+
import multiprocessing
|
9
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
10
|
+
|
11
|
+
logger = logging.getLogger("quantumflow")
|
12
|
+
|
13
|
+
class Executor:
|
14
|
+
"""Base class for flow executors."""
|
15
|
+
|
16
|
+
def execute(self, flow, *args, **kwargs):
|
17
|
+
"""Execute a flow."""
|
18
|
+
raise NotImplementedError("Subclasses must implement this method")
|
19
|
+
|
20
|
+
class SyncExecutor(Executor):
|
21
|
+
"""Executor that runs flows synchronously."""
|
22
|
+
|
23
|
+
def execute(self, flow, *args, **kwargs):
|
24
|
+
"""Execute a flow synchronously."""
|
25
|
+
return flow(*args, **kwargs)
|
26
|
+
|
27
|
+
class AsyncExecutor(Executor):
|
28
|
+
"""Executor that runs flows asynchronously."""
|
29
|
+
|
30
|
+
async def execute(self, flow, *args, **kwargs):
|
31
|
+
"""Execute a flow asynchronously."""
|
32
|
+
if asyncio.iscoroutinefunction(flow):
|
33
|
+
return await flow(*args, **kwargs)
|
34
|
+
else:
|
35
|
+
loop = asyncio.get_event_loop()
|
36
|
+
return await loop.run_in_executor(None, lambda: flow(*args, **kwargs))
|
37
|
+
|
38
|
+
class ThreadPoolExecutor(Executor):
|
39
|
+
"""Executor that runs flows in a thread pool."""
|
40
|
+
|
41
|
+
def __init__(self, max_workers: Optional[int] = None):
|
42
|
+
self.max_workers = max_workers
|
43
|
+
|
44
|
+
def execute(self, flow, *args, **kwargs):
|
45
|
+
"""Execute a flow in a thread pool."""
|
46
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
47
|
+
future = executor.submit(flow, *args, **kwargs)
|
48
|
+
return future.result()
|
49
|
+
|
50
|
+
class ProcessPoolExecutor(Executor):
|
51
|
+
"""Executor that runs flows in a process pool."""
|
52
|
+
|
53
|
+
def __init__(self, max_workers: Optional[int] = None):
|
54
|
+
self.max_workers = max_workers
|
55
|
+
|
56
|
+
def execute(self, flow, *args, **kwargs):
|
57
|
+
"""Execute a flow in a process pool."""
|
58
|
+
with concurrent.futures.ProcessPoolExecutor(max_workers=self.max_workers) as executor:
|
59
|
+
future = executor.submit(flow, *args, **kwargs)
|
60
|
+
return future.result()
|
61
|
+
|
62
|
+
class DaskExecutor(Executor):
|
63
|
+
"""Executor that runs flows using Dask."""
|
64
|
+
|
65
|
+
def __init__(self, address: Optional[str] = None):
|
66
|
+
self.address = address
|
67
|
+
self._client = None
|
68
|
+
|
69
|
+
def _get_client(self):
|
70
|
+
"""Get or create a Dask client."""
|
71
|
+
if self._client is None:
|
72
|
+
try:
|
73
|
+
from dask.distributed import Client
|
74
|
+
self._client = Client(self.address)
|
75
|
+
except ImportError:
|
76
|
+
raise ImportError("Dask not installed. Install with 'pip install dask[distributed]'")
|
77
|
+
return self._client
|
78
|
+
|
79
|
+
def execute(self, flow, *args, **kwargs):
|
80
|
+
"""Execute a flow using Dask."""
|
81
|
+
client = self._get_client()
|
82
|
+
future = client.submit(flow, *args, **kwargs)
|
83
|
+
return future.result()
|
84
|
+
|
85
|
+
class RayExecutor(Executor):
|
86
|
+
"""Executor that runs flows using Ray."""
|
87
|
+
|
88
|
+
def __init__(self, address: Optional[str] = None):
|
89
|
+
self.address = address
|
90
|
+
self._initialized = False
|
91
|
+
|
92
|
+
def _initialize(self):
|
93
|
+
"""Initialize Ray."""
|
94
|
+
if not self._initialized:
|
95
|
+
try:
|
96
|
+
import ray
|
97
|
+
if self.address:
|
98
|
+
ray.init(address=self.address)
|
99
|
+
else:
|
100
|
+
ray.init()
|
101
|
+
self._initialized = True
|
102
|
+
except ImportError:
|
103
|
+
raise ImportError("Ray not installed. Install with 'pip install ray'")
|
104
|
+
|
105
|
+
def execute(self, flow, *args, **kwargs):
|
106
|
+
"""Execute a flow using Ray."""
|
107
|
+
self._initialize()
|
108
|
+
|
109
|
+
import ray
|
110
|
+
|
111
|
+
@ray.remote
|
112
|
+
def ray_wrapper(f, *args, **kwargs):
|
113
|
+
return f(*args, **kwargs)
|
114
|
+
|
115
|
+
future = ray_wrapper.remote(flow, *args, **kwargs)
|
116
|
+
return ray.get(future)
|
quantumflow/metrics.py
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
"""
|
2
|
+
Metrics and observability for QuantumFlow.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import asyncio
|
6
|
+
import logging
|
7
|
+
import threading
|
8
|
+
import time
|
9
|
+
from typing import Any, Dict, List, Optional, Union
|
10
|
+
|
11
|
+
logger = logging.getLogger("quantumflow")
|
12
|
+
|
13
|
+
class Metric:
|
14
|
+
"""Base class for metrics."""
|
15
|
+
|
16
|
+
def __init__(self, name: str, description: str):
|
17
|
+
self.name = name
|
18
|
+
self.description = description
|
19
|
+
|
20
|
+
def get_value(self) -> Any:
|
21
|
+
"""Get the current value of the metric."""
|
22
|
+
raise NotImplementedError("Subclasses must implement this method")
|
23
|
+
|
24
|
+
class Counter(Metric):
|
25
|
+
"""Counter metric that only increases."""
|
26
|
+
|
27
|
+
def __init__(self, name: str, description: str):
|
28
|
+
super().__init__(name, description)
|
29
|
+
self._value = 0
|
30
|
+
|
31
|
+
def inc(self, value: int = 1):
|
32
|
+
"""Increment the counter."""
|
33
|
+
if value < 0:
|
34
|
+
raise ValueError("Counter can only be incremented by non-negative values")
|
35
|
+
self._value += value
|
36
|
+
|
37
|
+
def get_value(self) -> int:
|
38
|
+
"""Get the current value of the counter."""
|
39
|
+
return self._value
|
40
|
+
|
41
|
+
class Gauge(Metric):
|
42
|
+
"""Gauge metric that can go up and down."""
|
43
|
+
|
44
|
+
def __init__(self, name: str, description: str):
|
45
|
+
super().__init__(name, description)
|
46
|
+
self._value = 0
|
47
|
+
|
48
|
+
def set(self, value: float):
|
49
|
+
"""Set the gauge value."""
|
50
|
+
self._value = value
|
51
|
+
|
52
|
+
def inc(self, value: float = 1):
|
53
|
+
"""Increment the gauge."""
|
54
|
+
self._value += value
|
55
|
+
|
56
|
+
def dec(self, value: float = 1):
|
57
|
+
"""Decrement the gauge."""
|
58
|
+
self._value -= value
|
59
|
+
|
60
|
+
def get_value(self) -> float:
|
61
|
+
"""Get the current value of the gauge."""
|
62
|
+
return self._value
|
63
|
+
|
64
|
+
class Histogram(Metric):
|
65
|
+
"""Histogram metric for measuring distributions."""
|
66
|
+
|
67
|
+
def __init__(self, name: str, description: str, buckets: List[float] = None):
|
68
|
+
super().__init__(name, description)
|
69
|
+
self.buckets = buckets or [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
|
70
|
+
self._values = []
|
71
|
+
|
72
|
+
def observe(self, value: float):
|
73
|
+
"""Observe a value."""
|
74
|
+
self._values.append(value)
|
75
|
+
|
76
|
+
def get_value(self) -> Dict[str, Any]:
|
77
|
+
"""Get the current value of the histogram."""
|
78
|
+
result = {"count": len(self._values), "sum": sum(self._values), "buckets": {}}
|
79
|
+
|
80
|
+
for bucket in self.buckets:
|
81
|
+
result["buckets"][bucket] = sum(1 for v in self._values if v <= bucket)
|
82
|
+
|
83
|
+
return result
|
84
|
+
|
85
|
+
# Global registry for metrics
|
86
|
+
_metrics_registry = {}
|
87
|
+
|
88
|
+
def counter(name: str, description: str) -> Counter:
|
89
|
+
"""Create or get a counter metric."""
|
90
|
+
if name not in _metrics_registry:
|
91
|
+
_metrics_registry[name] = Counter(name, description)
|
92
|
+
return _metrics_registry[name]
|
93
|
+
|
94
|
+
def gauge(name: str, description: str) -> Gauge:
|
95
|
+
"""Create or get a gauge metric."""
|
96
|
+
if name not in _metrics_registry:
|
97
|
+
_metrics_registry[name] = Gauge(name, description)
|
98
|
+
return _metrics_registry[name]
|
99
|
+
|
100
|
+
def histogram(name: str, description: str, buckets: List[float] = None) -> Histogram:
|
101
|
+
"""Create or get a histogram metric."""
|
102
|
+
if name not in _metrics_registry:
|
103
|
+
_metrics_registry[name] = Histogram(name, description, buckets)
|
104
|
+
return _metrics_registry[name]
|
105
|
+
|
106
|
+
class MetricsServer:
|
107
|
+
"""Server for exposing metrics."""
|
108
|
+
|
109
|
+
def __init__(self, host: str = "localhost", port: int = 8000):
|
110
|
+
self.host = host
|
111
|
+
self.port = port
|
112
|
+
self._server = None
|
113
|
+
self._running = False
|
114
|
+
|
115
|
+
async def start(self):
|
116
|
+
"""Start the metrics server."""
|
117
|
+
from aiohttp import web
|
118
|
+
|
119
|
+
async def metrics_handler(request):
|
120
|
+
"""Handle metrics requests."""
|
121
|
+
output = []
|
122
|
+
|
123
|
+
for name, metric in _metrics_registry.items():
|
124
|
+
output.append(f"# HELP {name} {metric.description}")
|
125
|
+
|
126
|
+
if isinstance(metric, Counter):
|
127
|
+
output.append(f"# TYPE {name} counter")
|
128
|
+
output.append(f"{name} {metric.get_value()}")
|
129
|
+
elif isinstance(metric, Gauge):
|
130
|
+
output.append(f"# TYPE {name} gauge")
|
131
|
+
output.append(f"{name} {metric.get_value()}")
|
132
|
+
elif isinstance(metric, Histogram):
|
133
|
+
output.append(f"# TYPE {name} histogram")
|
134
|
+
value = metric.get_value()
|
135
|
+
|
136
|
+
for bucket, count in value["buckets"].items():
|
137
|
+
output.append(f"{name}_bucket{{le=\"{bucket}\"}} {count}")
|
138
|
+
|
139
|
+
output.append(f"{name}_count {value['count']}")
|
140
|
+
output.append(f"{name}_sum {value['sum']}")
|
141
|
+
|
142
|
+
return web.Response(text="\n".join(output))
|
143
|
+
|
144
|
+
app = web.Application()
|
145
|
+
app.router.add_get("/metrics", metrics_handler)
|
146
|
+
|
147
|
+
runner = web.AppRunner(app)
|
148
|
+
await runner.setup()
|
149
|
+
site = web.TCPSite(runner, self.host, self.port)
|
150
|
+
await site.start()
|
151
|
+
|
152
|
+
self._server = runner
|
153
|
+
self._running = True
|
154
|
+
|
155
|
+
logger.info(f"Metrics server started at http://{self.host}:{self.port}/metrics")
|
156
|
+
return self
|
157
|
+
|
158
|
+
async def stop(self):
|
159
|
+
"""Stop the metrics server."""
|
160
|
+
if self._server and self._running:
|
161
|
+
await self._server.cleanup()
|
162
|
+
self._running = False
|
163
|
+
logger.info("Metrics server stopped")
|
164
|
+
|
165
|
+
async def serve_metrics(host: str = "localhost", port: int = 8000) -> MetricsServer:
|
166
|
+
"""Start a metrics server."""
|
167
|
+
server = MetricsServer(host, port)
|
168
|
+
return await server.start()
|