visionscaper-pybase 0.4.1
Basic functionality for Python projects: logging, base class, and validation utilities.
License: MIT
Requires Python: >=3.8

# pybase

Basic functionality for Python projects: logging setup, a `Base` class with built-in logger, validation utilities, and exception logging helpers.

## Installation

```bash
pip install visionscaper-pybase
```

## Logging

### Setup

Call `setup_logging()` at application startup to configure the root logger:

```python
from basics.logging import setup_logging, get_logger

setup_logging()

logger = get_logger('my_module')
logger.info('Application started')
```

Output:

```
20260209-151557.123 INFO: my_module::<module>: Application started
```

### Getting a Logger

Use `get_logger()` to create named loggers. The default log level is `DEBUG`, but can be overridden:

```python
from basics.logging import get_logger

# Module-level logger (common pattern)
module_logger = get_logger(__name__)

# Logger with custom log level
import logging
quiet_logger = get_logger('noisy_lib', log_level=logging.WARNING)
```

### Log Format

The default format includes a timestamp prefix:

```
YYYYMMDD-HHMMSS.msecs LEVEL: name::funcName: message
```

Example:

```
20260209-151557.123 INFO: train::main: Starting training
20260209-151557.456 WARNING: train::main: Low memory
```

### Environment Variables

| Variable | Default | Description |
|---|---|---|
| `PYBASE_LOG_PID` | unset | Set to `1` to prefix log lines with the process ID (`[PID]`) |
| `PYBASE_NO_TIMESTAMP_LOG` | unset | Set to `1` to disable the timestamp prefix |
| `PYBASE_SUPPRESS_LEGACY_WARNINGS` | unset | Set to `1` to suppress the deprecation warning about auto-configuration |

Example with PID enabled:

```
[4521] 20260209-151557.123 INFO: train::main: Starting training
```

### Custom Format

You can pass a custom format string and date format to `setup_logging()`:

```python
from basics.logging import setup_logging

setup_logging(
    format_string='%(asctime)s %(levelname)s %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S',
)
```

### Advanced: Building Format Strings

Use `build_logging_format()` to programmatically construct format strings with optional timestamp and PID, independent of environment variables:

```python
from basics.logging import build_logging_format

fmt = build_logging_format(include_timestamp=True, include_pid=True)
```

### Legacy Compatibility

Importing `basics.logging` currently configures the root logger automatically via `logging.basicConfig()`. This ensures existing code continues to work, but will be removed in a future version. New code should call `setup_logging()` explicitly.

Set `PYBASE_SUPPRESS_LEGACY_WARNINGS=1` to suppress the deprecation warning.

## Base Class

The `Base` class provides a built-in logger for any class that inherits from it:

```python
from basics.base import Base

class MyProcessor(Base):

    def __init__(self, name, **kwargs):
        super().__init__(**kwargs)
        self._log.info(f'Processor created: {name}')

    def process(self, data):
        self._log.debug(f'Processing {len(data)} items')
        return [item * 2 for item in data]

proc = MyProcessor('example')
proc.process([1, 2, 3])
```

Output:

```
20260209-151557.123 INFO: MyProcessor::__init__: Processor created: example
20260209-151557.124 DEBUG: MyProcessor::process: Processing 3 items
```

The logger name defaults to the class name. Override it with `pybase_logger_name`:

```python
proc = MyProcessor('example', pybase_logger_name='custom_name')
# Logs will show: custom_name::__init__: ...
```

### Customizing the Logger

Subclasses can override `_pybase_get_logger_name()` to customize the logger name dynamically, or `_get_logger()` to use a different logger implementation entirely:

```python
class MyProcessor(Base):

    def __init__(self, name, **kwargs):
        self._name = name
        super().__init__(**kwargs)

    def _pybase_get_logger_name(self):
        return f'MyProcessor[{self._name}]'
```

## Exception Utilities

### log_exception

Log an exception with message, type, and traceback as a single atomic log record:

```python
from basics.logging import get_logger
from basics.logging_utils import log_exception

logger = get_logger(__name__)

try:
    result = 1 / 0
except Exception as e:
    log_exception(logger, 'Division failed', e)
```

### Exception Chain Summarization

`summarize_exception_chain()` produces a readable summary of chained exceptions:

```python
from basics.logging_utils import summarize_exception_chain

try:
    try:
        raise ConnectionError('timeout')
    except ConnectionError as ce:
        raise ValueError('failed to process request') from ce
except Exception as e:
    print(summarize_exception_chain(e))
```

Output:

```
 * ValueError: failed to process request
 * ConnectionError: timeout
```

For a single exception (no chain), it returns a simple one-liner:

```
KeyError: 'missing_key'
```

Use `include_traceback=True` to append the full traceback.

### get_all_exception_info

Get the class and message for each exception in the chain as a list of tuples:

```python
from basics.logging_utils import get_all_exception_info

try:
    ...
except Exception as e:
    for cls, message in get_all_exception_info(e):
        print(f'{cls.__name__}: {message}')
```

## Validation Utilities

Type-checking helpers available from `basics.base_utils` or `basics.validation_utils`:

```python
from basics.base_utils import is_number, is_dict, is_sequence, is_callable

is_number(42)           # True
is_dict({'a': 1})       # True
is_sequence([1, 2, 3])  # True
is_callable(print)      # True
```

| Function | Description |
|---|---|
| `is_number(n)` | Instance of `numbers.Number` |
| `is_mapping(d)` | Instance of `collections.abc.Mapping` |
| `is_dict(d)` | Instance of `dict` |
| `is_bool(d)` | Instance of `bool` |
| `is_class(c)` | Instance of `type` |
| `is_sequence(l)` | Instance of `collections.abc.Sequence` |
| `is_non_empty_string(s)` | `str` with length > 0 |
| `is_file(fname)` | `os.path.isfile()` check |
| `is_callable(func)` | `callable()` check |
| `is_int_sequence(seq)` | Sequence where all elements are numbers |
| `sequences_are_compatible(a, b)` | Two sequences with equal length |

## License

MIT License. See [LICENSE](LICENSE) for details.