Skip to main content

Universal object-isolation and tenancy-scoping framework

Project description

Scoped

Universal object-isolation and tenancy framework for Python.

Scoped guarantees that anything built on it can be isolated, shared, traced, and rolled back — to any degree, at any time, by anyone with the right to do so.

Postgres ready. 1,680+ tests. Python 3.11+.

pip install pyscoped

Why Scoped

Most frameworks give you a database and leave isolation, sharing, auditing, and rollback as your problem. Scoped makes them structural guarantees:

  • Every object is creator-private by default. Sharing requires explicit projection into a scope.
  • Every mutation creates a new version. No in-place updates. Full history preserved.
  • Every action is hash-chained. Tamper-evident audit trail with before/after state.
  • Everything is rollbackable. Any action can be reversed to any point in time.
  • Deny always wins. When rules conflict, DENY overrides ALLOW. No exceptions.

This makes Scoped the base layer for multi-tenant applications, clinical systems, financial platforms, compliance-sensitive workflows, and AI agent orchestration.

Quick Start

import scoped

# 1. Initialize (zero config — in-memory SQLite)
scoped.init()
# Or: scoped.init(database_url="postgresql://user:pass@localhost/mydb")

# 2. Create principals (the acting users)
alice = scoped.principals.create("Alice")
bob = scoped.principals.create("Bob")

# 3. Set the acting principal and create objects
with scoped.as_principal(alice):
    # Create an object — it's creator-private by default
    doc, v1 = scoped.objects.create(
        "document", data={"title": "Q4 Report", "status": "draft"},
    )

    # Alice can read it
    assert scoped.objects.get(doc.id) is not None

# Bob cannot (isolation enforced)
with scoped.as_principal(bob):
    assert scoped.objects.get(doc.id) is None

# 4. Update creates a new immutable version
with scoped.as_principal(alice):
    doc, v2 = scoped.objects.update(
        doc.id, data={"title": "Q4 Report", "status": "final"},
    )
    assert v2.version == 2  # v1 is preserved, untouched

    # 5. Share via scope
    team = scoped.scopes.create("Finance Team")
    scoped.scopes.add_member(team, bob, role="viewer")
    scoped.scopes.project(doc, team)

# 6. Every action was traced with hash-chained audit
trail = scoped.audit.for_object(doc.id)
assert len(trail) >= 2  # CREATE + UPDATE, each with before/after state
assert scoped.audit.verify().valid  # Hash chain is intact

Architecture

16 composable layers. Each depends only on layers below it.

Layer 0   Compliance    Validates all invariants across layers
Layer 1   Registry      Universal construct registration (URNs)
Layer 2   Identity      Generic principal machinery + ScopedContext
Layer 3   Objects       Versioned, isolated data objects
Layer 4   Tenancy       Scopes, membership, projection (sharing)
Layer 5   Rules         Deny-overrides policy engine
Layer 6   Audit         Hash-chained, immutable, append-only trace
Layer 7   Temporal      Point-in-time reconstruction + rollback
Layer 8   Environments  Ephemeral workspaces
Layer 9   Flow          Stages, pipelines, promotions
Layer 10  Deployments   Graduation to external targets with gates
Layer 11  Secrets       Encrypted vault with zero-trust access
Layer 12  Integrations  Sandboxed plugins, hooks, external systems
Layer 13  Connector     Cross-org meshing, federation, marketplace
Layer 14  Events        Asynchronous scoped event bus + webhooks
Layer 15  Notifications Principal-targeted messages
Layer 16  Scheduling    Recurring schedules, scoped job execution

9 extensions enrich existing layers: Migrations (A1), Contracts (A2), Rule Extensions (A3), Blobs (A4), Config Hierarchy (A5), Search (A6), Templates (A7), Tiering (A8), Import/Export (A9).

The 10 Invariants

These are absolute. The compliance engine (Layer 0) validates them.

  1. Nothing exists without registration. Every construct has a URN in the registry.
  2. Nothing happens without identity. Every operation requires an acting principal.
  3. Nothing is shared by default. Objects start creator-private. Sharing is explicit.
  4. Nothing happens without a trace. Every action produces a hash-chained audit entry.
  5. Nothing is truly deleted. Objects are tombstoned. Versions retained. Audit is append-only.
  6. Deny always wins. DENY overrides ALLOW when rules conflict.
  7. Revocation is immediate. Same-transaction enforcement.
  8. Everything is versioned. Every mutation creates a new immutable version.
  9. Everything is rollbackable. Any action can be reversed to any point in time.
  10. Secrets never leak. Values never appear in audit, snapshots, or connector traffic.

Core API

Objects (Layer 3)

from scoped.objects.manager import ScopedManager

manager = ScopedManager(backend, audit_writer=audit)

