Metadata-Version: 2.4
Name: dj-corekit
Version: 0.3.0
Summary: Small Django utilities built on Django core.
Author: Kitcat
License-Expression: MIT
Keywords: django,cache,toolkit
Classifier: Framework :: Django
Classifier: Framework :: Django :: 5.0
Classifier: Framework :: Django :: 5.1
Classifier: Framework :: Django :: 5.2
Classifier: Framework :: Django :: 6.0
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: Django>=5.0
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: pytest-django; extra == "dev"
Requires-Dist: ruff; extra == "dev"
Requires-Dist: tox; extra == "dev"
Dynamic: license-file

# dj-corekit

[![CI](https://github.com/himkit/dj-corekit/actions/workflows/ci.yml/badge.svg)](https://github.com/himkit/dj-corekit/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/dj-corekit.svg)](https://pypi.org/project/dj-corekit/)
[![Python](https://img.shields.io/pypi/pyversions/dj-corekit.svg)](https://pypi.org/project/dj-corekit/)
[![Django](https://img.shields.io/badge/django-%3E%3D5.0-092E20.svg)](https://www.djangoproject.com/)
[![License](https://img.shields.io/pypi/l/dj-corekit.svg)](https://github.com/himkit/dj-corekit/blob/main/LICENSE)

Small Django utilities built on Django core.

## Installation

```bash
python -m pip install dj-corekit
```

## Toolkit

### Cache

- Cache function views with Django-style default keys or caller-owned cache keys.
- Cache class-based views with `method_decorator` or `CachePageMixin`.
- Apply headers before storing responses.
- Set Cloudflare edge cache headers separately from browser cache headers.
- Cache `TemplateResponse` objects after rendering.
- Bypass caching per request with `request.do_not_cache` or `only_if`.

More utilities can be added here as new modules land.

## Cache

### Function Views

```python
from dj_corekit.cache import cache_page


@cache_page(
    timeout=300,
    headers=lambda request, response: {
        "X-Cache-Scope": "product",
        "Cache-Control": "public, max-age=300",
    },
)
def product_detail(request):
    ...
```

### Cloudflare Edge Cache

`cloudflare_cache_page` sets origin response headers for Cloudflare, but it does
not configure your Cloudflare zone. For HTML or other dynamic pages, Cloudflare
requires a Cache Rule that makes the route eligible for cache.

```python
from dj_corekit.cache import cloudflare_cache_page


@cloudflare_cache_page(
    timeout=300,
    key_func=lambda request: f"product:{request.GET.get('id')}",
    cloudflare={
        "browser_ttl": 0,
        "vary": ("CF-IPCountry", "CF-Device-Type"),
    },
    bypass_cache={
        "mode": "skip",
        "cookies": True,
        "headers": ("Authorization",),
    },
)
def product_detail(request):
    ...
```

Cloudflare behavior used by this decorator:

- `Cloudflare-CDN-Cache-Control: max-age=N` controls Cloudflare edge TTL without
  forwarding that header downstream. Cloudflare treats this as the
  Cloudflare-specific version of `CDN-Cache-Control`.
- `Cache-Control: public, max-age=N` controls browser-facing freshness. The
  default browser TTL is `0`, so browsers must revalidate while Cloudflare can
  still keep an edge copy.
- `cloudflare["vary"]` does two things: it adds the listed request headers to
  this package's Django cache key, and it emits `Vary` so Cloudflare can cache
  separate variants when the zone has matching Cache Rules Vary/custom cache key
  configuration.
- Dynamic content such as HTML is not cached by Cloudflare by default. Use a
  Cache Rule for routes that should be cached at the edge.

Cloudflare references:

- [`Cloudflare-CDN-Cache-Control`](https://developers.cloudflare.com/cache/concepts/cdn-cache-control/)
- [`Vary` and Cache Rules Vary](https://developers.cloudflare.com/cache/concepts/vary/)
- [Cache Rules settings](https://developers.cloudflare.com/cache/how-to/cache-rules/settings/)
- [Default cache behavior](https://developers.cloudflare.com/cache/concepts/default-cache-behavior/)
- [Get started with Cache](https://developers.cloudflare.com/cache/get-started/)

### Class-Based Views

Use Django's `method_decorator` when you want to keep caching outside the view
class:

```python
from django.utils.decorators import method_decorator
from django.views import View
from dj_corekit.cache import cache_page


@method_decorator(
    cache_page(timeout=300, key_func=lambda request: "product:list"),
    name="dispatch",
)
class ProductListView(View):
    ...
```

Use `CachePageMixin` when each view owns its cache settings:

```python
from django.views import View
from dj_corekit.cache import CachePageMixin


class ProductDetailView(CachePageMixin, View):
    cache_timeout = 300
    cache_headers = {"X-Cache-Scope": "product"}

    def get_cache_key(self, request):
        return f"product:{self.kwargs['pk']}"
```

Use `CloudflareCachePageMixin` for the same class-based pattern with
Cloudflare-aware headers:

```python
from django.views import View
from dj_corekit.cache import CloudflareCachePageMixin


class ProductDetailView(CloudflareCachePageMixin, View):
    cache_timeout = 300
    cache_cloudflare = {"vary": ("CF-IPCountry",)}
```

### Behavior

Without extension params, `cache_page` delegates to Django's
`django.views.decorators.cache.cache_page`, using `cache_alias` as Django's
`cache` argument. With `headers`, `only_if`, or `bypass_cache` but no
`key_func`, Django still owns cache keying and storage; dj-corekit only wraps
the view to apply headers or bypass caching. With `key_func`, the callable
returns the full cache key and dj-corekit stores the response through
`django.core.cache.caches[cache_alias]`.

POST responses are only cached when you provide `key_func`; include the request
method, body/form data, user, or any other response-changing input in that key.

Only `200` responses are cached. `TemplateResponse` objects are cached after
rendering. Set `request.do_not_cache = True` or return `False` from `only_if`
to bypass caching.

Request headers and cookies are not special by default. Set `bypass_cache` with
`mode="skip"` to skip cache when the request has any listed header or any
cookie.

## API

### `cache_page`

```python
cache_page(
    timeout,
    key_func=None,
    *,
    headers=None,
    cache_alias="default",
    only_if=None,
    bypass_cache=None,
)
```

### `cloudflare_cache_page`

```python
cloudflare_cache_page(
    timeout,
    key_func=None,
    *,
    cloudflare=None,
    headers=None,
    cache_alias="default",
    only_if=None,
    bypass_cache=None,
)
```

### `CachePageMixin`

```python
class CachePageMixin:
    cache_timeout = 300
    cache_key_func = None
    cache_headers = None
    cache_alias = "default"
    cache_only_if = None
    cache_bypass = None

    def get_cache_key(self, request):
        ...
```

### `CloudflareCachePageMixin`

```python
class CloudflareCachePageMixin(CachePageMixin):
    cache_cloudflare = None
```

- `timeout`: seconds, or a callable receiving the response.
- `key_func`: optional callable receiving the request and returning the full
  cache key.
- `headers`: dict or callable receiving `(request, response)` and returning a
  header dict. These headers are applied before the response is cached.
- `cloudflare`: optional dict for `cloudflare_cache_page`:
  - `edge_ttl`: `Cloudflare-CDN-Cache-Control` max-age. Defaults to `timeout`.
  - `browser_ttl`: browser `Cache-Control` max-age. Defaults to `0`.
  - `vary`: request header names to include in the cache key and `Vary`.
- `cache_cloudflare`: mixin equivalent of `cloudflare`.
- Cloudflare TTL values must resolve to non-negative integers.
- `cache_alias`: Django cache alias, default `"default"`.
- `only_if`: callable receiving the request. Return `False` to bypass cache.
- `bypass_cache`: optional dict. `{"mode": "skip", "cookies": True,
  "headers": ("Authorization",)}` skips cache when the request has any cookie
  or any listed header.

## Development

### Local Checks

```bash
python -m pip install -e ".[dev]"
python -m pytest
ruff check .
ruff format --check .
tox
```

### CI

GitHub Actions runs tests on Python 3.10, 3.11, 3.12, and 3.13. Ruff linting
and formatting checks run once on Python 3.12.
