Skip to content

Expose a minimal public extension API for metadata-driven model packages #26

@ebrake

Description

@ebrake

I’d like to propose a narrow public extension surface for model-level packages, without introducing a full plugin framework.

The goal is to expose just enough public API for metadata-driven extensions / packages like oxyde-admin to work cleanly, while deferring broader questions like queryset mutation observers or a general event system.

Why this would help

Oxyde already preserves unknown Meta attributes in _db_meta.extra, which makes metadata-driven extensions feel like a natural fit.

What is still missing is a small public surface for packages to:

  • act once a model is fully finalized
  • serialize model instances into DB-relevant payloads
  • participate in the same DB/transaction context as the original model hook

Without that, extension packages depend on private internals, duplicate ORM logic, or rely on import-order/bootstrap workarounds.

Proposed public API

This proposal is intentionally limited to 3 additions.

1. Add a public on_model_finalized(...) hook

A callback registry for fully finalized table models.

Example:

from oxyde.models import on_model_finalized

def my_extension(model: type[Model]) -> None:
    if not model._db_meta.extra.get("my_feature"):
        return
    # inspect model metadata, register related objects, etc.

on_model_finalized(my_extension)
  • runs only after FK resolution, field metadata parsing, PK caching, and column type caching are complete
  • runs once per finalized table model
  • can replay already-finalized models if registered late
  • is safe for packages that need to inspect model metadata at startup

This would provide a public alternative to relying on internal model finalization behavior.

2. Add a public DB payload serializer

Expose a stable public serializer for DB-relevant model payloads.

Example:

from oxyde.models import dump_db_payload

payload = dump_db_payload(instance)
payload = dump_db_payload(instance, include_none=True)
payload = dump_db_payload(instance, fields={"name", "status"}, include_none=True)

A minimal initial shape could be:

dump_db_payload(instance, *, include_none=False, fields=None)

Expected behavior:

  • excludes virtual relation fields and computed fields
  • handles FK fields consistently
  • works for both insert-style payloads and field-scoped updates
  • returns field-keyed data

This would give package authors a stable alternative to private serializer helpers.

3. Pass using / client into existing lifecycle hooks

Extend the existing hooks so related writes can participate in the same execution context.

Example:

async def pre_save(
    self,
    *,
    is_create: bool,
    update_fields: set[str] | None = None,
    using: str | None = None,
    client: SupportsExecute | None = None,
) -> None: ...

async def post_save(
    self,
    *,
    is_create: bool,
    update_fields: set[str] | None = None,
    using: str | None = None,
    client: SupportsExecute | None = None,
) -> None: ...

async def pre_delete(
    self,
    *,
    using: str | None = None,
    client: SupportsExecute | None = None,
) -> None: ...

async def post_delete(
    self,
    *,
    using: str | None = None,
    client: SupportsExecute | None = None,
) -> None: ...

Expected behavior:

  • hooks can tell which DB alias is active
  • hooks can use the active transaction/client for related writes
  • multi-DB and transactional extension logic becomes straightforward
  • the existing hook model stays intact, with minimal API expansion

What this proposal is not asking for

This proposal intentionally does not ask for:

  • a general plugin framework
  • a signal/event bus
  • queryset mutation observers
  • bulk mutation interception
  • a broad new extension architecture

Those may be useful later, but this seems like the smallest useful public contract.

Why this feels like the right scope

These 3 additions would let package authors build a first generation of metadata-driven extensions using public APIs, without forcing Oxyde to commit to a larger extension model yet.

It would cover the core needs of:

  • safe model discovery after finalization
  • stable DB-facing serialization
  • correct transaction/connection context in hooks

Acceptance criteria

  • model finalization timing
  • DB payload serialization
  • execution context inside lifecycle hooks

Open question

Would maintainers prefer these additions as:

  1. direct exports from oxyde.models
  2. a small oxyde.extensions module
  3. a mix of both

My bias is to keep the surface small and place these in the most obvious public location, even if the longer-term extension story evolves later.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions