Layer 4: Operation
Multi-step workflow operations that coordinate across repositories.
Base Classes
ftm_lakehouse.operation.base.DatasetJobOperation
Bases: DatasetHandle, Generic[DJ]
A (long-running) operation for a specific dataset that updates tags and checks dependencies for freshness to be able to skip this operation. The job result is stored after successful run.
Repositories are resolved through the LRU-cached factories, so an operation shares its repository instances with every other path that addresses the same dataset.
Subclasses can either set class attributes target and dependencies,
or override get_target() and get_dependencies() for dynamic values.
Source code in ftm_lakehouse/operation/base.py
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | |
get_dependencies()
get_target()
is_fresh()
Whether the target is newer than every dependency – nothing to do.
Tag-pair comparison by default. Override where the question is not a
pair of timestamps (OptimizeOperation asks the statement store
directly).
Source code in ftm_lakehouse/operation/base.py
prepare()
Bring the dataset into the state handle reads from.
Runs before the freshness check and before the target tag's window
opens, which is what makes it usable at all: Tags.touch stamps
the target with the timestamp it entered, so preparation that writes
a dependency tag from inside the window would mark the result stale the
moment it is written. Ahead of the window, the timestamps stay honest –
prepare moves the dependency, then the target is stamped after it.
No-op by default;
ExportOperation drains the
journal and merges the statement store here, since exports read
canonical rows.
Source code in ftm_lakehouse/operation/base.py
run(force=False, *args, **kwargs)
Execute the handle function, force to run it regardless of freshness
dependencies. In api mode the whole job is delegated to the remote
operations endpoint (_api_run).
Source code in ftm_lakehouse/operation/base.py
CrawlOperation
Batch file ingestion from a source location.
ftm_lakehouse.operation.crawl.CrawlJob
Bases: DatasetJobModel
Job model for crawl operations.
Tracks the state and configuration of a crawl job.
Attributes:
| Name | Type | Description |
|---|---|---|
uri |
Uri
|
Source location URI to crawl |
prefix |
str | None
|
Include only keys with this prefix |
exclude_prefix |
str | None
|
Exclude keys with this prefix |
glob |
str | None
|
Include only keys matching this glob pattern |
exclude_glob |
str | None
|
Exclude keys matching this glob pattern |
make_entities |
bool
|
Add document entities to statement store |
store_metadata |
bool
|
Write file.json metadata alongside archive blobs |
Source code in ftm_lakehouse/operation/crawl.py
ftm_lakehouse.operation.CrawlOperation
Bases: DatasetJobOperation[CrawlJob]
Crawl workflow that archives files and creates entities.
Iterates through files in a source store, archives them to the file repository, and creates corresponding entities in the entities repository.
Example
Source code in ftm_lakehouse/operation/crawl.py
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 | |
get_uris()
Generate file uris to crawl.
Applies prefix, glob, and exclude filters to the source store.
Yields:
| Type | Description |
|---|---|
str
|
File uris to be crawled |
Source code in ftm_lakehouse/operation/crawl.py
handle_crawl(uri, run)
Handle a single crawl task.
Archives the file and creates a corresponding entity.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
uri
|
str
|
File uri to crawl |
required |
run
|
JobRun[CrawlJob]
|
Current job run context |
required |
Returns:
| Type | Description |
|---|---|
datetime
|
Timestamp when the task was processed |
Source code in ftm_lakehouse/operation/crawl.py
ExportOperation
One operation for all exports, selected by ExportKind. all (the default) writes every artifact that is a function of the entity stream from a single pass over the statement store – exports/statements.csv, entities.ftm.json, exports/documents.csv and exports/documents.crawl.csv (scoped to crawled files), each with its own diff series. A diff entry costs nothing extra: the payload a diff publishes is the payload the export just wrote, so it is emitted from the same loop rather than re-read afterwards.
The individual kinds open a subset of the same writers: statements, entities, documents. The two artifacts that are not functions of the entity stream stay outside the sweep – statistics (exports/statistics.json, a global SQL aggregate) and index (index.json, store metadata, which registers what the others produced and so runs last).
Diff entries carry one of three ops, per the OpenSanctions delta format: ADD for an entity whose every statement is new, MOD for one that predates the diff window and changed in it, and DEL for one that is gone. ADD and MOD both carry the entity whole, so a consumer indexes either the same way.
ftm_lakehouse.operation.export.ExportKind
Bases: StrEnum
The available dataset exports.
Lives here rather than with the export operation because each Artifact
declares the kind it answers to, and repository/ cannot import
operation/. ftm_lakehouse.operation.export re-exports it.
Source code in ftm_lakehouse/repository/artifacts.py
ftm_lakehouse.operation.export.ExportJob
Bases: DatasetJobModel
Job model for all export kinds.
Source code in ftm_lakehouse/operation/export.py
make_diff = True
class-attribute
instance-attribute
Also export delta diff files (entities / documents kinds).
result = None
class-attribute
instance-attribute
What the run wrote, per artifact and per diff op.
ftm_lakehouse.operation.ExportOperation
Bases: DatasetJobOperation[ExportJob]
Export the dataset, in one sweep over the entity stream.
Flushes and merges first (prepare) – exports
read canonical rows. Skips if the target is newer than the last optimize.
A run stamps a freshness tag per artifact it wrote, so a later single-kind
export sees itself up to date and index.json still finds the
dependencies it registers.
Source code in ftm_lakehouse/operation/export.py
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | |
kinds
cached
property
The sweep artifacts this run writes.
export(now)
Write every requested artifact from one pass over the entities.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
now
|
datetime
|
Timestamp the run started – the diff files are named after it and the diff states are recorded at it. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, int]
|
Counts per artifact and per diff op. |
Source code in ftm_lakehouse/operation/export.py
export_index()
Write index.json, registering what the exports produced.
Source code in ftm_lakehouse/operation/export.py
export_statistics()
iterate()
Every entity in the store, folded from one scan.
Writes statements.csv from the same Arrow batches when this run
covers it, so the csv costs a tee rather than a second pass. Rows are
only materialised when something downstream needs them – a
statements-only export stays columnar end to end.
Source code in ftm_lakehouse/operation/export.py
prepare()
Drain the journal and merge, so the export reads canonical rows.
Source code in ftm_lakehouse/operation/export.py
ftm_lakehouse.repository.artifacts.DiffOp
Bases: StrEnum
What a diff entry says happened to an entity.
Ref. https://www.opensanctions.org/docs/bulk/delta/
Source code in ftm_lakehouse/repository/artifacts.py
ADD = 'ADD'
class-attribute
instance-attribute
The entity is new – every statement it has arrived in this window.
DEL = 'DEL'
class-attribute
instance-attribute
The entity is gone entirely.
MOD = 'MOD'
class-attribute
instance-attribute
The entity predates this window and changed in it – it gained statements, or lost some to a tombstone while staying alive.
OptimizeOperation
Optimize the parquet statement store in one pass: merge (per-partition rewrite that collapses duplicates, folds first_seen to the min, last_seen to the max, drops tombstones older than the grace cutoff per LAKEHOUSE_GRACE_PERIOD_DAYS), compact (bin-pack small files) and vacuum (delete obsolete files). Each step acquires the exclusive maintenance fence (.LOCK) and waits for in-flight append markers to drain.
ftm_lakehouse.operation.maintenance.OptimizeJob
Bases: DatasetJobModel
Source code in ftm_lakehouse/operation/maintenance.py
retention_hours = 0
class-attribute
instance-attribute
Vacuum: retain obsolete files newer than this many hours.
ftm_lakehouse.operation.OptimizeOperation
Bases: DatasetJobOperation[OptimizeJob]
Optimize the parquet statement store: merge, compact, vacuum.
For each (shard, bucket, origin) partition: keep the most-recent row
per statement id, fold first_seen down to the minimum, drop tombstones
older than the grace period – then bin-pack small files and delete
obsolete ones. Each step is held under the dataset write fence.
Source code in ftm_lakehouse/operation/maintenance.py
is_fresh()
Ask the statement store whether any partition is unmerged.
The tag pair cannot answer this one. merge stamps
STATEMENTS_OPTIMIZED on
completion while the target tag records when this operation started,
so a successful optimize always finishes behind its own dependency and
reads as stale – costing a redundant full pass every time. The
per-partition tags
ParquetStore.merge
compares internally are the sound predicate, and needs_merge is
that comparison.
Source code in ftm_lakehouse/operation/maintenance.py
ShardOperation
Change the dataset's shard count after the fact: drain the journal, rewrite every (bucket, origin) group into the new shard partitions (streamed, one atomic Delta commit per group), then record the new count in config.yml. Neither dedupes nor sorts – it moves rows – so every rewritten partition comes out dirty and wants an optimize afterwards. Run it with writers stopped: the maintenance fence covers parquet appends, not journal writes, and a flush landing between the rewrite and the config write still resolves the old count.
ftm_lakehouse.operation.maintenance.ShardJob
Bases: DatasetJobModel
Source code in ftm_lakehouse/operation/maintenance.py
shards = Field(ge=0)
class-attribute
instance-attribute
Target number of entity-id hash shards. 0 / 1 means a single
shard; the value is bounded below because it becomes a partition key.
ftm_lakehouse.operation.ShardOperation
Bases: DatasetJobOperation[ShardJob]
Change the dataset's shard count: rewrite the store, then the config.
The shard count is otherwise fixed at creation – every reader and
writer resolves it from config.yml – so growing it is a full
rewrite of the statement store. The typical trigger is a dataset that
outgrew its layout: one shard means one partition per
(bucket, origin), and queries that have to scan it whole get
slow.
Two steps, in this order:
sharddrains the journal and rewrites every(bucket, origin)group into the new shard partitions, streamed, one atomic Delta commit per group.- the new count is written to
config.yml(versioned like every other config write) and the repository factory caches are invalidated, so repositories fetched afterwards resolve the new layout.
The config write goes last on purpose: it is what declares the layout
to every other process, so it must not run ahead of the data. A run
that dies in between leaves the config on the old count and is
repaired by running it again – the rewrite recomputes each shard from
entity_id alone, so it is idempotent.
The rewrite is neither sorted nor deduped, which leaves every
partition marked dirty – run optimize afterwards to restore
canonical content and file sort order.
Source code in ftm_lakehouse/operation/maintenance.py
is_fresh()
Whether the dataset is already configured for the target count.
Not a tag pair: what a re-shard changes is the configured layout,
so the config is the freshness state. Consequently a config
edited by hand to a count the store was never rewritten for reads
as fresh – force is the way out of that.
Source code in ftm_lakehouse/operation/maintenance.py
MigrateOperation
Apply the storage-layout migrations a dataset has not seen yet – the functions registered in ftm_lakehouse.operation.migrations, run in registry order and stamped with a migrations/<function name> tag each, so the function name is the migration id. Migrations are forward-only (no down-migration, no compatibility shim in the read path) and idempotent: force re-runs the whole registry, and a run that dies halfway resumes at the first untagged migration.
ftm_lakehouse.operation.maintenance.MigrateJob
Bases: DatasetJobModel
No parameters – a migrate run is always "everything outstanding".
Source code in ftm_lakehouse/operation/maintenance.py
ftm_lakehouse.operation.MigrateOperation
Bases: DatasetJobOperation[MigrateJob]
Apply the storage-layout migrations this dataset has not seen yet.
Runs the functions registered in ftm_lakehouse.operation.migrations in
registry order, stamping each with
tag.migration on
completion. Per-migration tags rather than one version number: a run that
dies halfway keeps what it finished and the next one picks up at the first
untagged migration. force re-runs the whole registry – migrations are
idempotent.
Source code in ftm_lakehouse/operation/maintenance.py
outstanding
property
The registered migrations this dataset carries no tag for.
is_fresh()
Whether every registered migration has run against this dataset.
Not a tag pair: a migration is done or not, and no dependency's timestamp can make an applied one stale again.
Source code in ftm_lakehouse/operation/maintenance.py
MakeOperation
Full workflow: flush journal + all exports.
ftm_lakehouse.operation.make.MakeJob
Bases: DatasetJobModel
ftm_lakehouse.operation.MakeOperation
Bases: DatasetJobOperation[MakeJob]
Source code in ftm_lakehouse/operation/make.py
handle(run, *args, **kwargs)
Run the export sweep, then the two artifacts computed from it.
Source code in ftm_lakehouse/operation/make.py
prepare()
Drain the journal; each export merges for itself in its own
ExportOperation.prepare.
DownloadArchiveOperation
Export archive files to their original paths.
ftm_lakehouse.operation.download.DownloadArchiveJob
ftm_lakehouse.operation.DownloadArchiveOperation
Bases: DatasetJobOperation[DownloadArchiveJob]
Download the archive files to a target transforming into nice paths based on exported documents.csv