Metadata-Version: 2.4
Name: sqlalchemy-activitylog
Version: 0.1.0
Summary: A comprehensive activity logging package for SQLAlchemy 2.0, inspired by Laravel Spatie Activity Log
Author: sqlalchemy-activitylog contributors
License: MIT
Project-URL: Homepage, https://github.com/yourusername/sqlalchemy-activitylog
Project-URL: Documentation, https://sqlalchemy-activitylog.readthedocs.io
Project-URL: Repository, https://github.com/yourusername/sqlalchemy-activitylog
Project-URL: Issues, https://github.com/yourusername/sqlalchemy-activitylog/issues
Keywords: sqlalchemy,activity-log,audit-trail,change-tracking,event-logging
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Database
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: sqlalchemy<3.0,>=2.0
Requires-Dist: typing-extensions>=4.5
Provides-Extra: fastapi
Requires-Dist: fastapi>=0.100; extra == "fastapi"
Requires-Dist: starlette>=0.20; extra == "fastapi"
Provides-Extra: flask
Requires-Dist: flask>=2.3; extra == "flask"
Provides-Extra: cli
Requires-Dist: typer>=0.9; extra == "cli"
Requires-Dist: click>=8.1; extra == "cli"
Provides-Extra: postgres
Requires-Dist: psycopg[binary]>=3.0; extra == "postgres"
Provides-Extra: mysql
Requires-Dist: pymysql>=1.1; extra == "mysql"
Provides-Extra: dev
Requires-Dist: pytest>=7.4; extra == "dev"
Requires-Dist: pytest-cov>=4.1; extra == "dev"
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
Requires-Dist: black>=23.0; extra == "dev"
Requires-Dist: ruff>=0.1; extra == "dev"
Requires-Dist: mypy>=1.5; extra == "dev"
Requires-Dist: sqlalchemy-stubs>=0.4; extra == "dev"
Provides-Extra: docs
Requires-Dist: mkdocs>=1.5; extra == "docs"
Requires-Dist: mkdocs-material>=9.0; extra == "docs"
Dynamic: license-file

# sqlalchemy-activitylog

