Skip to content

Core

Cross-cutting concerns used by all layers.

Settings

Configuration from environment variables.

from ftm_lakehouse.core.settings import Settings

settings = Settings()
print(settings.uri)          # LAKEHOUSE_URI
print(settings.journal_uri)  # LAKEHOUSE_JOURNAL_URI

ftm_lakehouse.core.settings.Settings

Bases: BaseSettings

Source code in ftm_lakehouse/core/settings.py
class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_prefix="lakehouse_",
        env_nested_delimiter="__",
        env_file=".env",
        secrets_dir=str(SECRETS_DIR) if SECRETS_DIR.is_dir() else None,
        nested_model_default_partial_update=True,
        extra="ignore",
    )

    uri: str = "data"
    journal_uri: str = "sqlite:///:memory:"
    statements_uri: str | None = None
    """Place parquet different from main `uri`"""
    api_key: str | None = None
    api_secret: str | None = None
    on_zfs: bool = False
    zfs_pool: str | None = None
    """ZFS dataset path the lakehouse's datasets are created under"""
    zfs_statements_pool: str | None = None
    """ZFS dataset path the statements tables are created under"""

    grace_period_days: int = 30
    max_buffer_rows: int = 1_000_000

    journal_drain_rows: int = 1_000_000
    """Rows per Arrow table a journal flush hands to the parquet store"""

    journal_pool_size: int = 5
    """Postgres journal connections (adbc)"""

    write_buffer: str = "1GiB"
    """Arrow bytes a bulk import holds across its partitions before it writes
    the largest ones (``LAKEHOUSE_WRITE_BUFFER``) – a byte size."""

    lock_max_retries: int = 10
    """Retry bound for every wait on a dataset lock: acquiring ``.LOCK`` or
    ``.LOCK-MERGE``. A lock left behind can be released via ``ftm-lakehouse
    maintenance unlock``."""

    duckdb_memory_limit: str = "8GB"
    """DuckDB's memory budget as a byte size (``8GB``, ``512MiB``) – it is split
    between the workers, so a share of RAM such as ``80%`` is rejected."""

    workers: int = Field(default=1, ge=1)
    """Processes ``merge`` and the export sweep fan their partitions out to
    (``LAKEHOUSE_WORKERS``); ``1`` runs in-process. `duckdb_memory_limit` and
    the CPU threads are split between them. A sweep uses at most one worker per
    ``(shard, bucket)`` pair."""

    duckdb_temp_directory: str | None = Field(
        default_factory=lambda: str(Path(gettempdir()) / "duckdb")
    )
    """Where DuckDB spills a query that outgrows `duckdb_memory_limit` – each
    DuckDB instance into its own subdirectory, removed on close."""

    duckdb_extension_directory: str | None = None

    public_url_prefix: str | None = None

    allow_sha1_documents: bool = False
    """Allow sha1 checksums during exports (legacy read-only datasets)"""

    @field_validator("duckdb_memory_limit", "write_buffer")
    @classmethod
    def _byte_size(cls, value: str) -> str:
        parse_byte_size(value)
        return value

    @model_validator(mode="after")
    def _zfs_statements_pool(self) -> Self:
        # one without the other provisions the statements child where no table is
        if self.on_zfs and bool(self.statements_uri) != bool(self.zfs_statements_pool):
            raise ValueError(
                "On ZFS, set LAKEHOUSE_STATEMENTS_URI and "
                "LAKEHOUSE_ZFS_STATEMENTS_POOL together"
            )
        return self

    @property
    def api_mode(self) -> bool:
        return self.uri.startswith("http")

    @property
    def resolved_journal_uri(self) -> str:
        if self.api_mode:
            # force journal uri to use api as well
            return self.uri
        return self.journal_uri

allow_sha1_documents = False class-attribute instance-attribute

Allow sha1 checksums during exports (legacy read-only datasets)

duckdb_memory_limit = '8GB' class-attribute instance-attribute

DuckDB's memory budget as a byte size (8GB, 512MiB) – it is split between the workers, so a share of RAM such as 80% is rejected.

duckdb_temp_directory = Field(default_factory=lambda: str(Path(gettempdir()) / 'duckdb')) class-attribute instance-attribute

Where DuckDB spills a query that outgrows duckdb_memory_limit – each DuckDB instance into its own subdirectory, removed on close.