# Create — returns (ScopedObject, ObjectVersion)
obj, ver = manager.create(object_type="task", owner_id=user.id, data={"title": "Ship it"})

# Read — returns None if principal cannot access
obj = manager.get(obj.id, principal_id=user.id)

# Update — creates new version, never modifies old
obj, ver = manager.update(obj.id, principal_id=user.id, data={"title": "Ship it", "done": True})

# Soft delete — tombstones, never physically deletes
tombstone = manager.tombstone(obj.id, principal_id=user.id, reason="Obsolete")

Tenancy (Layer 4)

from scoped.tenancy.lifecycle import ScopeLifecycle
from scoped.tenancy.projection import ProjectionManager
from scoped.tenancy.models import ScopeRole, AccessLevel

scopes = ScopeLifecycle(backend, audit_writer=audit)
projections = ProjectionManager(backend, audit_writer=audit)

# Create scope (owner auto-added as OWNER member)
scope = scopes.create_scope(name="Team Alpha", owner_id=alice.id)

# Add members
scopes.add_member(scope.id, principal_id=bob.id, role=ScopeRole.EDITOR, granted_by=alice.id)

# Project an object into the scope (sharing it with members)
projections.project(scope_id=scope.id, object_id=obj.id, projected_by=alice.id)

# Revoke sharing
projections.revoke_projection(scope_id=scope.id, object_id=obj.id, revoked_by=alice.id)

Rules (Layer 5)

from scoped.rules.engine import RuleStore, RuleEngine
from scoped.rules.models import RuleType, RuleEffect, BindingTargetType

store = RuleStore(backend)
rule = store.create_rule(
    name="deny-external-access",
    rule_type=RuleType.ACCESS,
    effect=RuleEffect.DENY,
    priority=10,
    created_by=admin.id,
)
store.bind_rule(rule.id, target_type=BindingTargetType.SCOPE, target_id=scope.id, bound_by=admin.id)

engine = RuleEngine(backend)
result = engine.evaluate(action="read", principal_id=user.id, scope_id=scope.id)
# result.allowed, result.deny_rules, result.allow_rules

Audit (Layer 6)

from scoped.audit.writer import AuditWriter
from scoped.audit.query import AuditQuery

writer = AuditWriter(backend)
entry = writer.record(
    actor_id=user.id,
    action=ActionType.CREATE,
    target_type="document",
    target_id=obj.id,
    after_state={"title": "Draft"},
)
# entry.hash — SHA-256 hash linking to previous entry
# entry.previous_hash — hash of the entry before this one
# entry.sequence — monotonically increasing sequence number

query = AuditQuery(backend)
trail = query.for_target("document", obj.id)  # full history
trail = query.for_actor(user.id)               # everything a user did

Temporal (Layer 7)

from scoped.temporal.rollback import RollbackExecutor
from scoped.temporal.reconstruction import StateReconstructor

# Roll back a single action
executor = RollbackExecutor(backend, audit_writer=audit)
result = executor.rollback_action(trace_id, actor_id=admin.id, reason="Mistake")

# Roll back to a point in time
result = executor.rollback_to_timestamp("document", doc.id, at=yesterday, actor_id=admin.id)

# Cascading rollback (action + all dependent actions)
result = executor.rollback_cascade(trace_id, actor_id=admin.id)

# Reconstruct state at a past timestamp
reconstructor = StateReconstructor(backend)
state = reconstructor.at_timestamp("document", doc.id, timestamp)

Framework Adapters

Install with extras for your framework:

pip install pyscoped[django]    # Django ORM backend + middleware
pip install pyscoped[fastapi]   # FastAPI middleware + Pydantic schemas
pip install pyscoped[flask]     # Flask extension + admin blueprint
pip install pyscoped[mcp]       # MCP server for AI agents

Django

# settings.py
INSTALLED_APPS = ["scoped.contrib.django"]
MIDDLEWARE = ["scoped.contrib.django.middleware.ScopedContextMiddleware"]

# Uses the Django database connection as the storage backend.
# Management commands: scoped_health, scoped_audit, scoped_compliance

FastAPI

from fastapi import FastAPI
from scoped.contrib.fastapi.middleware import ScopedContextMiddleware
from scoped.contrib.fastapi.router import router as scoped_router

app = FastAPI()
app.add_middleware(ScopedContextMiddleware, backend=backend)
app.include_router(scoped_router)  # /scoped/health, /scoped/audit

Flask

from flask import Flask
from scoped.contrib.flask.extension import ScopedExtension

app = Flask(__name__)
scoped = ScopedExtension(app)  # auto-inits backend, injects g.scoped_context

MCP (Model Context Protocol)

from scoped.contrib.mcp.server import create_scoped_server

mcp = create_scoped_server(backend)
mcp.run()
# Tools: create_principal, create_object, get_object, create_scope, list_audit, health_check
# Resources: scoped://principals, scoped://health, scoped://audit/recent

