Contract Store & Schema Registry¶
Learn to store, version, and manage your data contracts in a centralized registry.
What You'll Learn¶
- Set up different contract store backends
- Store and version schemas
- Manage coercion and validation rules
- Track ownership and governance
- Query and retrieve metadata
- Handle schema evolution
Prerequisites¶
For specific backends:
Part 1: Store Backends¶
PyCharter supports multiple storage backends:
| Backend | Best For |
|---|---|
InMemoryContractStore |
Testing, development |
SQLiteContractStore |
Single-user, local dev |
PostgresContractStore |
Production, multi-user |
MongoDBContractStore |
Document-oriented |
RedisContractStore |
High-performance caching |
In-Memory Store¶
from pycharter import InMemoryContractStore
store = InMemoryContractStore()
store.connect()
# Data is lost when the program exits
SQLite Store¶
from pycharter import SQLiteContractStore
store = SQLiteContractStore("metadata.db")
store.connect()
# Database file persists between runs
PostgreSQL Store¶
from pycharter import PostgresContractStore
store = PostgresContractStore(
"postgresql://user:password@localhost:5432/pycharter"
)
store.connect()
# Production-ready with full ACID compliance
MongoDB Store¶
from pycharter import MongoDBContractStore
store = MongoDBContractStore(
"mongodb://localhost:27017/pycharter"
)
store.connect()
# Document-oriented storage
Redis Store¶
from pycharter import RedisContractStore
store = RedisContractStore("redis://localhost:6379/0")
store.connect()
# High-performance key-value storage
Part 2: Storing Schemas¶
Basic Schema Storage¶
from pycharter import SQLiteContractStore
store = SQLiteContractStore("metadata.db")
store.connect()
# Define a schema
schema = {
"type": "object",
"version": "1.0.0",
"properties": {
"id": {"type": "integer"},
"name": {"type": "string", "minLength": 1},
"email": {"type": "string", "format": "email"},
"age": {"type": "integer", "minimum": 0}
},
"required": ["id", "name", "email"]
}
# Store the schema (keyed by contract name and version)
store.store_schema("user", "1.0.0", schema)
print("Stored contract user @ 1.0.0")
Schema Versioning¶
# Store version 1.0.0
schema_v1 = {
"type": "object",
"version": "1.0.0",
"properties": {
"name": {"type": "string"},
"email": {"type": "string"}
}
}
store.store_schema("user", "1.0.0", schema_v1)
# Store version 2.0.0 (added age field)
schema_v2 = {
"type": "object",
"version": "2.0.0",
"properties": {
"name": {"type": "string"},
"email": {"type": "string"},
"age": {"type": "integer"} # New field
}
}
store.store_schema("user", "2.0.0", schema_v2)
# Retrieve by contract name and version
schema = store.get_schema("user", "1.0.0")
schema_v2_retrieved = store.get_schema("user", "2.0.0")
Listing Contracts¶
# List all stored contracts
contracts = store.list_contracts()
for c in contracts:
print(f"{c['name']} @ {c['version']}")
Part 3: Coercion and Validation Rules¶
Storing Rules¶
# Store coercion rules
coercion_rules = {
"version": "1.0.0",
"rules": {
"age": "coerce_to_integer",
"email": "coerce_to_lowercase",
"created_at": "coerce_to_datetime"
}
}
store.store_coercion_rules("user", "1.0.0", coercion_rules)
# Store validation rules
validation_rules = {
"version": "1.0.0",
"rules": {
"age": {
"is_positive": {},
"less_than_or_equal_to": {"threshold": 150}
},
"email": {
"is_email": {}
},
"name": {
"min_length": {"threshold": 1},
"max_length": {"threshold": 100}
}
}
}
store.store_validation_rules("user", "1.0.0", validation_rules)
Retrieving Rules¶
# Get coercion and validation rules (by contract name and version)
coercion = store.get_coercion_rules("user", "1.0.0")
validation = store.get_validation_rules("user", "1.0.0")
print(f"Coercion rules: {coercion}")
print(f"Validation rules: {validation}")
Part 4: Metadata and Ownership¶
Storing Metadata¶
# Store metadata for a schema
metadata = {
"title": "User Schema",
"description": "Defines the structure of user records",
"domain": "customer",
"data_classification": "pii",
"retention_period": "7 years",
"business_owners": ["product-team"],
"technical_owners": ["data-engineering"],
"steward": "alice@example.com",
"tags": ["user", "customer", "pii"]
}
store.store_metadata("user", "1.0.0", metadata)
Retrieving Metadata¶
# Get metadata (by contract name and version)
metadata = store.get_metadata("user", "1.0.0")
print(f"Title: {metadata.get('title')}")
print(f"Owner: {metadata.get('business_owners')}")
print(f"Tags: {metadata.get('tags')}")
To find contracts by tag or owner, use list_contracts() and filter the list in your application, or query the store's backend directly.
Part 5: Using with Validator¶
Creating Validators from Store¶
from pycharter import Validator, SQLiteContractStore
store = SQLiteContractStore("metadata.db")
store.connect()
# Create validator from store (by contract name and version)
validator = Validator(store=store, contract_name="user", contract_version="1.0.0")
# Validate data
result = validator.validate({
"id": "123", # Will be coerced
"name": "Alice",
"email": "ALICE@EXAMPLE.COM", # Will be lowercased
"age": "30" # Will be coerced
})
if result.is_valid:
print(f"Valid: {result.data}")
Building Contracts from Store¶
from pycharter import build_contract_from_store
# Build a consolidated contract
# Note: Contract has RAW schema + separate rules (not merged)
contract = build_contract_from_store(store, "user", "1.0.0")
# contract is a DataContract (use .json_schema, .coercion_rules, etc.)
print(f"Schema (raw): {contract.json_schema}")
print(f"Coercion rules (separate): {contract.coercion_rules}")
print(f"Validation rules (separate): {contract.validation_rules}")
Part 6: Schema Evolution¶
Checking Compatibility¶
from pycharter.schema_evolution import check_compatibility, CompatibilityMode
# Check if new schema is backward compatible
old_schema = store.get_schema("user", "1.0.0")
new_schema = store.get_schema("user", "2.0.0")
result = check_compatibility(
old_schema=old_schema,
new_schema=new_schema,
mode=CompatibilityMode.BACKWARD
)
print(f"Compatible: {result.is_compatible}")
if not result.is_compatible:
for issue in result.issues:
print(f" - {issue}")
Computing Schema Diff¶
from pycharter.schema_evolution import compute_diff
diff = compute_diff(old_schema, new_schema)
print("Added fields:")
for field in diff.added_fields:
print(f" + {field}")
print("Removed fields:")
for field in diff.removed_fields:
print(f" - {field}")
print("Modified fields:")
for field, changes in diff.modified_fields.items():
print(f" ~ {field}: {changes}")
Migration Strategies¶
# Check compatibility before storing new version
def store_schema_safe(store, name, schema, version, previous_version=None):
"""Store schema only if backward compatible with previous_version."""
if previous_version:
current = store.get_schema(name, previous_version)
# Check compatibility
result = check_compatibility(
old_schema=current,
new_schema=schema,
mode=CompatibilityMode.BACKWARD
)
if not result.is_compatible:
raise ValueError(
f"Schema not backward compatible: {result.issues}"
)
return store.store_schema(name, version, schema)
Part 7: Database Initialization¶
CLI Commands¶
# Initialize database (create tables)
pycharter db init
# Run migrations
pycharter db migrate
# Check migration status
pycharter db status
# Rollback last migration
pycharter db rollback
Programmatic Initialization¶
from pycharter.db import init_database, run_migrations
# Initialize database
init_database("postgresql://user:pass@localhost/pycharter")
# Run migrations
run_migrations("postgresql://user:pass@localhost/pycharter")
Part 8: Best Practices¶
1. Use Semantic Versioning¶
# Follow semver: MAJOR.MINOR.PATCH
# MAJOR: Breaking changes
# MINOR: Backward-compatible additions
# PATCH: Backward-compatible fixes
store.store_schema("user", "1.0.0", schema) # Initial
store.store_schema("user", "1.1.0", schema) # Added optional field
store.store_schema("user", "2.0.0", schema) # Breaking change
2. Document Ownership¶
# Always include ownership metadata
metadata = {
"business_owners": ["product-team"],
"technical_owners": ["data-engineering"],
"steward": "alice@example.com",
"slack_channel": "#data-contracts",
"oncall_rotation": "data-team"
}
3. Tag for Discovery¶
# Use consistent tags
metadata = {
"tags": [
"customer", # Domain
"pii", # Data classification
"production", # Environment
"v2" # Major version
]
}
4. Track Lineage¶
# Include lineage in metadata
metadata = {
"source_systems": ["crm", "billing"],
"downstream_consumers": ["analytics", "marketing"],
"data_product": "customer-360"
}
5. Automate Sync¶
# Sync schemas from contract files on deploy
from pathlib import Path
def sync_contracts(store, contracts_dir):
"""Sync all contract files to the contract store."""
for contract_file in Path(contracts_dir).glob("**/*.yaml"):
with open(contract_file) as f:
contract = yaml.safe_load(f)
name = contract_file.stem
version = contract["json_schema"]["version"]
store.store_schema(name, version, contract["json_schema"])
if contract.get("coercion_rules"):
store.store_coercion_rules(name, version, contract["coercion_rules"])
if contract.get("validation_rules"):
store.store_validation_rules(name, version, contract["validation_rules"])
if contract.get("metadata"):
store.store_metadata(name, version, contract["metadata"])
print(f"Synced {name} v{version}")
Exercises¶
-
Setup: Create a SQLite store and store a schema with coercion/validation rules.
-
Versioning: Store multiple versions of a schema and retrieve specific versions.
-
Metadata: Add ownership and governance metadata, then search by tags.
-
Evolution: Check compatibility between two schema versions and compute the diff.
-
Integration: Create a validator from the store and validate a batch of records.
Next Steps¶
- REST API Tutorial - Access metadata via API
- Schema Evolution Guide - Advanced evolution patterns
- API Reference: Contract Store - Complete documentation