Metadata-Version: 2.4
Name: cacheow
Version: 0.1.1
Summary: Development-only caching and mocking library for expensive Python functions
Project-URL: Homepage, https://code.dofi4ka.ru/dofi4ka/cacheow
Project-URL: Repository, https://code.dofi4ka.ru/dofi4ka/cacheow
Project-URL: Documentation, https://code.dofi4ka.ru/dofi4ka/cacheow#readme
Project-URL: Issues, https://code.dofi4ka.ru/dofi4ka/cacheow/issues
Project-URL: Changelog, https://code.dofi4ka.ru/dofi4ka/cacheow/releases
Author-email: dofi4ka <dofi4ka@yandex.ru>
Maintainer-email: dofi4ka <dofi4ka@yandex.ru>
License-Expression: MIT
License-File: LICENSE
Keywords: cache,caching,debugging,development,fastapi,mock,mocking,testing
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Debuggers
Classifier: Topic :: Software Development :: Testing
Classifier: Typing :: Typed
Requires-Python: >=3.13
Requires-Dist: fastapi>=0.115.0
Requires-Dist: pydantic>=2.13.4
Requires-Dist: uvicorn>=0.34.0
Provides-Extra: dev
Requires-Dist: httpx>=0.28.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.25.0; extra == 'dev'
Requires-Dist: pytest>=8.3.0; extra == 'dev'
Description-Content-Type: text/markdown

# cacheow

[![PyPI version](https://img.shields.io/pypi/v/cacheow.svg)](https://pypi.org/project/cacheow/)
[![Python versions](https://img.shields.io/pypi/pyversions/cacheow.svg)](https://pypi.org/project/cacheow/)

Development-only caching and mocking for expensive Python functions. Add one decorator, then control runtime behavior through persisted disk storage and a small web UI.

> **Development use only.** Do not use cacheow in production. When disabled, the decorator is a no-op with near-zero overhead.

## Requirements

- Python 3.13+

## Installation

```bash
pip install cacheow
```

## Quick start

Enable cacheow with an environment variable:

```bash
export CACHEOW_ENABLE=1
```

```python
from cacheow import cacheow

@cacheow()
def expensive_function(x: int) -> int:
    return x * x

expensive_function(3)  # recorded to disk in default mode
```

When `CACHEOW_ENABLE` is not `1`, `@cacheow()` returns the original function unchanged.

A control panel starts automatically at [http://127.0.0.1:8765/](http://127.0.0.1:8765/) (configurable via environment variables below).

## Runtime modes

Each decorated function has one mode, persisted in SQLite:

| Mode | Behavior |
|------|----------|
| `default` | Call the function normally; persist every outcome to disk. Never read cache. |
| `cache` | Return cached outcomes matching call arguments; call the function on miss. |
| `mock` | Never call the original; replay stored outcomes (weighted random by default). |

Always set mode using `function.cacheow_function_id` on the **wrapped** function. Do not look up functions by name in SQLite; stale registrations can remain after source edits.

Modes can also be changed via the web UI or JSON API (see below).

## Complete example

Save as `cacheow_example.py` and run with `python cacheow_example.py`:

```python
import asyncio
import os
import time

os.environ.setdefault("CACHEOW_ENABLE", "1")
os.environ.setdefault("CACHEOW_DATA_DIR", ".cacheow")
os.environ.setdefault("CACHEOW_START_SERVER", "1")

from cacheow import cacheow
from cacheow.manager import get_manager


@cacheow()
def sync_compute(n: int) -> int:
    print(f"computing {n}")
    time.sleep(0.1)
    return n * n


@cacheow()
async def async_compute(n: int) -> int:
    print(f"async computing {n}")
    await asyncio.sleep(0.1)
    return n + 10


def main() -> None:
    print("=== Default mode: record outcomes ===")
    print("sync_compute(3) =", sync_compute(3))
    print("async_compute(5) =", asyncio.run(async_compute(5)))

    manager = get_manager()
    assert manager is not None
    fn_id: str = sync_compute.cacheow_function_id  # type: ignore[attr-defined]

    print("\n=== Cache mode: replay from disk ===")
    manager.db.set_mode(fn_id, "cache")
    start = time.perf_counter()
    print(
        "sync_compute(3) cached =",
        sync_compute(3),
        f"in {(time.perf_counter() - start) * 1000:.1f}ms",
    )

    print("\n=== Mock mode: never call original ===")
    manager.db.set_mode(fn_id, "mock")
    print("sync_compute(999) mocked =", sync_compute(999))

    host = manager.settings.server_host
    port = manager.settings.server_port
    print(f"\nControl panel: http://{host}:{port}/")
    print(f"Data directory: {manager.settings.data_dir}")


if __name__ == "__main__":
    main()
```

Expected output (approximate):

```text
=== Default mode: record outcomes ===
computing 3
sync_compute(3) = 9
async computing 5
async_compute(5) = 15

=== Cache mode: replay from disk ===
sync_compute(3) cached = 9 in 0.5ms

=== Mock mode: never call original ===
sync_compute(999) mocked = 9

Control panel: http://127.0.0.1:8765/
Data directory: /path/to/.cacheow
```

In cache and mock modes, `computing ...` is not printed — the original function is not called.

## Environment variables

| Variable | Default | Description |
|----------|---------|-------------|
| `CACHEOW_ENABLE` | unset | Set to `1` to enable cacheow |
| `CACHEOW_DATA_DIR` | `.cacheow` | Persistence directory |
| `CACHEOW_SERVER_HOST` | `127.0.0.1` | HTTP server bind address |
| `CACHEOW_SERVER_PORT` | `8765` | HTTP server port |
| `CACHEOW_START_SERVER` | `1` | Set to `0` to disable auto-start |

## Supported function types

- Sync and async functions
- Sync and async generators
- Functions that raise exceptions
- Arbitrary return types (JSON-first serialization with pickle fallback)

## On-disk layout

```text
.cacheow/
  cacheow.db              # SQLite: metadata, modes, stats, outcome index
  functions/
    <function-id>/
      outcomes/
        <outcome-id>.json # Human-inspectable outcome payloads
```

SQLite stores registration, modes, configuration, stats, and an index of outcome files. JSON files store the actual return values, exceptions, and generator sequences so outcomes stay easy to inspect, diff, and edit manually.

## HTTP API

| Endpoint | Description |
|----------|-------------|
| `GET /api/functions` | List all decorated functions |
| `GET /api/functions/{id}` | Function detail, outcomes, stats |
| `POST /api/functions/{id}/mode` | Set mode (`default`, `cache`, `mock`) |
| `POST /api/functions/{id}/latency` | Set artificial delay (ms) |
| `POST /api/functions/{id}/mock-outcomes` | Configure weighted mock selection |
| `POST /api/functions/{id}/outcomes/manual` | Add manual return/exception outcome |
| `POST /api/functions/{id}/rebuild-index` | Rebuild outcome index from disk |

HTML UI: `/` and `/ui/functions/{id}`.

## Safety

In `default` mode, persistence or stats failures log a warning and **never** prevent the wrapped function from returning its result. In `cache` mode, cache read failures fall back to executing the original function when safe.

## Public API

```python
from cacheow import cacheow
```

That is the intended user-facing entry point. Wrapped functions expose `cacheow_function_id` for runtime control.
