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.
- Nothing exists without registration. Every construct has a URN in the registry.
- Nothing happens without identity. Every operation requires an acting principal.
- Nothing is shared by default. Objects start creator-private. Sharing is explicit.
- Nothing happens without a trace. Every action produces a hash-chained audit entry.
- Nothing is truly deleted. Objects are tombstoned. Versions retained. Audit is append-only.
- Deny always wins. DENY overrides ALLOW when rules conflict.
- Revocation is immediate. Same-transaction enforcement.
- Everything is versioned. Every mutation creates a new immutable version.
- Everything is rollbackable. Any action can be reversed to any point in time.
- 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 (no additional dependencies beyond pydantic and sqlalchemy). PostgreSQL is supported for production via psycopg v3 with connection pooling:
pip install pyscoped[postgres]
from scoped.storage.sa_sqlite import SASQLiteBackend
# In-memory (tests, prototyping)
backend = SASQLiteBackend(":memory:")
backend.initialize()
# File-based (production single-node)
backend = SASQLiteBackend("app.db")
backend.initialize()
# PostgreSQL (production)
from scoped.storage.sa_postgres import SAPostgresBackend
backend = SAPostgresBackend("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
- Architecture Overview
- Getting Started Guide
- API Reference
- Framework Adapters
- Layer Documentation (Layers 0-16)
- Extension Documentation (A1-A9)
- Changelog
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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pyscoped-1.3.0.tar.gz.
File metadata
- Download URL: pyscoped-1.3.0.tar.gz
- Upload date:
- Size: 610.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9492415c828e133687cb81ce3fcb7438983ecb784c7404b949db7b869d57eee0
|
|
| MD5 |
f893de239b0340922d1ea659005eceab
|
|
| BLAKE2b-256 |
2b93653c41bbe6bf811c5a442ec1665505ca91deae07889d59653756d9b5c96a
|
File details
Details for the file pyscoped-1.3.0-py3-none-any.whl.
File metadata
- Download URL: pyscoped-1.3.0-py3-none-any.whl
- Upload date:
- Size: 396.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
34fd399175491d482e39b09c6b4d9f0e6918047baa2c4fd980f17d027db4e3a8
|
|
| MD5 |
b44b387011df946d8c221957f5c38831
|
|
| BLAKE2b-256 |
e6bd8fb1d1b0511e2f32e28db145039e02d11d90295ce3f3426204dda1e6814c
|