pyvitals-profiler 1.0.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Antoine
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,126 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyvitals-profiler
3
+ Version: 1.0.0
4
+ Summary: An intelligent, lightweight Wall/CPU time and memory profiler with beautiful terminal reports.
5
+ Author-email: Antoine M <projet24.apprentissage@gmail.com>
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Topic :: Software Development :: Debuggers
11
+ Requires-Python: >=3.8
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENCE
14
+ Requires-Dist: rich>=10.0.0
15
+ Dynamic: license-file
16
+
17
+ # PyVitals
18
+
19
+ [![PyPI version](https://img.shields.io/pypi/v/pyvitals-profiler.svg)](https://pypi.org/project/pyvitals-profiler/)
20
+ [![Python versions](https://img.shields.io/pypi/pyversions/pyvitals-profiler.svg)](https://pypi.org/project/pyvitals-profiler/)
21
+ [![CI Status](https://github.com/yourusername/pyvitals/actions/workflows/tests.yml/badge.svg)](https://github.com/yourusername/pyvitals/actions)
22
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
23
+
24
+ **PyVitals** is an intelligent, lightweight performance profiler for Python.
25
+ It tracks **Wall Time**, **CPU Time**, and **Peak Memory** usage with surgical precision, generating beautiful terminal reports and JSON history logs.
26
+
27
+ Whether you are debugging a slow API or hunting down a memory leak, PyVitals tells you exactly if your code is *CPU Bound* or *I/O Bound*.
28
+
29
+ ---
30
+
31
+ ## Features
32
+
33
+ - **Universal:** Works seamlessly with both synchronous (`def`) and asynchronous (`async def`) functions.
34
+ - **Flexible:** Use it as a `@decorator` or as a `with` context manager for specific code blocks.
35
+ - **Smart Diagnostics:** Automatically calculates CPU vs. Wall Time to detect I/O or CPU bottlenecks.
36
+ - **Threshold Alerts:** Set limits for execution time or memory and get visual warnings if exceeded.
37
+ - **Zero-Overhead Production:** Built-in Kill-Switch environment variable to disable tracking instantly in production.
38
+ - **CLI Analyzer:** Export your metrics to JSON and use the built-in CLI tool to track performance regressions over time.
39
+
40
+ ---
41
+
42
+ ## Installation
43
+
44
+ Install via `pip`:
45
+
46
+ ```bash
47
+ pip install pyvitals-profiler
48
+ ```
49
+
50
+ ## Quickstart
51
+ 1. As a Decorator (Sync & Async)
52
+ Simply add @track() to any function you want to profile.
53
+
54
+ ```Python
55
+ import asyncio
56
+ from pyvitals import track
57
+
58
+ @track(name="Database Query", max_time_s=1.5)
59
+ async def fetch_users():
60
+ await asyncio.sleep(1.0) # Simulating I/O Wait
61
+ return {"status": "success"}
62
+
63
+ asyncio.run(fetch_users())
64
+ ```
65
+
66
+ 2. As a Context Manager
67
+ If you only want to profile a specific loop without decorating the whole function, use the with statement.
68
+
69
+ ```Python
70
+ from pyvitals import track
71
+
72
+ def process_heavy_data():
73
+ print("Initializing...")
74
+
75
+ # Only profile this specific block
76
+ with track(name="Matrix Math", max_mem_mb=50):
77
+ data = [x**2 for x in range(1_000_000)]
78
+
79
+ print("Done!")
80
+
81
+ process_heavy_data()
82
+ ```
83
+
84
+ ## Exporting & Analyzing Data (CLI)
85
+ PyVitals allows you to save your performance history to detect regressions.
86
+
87
+ Step 1: Export to JSON
88
+ Use the export_to parameter. You can also use quiet=True to hide the terminal output if you are running the function thousands of times.
89
+
90
+ ```Python
91
+ from pyvitals import track
92
+
93
+ @track(export_to="vitals_history.json", quiet=True)
94
+ def background_task():
95
+ # ... task logic ...
96
+ pass
97
+ ```
98
+
99
+ Step 2: Use the CLI Analyzer
100
+ PyVitals comes with a built-in command-line tool. Point it to your JSON file to generate a rich historical trend analysis table:
101
+
102
+ ```Bash
103
+ pyvitals vitals_history.json
104
+ ```
105
+
106
+ The CLI will display the average Wall/CPU time, the maximum peak memory, the workload profile (I/O vs CPU), and highlight performance regressions (e.g., ▲ +12.5% slower).
107
+
108
+ ## Production Kill-Switch
109
+ You don't need to remove all your @track decorators before deploying to production.
110
+ Set the following environment variable to 0 to instantly disable PyVitals. It will bypass the tracking logic entirely, ensuring 0ms performance overhead.
111
+
112
+ ```Bash
113
+ export PYVITALS_ENABLED=0
114
+ ```
115
+
116
+ ## Contributing
117
+ Contributions are welcome! Please feel free to submit a Pull Request.
118
+
119
+ Clone the repository.
120
+
121
+ Install dependencies: pip install -e .
122
+
123
+ Run tests: pytest
124
+
125
+ ## License
126
+ This project is licensed under the MIT License - see the LICENSE file for details.
@@ -0,0 +1,110 @@
1
+ # PyVitals
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/pyvitals-profiler.svg)](https://pypi.org/project/pyvitals-profiler/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/pyvitals-profiler.svg)](https://pypi.org/project/pyvitals-profiler/)
5
+ [![CI Status](https://github.com/yourusername/pyvitals/actions/workflows/tests.yml/badge.svg)](https://github.com/yourusername/pyvitals/actions)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ **PyVitals** is an intelligent, lightweight performance profiler for Python.
9
+ It tracks **Wall Time**, **CPU Time**, and **Peak Memory** usage with surgical precision, generating beautiful terminal reports and JSON history logs.
10
+
11
+ Whether you are debugging a slow API or hunting down a memory leak, PyVitals tells you exactly if your code is *CPU Bound* or *I/O Bound*.
12
+
13
+ ---
14
+
15
+ ## Features
16
+
17
+ - **Universal:** Works seamlessly with both synchronous (`def`) and asynchronous (`async def`) functions.
18
+ - **Flexible:** Use it as a `@decorator` or as a `with` context manager for specific code blocks.
19
+ - **Smart Diagnostics:** Automatically calculates CPU vs. Wall Time to detect I/O or CPU bottlenecks.
20
+ - **Threshold Alerts:** Set limits for execution time or memory and get visual warnings if exceeded.
21
+ - **Zero-Overhead Production:** Built-in Kill-Switch environment variable to disable tracking instantly in production.
22
+ - **CLI Analyzer:** Export your metrics to JSON and use the built-in CLI tool to track performance regressions over time.
23
+
24
+ ---
25
+
26
+ ## Installation
27
+
28
+ Install via `pip`:
29
+
30
+ ```bash
31
+ pip install pyvitals-profiler
32
+ ```
33
+
34
+ ## Quickstart
35
+ 1. As a Decorator (Sync & Async)
36
+ Simply add @track() to any function you want to profile.
37
+
38
+ ```Python
39
+ import asyncio
40
+ from pyvitals import track
41
+
42
+ @track(name="Database Query", max_time_s=1.5)
43
+ async def fetch_users():
44
+ await asyncio.sleep(1.0) # Simulating I/O Wait
45
+ return {"status": "success"}
46
+
47
+ asyncio.run(fetch_users())
48
+ ```
49
+
50
+ 2. As a Context Manager
51
+ If you only want to profile a specific loop without decorating the whole function, use the with statement.
52
+
53
+ ```Python
54
+ from pyvitals import track
55
+
56
+ def process_heavy_data():
57
+ print("Initializing...")
58
+
59
+ # Only profile this specific block
60
+ with track(name="Matrix Math", max_mem_mb=50):
61
+ data = [x**2 for x in range(1_000_000)]
62
+
63
+ print("Done!")
64
+
65
+ process_heavy_data()
66
+ ```
67
+
68
+ ## Exporting & Analyzing Data (CLI)
69
+ PyVitals allows you to save your performance history to detect regressions.
70
+
71
+ Step 1: Export to JSON
72
+ Use the export_to parameter. You can also use quiet=True to hide the terminal output if you are running the function thousands of times.
73
+
74
+ ```Python
75
+ from pyvitals import track
76
+
77
+ @track(export_to="vitals_history.json", quiet=True)
78
+ def background_task():
79
+ # ... task logic ...
80
+ pass
81
+ ```
82
+
83
+ Step 2: Use the CLI Analyzer
84
+ PyVitals comes with a built-in command-line tool. Point it to your JSON file to generate a rich historical trend analysis table:
85
+
86
+ ```Bash
87
+ pyvitals vitals_history.json
88
+ ```
89
+
90
+ The CLI will display the average Wall/CPU time, the maximum peak memory, the workload profile (I/O vs CPU), and highlight performance regressions (e.g., ▲ +12.5% slower).
91
+
92
+ ## Production Kill-Switch
93
+ You don't need to remove all your @track decorators before deploying to production.
94
+ Set the following environment variable to 0 to instantly disable PyVitals. It will bypass the tracking logic entirely, ensuring 0ms performance overhead.
95
+
96
+ ```Bash
97
+ export PYVITALS_ENABLED=0
98
+ ```
99
+
100
+ ## Contributing
101
+ Contributions are welcome! Please feel free to submit a Pull Request.
102
+
103
+ Clone the repository.
104
+
105
+ Install dependencies: pip install -e .
106
+
107
+ Run tests: pytest
108
+
109
+ ## License
110
+ This project is licensed under the MIT License - see the LICENSE file for details.
@@ -0,0 +1,37 @@
1
+ # pyproject.toml
2
+
3
+ [build-system]
4
+ requires = ["setuptools>=61.0"]
5
+ build-backend = "setuptools.build_meta"
6
+
7
+ [project]
8
+ name = "pyvitals-profiler" # Choose a unique name on PyPI
9
+ version = "1.0.0"
10
+ authors = [
11
+ { name="Antoine M", email="projet24.apprentissage@gmail.com" },
12
+ ]
13
+ description = "An intelligent, lightweight Wall/CPU time and memory profiler with beautiful terminal reports."
14
+ readme = "README.md"
15
+ requires-python = ">=3.8"
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent",
20
+ "Intended Audience :: Developers",
21
+ "Topic :: Software Development :: Debuggers",
22
+ ]
23
+ dependencies = [
24
+ "rich>=10.0.0",
25
+ ]
26
+
27
+ [project.scripts]
28
+ # This creates a global terminal command automatically when the package is installed!
29
+ pyvitals = "pyvitals.analyzer:main"
30
+
31
+ [tool.ruff]
32
+ line-length = 88
33
+ target-version = "py38"
34
+
35
+ [tool.ruff.lint]
36
+ select = ["E", "F", "I"]
37
+ ignore = []
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ # src/pyvitals/__init__.py
2
+ """PyVitals: An intelligent lightweight performance profiler."""
3
+
4
+ from .profiler import track
5
+
6
+ __all__ = ["track"]
@@ -0,0 +1,133 @@
1
+ import argparse
2
+ import json
3
+ import os
4
+
5
+ from rich.console import Console
6
+ from rich.table import Table
7
+
8
+ console = Console()
9
+
10
+
11
+ def load_vitals_data(filepath: str) -> list:
12
+ """Loads and parses the JSON vitals data file."""
13
+ if not os.path.exists(filepath):
14
+ console.print(f"[bold red]Error:[/] File '{filepath}' not found.")
15
+ return []
16
+
17
+ try:
18
+ with open(filepath, "r", encoding="utf-8") as f:
19
+ return json.load(f)
20
+ except json.JSONDecodeError:
21
+ console.print(
22
+ f"[bold red]Error:[/] File '{filepath}' is not a valid JSON file."
23
+ )
24
+ return []
25
+
26
+
27
+ def analyze_and_display(data: list):
28
+ """
29
+ Analyzes the raw profiling data, groups it by function name,
30
+ calculates Wall/CPU statistics, and renders a rich terminal table.
31
+ """
32
+ if not data:
33
+ return
34
+
35
+ # Group data by function name
36
+ grouped_data = {}
37
+ for entry in data:
38
+ name = entry.get("name", "Unknown")
39
+ if name not in grouped_data:
40
+ grouped_data[name] = {"wall_times": [], "cpu_times": [], "peaks": []}
41
+
42
+ # Handle backward compatibility with Phase 2 data
43
+ wall = entry.get("wall_time_s", entry.get("execution_time_s", 0))
44
+ # If cpu_time_s is missing (old data), default to 0
45
+ cpu = entry.get("cpu_time_s", 0)
46
+
47
+ grouped_data[name]["wall_times"].append(wall)
48
+ grouped_data[name]["cpu_times"].append(cpu)
49
+ grouped_data[name]["peaks"].append(entry.get("peak_memory_mb", 0))
50
+
51
+ # Prepare the Rich Table
52
+ table = Table(title="Performance Analysis", border_style="blue")
53
+ table.add_column("Function Name", style="bold yellow")
54
+ table.add_column("Runs", justify="right", style="cyan")
55
+ table.add_column("Avg Wall Time", justify="right")
56
+ table.add_column("Avg CPU Time", justify="right", style="magenta")
57
+ table.add_column("Workload Profile", justify="center")
58
+ table.add_column("Trend (Wall)", justify="right")
59
+ table.add_column("Max Peak Mem", justify="right", style="green")
60
+
61
+ for name, metrics in grouped_data.items():
62
+ wall_times = metrics["wall_times"]
63
+ cpu_times = metrics["cpu_times"]
64
+ peaks = metrics["peaks"]
65
+
66
+ runs = len(wall_times)
67
+ avg_wall = sum(wall_times) / runs if runs else 0
68
+ avg_cpu = sum(cpu_times) / runs if runs else 0
69
+ last_wall = wall_times[-1]
70
+ max_peak = max(peaks) if peaks else 0
71
+
72
+ # Calculate performance trend percentage (based on Wall Time)
73
+ if avg_wall > 0:
74
+ diff_pct = ((last_wall - avg_wall) / avg_wall) * 100
75
+ else:
76
+ diff_pct = 0.0
77
+
78
+ # Format trend coloring
79
+ if diff_pct > 5.0:
80
+ trend_str = f"[bold red]▲ +{diff_pct:.1f}%[/bold red]"
81
+ elif diff_pct < -5.0:
82
+ trend_str = f"[bold green]▼ {diff_pct:.1f}%[/bold green]"
83
+ else:
84
+ trend_str = f"[dim]~ {diff_pct:.1f}%[/dim]"
85
+
86
+ # Determine Workload Profile based on averages
87
+ if avg_wall > 0 and avg_cpu > 0:
88
+ utilization = (avg_cpu / avg_wall) * 100
89
+ if utilization > 80:
90
+ workload = "[bold red]CPU Bound[/bold red]"
91
+ elif utilization < 20:
92
+ workload = "[bold blue]I/O Bound[/bold blue]"
93
+ else:
94
+ workload = "[bold yellow]Mixed[/bold yellow]"
95
+ elif avg_wall > 0 and avg_cpu == 0:
96
+ # Catch strict I/O bounds where CPU time might round to exactly 0
97
+ workload = "[bold blue]I/O Bound[/bold blue]"
98
+ else:
99
+ workload = "[dim]Unknown[/dim]"
100
+
101
+ table.add_row(
102
+ name,
103
+ str(runs),
104
+ f"{avg_wall:.4f} s",
105
+ f"{avg_cpu:.4f} s",
106
+ workload,
107
+ trend_str,
108
+ f"{max_peak:.4f} MB",
109
+ )
110
+
111
+ console.print(table)
112
+ console.print(
113
+ "[dim]Trend compares the last Wall Time to the historical average.[/dim]"
114
+ )
115
+
116
+
117
+ def main():
118
+ """CLI entry point."""
119
+ parser = argparse.ArgumentParser(description="Analyze PyVitals JSON export files.")
120
+ parser.add_argument(
121
+ "filepath",
122
+ type=str,
123
+ help="Path to the JSON vitals file (e.g., vitals_history.json)",
124
+ )
125
+
126
+ args = parser.parse_args()
127
+
128
+ data = load_vitals_data(args.filepath)
129
+ analyze_and_display(data)
130
+
131
+
132
+ if __name__ == "__main__":
133
+ main()
@@ -0,0 +1,202 @@
1
+ import functools
2
+ import inspect
3
+ import json
4
+ import os
5
+ import time
6
+ import tracemalloc
7
+ import warnings
8
+ from datetime import datetime
9
+
10
+ from rich.console import Console
11
+ from rich.panel import Panel
12
+ from rich.table import Table
13
+
14
+ console = Console()
15
+
16
+
17
+ class track:
18
+ """
19
+ Intelligent profiler for execution time (Wall/CPU) and memory usage.
20
+ Can be used as a decorator (@track) or a context manager (with track()).
21
+ """
22
+
23
+ def __init__(
24
+ self, name=None, max_time_s=None, max_mem_mb=None, export_to=None, quiet=False
25
+ ):
26
+ """
27
+ Initializes the tracker with optional thresholds, data export, and verbosity control.
28
+
29
+ Args:
30
+ name (str, optional): Custom name for the display panel.
31
+ max_time_s (float, optional): Maximum allowed Wall Time in seconds.
32
+ max_mem_mb (float, optional): Maximum allowed peak memory in megabytes.
33
+ export_to (str, optional): File path to export the results in JSON format.
34
+ quiet (bool, optional): If True, suppresses the terminal output.
35
+ """
36
+ self.name = name
37
+ self.max_time_s = max_time_s
38
+ self.max_mem_mb = max_mem_mb
39
+ self.export_to = export_to
40
+ self.quiet = quiet
41
+ self._start_time = None
42
+ self._start_cpu_time = None
43
+ self._is_enabled = os.getenv("PYVITALS_ENABLED", "1") != "0"
44
+
45
+ def _render_vitals(self, exec_time, cpu_time, current_mem, peak_mem, display_name):
46
+ """Generates and prints the visual terminal interface with threshold checks."""
47
+ peak_mb = peak_mem / (1024 * 1024)
48
+ current_mb = current_mem / (1024 * 1024)
49
+
50
+ time_color = "bold cyan"
51
+ mem_color = "bold yellow"
52
+ panel_border = "blue"
53
+
54
+ # Check execution time threshold
55
+ if self.max_time_s and exec_time > self.max_time_s:
56
+ time_color = "bold red"
57
+ panel_border = "red"
58
+ warnings.warn(
59
+ f"[{display_name}] Wall Time ({exec_time:.4f}s) exceeded limit ({self.max_time_s}s)!",
60
+ RuntimeWarning,
61
+ )
62
+
63
+ # Check peak memory threshold
64
+ if self.max_mem_mb and peak_mb > self.max_mem_mb:
65
+ mem_color = "bold red"
66
+ panel_border = "red"
67
+ warnings.warn(
68
+ f"[{display_name}] Peak memory ({peak_mb:.4f}MB) exceeded limit ({self.max_mem_mb}MB)!",
69
+ RuntimeWarning,
70
+ )
71
+
72
+ # Calculate CPU Utilization to determine the bottleneck type
73
+ cpu_utilization = (cpu_time / exec_time) * 100 if exec_time > 0 else 0
74
+ if cpu_utilization > 80:
75
+ bottleneck = "[bold red]CPU Bound (Heavy Math/Loops)[/bold red]"
76
+ elif cpu_utilization < 20:
77
+ bottleneck = "[bold blue]I/O Bound (Waiting/Network/Sleep)[/bold blue]"
78
+ else:
79
+ bottleneck = "[bold yellow]Mixed Workload[/bold yellow]"
80
+
81
+ # Table Construction (Using explicit spacing to avoid emoji-width bugs)
82
+ table = Table(show_header=False, box=None)
83
+ table.add_column(width=24)
84
+ table.add_column(justify="right")
85
+
86
+ table.add_row(
87
+ "Wall Time (Total)", f"[{time_color}]{exec_time:.4f} s[/{time_color}]"
88
+ )
89
+ table.add_row(
90
+ "CPU Time (Active)", f"[bold magenta]{cpu_time:.4f} s[/bold magenta]"
91
+ )
92
+ table.add_row("Workload Profile", bottleneck)
93
+ table.add_row("", "") # Blank row for visual spacing
94
+ table.add_row(
95
+ "Max Memory (Peak)", f"[{mem_color}]{peak_mb:.4f} MB[/{mem_color}]"
96
+ )
97
+ table.add_row(
98
+ "Remaining Memory", f"[bold green]{current_mb:.4f} MB[/bold green]"
99
+ )
100
+
101
+ # Panel Display
102
+ panel = Panel(
103
+ table,
104
+ title=f"Results : [bold yellow]{display_name}[/bold yellow]",
105
+ expand=False,
106
+ border_style=panel_border,
107
+ )
108
+ console.print(panel)
109
+
110
+ def _export_to_json(self, exec_time, cpu_time, current_mem, peak_mem, display_name):
111
+ """Appends the profiling metrics to a JSON file."""
112
+ if not self.export_to:
113
+ return
114
+
115
+ peak_mb = peak_mem / (1024 * 1024)
116
+ current_mb = current_mem / (1024 * 1024)
117
+
118
+ payload = {
119
+ "timestamp": datetime.now().isoformat(),
120
+ "name": display_name,
121
+ "wall_time_s": round(exec_time, 4),
122
+ "cpu_time_s": round(cpu_time, 4),
123
+ "peak_memory_mb": round(peak_mb, 4),
124
+ "remaining_memory_mb": round(current_mb, 4),
125
+ }
126
+
127
+ data = []
128
+ if os.path.exists(self.export_to):
129
+ try:
130
+ with open(self.export_to, "r", encoding="utf-8") as f:
131
+ data = json.load(f)
132
+ except (json.JSONDecodeError, IOError):
133
+ pass
134
+
135
+ data.append(payload)
136
+
137
+ with open(self.export_to, "w", encoding="utf-8") as f:
138
+ json.dump(data, f, indent=4)
139
+
140
+ # ---------------------------------------------------------
141
+ # CONTEXT MANAGER IMPLEMENTATION
142
+ # ---------------------------------------------------------
143
+ def __enter__(self):
144
+ """Triggered when entering a 'with' block."""
145
+ if not self._is_enabled:
146
+ return self
147
+
148
+ tracemalloc.start()
149
+ # Track both Wall Time and CPU Time
150
+ self._start_time = time.perf_counter()
151
+ self._start_cpu_time = time.process_time()
152
+ return self
153
+
154
+ def __exit__(self, exc_type, exc_val, exc_tb):
155
+ """Triggered when exiting a 'with' block."""
156
+ if not self._is_enabled:
157
+ return False
158
+
159
+ # Stop trackers
160
+ end_time = time.perf_counter()
161
+ end_cpu_time = time.process_time()
162
+ current_mem, peak_mem = tracemalloc.get_traced_memory()
163
+ tracemalloc.stop()
164
+
165
+ exec_time = end_time - self._start_time
166
+ cpu_time = end_cpu_time - self._start_cpu_time
167
+ display_name = self.name or "Code Block"
168
+
169
+ if not self.quiet:
170
+ self._render_vitals(
171
+ exec_time, cpu_time, current_mem, peak_mem, display_name
172
+ )
173
+
174
+ self._export_to_json(exec_time, cpu_time, current_mem, peak_mem, display_name)
175
+
176
+ return False
177
+
178
+ # ---------------------------------------------------------
179
+ # DECORATOR IMPLEMENTATION
180
+ # ---------------------------------------------------------
181
+ def __call__(self, func):
182
+ """Triggered when used as a decorator (@track)."""
183
+ if not self._is_enabled:
184
+ return func
185
+
186
+ display_name = self.name or f"{func.__name__}()"
187
+
188
+ @functools.wraps(func)
189
+ def sync_wrapper(*args, **kwargs):
190
+ self.name = display_name
191
+ with self:
192
+ return func(*args, **kwargs)
193
+
194
+ @functools.wraps(func)
195
+ async def async_wrapper(*args, **kwargs):
196
+ self.name = display_name
197
+ with self:
198
+ return await func(*args, **kwargs)
199
+
200
+ if inspect.iscoroutinefunction(func):
201
+ return async_wrapper
202
+ return sync_wrapper
@@ -0,0 +1,126 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyvitals-profiler
3
+ Version: 1.0.0
4
+ Summary: An intelligent, lightweight Wall/CPU time and memory profiler with beautiful terminal reports.
5
+ Author-email: Antoine M <projet24.apprentissage@gmail.com>
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Topic :: Software Development :: Debuggers
11
+ Requires-Python: >=3.8
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENCE
14
+ Requires-Dist: rich>=10.0.0
15
+ Dynamic: license-file
16
+
17
+ # PyVitals
18
+
19
+ [![PyPI version](https://img.shields.io/pypi/v/pyvitals-profiler.svg)](https://pypi.org/project/pyvitals-profiler/)
20
+ [![Python versions](https://img.shields.io/pypi/pyversions/pyvitals-profiler.svg)](https://pypi.org/project/pyvitals-profiler/)
21
+ [![CI Status](https://github.com/yourusername/pyvitals/actions/workflows/tests.yml/badge.svg)](https://github.com/yourusername/pyvitals/actions)
22
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
23
+
24
+ **PyVitals** is an intelligent, lightweight performance profiler for Python.
25
+ It tracks **Wall Time**, **CPU Time**, and **Peak Memory** usage with surgical precision, generating beautiful terminal reports and JSON history logs.
26
+
27
+ Whether you are debugging a slow API or hunting down a memory leak, PyVitals tells you exactly if your code is *CPU Bound* or *I/O Bound*.
28
+
29
+ ---
30
+
31
+ ## Features
32
+
33
+ - **Universal:** Works seamlessly with both synchronous (`def`) and asynchronous (`async def`) functions.
34
+ - **Flexible:** Use it as a `@decorator` or as a `with` context manager for specific code blocks.
35
+ - **Smart Diagnostics:** Automatically calculates CPU vs. Wall Time to detect I/O or CPU bottlenecks.
36
+ - **Threshold Alerts:** Set limits for execution time or memory and get visual warnings if exceeded.
37
+ - **Zero-Overhead Production:** Built-in Kill-Switch environment variable to disable tracking instantly in production.
38
+ - **CLI Analyzer:** Export your metrics to JSON and use the built-in CLI tool to track performance regressions over time.
39
+
40
+ ---
41
+
42
+ ## Installation
43
+
44
+ Install via `pip`:
45
+
46
+ ```bash
47
+ pip install pyvitals-profiler
48
+ ```
49
+
50
+ ## Quickstart
51
+ 1. As a Decorator (Sync & Async)
52
+ Simply add @track() to any function you want to profile.
53
+
54
+ ```Python
55
+ import asyncio
56
+ from pyvitals import track
57
+
58
+ @track(name="Database Query", max_time_s=1.5)
59
+ async def fetch_users():
60
+ await asyncio.sleep(1.0) # Simulating I/O Wait
61
+ return {"status": "success"}
62
+
63
+ asyncio.run(fetch_users())
64
+ ```
65
+
66
+ 2. As a Context Manager
67
+ If you only want to profile a specific loop without decorating the whole function, use the with statement.
68
+
69
+ ```Python
70
+ from pyvitals import track
71
+
72
+ def process_heavy_data():
73
+ print("Initializing...")
74
+
75
+ # Only profile this specific block
76
+ with track(name="Matrix Math", max_mem_mb=50):
77
+ data = [x**2 for x in range(1_000_000)]
78
+
79
+ print("Done!")
80
+
81
+ process_heavy_data()
82
+ ```
83
+
84
+ ## Exporting & Analyzing Data (CLI)
85
+ PyVitals allows you to save your performance history to detect regressions.
86
+
87
+ Step 1: Export to JSON
88
+ Use the export_to parameter. You can also use quiet=True to hide the terminal output if you are running the function thousands of times.
89
+
90
+ ```Python
91
+ from pyvitals import track
92
+
93
+ @track(export_to="vitals_history.json", quiet=True)
94
+ def background_task():
95
+ # ... task logic ...
96
+ pass
97
+ ```
98
+
99
+ Step 2: Use the CLI Analyzer
100
+ PyVitals comes with a built-in command-line tool. Point it to your JSON file to generate a rich historical trend analysis table:
101
+
102
+ ```Bash
103
+ pyvitals vitals_history.json
104
+ ```
105
+
106
+ The CLI will display the average Wall/CPU time, the maximum peak memory, the workload profile (I/O vs CPU), and highlight performance regressions (e.g., ▲ +12.5% slower).
107
+
108
+ ## Production Kill-Switch
109
+ You don't need to remove all your @track decorators before deploying to production.
110
+ Set the following environment variable to 0 to instantly disable PyVitals. It will bypass the tracking logic entirely, ensuring 0ms performance overhead.
111
+
112
+ ```Bash
113
+ export PYVITALS_ENABLED=0
114
+ ```
115
+
116
+ ## Contributing
117
+ Contributions are welcome! Please feel free to submit a Pull Request.
118
+
119
+ Clone the repository.
120
+
121
+ Install dependencies: pip install -e .
122
+
123
+ Run tests: pytest
124
+
125
+ ## License
126
+ This project is licensed under the MIT License - see the LICENSE file for details.
@@ -0,0 +1,13 @@
1
+ LICENCE
2
+ README.md
3
+ pyproject.toml
4
+ src/pyvitals/__init__.py
5
+ src/pyvitals/analyzer.py
6
+ src/pyvitals/profiler.py
7
+ src/pyvitals_profiler.egg-info/PKG-INFO
8
+ src/pyvitals_profiler.egg-info/SOURCES.txt
9
+ src/pyvitals_profiler.egg-info/dependency_links.txt
10
+ src/pyvitals_profiler.egg-info/entry_points.txt
11
+ src/pyvitals_profiler.egg-info/requires.txt
12
+ src/pyvitals_profiler.egg-info/top_level.txt
13
+ tests/test_profiler.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pyvitals = pyvitals.analyzer:main
@@ -0,0 +1,85 @@
1
+ import asyncio
2
+ import json
3
+
4
+ import pytest
5
+
6
+ from pyvitals import track
7
+
8
+
9
+ # ---------------------------------------------------------
10
+ # 1. Test Synchronous Decorator
11
+ # ---------------------------------------------------------
12
+ def test_sync_decorator_returns_correct_value():
13
+ @track(quiet=True)
14
+ def compute(a, b):
15
+ return a + b
16
+
17
+ result = compute(5, 7)
18
+ assert result == 12, "The decorator modified the function's return value!"
19
+
20
+
21
+ # ---------------------------------------------------------
22
+ # 2. Test Asynchronous Decorator
23
+ # ---------------------------------------------------------
24
+ @pytest.mark.asyncio
25
+ async def test_async_decorator_returns_correct_value():
26
+ @track(quiet=True)
27
+ async def fetch_data():
28
+ await asyncio.sleep(0.01)
29
+ return {"status": "ok"}
30
+
31
+ result = await fetch_data()
32
+ assert result["status"] == "ok", "The async decorator failed to return the value!"
33
+
34
+
35
+ # ---------------------------------------------------------
36
+ # 3. Test Context Manager
37
+ # ---------------------------------------------------------
38
+ def test_context_manager_executes_without_error():
39
+ data = []
40
+ with track(name="Test Loop", quiet=True):
41
+ for i in range(10):
42
+ data.append(i)
43
+
44
+ assert len(data) == 10, "Context manager blocked code execution!"
45
+
46
+
47
+ # ---------------------------------------------------------
48
+ # 4. Test JSON Export
49
+ # ---------------------------------------------------------
50
+ def test_json_export_appends_data(tmp_path):
51
+ # tmp_path is a built-in pytest fixture for temporary directories
52
+ export_file = tmp_path / "test_vitals.json"
53
+
54
+ @track(export_to=str(export_file), quiet=True)
55
+ def dummy_task():
56
+ pass
57
+
58
+ # Run twice
59
+ dummy_task()
60
+ dummy_task()
61
+
62
+ # Verify the file was created and contains exactly 2 records
63
+ assert export_file.exists(), "JSON export file was not created!"
64
+
65
+ with open(export_file, "r") as f:
66
+ data = json.load(f)
67
+
68
+ assert len(data) == 2, f"Expected 2 records in JSON, found {len(data)}"
69
+ assert "wall_time_s" in data[0], "Missing keys in JSON payload"
70
+
71
+
72
+ # ---------------------------------------------------------
73
+ # 5. Test Global Kill-Switch
74
+ # ---------------------------------------------------------
75
+ def test_kill_switch_disables_profiler(monkeypatch):
76
+ # Simulate setting the environment variable in production
77
+ monkeypatch.setenv("PYVITALS_ENABLED", "0")
78
+
79
+ @track(quiet=True)
80
+ def simple_func():
81
+ return "alive"
82
+
83
+ assert simple_func() == "alive"
84
+ # If the kill-switch works, tracemalloc won't crash if called outside,
85
+ # and the function should behave exactly as if undecorated.