lacing.store#
Interval-keyed annotation stores.
Public surface: IntervalAnnotationStore (the facade), MemoryStore,
MappingStore (persisted to any injected MutableMapping),
SqliteStore (the .annot on-disk format), the optional
PostgresStore, and the store-schema migration ladder
(lacing.store.migrations).
Functions
|
Register a forward store migration from |
|
Migrate a |
|
Versions reachable from |
Rebuild the interval index from the |
|
|
Return a process-wide |
Close every registered connection pool (test teardown / shutdown). |
Classes
|
Protocol for any interval-keyed annotation store. |
|
|
|
An |
|
How a JSON-ready |
|
SQLite-backed |
|
PostgreSQL-backed |
Exceptions
Raised when opening a |
|
Raised when a store migration step is missing or fails. |
|
Raised on opening a database whose schema_version differs. |
|
Raised when an insert would violate a per-tier no-overlap constraint. |
|
Raised when an annotation's interval cannot be represented at the project rate. |
- class lacing.store.IntervalAnnotationStore(*args, **kwargs)[source]#
Bases:
ProtocolProtocol for any interval-keyed annotation store.
Conceptually a
MutableMapping[TimeInterval, list[Annotation]]: keys areTimeInterval; values are lists because multiple annotations can share an interval (different tiers, multiple annotators, soft labels).We use
Protocolrather than inheriting fromMutableMappingso backends (in-memory, SQLite, Postgres) can structurally conform without forcing a single class hierarchy. The mapping methods below match theMutableMappingABC; concrete backends likeMemoryStoreimplement the full interface.- contains(query)[source]#
Annotations whose interval strictly contains
query(Allendi).- Return type:
- extend(annotations)[source]#
Add many; equivalent to repeated
.addbut adapters can optimize.- Return type:
- lock(key)[source]#
Exclusive lock named
key, held for thewithblock.Scoped to this store’s data (for
PostgresStore, its owner and project): holders of the same key exclude each other, different keys do not. The reach of the exclusion is the backend’s:MemoryStoreexcludes threads of this process,SqliteStorealso other processes on the host,PostgresStoreevery client of the database, on any host. A holder that dies releases the lock. Not re-entrant.- Return type:
- relate(query, relations)[source]#
Annotations whose interval has any of the named
relationstoquery.Generic dispatch — useful when relations are computed at runtime.
- Return type:
- remove(annotation_id)[source]#
Remove and return the annotation with this id, or None if absent.
- Return type:
- tiers()[source]#
Registered tiers. Annotations may reference tiers not yet registered; callers decide whether that’s an error.
- update(annotation)[source]#
Replace the stored annotation that has
annotation.id, atomically.One commit: a concurrent reader sees the old annotation or the new one, never neither (
remove(id)thenadd(...)leaves a gap in between). The replacement may move to a different interval or tier. RaisesKeyErrorwhen no annotation has that id — useadd()for a new one.- Return type:
- class lacing.store.MappingCodec(encode, decode)[source]#
Bases:
NamedTupleHow a JSON-ready
dictis turned into what the mapping stores, and back.
- class lacing.store.MappingStore(mapping, *, codec=(<function <lambda>>, <function <lambda>>), tiers_key='__lacing_tiers__')[source]#
Bases:
MemoryStoreAn
IntervalAnnotationStorepersisted to an injectedMutableMapping.>>> from lacing.store import MappingStore >>> backing = {} >>> store = MappingStore(backing) >>> len(backing) # nothing written until something is added 0
Reopening over the same mapping sees the same annotations and tiers.
Raises
ValueErroron a duplicate annotation id (asSqliteStoredoes) and when an annotation is assigned under a key that is not its own interval; both would otherwise not survive a reload.
- class lacing.store.MemoryStore[source]#
Bases:
objectIntervalAnnotationStoreimplementation overintervaltree.Conforms to the protocol in
lacing.store.base. We don’t formally inherit fromIntervalAnnotationStorebecause it’s aProtocolwith method bodies — structural typing is enough.
- exception lacing.store.PgSchemaMismatchError[source]#
Bases:
RuntimeErrorRaised on opening a database whose schema_version differs.
- class lacing.store.PostgresStore(connection_string, *, rate=24000, owner_id='default', project_id='default', autocommit=True, use_pool=True)[source]#
Bases:
objectPostgreSQL-backed
IntervalAnnotationStore, scoped to one tenant.A store instance is bound to one
(owner_id, project_id)pair; multiple instances over the same database (different project ids) coexist without seeing each other’s annotations or tiers (see the module docstring on multi-tenancy). The mapping/Allen/tier surface is unchanged — the tenant scoping is transparent.- Parameters:
connection_string (
str|dict) – A psycopg-compatible conninfo URL, or a kwargs dict (host/port/user/password/dbname).rate (
int) – Project-wide rate for this(owner, project). Set on first init for the pair; re-opening the same pair with a different rate raisesPgSchemaMismatchError. Different projects in the same DB may have different rates.owner_id (
str) – Tenant owner. Defaults toDEFAULT_OWNER_ID— the forward seam for the access layer (reelee#174); enforcement deferred.project_id (
str) – Logical project key. Defaults toDEFAULT_PROJECT_ID.nwpasses the project’sproject_asset_idhere.autocommit (
bool) – If True (default), statements run in their own transaction.use_pool (
bool) – When True (default) and the connection is given as a string, borrow a connection from a process-wide pool keyed by the conninfo (amortizes per-op connect cost; seeget_pool()). Falls back to a dedicated connection whenpsycopg_poolis unavailable, when the connection is given as a dict, or whenuse_pool=False.
- lock(key)[source]#
Exclusive lock named
keyfor this(owner, project), across hosts.A session-level
pg_advisory_lockkeyed on a 64-bit hash ofowner_id+project_id+key, taken on a dedicated connection (never the store’s own, which other threads and transactions share) and released infinally. If the holder crashes, Postgres drops the lock when its connection dies, so nothing can go stale. Blocks until available; not re-entrant.
- update(annotation)[source]#
Replace the row with this id in ONE
UPDATEstatement (lacing#66).A single statement is atomic in any isolation mode, so a concurrent reader sees the old row or the new one — unlike
removethenadd, which commit twice. RaisesKeyErrorif the id is not in this(owner, project)andTierOverlapErrorlikeadd().- Return type:
- exception lacing.store.RateMismatchError[source]#
Bases:
LossyTimeConversionErrorRaised when an annotation’s interval cannot be represented at the project rate.
- exception lacing.store.SchemaMismatchError[source]#
Bases:
RuntimeErrorRaised when opening a
.annotfile with an incompatible schema.
- class lacing.store.SqliteStore(path, *, check_same_thread=True, migrate=False)[source]#
Bases:
objectSQLite-backed
IntervalAnnotationStore.- Parameters:
path (
str|PathLike) – Path to the.annotfile. Use":memory:"for an ephemeral in-memory database.check_same_thread (
bool) – Forwarded tosqlite3.connect. We hold a single connection guarded by a lock; passFalsewhen sharing across threads.migrate (
bool) – Opt-in to upgrading a file written at an olderschema_versionon open, via the ladder inlacing.store.migrations. Off by default — silently rewriting someone’s file on open is worse than refusing. Every open-time schema failure — refusal or failed migration — raisesSchemaMismatchError; a failed migration chains the ladder’sStoreMigrationErroras its cause.
- lock(key)[source]#
Exclusive lock named
key: a file lock beside the database.Excludes other threads and other processes on this host that open the same
.annotfile; the kernel frees it if the holder dies. An in-memory database (":memory:") has no file, so it falls back to a process-local lock.- Return type:
- update(annotation)[source]#
Replace the row with this id (and its R*Tree entry) in one transaction.
Readers take the same lock (
_fetchall()), so on a connection shared between threads — where a reader would otherwise see this transaction’s uncommitted state — they get the old annotation or the new one, never neither (lacing#66).- Return type:
- exception lacing.store.StoreMigrationError[source]#
Bases:
RuntimeErrorRaised when a store migration step is missing or fails.
- exception lacing.store.TierOverlapError[source]#
Bases:
RuntimeErrorRaised when an insert would violate a per-tier no-overlap constraint.
- lacing.store.close_all_pools()[source]#
Close every registered connection pool (test teardown / shutdown).
- Return type:
- lacing.store.get_pool(connection_string, **pool_kwargs)[source]#
Return a process-wide
psycopg_pool.ConnectionPoolfor a conninfo.One pool per distinct
connection_string(the keying happens here, not at the call-site), created lazily on first request. RaisesImportErrorifpsycopg_poolis not installed — callers that want a graceful fallback catch it (seePostgresStore).- Parameters:
connection_string (
str) – psycopg conninfo URL (the registry key).**pool_kwargs – forwarded to
ConnectionPoolon first creation (e.g.min_size,max_size); ignored on reuse.
- Returns:
The shared pool for this conninfo.
- lacing.store.migrate_annot_file(path, *, to_version=None)[source]#
Migrate a
.annotfile in place, returning(from, to)versions.to_versiondefaults to the current build’slacing.store.sqlite.SCHEMA_VERSION. Already-current files are a no-op (from == to). Each step runs in its ownBEGIN IMMEDIATEtransaction with the version re-checked under the lock, so concurrent migrators converge and an interrupted chain resumes from the last version that completed (idempotent).Raises
StoreMigrationErrorwhen the file does not exist, is not a.annotfile, a step is missing, fails, or breaks one of the runner’s in-transaction guarantees.
- lacing.store.reachable_versions(store_kind, from_version)[source]#
Versions reachable from
from_versionby chaining registered steps.Ascending, excluding
from_versionitself. Empty when no step leavesfrom_version— which is what a refusal message should say out loud instead of the bare “run a migration” it used to say.
- lacing.store.rebuild_annotations_rtree(conn)[source]#
Rebuild the interval index from the
annotationstable, in place.For use inside a migration step after a table rebuild. Reproduces the store’s ULP-widening contract (bounds widened by one float ULP so float→exact-bound comparisons never drop hits — see
lacing.store.sqlite). Returns the number of rows indexed.- Return type:
- lacing.store.register_store_migration(*, store_kind, from_version, to_version)[source]#
Register a forward store migration from
from_versiontoto_version.The decorated function takes the backend’s open connection and must perform every change of the step — DDL, row rewrites, and the
meta.schema_versionwrite. Steps must be one version at a time (to_version == from_version + 1); the runner chains them.Step authors: read the module docstring’s rules — no
executescript/COMMIT/ROLLBACKinside a step, preserve rowids on table rebuilds, and rebuild the interval index withrebuild_annotations_rtree()if theannotationstable was rebuilt.Re-registering the same
(store_kind, from_version)pair replaces the previous entry.
Modules
|
|
Named exclusive locks for stores: the mechanisms behind |
|
|
|
In-memory annotation store backed by an interval tree. |
|
Store-level schema migrations — the on-disk counterpart of the body ladder. |
|
PostgreSQL-backed annotation store using |
|
SQLite-backed annotation store and the |