# Gattopy

[![Tests](https://github.com/jeanpoolSM/gattopy/actions/workflows/tests.yml/badge.svg)](https://github.com/jeanpoolSM/gattopy/actions)
[![Python Versions](https://img.shields.io/pypi/pyversions/gattopy.svg)](https://pypi.org/project/gattopy/)
[![Coverage](https://img.shields.io/codecov/c/github/jeanpoolSM/gattopy.svg)](https://codecov.io/gh/jeanpoolSM/gattopy)
[![PyPI](https://img.shields.io/pypi/v/gattopy.svg)](https://pypi.org/project/gattopy/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

**[English](README.md)** | **[Español](README_ES.md)**

> A minimalist Python profiling library - simple, elegant, and powerful

**Gattopy** makes it easy to measure the performance of your Python code. With just a decorator or a context manager, you can track execution time and memory usage with clean, human-readable output.

## Features

- **Simple to use** - Just add a decorator or use a context manager
- **Time profiling** - Accurate execution time measurement
- **Memory tracking** - Monitor peak and current memory usage
- **Clean output** - Formatted results with detailed statistics
- **Multiple export formats** - JSON, CSV, and Markdown reports
- **Flexible** - Use as decorator or context manager
- **Benchmarking tools** - Compare and analyze function performance
- **Automatic accumulation** - Track multiple function calls automatically
- **Zero dependencies** - Uses only Python standard library

## Installation

```bash
pip install gattopy
```

## Quick Start

### Using the @gatto Decorator

The `@gatto` decorator is the recommended way to profile functions. It automatically accumulates statistics across multiple calls and provides convenient methods to access results.

```python
from gattopy import gatto

# Basic usage - automatic accumulation
@gatto
def process_data(n):
    return sum(range(n))

# Call the function multiple times
process_data(1000)
process_data(5000)
process_data(10000)

# Print accumulated statistics
process_data.print_summary()

# Export results
process_data.export_stats('results.json')
process_data.export_stats('results.csv')

# Get statistics programmatically
stats = process_data.get_stats()
total_calls = process_data.call_count()
```

#### @gatto Parameters

- **`silent`** (bool, default=False): When False, prints execution details for each call. Set to True for batch processing without console output.
- **`return_data`** (bool, default=False): When False, function returns only its result. When True, returns tuple (result, stats).
- **`accumulate`** (bool, default=True): When True, stores all execution statistics in memory for later analysis.
- **`show_args`** (bool, default=True): Display function arguments in output.
- **`track_memory`** (bool, default=True): Track memory usage during execution.

#### @gatto Methods

Decorated functions gain the following methods:

- **`print_summary()`**: Print aggregated statistics for all accumulated calls
- **`get_stats()`**: Return list of all accumulated statistics dictionaries
- **`get_summary()`**: Return summary string without printing
- **`export_stats(filepath)`**: Export statistics to JSON, CSV, or Markdown (auto-detected by extension)
- **`call_count()`**: Return number of times function has been called
- **`clear_stats()`**: Clear all accumulated statistics

#### Silent Mode Example

```python
@gatto(silent=True)
def compute(x):
    return x ** 2

# Run 100 times without console spam
for i in range(100):
    compute(i)

# View summary
compute.print_summary()
print(f"Total executions: {compute.call_count()}")
```

### Using Context Managers

Context managers are ideal for profiling code blocks without modifying function definitions.

```python
from gattopy import Profiler, TimeProfiler, MemoryProfiler

# Time profiling
with TimeProfiler("Data Loading") as profiler:
    data = load_large_dataset()

print(profiler.result)

# Memory profiling
with MemoryProfiler("Data Processing") as profiler:
    processed = process_data(data)

print(profiler.result)

# Combined profiling
with Profiler("Complete Pipeline") as profiler:
    result = run_pipeline()

print(f"Took {profiler.elapsed:.2f} seconds")
```

#### Context Manager Parameters

**Profiler(name, track_memory=True)**
- **`name`** (str): Description of the profiled operation
- **`track_memory`** (bool, default=True): Enable memory tracking

**TimeProfiler(name)**
- **`name`** (str): Description of the profiled operation

**MemoryProfiler(name)**
- **`name`** (str): Description of the profiled operation

## Reporting & Exporting

### Using Reporter for Manual Collection

The Reporter class allows manual collection and aggregation of profiling results.

```python
from gattopy import Reporter
from gattopy.core import Profiler

reporter = Reporter()

# Collect results from multiple profiled operations
with Profiler("Operation 1") as p1:
    result1 = sum(range(100000))

reporter.add_result(p1.result)

with Profiler("Operation 2") as p2:
    result2 = [x ** 2 for x in range(50000)]

reporter.add_result(p2.result)

# Print summary
reporter.print_summary()
```

### Export Formats

```python
# Export to JSON
reporter.save_json("profile_results.json")

# Export to CSV
reporter.save_csv("profile_results.csv")

# Export to Markdown
reporter.save_markdown("profile_results.md")

# Or use @gatto's built-in export
@gatto
def my_function():
    pass

my_function()
my_function.export_stats('results.json')  # Auto-detects format
```

## Benchmarking

Gattopy provides powerful benchmarking tools for performance analysis and function comparison.

### benchmark()

Run a function multiple times and collect detailed statistics.

```python
from gattopy import benchmark, format_time

def my_function():
    return sum(range(1000))

results = benchmark(
    my_function,
    iterations=1000,
    track_memory=True,
    include_iterations=False
)

print(f"Average: {format_time(results['average'])}")
print(f"Median: {format_time(results['median'])}")
print(f"Min: {format_time(results['min'])}")
print(f"Max: {format_time(results['max'])}")
print(f"P95: {format_time(results['p95'])}")
print(f"P99: {format_time(results['p99'])}")
print(f"Std Dev: {format_time(results['std_dev'])}")

if results.get('memory_peak'):
    print(f"Peak Memory: {results['memory_peak']:.2f} MB")
```

#### benchmark() Parameters

- **`func`** (callable): Function to benchmark
- **`iterations`** (int, default=100): Number of times to execute the function
- **`track_memory`** (bool, default=False): Track memory usage during execution
- **`include_iterations`** (bool, default=False): Include individual iteration data in results

#### benchmark() Return Value

Returns a dictionary with:
- `function_name`: Name of the benchmarked function
- `iterations`: Number of iterations executed
- `times`: List of all execution times (in seconds)
- `average`: Mean execution time
- `median`: Median execution time
- `min`: Minimum execution time
- `max`: Maximum execution time
- `std_dev`: Standard deviation
- `p95`: 95th percentile
- `p99`: 99th percentile
- `memory_peak`: Peak memory usage in MB (if track_memory=True)
- `memory_current`: Current memory usage in MB (if track_memory=True)
- `iteration_details`: List of per-iteration stats (if include_iterations=True)

### compare_functions()

Compare the performance of multiple functions.

```python
from gattopy import compare_functions

def list_comprehension():
    return [x ** 2 for x in range(1000)]

def map_function():
    return list(map(lambda x: x ** 2, range(1000)))

def generator():
    return list(x ** 2 for x in range(1000))

results = compare_functions(
    list_comprehension,
    map_function,
    generator,
    iterations=500,
    track_memory=True
)

for name, stats in results.items():
    print(f"{name}:")
    print(f"  Average: {format_time(stats['average'])}")
    print(f"  Memory: {stats.get('memory_peak', 0):.2f} MB")
```

#### compare_functions() Parameters

- **`*functions`**: Variable number of functions to compare
- **`iterations`** (int, default=100): Number of iterations per function
- **`track_memory`** (bool, default=False): Track memory usage
- **`include_iterations`** (bool, default=False): Include per-iteration data

#### compare_functions() Return Value

Returns a dictionary where keys are function names and values are benchmark results (same structure as benchmark()).

### Benchmarking Functions with Parameters

Use lambda functions or functools.partial:

```python
from functools import partial
from gattopy import benchmark

def process(data, multiplier):
    return [x * multiplier for x in data]

# Using lambda
results = benchmark(lambda: process([1, 2, 3], 10), iterations=1000)

# Using partial
results = benchmark(partial(process, [1, 2, 3], 10), iterations=1000)
```

## Utilities

### Formatting Functions

```python
from gattopy import format_time, format_bytes

# Format time (seconds to human-readable)
print(format_time(0.000001))    # "1.00 μs"
print(format_time(0.001))       # "1.00 ms"
print(format_time(1.5))         # "1.50 s"
print(format_time(65))          # "1m 5.0s"
print(format_time(3665))        # "1h 1m 5.0s"

# Format bytes
print(format_bytes(1024))       # "1.00 KB"
print(format_bytes(1048576))    # "1.00 MB"
print(format_bytes(1073741824)) # "1.00 GB"
```

### Aggregation

```python
from gattopy import aggregate_results

# Aggregate multiple ProfileResult objects
results = [result1, result2, result3]
aggregated = aggregate_results(results)

print(f"Average time: {aggregated['time']['average']}")
print(f"Max memory: {aggregated['memory']['max_peak']}")
print(f"Total calls: {aggregated['count']}")
```

## API Reference

### Decorators

#### @gatto()

Primary decorator for function profiling with automatic accumulation.

**Parameters:**
- `silent` (bool, default=False): Suppress per-call output
- `return_data` (bool, default=False): Return (result, stats) tuple instead of just result
- `accumulate` (bool, default=True): Store statistics for all calls
- `show_args` (bool, default=True): Display function arguments in output
- `track_memory` (bool, default=True): Track memory usage

**Added Methods:**
- `print_summary()`: Print aggregated statistics
- `get_stats()`: Get list of all statistics dictionaries
- `get_summary()`: Get summary string without printing
- `export_stats(filepath)`: Export to JSON/CSV/Markdown
- `call_count()`: Get number of calls
- `clear_stats()`: Clear accumulated data

### Context Managers

#### Profiler(name, track_memory=True)

Profile both time and memory usage.

**Parameters:**
- `name` (str): Description of the operation
- `track_memory` (bool, default=True): Enable memory tracking

**Attributes:**
- `elapsed`: Execution time in seconds
- `result`: ProfileResult object with all data

#### TimeProfiler(name)

Profile execution time only.

**Parameters:**
- `name` (str): Description of the operation

**Attributes:**
- `elapsed`: Execution time in seconds
- `result`: ProfileResult object

#### MemoryProfiler(name)

Profile memory usage only.

**Parameters:**
- `name` (str): Description of the operation

**Attributes:**
- `result`: ProfileResult object with memory data

### Functions

#### benchmark(func, iterations=100, track_memory=False, include_iterations=False)

Benchmark a function with detailed statistics.

**Parameters:**
- `func` (callable): Function to benchmark
- `iterations` (int, default=100): Number of executions
- `track_memory` (bool, default=False): Track memory usage
- `include_iterations` (bool, default=False): Include per-iteration details

**Returns:** Dictionary with statistical analysis

#### compare_functions(*functions, iterations=100, track_memory=False, include_iterations=False)

Compare multiple functions.

**Parameters:**
- `*functions`: Functions to compare
- `iterations` (int, default=100): Iterations per function
- `track_memory` (bool, default=False): Track memory usage
- `include_iterations` (bool, default=False): Include per-iteration details

**Returns:** Dictionary mapping function names to benchmark results

#### format_time(seconds)

Convert seconds to human-readable format.

**Parameters:**
- `seconds` (float): Time in seconds

**Returns:** Formatted string (e.g., "1.50 ms", "2m 30.5s")

#### format_bytes(bytes)

Convert bytes to human-readable format.

**Parameters:**
- `bytes` (int): Number of bytes

**Returns:** Formatted string (e.g., "1.50 MB", "2.30 GB")

#### aggregate_results(results)

Aggregate multiple ProfileResult objects.

**Parameters:**
- `results` (list): List of ProfileResult objects

**Returns:** Dictionary with aggregated statistics

### Classes

#### ProfileResult

Stores profiling results with metadata.

**Attributes:**
- `function_name` (str): Name of profiled function
- `execution_time` (float): Execution time in seconds
- `execution_time_formatted` (str): Human-readable time
- `memory_peak` (float): Peak memory in MB
- `memory_current` (float): Current memory in MB
- `timestamp` (str): ISO format timestamp
- `arguments` (dict): Function arguments used

**Methods:**
- `to_dict()`: Convert to dictionary
- `to_json()`: Convert to JSON string

#### Reporter

Collect and export multiple profiling results.

**Methods:**
- `add_result(result)`: Add a ProfileResult
- `print_summary()`: Print summary of all results
- `save_json(filepath)`: Export to JSON
- `save_csv(filepath)`: Export to CSV
- `save_markdown(filepath)`: Export to Markdown

## Usage Comparison

### When to Use Each Tool

**Use @gatto when:**
- You want to track a function across multiple calls
- You need automatic accumulation of statistics
- You want built-in export and summary methods
- You're profiling in production code

**Use benchmark() when:**
- You need statistical analysis (median, percentiles, std dev)
- You want to test a function a specific number of times
- You need per-iteration data for analysis
- You're doing performance testing

**Use compare_functions() when:**
- You need to compare different implementations
- You want side-by-side performance analysis
- You're optimizing algorithm choice

**Use context managers when:**
- You need to profile a code block, not a function
- You want one-time profiling
- You're debugging specific sections

## Development

### Setup Development Environment

```bash
# Clone the repository
git clone https://github.com/jeanpoolSM/gattopy.git
cd gattopy

# Quick setup (recommended)
./scripts/setup_dev.sh

# Or manual setup:
# Create virtual environment
python -m venv .venv
source .venv/bin/activate  # On Windows: .venv\Scripts\activate

# Install in development mode
pip install -e ".[dev]"

# Install pre-commit hooks
pre-commit install
```

### Running Tests

```bash
# Run all tests
pytest tests/

# Run with coverage
pytest --cov=gattopy tests/

# Run specific test file
pytest tests/test_core.py -v
```

### Code Quality

```bash
# Format code
black gattopy/

# Sort imports
isort gattopy/

# Lint code
flake8 gattopy/

# Type checking
mypy gattopy/
```

## Examples

Check the `examples/` directory for comprehensive examples:

### Basic Examples
- `gatto_ultra_simple.py` - Simple examples using @gatto decorator
- `gatto_super_simple.py` - Advanced decorator configuration
- `gatto_multiple_calls.py` - Automatic statistics accumulation
- `benchmark_examples.py` - Benchmarking and comparison examples
- `demo_colores.py` - Colorized console output demonstration

### Advanced Examples
- `advanced_sorting_analysis.py` - Complete sorting algorithm performance analysis
- `api_monitoring.py` - Real-world API performance monitoring simulation
- `gattopy_tutorial.ipynb` - Interactive Jupyter notebook tutorial

See `examples/README.md` for detailed documentation on each example.

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

1. Fork the repository
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
4. Push to the branch (`git push origin feature/AmazingFeature`)
5. Open a Pull Request

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

## Changelog

See [CHANGELOG.md](CHANGELOG.md) for a list of changes.

## Why "Gattopy"?

**Gatto** means "cat" in Italian, and cats are known for being quick and efficient - just like this profiling library. The "py" suffix indicates it's for Python.

---

Made by the Gattopy team
