Skip to main content

django-icv-core

CI PyPI version Python versions Django versions License: MIT

django-icv-core is the foundation layer for the ICV Django ecosystem. It gives every model in your project a UUID primary key, auto-managed timestamps, and optional soft-delete behaviour, with zero boilerplate. Add the optional audit subsystem and you get immutable event logs, admin activity tracking, and system alerts with a single setting.

Use it as a standalone package or as the base for other django-icv-* packages.


Features

  • BaseModel: UUID primary key (v4 by default, v7 opt-in) and auto-managed created_at/updated_at timestamps
  • SoftDeleteModel: Safe record removal with soft_delete() and restore(); default manager excludes deleted records automatically
  • SoftDeleteManager / SoftDeleteQuerySet: .active(), .deleted(), .with_deleted() on every soft-delete model
  • CurrentUserMiddleware: Async-safe (contextvars-backed) request user for automatic created_by/updated_by population
  • Audit subsystem (opt-in via ICV_CORE_AUDIT_ENABLED): Immutable AuditEntry, AdminActivityLog, SystemAlert, AuditMixin, @audited decorator, and management commands
  • Template tags: cents_to_currency, cents_to_amount, time_since_short
  • Django system checks: Configuration validation at startup
  • Test utilities: icv_core.testing provides factory-boy factories and pytest fixtures for consuming projects

Requirements

  • Python 3.11+
  • Django 5.2 or 6.0

Installation

pip install django-icv-core

Add icv_core to INSTALLED_APPS:

INSTALLED_APPS = [
    # ...
    "icv_core",
]

Run migrations:

python manage.py migrate

Note: The audit subsystem tables are only created when ICV_CORE_AUDIT_ENABLED = True. Add django.contrib.contenttypes to INSTALLED_APPS before enabling audit.


Quick Start

BaseModel: UUID primary keys and timestamps

Inherit from BaseModel and every record gets a UUID primary key and automatic timestamps. No extra fields to define.

from django.db import models
from icv_core.models import BaseModel


class Article(BaseModel):
    title = models.CharField(max_length=255)
    body = models.TextField()

    class Meta(BaseModel.Meta):
        verbose_name_plural = "articles"
article = Article.objects.create(title="Hello, world", body="...")

article.id          # UUID4: e.g. '3f2504e0-4f89-11d3-9a0c-0305e82c3301'
article.created_at  # datetime: set on creation, never changes
article.updated_at  # datetime: updated automatically on every save

UUID version: project-wide and per-model

