hyperprobe-agent 1.2.24__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.
@@ -0,0 +1,119 @@
1
+ import re
2
+ import ast
3
+ from functools import lru_cache
4
+
5
+ from hyperprobe.core.logger import get_logger
6
+
7
+ logger = get_logger("hyperprobe:evaluator")
8
+
9
+ SAFE_BUILTINS = {
10
+ "len": len, "str": str, "int": int, "float": float,
11
+ "bool": bool, "list": list, "dict": dict, "set": set,
12
+ "max": max, "min": min, "abs": abs, "round": round,
13
+ "isinstance": isinstance, "type": type
14
+ }
15
+
16
+ COMPILED_CACHE_SIZE = 200
17
+
18
+ SAFE_METHODS = {
19
+ # Dictionary/List read-only methods
20
+ "keys", "values", "items", "count",
21
+ # String read-only methods
22
+ "lower", "upper",
23
+ "isdigit", "isalpha", "isnumeric", "isalnum",
24
+ }
25
+
26
+ class SafeASTVisitor(ast.NodeVisitor):
27
+ def visit_Call(self, node):
28
+ # 1. Is it a direct function call? e.g. len(items)
29
+ if isinstance(node.func, ast.Name):
30
+ if node.func.id not in SAFE_BUILTINS:
31
+ raise ValueError(f"Function call '{node.func.id}' is not allowed.")
32
+
33
+ # 2. Is it a method call on an object? e.g. user.get_id() or my_dict.keys()
34
+ elif isinstance(node.func, ast.Attribute):
35
+ method_name = node.func.attr
36
+ if method_name not in SAFE_METHODS:
37
+ raise ValueError(f"Method call '.{method_name}()' is strictly forbidden.")
38
+
39
+ else:
40
+ raise ValueError("Complex dynamic calls are strictly forbidden.")
41
+
42
+ self.generic_visit(node)
43
+
44
+
45
+ class ProbeEvaluator:
46
+ _placeholder_regex = re.compile(r'\$?\{([^}]+)\}')
47
+
48
+ @classmethod
49
+ def _get_or_compile_safe(cls, expr: str):
50
+ # Normalize the string to prevent duplicate cache entries
51
+ clean_expr = expr.strip()
52
+
53
+ return cls._compile_safe(clean_expr)
54
+
55
+ @staticmethod
56
+ @lru_cache(maxsize=COMPILED_CACHE_SIZE)
57
+ def _compile_safe(clean_expr: str):
58
+ # Cache misses are parsed and compiled once; least-recently-used
59
+ # expressions are evicted after the bounded cache reaches capacity.
60
+ if len(clean_expr) > 256:
61
+ raise ValueError("Expression is too long (max 256 characters).")
62
+
63
+ tree = ast.parse(clean_expr, mode='eval')
64
+ visitor = SafeASTVisitor()
65
+ visitor.visit(tree)
66
+
67
+ # Compile it to bytecode
68
+ compiled_code = compile(tree, filename="<string>", mode="eval")
69
+
70
+ return compiled_code
71
+
72
+ @classmethod
73
+ def safe_eval(cls, expr: str, globals_dict: dict, locals_dict: dict):
74
+ # Create a shadow copy of globals, forcefully removing dangerous builtins
75
+ safe_globals = globals_dict.copy()
76
+ safe_globals["__builtins__"] = SAFE_BUILTINS
77
+
78
+ # Grab the pre-compiled bytecode instantly
79
+ compiled = cls._get_or_compile_safe(expr)
80
+
81
+ # Execute the bytecode
82
+ return eval(compiled, safe_globals, locals_dict)
83
+
84
+ @classmethod
85
+ def evaluate_log_template(cls, template: str, globals_dict: dict, locals_dict: dict) -> str:
86
+ """Parses and safely formats a log template string."""
87
+ if not template:
88
+ return ""
89
+
90
+ result = []
91
+ last_idx = 0
92
+ for match in cls._placeholder_regex.finditer(template):
93
+ # Append plain text preceding placeholder
94
+ result.append(template[last_idx:match.start()])
95
+ expr = match.group(1).strip()
96
+
97
+ try:
98
+ # Safely execute expression
99
+ val = cls.safe_eval(expr, globals_dict, locals_dict)
100
+ result.append(str(val))
101
+ except Exception as e:
102
+ result.append(f"<Error: {type(e).__name__}: {str(e)}>")
103
+ last_idx = match.end()
104
+
105
+ result.append(template[last_idx:])
106
+ return "".join(result)
107
+
108
+ @classmethod
109
+ def evaluate_watches(cls, watch_expressions: list, globals_dict: dict, locals_dict: dict) -> dict:
110
+ """Evaluates a set of independent watch expressions."""
111
+ results = {}
112
+ if not watch_expressions:
113
+ return results
114
+ for expr in watch_expressions:
115
+ try:
116
+ results[expr] = cls.safe_eval(expr, globals_dict, locals_dict)
117
+ except Exception as e:
118
+ results[expr] = f"Error: {type(e).__name__}: {str(e)}"
119
+ return results
File without changes
@@ -0,0 +1,18 @@
1
+ import os
2
+ import sys
3
+
4
+ # This file acts as an automatic injection point for Python.
5
+ # When a directory containing this file is injected into PYTHONPATH,
6
+ # Python will automatically execute it before the main application code starts.
7
+
8
+ try:
9
+ from hyperprobe.agent import HyperProbeAgent
10
+
11
+ HyperProbeAgent.start({
12
+ "broker_url": os.getenv("HYPERPROBE_BROKER_URL"),
13
+ "service_id": os.getenv("HYPERPROBE_SERVICE_ID"),
14
+ "environment": os.getenv("HYPERPROBE_ENVIRONMENT"),
15
+ "commit_sha": os.getenv("HYPERPROBE_COMMIT_SHA") or os.getenv("GIT_COMMIT")
16
+ })
17
+ except Exception as e:
18
+ print(f"\033[1m\033[33m⚠️ [HyperProbe] CRITICAL: Agent failed to initialize ({type(e).__name__}: {e}). Running application uninstrumented.", file=sys.stderr)
@@ -0,0 +1,121 @@
1
+ """Small, dependency-free namespace debugging for the HyperProbe SDK.
2
+
3
+ The ``DEBUG`` environment variable accepts comma- or whitespace-separated
4
+ namespace patterns. ``*`` matches any characters and a pattern prefixed with
5
+ ``-`` excludes a namespace.
6
+ """
7
+
8
+ import os
9
+ import re
10
+ import sys
11
+ import threading
12
+
13
+
14
+ _SELECTOR_SEPARATOR = re.compile(r"[\s,]+")
15
+ _COLOR_CODES = (31, 32, 33, 34, 35, 36, 91, 92, 93, 94, 95, 96)
16
+ _DISABLED_COLOR_VALUES = {"0", "false", "no", "off"}
17
+ _logger_cache: dict[str, "HyperProbeLogger"] = {}
18
+ _logger_cache_lock = threading.Lock()
19
+
20
+
21
+ def _compile_selector(pattern: str) -> re.Pattern[str]:
22
+ """Compile a Node-style namespace selector where only ``*`` is special."""
23
+ return re.compile("^" + re.escape(pattern).replace(r"\*", ".*?") + "$")
24
+
25
+
26
+ def _parse_selectors(value: str | None) -> tuple[list[re.Pattern[str]], list[re.Pattern[str]]]:
27
+ enabled: list[re.Pattern[str]] = []
28
+ skipped: list[re.Pattern[str]] = []
29
+
30
+ for selector in _SELECTOR_SEPARATOR.split(value or ""):
31
+ if not selector:
32
+ continue
33
+
34
+ is_skip = selector.startswith("-")
35
+ pattern = selector[1:] if is_skip else selector
36
+ if not pattern:
37
+ continue
38
+
39
+ (skipped if is_skip else enabled).append(_compile_selector(pattern))
40
+
41
+ return enabled, skipped
42
+
43
+
44
+ def _matches_selector(pattern: re.Pattern[str], namespace: str) -> bool:
45
+ return pattern.fullmatch(namespace) is not None
46
+
47
+
48
+ def is_namespace_enabled(namespace: str, debug_value: str | None = None) -> bool:
49
+ """Return whether ``namespace`` is selected by ``debug_value`` or ``DEBUG``."""
50
+ enabled, skipped = _parse_selectors(
51
+ os.getenv("DEBUG") if debug_value is None else debug_value
52
+ )
53
+ return (
54
+ bool(enabled)
55
+ and not any(_matches_selector(pattern, namespace) for pattern in skipped)
56
+ and any(_matches_selector(pattern, namespace) for pattern in enabled)
57
+ )
58
+
59
+
60
+ def _namespace_color(namespace: str) -> int:
61
+ # Avoid Python's randomized hash so a namespace keeps its color between runs.
62
+ value = sum((index + 1) * ord(character) for index, character in enumerate(namespace))
63
+ return _COLOR_CODES[value % len(_COLOR_CODES)]
64
+
65
+
66
+ def _colors_enabled(stream: object) -> bool:
67
+ configured = os.getenv("DEBUG_COLORS")
68
+ if configured is not None:
69
+ return configured.strip().lower() not in _DISABLED_COLOR_VALUES
70
+ return bool(getattr(stream, "isatty", lambda: False)())
71
+
72
+
73
+ class HyperProbeLogger:
74
+ """A namespace logger controlled by the ``DEBUG`` environment variable."""
75
+
76
+ def __init__(self, namespace: str = "hyperprobe:agent"):
77
+ self.namespace = namespace
78
+ self.enabled = is_namespace_enabled(namespace)
79
+ self.color = _namespace_color(namespace)
80
+
81
+ def _prefix(self, stream: object) -> str:
82
+ if _colors_enabled(stream):
83
+ return f"\033[{self.color}m{self.namespace}\033[0m"
84
+ return self.namespace
85
+
86
+ def _write(self, message: str, stream: object) -> None:
87
+ print(f"{self._prefix(stream)} {message}", file=stream, flush=True)
88
+
89
+ def info(self, message: str) -> None:
90
+ if self.enabled:
91
+ self._write(message, sys.stdout)
92
+
93
+ def error(self, message: str) -> None:
94
+ if self.enabled:
95
+ self._write(message, sys.stderr)
96
+
97
+ def forceInfo(self, message: str) -> None:
98
+ """Write an informational message without applying the ``DEBUG`` gate."""
99
+ print(message, file=sys.stdout, flush=True)
100
+
101
+ def forceError(self, message: str) -> None:
102
+ """Write an error message without applying the ``DEBUG`` gate."""
103
+ print(message, file=sys.stderr, flush=True)
104
+
105
+
106
+ def get_logger(namespace: str) -> HyperProbeLogger:
107
+ """Return the process-wide logger for a namespace.
108
+
109
+ ``DEBUG`` is intentionally read when a namespace is first created, matching
110
+ the launch-time configuration behavior of the previous logger.
111
+ """
112
+ with _logger_cache_lock:
113
+ logger = _logger_cache.get(namespace)
114
+ if logger is None:
115
+ logger = HyperProbeLogger(namespace)
116
+ _logger_cache[namespace] = logger
117
+ return logger
118
+
119
+
120
+ # Existing imports continue to work and now use the primary agent namespace.
121
+ logger = get_logger("hyperprobe:agent")