Storage

The default backend is SQLite (zero dependencies, included). PostgreSQL is supported for production via psycopg v3 with connection pooling:

pip install pyscoped[postgres]
from scoped.storage.sqlite import SQLiteBackend

# In-memory (tests, prototyping)
backend = SQLiteBackend(":memory:")
backend.initialize()

# File-based (production single-node)
backend = SQLiteBackend("app.db")
backend.initialize()

# PostgreSQL (production)
from scoped.storage.postgres import PostgresBackend
backend = PostgresBackend("postgresql://user:pass@localhost/mydb")
backend.initialize()

# Django ORM backend
from scoped.contrib.django import get_backend
backend = get_backend()

Cloud Integrations

pip install pyscoped[aws]     # S3 blob storage + AWS KMS encryption
pip install pyscoped[gcp]     # GCS blob storage + GCP Cloud KMS encryption
pip install pyscoped[otel]    # OpenTelemetry instrumentation
# AWS KMS for secrets (Layer 11)
from scoped.secrets import AWSKMSBackend
encryption = AWSKMSBackend(region_name="us-east-1")

# S3 for blob storage (Extension A4)
from scoped.storage import S3BlobBackend
blobs = S3BlobBackend("my-bucket")

# OpenTelemetry instrumentation
from scoped.contrib.otel import instrument
services = build_services(backend)
instrument(services)  # All operations now emit OTel spans

Testing

Scoped includes a compliance testing engine, test factories, assertion helpers, and importable pytest fixtures:

from scoped.testing.factories import ScopedFactory
from scoped.testing.assertions import assert_isolated, assert_version_count

def test_isolation(scoped_services, alice, bob):
    factory = ScopedFactory(scoped_services)
    doc, _ = factory.object(alice, data={"title": "Private"})

    assert_isolated(scoped_services.backend, doc.id, alice.id, bob.id)

def test_versioning(scoped_services, alice):
    factory = ScopedFactory(scoped_services)
    doc, _ = factory.object(alice, data={"v": 1})
    scoped_services.manager.update(doc.id, principal_id=alice.id, data={"v": 2})

    assert_version_count(scoped_services.backend, doc.id, 2)

Register the fixtures in your conftest.py:

from scoped.testing.fixtures import scoped_backend, scoped_services, alice, bob
pip install pyscoped[dev]
pytest                          # 1,680+ tests
pytest tests/test_objects/      # one layer
pytest tests/test_compliance/   # invariant validation

Test Coverage

Component Tests
Core Layers 1-13 820
Extensions A1-A9 386
Events, Notifications, Scheduling 117
Compliance Engine (Layer 0) 87
Framework Adapters (D1-D4) 83
Cloud Integrations (Postgres, AWS, GCP) 46
Auto-Rotation + Testing Utilities 36
SDK Client + Namespaces 58
OTel Instrumentation 6
Sync Agent + Contract Models 37
Total 1,688+

Documentation

Requirements

  • Python 3.11+
  • No required dependencies (SQLite backend included)
  • Optional: pyscoped[postgres], pyscoped[aws], pyscoped[gcp], pyscoped[otel]

License

MIT License. See LICENSE for details.

Project details


Download files

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

Source Distribution

pyscoped-0.9.5.tar.gz (594.7 kB view details)

Uploaded Source

Built Distribution

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

pyscoped-0.9.5-py3-none-any.whl (386.8 kB view details)

Uploaded Python 3

File details

Details for the file pyscoped-0.9.5.tar.gz.

File metadata

  • Download URL: pyscoped-0.9.5.tar.gz
  • Upload date:
  • Size: 594.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.3

File hashes

Hashes for pyscoped-0.9.5.tar.gz
Algorithm Hash digest
SHA256 bf23fd34f8c3b90069bccc16e637509679f202ddf58f883578fd9ddc61032932
MD5 ca27644a5e055eccb16b4d985a3c299b
BLAKE2b-256 f9edfa8dd85f64ac11043fcb01d1bba388a70c6e8ef3795c7fc2bdda05093e63

See more details on using hashes here.

File details

Details for the file pyscoped-0.9.5-py3-none-any.whl.

File metadata

  • Download URL: pyscoped-0.9.5-py3-none-any.whl
  • Upload date:
  • Size: 386.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.3

File hashes

Hashes for pyscoped-0.9.5-py3-none-any.whl
Algorithm Hash digest
SHA256 87c34f8e9f761eb44faf87482f18164cbec540dc5e79c5f31e43b0926ba0e2d0
MD5 205003b5ddd1fd33fceee24138686bae
BLAKE2b-256 a6aba8f623388ea0b1f1a409b52182a7bb5c45c8e1d4a9f12286bd9ebedeeb94

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page