journal_drain_rows = 1000000 class-attribute instance-attribute

Rows per Arrow table a journal flush hands to the parquet store

journal_pool_size = 5 class-attribute instance-attribute

Postgres journal connections (adbc)

lock_max_retries = 10 class-attribute instance-attribute

Retry bound for every wait on a dataset lock: acquiring .LOCK or .LOCK-MERGE. A lock left behind can be released via ftm-lakehouse maintenance unlock.

statements_uri = None class-attribute instance-attribute

Place parquet different from main uri

workers = Field(default=1, ge=1) class-attribute instance-attribute

Processes merge and the export sweep fan their partitions out to (LAKEHOUSE_WORKERS); 1 runs in-process. duckdb_memory_limit and the CPU threads are split between them. A sweep uses at most one worker per (shard, bucket) pair.

write_buffer = '1GiB' class-attribute instance-attribute

Arrow bytes a bulk import holds across its partitions before it writes the largest ones (LAKEHOUSE_WRITE_BUFFER) – a byte size.

zfs_pool = None class-attribute instance-attribute

ZFS dataset path the lakehouse's datasets are created under

zfs_statements_pool = None class-attribute instance-attribute

ZFS dataset path the statements tables are created under

Path Conventions

Standard paths within the lakehouse.

Path conventions for the FollowTheMoney data lakehouse.

The fundamental idea is to have a convention-based file system layout with well-known paths for metadata and information interchange between processing stages.

All paths are dataset-relative unless otherwise noted.

Dataset Layout

::

lakehouse/
    index.json                          # catalog index
    config.yml                          # catalog configuration
    versions/                           # versioned snapshots
        YYYY/MM/YYYY-MM-DDTHH:MM:SS/
            index.json
            config.yml

    [dataset]/
        index.json                      # dataset index
        config.yml                      # dataset configuration

        versions/                       # versioned snapshots
            YYYY/MM/...

        .LOCK                           # dataset-wide maintenance lock
        .LOCK-MERGE                     # merge / export-sweep lock
        .locks/{tenant}/                 # operation-specific locks
        tags/{tenant}/                  # workflow state / cache

        archive/                        # content-addressed file storage
            ab/cd/ef/{checksum}/        # SHA256 split into segments
                blob                    # file blob (stored once)
                {origin}.txt            # extracted text (one per engine)

        statements/                     # statement store (shard-partitioned)
            shard={shard}/
                bucket={bucket}/
                    origin={origin}/
                        *.parquet

        entities.ftm.json[.zst|gzip]    # aggregated entities export

        exports/
            statistics.json             # entity counts, facets
            statements.csv[.zst|gzip]   # sorted statements
            documents.csv[.zst|gzip]    # document metadata
            documents.{origin}.csv[...] # document metadata (origin-scoped)
            graph.cypher                # neo4j export (optional)

        diffs/                          # dirs are codec-free (they double
            entities.ftm.json/          #   as freshness tags); the files
                {ts}.delta.json[.zst|gzip]         # entities delta
            exports/
                documents.csv/
                    {ts}.diff.csv[.zst|gzip]       # documents delta
                documents.{origin}.csv/
                    {ts}.diff.csv[.zst|gzip]       # origin-scoped delta

        jobs/
            runs/
                {job_type}/
                    {timestamp}.json    # job run results

CONFIG = StoreKey('config.yml') module-attribute

user editable config filename

INDEX = StoreKey('index.json') module-attribute

generated index filename

ARCHIVE = 'archive' module-attribute

Base path for archive

STATEMENTS = 'statements' module-attribute

Base path for storing statement data (partitioned by shard, bucket, origin)

EXPORTS = StoreKey('exports') module-attribute

Base path for exports

Tag Conventions

Standard tags for freshness tracking.

Global tags used to identify actions. Used for cache keys of workflow runs etc.

Export operations don't have constants here – their freshness tag is the path.* export target itself (e.g. exports/statements.csv), touched by DatasetJobOperation._run_local after a successful run.

STATEMENTS_UPDATED = 'statements/last_updated' module-attribute

The statement store's content moved – rows appended (a flush) or an origin dropped. The one clock every export, statistic and diff depends on: a merge rewrites files but changes no content, so it does not touch it.

ARCHIVE_UPDATED = 'archive/last_updated' module-attribute

Archive last updated (file added)