By default the primary key is a random UUID v4. Set ICV_CORE_UUID_VERSION = 7 to make every BaseModel pk a time-sorted UUID v7 (RFC 9562, better index locality on high-write tables; note v7 encodes the row's creation timestamp).

The version is a runtime choice, it is never baked into migrations (the pk always freezes as UUIDField(default=uuid.uuid4)), so changing it generates no migration and affects only rows created afterwards.

To pin a single table to a version regardless of the project default, override its pk with VersionedUUIDField(uuid_version=...). Opt a high-write table into v7 without changing the default, or force v4 on a table whose id is public and must not leak a timestamp:

from icv_core.models import BaseModel
from icv_core.models.base import VersionedUUIDField


class SiteEvent(BaseModel):
    # time-sorted pk for this high-write table only; the project default is unchanged
    id = VersionedUUIDField(primary_key=True, editable=False, uuid_version=7)

The uuid_version override is runtime-only and does not appear in the field's migration state, so pinning a version produces no migration.


SoftDeleteModel: safe record removal

Records are never hard-deleted by default. soft_delete() sets is_active=False and records the timestamp. The default manager silently excludes deleted records from all queries.

from icv_core.models import SoftDeleteModel


class Subscription(SoftDeleteModel):
    plan = models.CharField(max_length=50)
    customer_email = models.EmailField()
sub = Subscription.objects.create(plan="pro", customer_email="user@example.com")

# Soft-delete: hides the record from default queries
sub.soft_delete()
sub.is_active   # False
sub.deleted_at  # datetime

# Default manager returns active records only
Subscription.objects.all()          # excludes deleted records
Subscription.objects.active()       # same as above: explicit alias

# Access deleted or all records when needed
Subscription.objects.deleted()      # deleted records only
Subscription.objects.with_deleted() # everything
Subscription.all_objects.all()      # raw manager: no filtering applied

# Restore
sub.restore()
sub.is_active   # True
sub.deleted_at  # None

Hard deletion is blocked unless you explicitly allow it:

# Raises ProtectedError by default
sub.delete()

# Permanent removal: use deliberately
sub.hard_delete()

# Or allow .delete() project-wide
ICV_CORE_ALLOW_HARD_DELETE = True

CurrentUserMiddleware: automatic created_by/updated_by

Add the middleware to make the current request user available to models without passing it through every service call. The user is stored in a contextvars.ContextVar, so concurrent requests handled on the same thread under ASGI (async views, or sync_to_async-bridged sync views) each see their own value; a request never observes another concurrently-running request's user.

# settings.py
MIDDLEWARE = [
    # ...
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "icv_core.middleware.CurrentUserMiddleware",  # must come after AuthenticationMiddleware
    # ...
]

ICV_CORE_TRACK_CREATED_BY = True
from icv_core.middleware import get_current_user

user = get_current_user()  # returns None outside of a request context

Audit subsystem

Enable the audit subsystem in settings:

ICV_CORE_AUDIT_ENABLED = True

AuditEntry: an immutable record of a system event. Raises ImmutableRecordError on update and ProtectedError on delete.

from icv_core.audit.services import log_event
from icv_core.audit.models import AuditEntry

# Record a security event
log_event(
    event_type=AuditEntry.EventType.SECURITY,
    action=AuditEntry.Action.PERMISSION_DENIED,
    user=request.user,
    description="Attempted access to restricted resource.",
    metadata={"path": request.path},
)

AuditMixin: add to any model to track CREATE, UPDATE, and DELETE automatically:

from icv_core.audit.mixins import AuditMixin
from icv_core.models import BaseModel


class Contract(AuditMixin, BaseModel):
    title = models.CharField(max_length=255)
    value = models.DecimalField(max_digits=10, decimal_places=2)

@audited decorator: wrap a view or service function to record its execution:

from icv_core.audit.decorators import audited
from icv_core.audit.models import AuditEntry


@audited(event_type=AuditEntry.EventType.DATA, action=AuditEntry.Action.DELETE)
def cancel_membership(user, membership):
    membership.soft_delete()

SystemAlert: raise and resolve operational alerts:

from icv_core.audit.services import raise_alert, resolve_alert
from icv_core.audit.models import SystemAlert

alert = raise_alert(
    alert_type=SystemAlert.AlertType.PAYMENT,
    severity="error",
    title="Payment processor unreachable",
    message="Stripe API returned 503 for the last 5 minutes.",
)

# Later, once resolved
resolve_alert(alert, resolved_by=request.user, notes="Stripe incident resolved.")

Tenancy (deprecated)

icv_core.tenancy (TenantAwareMixin, TenantOwnedMixin, TenantScopedManager, and the get/set/clear_current_tenant context helpers) is deprecated and will be removed in icv-core 1.0.0 per ADR-025 (ruling T3). Subclassing either mixin now emits a DeprecationWarning.

Use django-boundary instead. It provides row-level isolation backed by PostgreSQL Row Level Security, automatic query filtering, and a configurable tenant FK name.

Deprecated (icv_core.tenancy) Replacement (django-boundary)
TenantAwareMixin (FK on_delete=PROTECT) boundary.models.TenantModel
TenantOwnedMixin (FK on_delete=CASCADE) boundary.models.TenantModel (CASCADE is the default)
TenantScopedManager (explicit .for_tenant()) boundary.models.TenantManager (automatic filtering)
get_current_tenant() boundary.context.TenantContext.get()
set_current_tenant() boundary.context.TenantContext.set()
clear_current_tenant() boundary.context.TenantContext.clear()
tenant_context() boundary.context.TenantContext.using()

Template tags

{% load icv_core %}

{# Format pence/cents as a currency string #}
{{ order.total_pence|cents_to_currency:"GBP" }}  {# £35.00 #}
{{ order.total_pence|cents_to_currency:"USD" }}  {# $35.00 #}

{# Raw decimal amount without currency symbol #}
{{ order.total_pence|cents_to_amount }}           {# 35.00 #}

{# Short human-readable relative time #}
{{ comment.created_at|time_since_short }}         {# 2h ago / 3d ago / just now #}

Settings reference

All settings use the ICV_CORE_ prefix. Every setting has a sensible default: only override what you need.

Core

Setting Default Description
ICV_CORE_UUID_VERSION 4 UUID version for primary keys. 4 = random; 7 = time-sorted (requires Python 3.12+)
ICV_CORE_ALLOW_HARD_DELETE False When True, .delete() performs a hard delete on SoftDeleteModel instead of raising ProtectedError
ICV_CORE_TRACK_CREATED_BY False Enable created_by/updated_by tracking. Requires CurrentUserMiddleware
ICV_CORE_DEFAULT_ORDERING "-created_at" Default ordering applied to BaseModel subclasses

Audit

Setting Default Description
ICV_CORE_AUDIT_ENABLED False Master switch. No tables are created and no signals connect when False
ICV_CORE_AUDIT_RETENTION_DAYS 365 Days before audit entries are eligible for archival
ICV_CORE_AUDIT_EXCLUDE_MODELS [] Models excluded from AuditMixin auto-tracking. Format: ["app_label.ModelName"]
ICV_CORE_AUDIT_TRACK_FIELD_CHANGES True Capture old and new field values on UPDATE
ICV_CORE_AUDIT_CAPTURE_IP True Record the request IP address in audit entries
ICV_CORE_AUDIT_CAPTURE_USER_AGENT True Record the request user agent in audit entries
ICV_CORE_AUDIT_AUTO_MODEL_TRACKING False Automatically log CREATE/UPDATE/DELETE on all BaseModel subclasses
ICV_CORE_AUDIT_ALERT_SEVERITY_LEVELS ["info", "warning", "error", "critical"] Available severity levels for SystemAlert

Management commands

Command Description
icv_core_check Validate package configuration and emit any warnings
icv_core_audit_archive Archive audit entries older than ICV_CORE_AUDIT_RETENTION_DAYS
icv_core_audit_stats Print a summary of audit entry counts by event type and action

Testing utilities

icv_core.testing provides factory-boy factories and pytest fixtures for use in your own project's test suite.

# In your tests
from icv_core.testing.factories import AuditEntryFactory, SystemAlertFactory

entry = AuditEntryFactory(action="CREATE", event_type="DATA")
alert = SystemAlertFactory(severity="critical", alert_type="payment")

Import the included pytest fixtures by adding icv_core.testing.fixtures to your conftest.py:

# conftest.py
pytest_plugins = ["icv_core.testing.fixtures"]

Signals

icv-core emits the following signals. Connect to them from your own apps for loose coupling.

Signal Sent when
icv_core.signals.pre_soft_delete Before a record is soft-deleted
icv_core.signals.post_soft_delete After a record is soft-deleted
icv_core.signals.pre_restore Before a soft-deleted record is restored
icv_core.signals.post_restore After a soft-deleted record is restored
icv_core.audit.signals.audit_entry_created After an AuditEntry is written
icv_core.audit.signals.system_alert_raised After a SystemAlert is raised
icv_core.audit.signals.system_alert_resolved After a SystemAlert is resolved

Licence

MIT: see LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_icv_core-0.4.5.tar.gz (63.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_icv_core-0.4.5-py3-none-any.whl (50.7 kB view details)

Uploaded Python 3

File details

Details for the file django_icv_core-0.4.5.tar.gz.

File metadata

  • Download URL: django_icv_core-0.4.5.tar.gz
  • Upload date:
  • Size: 63.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for django_icv_core-0.4.5.tar.gz
Algorithm Hash digest
SHA256 2c024fc66a6e0ad3d24819d4c33d0268eb434b16a3df38e36e5bd94fe300e5c4
MD5 b64a4337c2f505813da495a00149ec1b
BLAKE2b-256 2a06fd77beadf3543734249cb7a93f841e07f3841826e50f007a0054b6004427

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_icv_core-0.4.5.tar.gz:

Publisher: publish.yml on icvoss/django-icv-core

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file django_icv_core-0.4.5-py3-none-any.whl.

File metadata

File hashes

Hashes for django_icv_core-0.4.5-py3-none-any.whl
Algorithm Hash digest
SHA256 6a97849c6fb95ea28759aed0c5f93ed46f503923685e370dbe699f056cf07cbb
MD5 4389ff5e91714e00cfa92989ce9fa63d
BLAKE2b-256 6270f00c30804b87e655d5080fe3ba75bc1744d076d9e6dd488647f04621923f

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_icv_core-0.4.5-py3-none-any.whl:

Publisher: publish.yml on icvoss/django-icv-core

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.5.0

2 files

This release

0.4.5 This release

2 files

0.4.4

2 files

0.4.3

2 files

0.4.2

2 files

0.4.1

2 files

0.4.0

2 files

0.3.0

2 files

0.2.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page