[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
[![SQLAlchemy 2.0+](https://img.shields.io/badge/sqlalchemy-2.0+-blue.svg)](https://docs.sqlalchemy.org/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

A comprehensive activity logging package for SQLAlchemy 2.0, inspired by [Laravel Spatie Activity Log](https://github.com/spatie/laravel-activity-log) but adapted for the Python ecosystem.

Track model changes automatically, log manual activities, query audit trails, and maintain complete change history with a clean, typed API.

## Features

- ✅ **Automatic logging** of model create/update/delete events
- ✅ **Manual activity logging** with a fluent builder API
- ✅ **Dirty tracking** - log only changed attributes
- ✅ **Change history** - detailed before/after values in JSON
- ✅ **Subject & Causer support** - track what changed and who changed it
- ✅ **Batch operations** - group activities with shared batch UUID
- ✅ **Request metadata** - automatically capture request context
- ✅ **Query API** - fluent helpers for searching activities
- ✅ **Soft delete support** - pluggable soft delete strategies
- ✅ **FastAPI integration** - automatic middleware and user extraction
- ✅ **Flask integration** - request context integration
- ✅ **PostgreSQL, MySQL, SQLite support**
- ✅ **Fully typed** with Python 3.11+ syntax
- ✅ **Production-ready** with comprehensive test coverage

## Installation

```bash
pip install sqlalchemy-activitylog
```

With FastAPI support:

```bash
pip install sqlalchemy-activitylog[fastapi]
```

With Flask support:

```bash
pip install sqlalchemy-activitylog[flask]
```

With CLI tools:

```bash
pip install sqlalchemy-activitylog[cli]
```

With PostgreSQL support:

```bash
pip install sqlalchemy-activitylog[postgres]
```

## Quick Start

### 1. Define Your Models

```python
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import declarative_base
from sqlalchemy_activitylog import LogsActivity, LogOptions

Base = declarative_base()


class User(Base, LogsActivity):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True)
    name = Column(String)
    email = Column(String)

    __activitylog__ = (
        LogOptions.defaults()
        .use_log_name("users")
        .log_only(["name", "email"])
        .log_only_dirty()
        .dont_submit_empty_logs()
        .set_description(lambda event, subject, causer: f"User {event}")
    )
```

### 2. Create Activity Table

#### Option A: Using Alembic (Recommended)

Initialize the migration file using the Spatie-style CLI tool. It will automatically detect your project's current migration head and append the `activities` table creation seamlessly:

```bash
# 1. Publish the migration template into your alembic directory
activitylog init

# 2. Run the migration to apply changes to your database
activitylog migrate
```

_Note: You can also use the standard `alembic upgrade head` command after running `activitylog init` if you prefer._

#### Option B: Manual Creation (For projects not using Alembic)

If you do not use a migration manager, you can import the model and create the table directly through SQLAlchemy's metadata engine:

```python
from my_project.database import engine, Base
# Importing the model registers it with your local Base metadata
from sqlalchemy_activitylog.models import Activity

# Create the activities table immediately
Base.metadata.create_all(engine)
```

### 3. Initialize the Logger

```python
from sqlalchemy_activitylog import ActivityLogger, Activity, set_activity_logger

# Create logger
logger = ActivityLogger(Activity)
logger.set_causer_resolver(lambda: current_user)
logger.register_events()  # Register SQLAlchemy event listeners

# Set global logger
set_activity_logger(logger)
```

### 4. Use It!

**Automatic logging:**

```python
user = User(name="John Doe", email="john@example.com")
db.add(user)
db.commit()  # Automatically logs "create" event
```

**Manual logging:**

```python
from sqlalchemy_activitylog import activity

activity()
    .event("published")
    .performed_on(post)
    .caused_by(current_user)
    .with_properties({"status": "published"})
    .log("Post published to production", session=db)
```

**Query activities:**

```python
# Get all activities for a user
activities = db.query(Activity).filter_by(subject_type="User", subject_id=user.id).all()

# Get latest activities
latest = db.query(Activity).order_by(Activity.created_at.desc()).limit(10).all()

# Get activities in a batch
batch_activities = db.query(Activity).filter_by(batch_uuid="...").all()
```

**View changes:**

```python
activity = db.query(Activity).filter_by(event="update").first()

# Get formatted changes
changes = activity.changes
# {
#     "name": {"old": "John", "new": "Johnny"},
#     "email": {"old": "john@example.com", "new": "johnny@example.com"}
# }
```

## Core Concepts

### Activity Model

The `Activity` model stores audit trail records:

```python
class Activity:
    id: int                    # Primary key
    log_name: str             # Channel name (e.g., "users", "posts")
    event: str                # Event type (create, update, delete, custom)
    description: str          # Human-readable description
    subject_type: str         # Type of modified model
    subject_id: int           # ID of modified model
    causer_type: str          # Type of user who made change
    causer_id: int            # ID of user who made change
    properties: dict          # JSON change tracking
    batch_uuid: str           # Batch operation ID
    created_at: datetime      # Timestamp
```

### Properties Structure

Changes are stored in a single JSON column named `properties`:

**Create event:**

```json
{
  "attributes": {
    "name": "John Doe",
    "email": "john@example.com"
  }
}
```

**Update event:**

```json
{
  "attributes": {
    "name": "Johnny",
    "email": "johnny@example.com"
  },
  "old": {
    "name": "John Doe",
    "email": "john@example.com"
  }
}
```

**Delete event:**

```json
{
  "old": {
    "name": "John Doe",
    "email": "john@example.com"
  }
}
```

### LogOptions Builder

Configure logging behavior per model:

```python
__activitylog__ = (
    LogOptions.defaults()

    # Set log channel
    .use_log_name("users")

    # Configure attribute tracking
    .log_all()                              # Log all attributes (default)
    .log_only(["name", "email"])            # Log only these
    .log_except(["password", "token"])      # Log all except these
    .exclude_attributes(["updated_at"])     # Alias for log_except

    # Dirty tracking
    .log_only_dirty()                       # Only log changed attributes

    # Empty log handling
    .dont_submit_empty_logs()               # Don't log if nothing changed

    # Attribute filters
    .dont_log_if_attributes_changed_only(["updated_at"])  # Ignore timestamp-only changes

    # Descriptions
    .set_description(
        lambda event, subject, causer: f"User {subject.name} was {event}"
    )
)
```

### Manual Logging

Log custom activities with the fluent builder:

```python
from sqlalchemy_activitylog import activity

activity()
    .event("published")                          # Custom event name
    .performed_on(post)                          # What was affected
    .caused_by(current_user)                     # Who did it
    .with_properties({
        "ip": request.client.host,
        "status": "published"
    })                                           # Custom metadata
    .use_log("posts")                            # Log channel
    .tap(lambda a: setattr(a, "custom", "value"))  # Modify before save
    .log("Post published to production", session=db)  # Description + save
```

### Batch Operations

Group related activities:

```python
from sqlalchemy_activitylog import activity_batch

with activity_batch():
    # Create multiple records
    db.add(User(name="Alice"))
    db.add(User(name="Bob"))
    db.commit()

    # All activities share the same batch_uuid
```

## Integration Guides

### FastAPI

```python
from fastapi import FastAPI
from sqlalchemy_activitylog.integrations.fastapi import setup_activitylog
from sqlalchemy_activitylog import Activity

app = FastAPI()

async def get_current_user():
    # Your user extraction logic
    pass

# Setup activity logging
logger = await setup_activitylog(
    app,
    Activity,
    causer_resolver=get_current_user,
    enable_middleware=True,  # Capture request metadata
)
```

### Flask

```python
from flask import Flask
from sqlalchemy_activitylog.integrations.flask import setup_activitylog
from sqlalchemy_activitylog import Activity
from flask_login import current_user

app = Flask(__name__)

# Setup activity logging
logger = setup_activitylog(
    app,
    Activity,
    causer_resolver=lambda: current_user,
    enable_middleware=True,
)
```

### Configuration

```python
from sqlalchemy_activitylog import ActivityLogConfig, set_config

config = ActivityLogConfig(
    enabled=True,
    table_name="activities",
    database_schema=None,
    default_log_name="default",
    submit_empty_logs=False,
    store_request_metadata=True,
)

set_config(config)
```

## Advanced Usage

### Soft Delete Support

```python
from sqlalchemy_activitylog import SoftDeleteMixin

class User(Base, LogsActivity, SoftDeleteMixin):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True)
    name = Column(String)
    deleted_at = Column(DateTime, nullable=True)

    __activitylog__ = LogOptions.defaults().use_log_name("users")

# Soft delete
user.soft_delete()

# Restore
user.restore()
```

### Custom Activity Model

```python
from sqlalchemy_activitylog import create_activity_model

CustomActivity = create_activity_model(
    table_name="audit_logs",
    schema="public"
)
```

### Request Metadata

Automatically capture request context:

```python
# In FastAPI/Flask, request metadata is auto-captured:
# - request id
# - IP address
# - user agent
# - URL
# - HTTP method

# Access in activity.properties:
activity.properties["request"]["ip"]
activity.properties["request"]["user_agent"]
```

## API Reference

See [API.md](docs/API.md) for complete API documentation.

## Testing

Run tests with pytest:

```bash
pytest tests/
```

With coverage:

```bash
pytest tests/ --cov=sqlalchemy_activitylog --cov-report=html
```

## License

MIT License - see LICENSE file for details.

## Contributing

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

## Inspiration

This package is inspired by [Laravel Spatie Activity Log](https://github.com/spatie/laravel-activity-log) and adapted for SQLAlchemy 2.0 and the Python ecosystem.
