Zero-Dependency Entity Resolution for SEC EDGAR Data
From company_tickers.json to enterprise-grade Knowledge Graph — without forcing dependencies.
Quick Start • Why EntitySpine? • Examples • Architecture • API • Contributing
EntitySpine solves the entity resolution problem for SEC EDGAR data:
"Is CIK 0000320193 the same company as ticker AAPL on NASDAQ?"
It provides:
- 🔍 Entity Resolution — Resolve tickers, CIKs, CUSIPs to canonical entities
- 📊 Knowledge Graph — Model companies, people, relationships, events
- 🗃️ Tiered Storage — JSON → SQLite → DuckDB → PostgreSQL
- ⚡ Zero Core Dependencies — stdlib-only for Tier 0-1
# Core (zero dependencies)
pip install entityspine
# With optional features
pip install "entityspine[pydantic]" # Validation wrappers
pip install "entityspine[orm]" # SQLModel/SQLAlchemy
pip install "entityspine[duckdb]" # Analytics tier
pip install "entityspine[full]" # Everythingfrom entityspine import SqliteStore
# Create store and load ~14,000 SEC companies (auto-downloads)
store = SqliteStore(":memory:")
store.initialize()
store.load_sec_data() # Fetches from SEC automatically
# Resolve by ticker
results = store.search_entities("AAPL")
entity, score = results[0]
print(f"{entity.primary_name} (CIK: {entity.source_id})")
# Apple Inc. (CIK: 0000320193)
# Resolve by CIK
entities = store.get_entities_by_cik("0000320193")
print(entities[0].primary_name)
# Apple Inc.SEC EDGAR data uses multiple identifiers that don't naturally connect:
| Identifier | Example | What It Identifies |
|---|---|---|
| CIK | 0000320193 | Legal filing entity |
| Ticker | AAPL | Exchange listing |
| CUSIP | 037833100 | Security instrument |
| LEI | HWUPKR0MPOU8FGXBT394 | Global legal entity |
Questions that are hard to answer:
- Is
AAPLand0000320193the same company? ✅ Yes - Did
FBbecomeMETA? How do I track that? 🤔 - Which company is
GOOGvsGOOGL? Same entity, different securities - What's the LEI for CIK 0001018724? 🤷
EntitySpine provides a canonical entity model with identifier claims:
┌─────────────────────────────────────────────────────────────────┐
│ Entity │
│ "Apple Inc." │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ CIK Claim │ │ LEI Claim │ │ EIN Claim │ │
│ │ 0000320193 │ │ HWUPKR... │ │ 94-2404110 │ │
│ │ source: SEC │ │ source:GLEIF│ │ source: IRS │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Security │ │
│ │ "Apple Common Stock" │ │
│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │
│ │ │CUSIP Claim │ │ISIN Claim │ │FIGI Claim │ │ │
│ │ │ 037833100 │ │US037833... │ │BBG000B9XRY4│ │ │
│ │ └────────────┘ └────────────┘ └────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌────────────────────────────────────────────────┐ │ │
│ │ │ Listing (NASDAQ) │ │ │
│ │ │ Ticker: AAPL | MIC: XNAS | Status: ACTIVE │ │ │
│ │ └────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
"""Load SEC's company_tickers.json into EntitySpine."""
from entityspine import SqliteStore
import httpx
import json
# Download SEC data
url = "https://www.sec.gov/files/company_tickers.json"
headers = {"User-Agent": "MyApp/1.0 (contact@example.com)"}
response = httpx.get(url, headers=headers)
sec_data = response.json()
# Load into EntitySpine
store = SqliteStore("./sec_entities.db")
store.initialize()
loaded = store.load_sec_json(sec_data)
print(f"Loaded {loaded} entities")
# Loaded 10,847 entities
# Query
results = store.search_entities("Microsoft")
for entity, score in results[:5]:
print(f" {score:.2f} | {entity.primary_name}")
# 1.00 | Microsoft Corporation
# 0.70 | Microsoft Payments Inc"""Extract entities and relationships from a 10-K filing."""
from entityspine import (
Entity, EntityType, EntityStatus,
IdentifierClaim, IdentifierScheme, VendorNamespace, ClaimStatus,
Relationship, NodeRef, NodeKind, RelationshipType,
SqliteStore,
)
store = SqliteStore("./knowledge_graph.db")
store.initialize()
# Create NVIDIA entity
nvidia = Entity(
primary_name="NVIDIA Corporation",
entity_type=EntityType.ORGANIZATION,
status=EntityStatus.ACTIVE,
jurisdiction="DE",
sic_code="3674",
source_system="sec-edgar",
source_id="0001045810",
)
store.save_entity(nvidia)
# Attach CIK with SEC filing evidence
cik_claim = IdentifierClaim(
entity_id=nvidia.entity_id,
scheme=IdentifierScheme.CIK,
value="0001045810",
namespace=VendorNamespace.SEC,
source="sec-edgar",
source_ref="0001045810-24-000029", # Accession number
confidence=1.0,
)
store.save_claim(cik_claim)
# Create supplier entity
tsmc = Entity(
primary_name="Taiwan Semiconductor Manufacturing Company",
entity_type=EntityType.ORGANIZATION,
source_system="sec-edgar",
)
store.save_entity(tsmc)
# Create supplier relationship with evidence
relationship = Relationship(
source_ref=NodeRef(NodeKind.ENTITY, nvidia.entity_id),
target_ref=NodeRef(NodeKind.ENTITY, tsmc.entity_id),
relationship_type=RelationshipType.SUPPLIER,
confidence=0.95,
evidence_filing_id="0001045810-24-000029",
evidence_snippet="TSMC manufactures substantially all of our GPUs...",
source_system="sec-edgar",
)
store.save_relationship(relationship)
print(f"Entities: {store.entity_count()}")
print(f"Relationships: {store.relationship_count()}")"""Ingest SEC filings using the integration module."""
from datetime import date
from entityspine.integration import (
FilingFacts,
FilingEvidence,
ingest_filing_facts,
)
from entityspine.integration.contracts import (
ExtractedEntity,
ExtractedRelationship,
)
from entityspine import SqliteStore
store = SqliteStore("./filings.db")
store.initialize()
# Build facts from a 10-K (this would come from py-sec-edgar)
facts = FilingFacts(
evidence=FilingEvidence(
accession_number="0001045810-24-000029",
form_type="10-K",
filed_date=date(2024, 2, 21),
cik="0001045810",
),
registrant_name="NVIDIA Corporation",
registrant_cik="0001045810",
registrant_ticker="NVDA",
registrant_exchange="NASDAQ",
registrant_sic="3674",
registrant_state="DE",
entities=[
ExtractedEntity(name="Jensen Huang", entity_type="person"),
ExtractedEntity(name="TSMC", entity_type="organization"),
ExtractedEntity(name="Microsoft", entity_type="organization"),
],
relationships=[
ExtractedRelationship(
source_name="NVIDIA Corporation",
target_name="TSMC",
relationship_type="SUPPLIER",
evidence_snippet="TSMC manufactures our GPUs",
),
ExtractedRelationship(
source_name="NVIDIA Corporation",
target_name="Microsoft",
relationship_type="CUSTOMER",
evidence_snippet="Microsoft is a major customer for datacenter",
),
],
)
# Ingest into knowledge graph
result = ingest_filing_facts(store, facts)
print(f"Created {result.entities_created} entities")
print(f"Created {result.relationships_created} relationships")
print(f"Created {result.claims_created} identifier claims")
# Created 4 entities
# Created 2 relationships
# Created 2 identifier claims"""Resolve entities across multiple identifier schemes."""
from entityspine import SqliteStore
store = SqliteStore("./entities.db")
store.initialize()
# Search works across CIK, ticker, and name
queries = ["AAPL", "0000320193", "Apple Inc", "Apple"]
for query in queries:
results = store.search_entities(query, limit=1)
if results:
entity, score = results[0]
print(f"'{query}' → {entity.primary_name} (score: {score:.2f})")
# 'AAPL' → Apple Inc. (score: 1.00)
# '0000320193' → Apple Inc. (score: 1.00)
# 'Apple Inc' → Apple Inc. (score: 1.00)
# 'Apple' → Apple Inc. (score: 0.70)"""Track mergers, name changes, and ticker changes."""
from entityspine import Entity, EntityStatus, SqliteStore
from entityspine.domain.timestamps import utc_now
store = SqliteStore("./corporate_actions.db")
store.initialize()
# Original entity
facebook = Entity(
primary_name="Facebook, Inc.",
entity_type=EntityType.ORGANIZATION,
source_system="sec-edgar",
source_id="0001326801",
)
store.save_entity(facebook)
# After rebranding - create redirect
meta = Entity(
primary_name="Meta Platforms, Inc.",
entity_type=EntityType.ORGANIZATION,
source_system="sec-edgar",
source_id="0001326801", # Same CIK
)
store.save_entity(meta)
# Mark old entity as merged
facebook_merged = facebook.with_update(
status=EntityStatus.MERGED,
redirect_to=meta.entity_id,
redirect_reason="Rebranded to Meta Platforms, Inc.",
merged_at=utc_now(),
)
store.save_entity(facebook_merged)
# Lookups automatically follow redirect
results = store.search_entities("Facebook")
entity, _ = results[0]
print(f"Facebook resolved to: {entity.primary_name}")
# Facebook resolved to: Meta Platforms, Inc.┌─────────────────────────────────────────────────────────────────┐
│ entityspine.domain │
│ (stdlib dataclasses only) │
│ ┌─────────┐ ┌──────────┐ ┌─────────┐ ┌─────────────────┐ │
│ │ Entity │ │ Security │ │ Listing │ │IdentifierClaim │ │
│ └─────────┘ └──────────┘ └─────────┘ └─────────────────┘ │
│ ┌─────────┐ ┌──────────┐ ┌─────────┐ ┌─────────────────┐ │
│ │ Asset │ │ Contract │ │ Product │ │ Relationship │ │
│ └─────────┘ └──────────┘ └─────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
↑ returns domain dataclasses
┌─────────────────────────────────────────────────────────────────┐
│ entityspine.stores │
│ ┌───────────────┐ ┌───────────────┐ │
│ │ JsonStore │ │ SqliteStore │ (Tier 0-1, stdlib) │
│ └───────────────┘ └───────────────┘ │
└─────────────────────────────────────────────────────────────────┘
↑ optional extras
┌─────────────────────────────────────────────────────────────────┐
│ entityspine.adapters │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ pydantic/ wrappers │ │ orm/ SqlModelStore │ │
│ │ to_domain/from_dom │ │ returns domain │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ pip install .[pydantic] pip install .[orm] │
└─────────────────────────────────────────────────────────────────┘
| Tier | Backend | Dependencies | Use Case | Temporal? |
|---|---|---|---|---|
| 0 | JSON file | None | Scripts, CLI | ❌ |
| 1 | SQLite | None | Local dev | ❌ |
| 2 | DuckDB | [duckdb] |
Analytics | ⏳ Planned |
| 3 | PostgreSQL | [postgres] |
Production | ✅ |
Lower tiers warn when they can't fulfill advanced queries:
result = store.resolve("AAPL", as_of="2015-01-01")
if not result.as_of_honored:
for warning in result.warnings:
print(f"⚠️ {warning}")
# ⚠️ as_of parameter ignored: temporal resolution requires Tier 2+| Model | Purpose | Key Fields |
|---|---|---|
Entity |
Legal/organizational identity | primary_name, entity_type, jurisdiction |
Security |
Tradeable instrument | security_type, entity_id, description |
Listing |
Exchange-specific ticker | ticker, exchange, mic, security_id |
IdentifierClaim |
Identifier with provenance | scheme, value, entity_id, confidence |
| Model | Purpose |
|---|---|
Person |
Natural persons (executives, directors) |
Asset |
Physical/tangible assets |
Contract |
Material agreements |
Product |
Products/services |
Brand |
Brand identities |
Event |
Discrete business events |
Case |
Legal proceedings |
Geo |
Geographic locations |
Address |
Physical addresses |
| Model | Purpose |
|---|---|
RoleAssignment |
Person→Org roles (CEO, CFO, Director) |
Relationship |
Generic node→node edges with evidence |
EntityRelationship |
Entity→Entity relationships |
# Storage path (default: ./entityspine.db)
ENTITYSPINE_DB_PATH=./data/entities.db
# Log level
ENTITYSPINE_LOG_LEVEL=INFOfrom entityspine import SqliteStore
# In-memory for testing
store = SqliteStore(":memory:")
# File-based
store = SqliteStore("./entities.db")
# With explicit WAL mode (better concurrency)
store = SqliteStore("./entities.db", wal_mode=True)# Run all tests
pytest
# With coverage
pytest --cov=entityspine --cov-report=html
# Only unit tests
pytest tests/unit -v
# Only integration tests
pytest tests/integration -ventityspine/
├── src/entityspine/
│ ├── __init__.py # Package exports
│ ├── domain/ # Canonical stdlib dataclasses
│ │ ├── entity.py # Entity model
│ │ ├── security.py # Security model
│ │ ├── listing.py # Listing model
│ │ ├── claim.py # IdentifierClaim model
│ │ ├── graph.py # KG nodes (Asset, Contract, etc.)
│ │ ├── enums.py # All enumerations
│ │ └── validators.py # Normalization + validation
│ ├── stores/ # Storage backends
│ │ ├── sqlite_store.py # Tier 1 (stdlib sqlite3)
│ │ ├── json_store.py # Tier 0 (JSON file)
│ │ └── mappers.py # Domain ↔ dict conversion
│ ├── adapters/ # Optional adapters
│ │ ├── pydantic/ # Pydantic validation wrappers
│ │ └── orm/ # SQLModel/SQLAlchemy layer
│ ├── integration/ # py-sec-edgar integration
│ │ ├── contracts.py # FilingFacts schema
│ │ ├── ingest.py # Ingestion functions
│ │ └── normalize.py # SEC identifier normalizers
│ └── core/ # Utilities
│ ├── ulid.py # ULID generation
│ └── timestamps.py # UTC timestamp utilities
├── tests/ # 303 tests
├── examples/ # Usage examples
└── docs/ # Documentation
- v0.3.x — Core entity resolution, KG nodes, integration module
- v0.4.x — DuckDB Tier 2, temporal queries
- v0.5.x — PostgreSQL Tier 3, full temporal support
- v0.6.x — FastAPI service, graph traversal API
- v1.0.0 — Production-ready, comprehensive documentation
We welcome contributions! See CONTRIBUTING.md for guidelines.
# Setup development environment
git clone https://github.com/ryansmccoy/entity-spine.git
cd entity-spine
pip install -e ".[dev]"
pre-commit install
# Run tests
pytest
# Run linting
ruff check src tests
mypy src/entityspineMIT License — see LICENSE for details.
- Repository: https://github.com/ryansmccoy/entity-spine
- PyPI: https://pypi.org/project/entityspine/
- Documentation: https://github.com/ryansmccoy/entity-spine/tree/main/docs
- Issues: https://github.com/ryansmccoy/entity-spine/issues
Built with ❤️ for the SEC EDGAR community