herostools.actor.archiver.postgres¶
Attributes¶
Classes¶
Pure SQL layer for the archiver schema — stores and retrieves clean JSON. |
|
Yield batches of changed record identifiers from PostgreSQL notifications. |
|
HEROS-independent PostgreSQL record store with artifact handling. |
|
HEROS event adapter for |
Functions¶
|
Return the UTC daily bucket derived from a storage default bucket. |
|
Recursively replace numpy arrays with artifact refs; drop other non-JSON values. |
|
Recursively collect all artifact_ref dicts. |
|
Recursively replace artifact_ref dicts using a pre-fetched id->array lookup. |
Module Contents¶
- herostools.actor.archiver.postgres._DROPPED¶
- herostools.actor.archiver.postgres._daily_artifact_bucket(default_bucket: str | None, now: datetime.datetime | None = None) str | None[source]¶
Return the UTC daily bucket derived from a storage default bucket.
- Parameters:
default_bucket – Static bucket used only for legacy-reference fallback.
now – Optional timestamp, used by tests to make routing deterministic.
- Returns:
Daily bucket name, or None for storage backends without buckets.
- herostools.actor.archiver.postgres._substitute_artifacts(obj: Any, bucket: str | None = None) tuple[Any, list[tuple[uuid.UUID, str, numpy.ndarray]]][source]¶
Recursively replace numpy arrays with artifact refs; drop other non-JSON values.
The artifact name for each array is its dot-separated key path within the original dict (e.g. “sensor.spectrum” for a nested key, “items.1” for a list element at index 1). Each artifact_ref dict includes intrinsic array metadata:
size_bytes(element data only, excluding the npy header),shape, anddtype.bucketis the storage bucket name at write time (Nonewhen no storage backend is configured).- Parameters:
obj – Dict or list to walk.
bucket – Bucket name to embed in every artifact_ref produced.
- Returns:
Tuple of (cleaned_obj, artifacts) where artifacts is a list of (artifact_id, artifact_name, array) triples.
- herostools.actor.archiver.postgres._collect_artifact_refs(obj: Any) list[dict][source]¶
Recursively collect all artifact_ref dicts.
- Parameters:
obj – Dict, list, or scalar to walk.
- Returns:
List of artifact_ref dicts found.
- herostools.actor.archiver.postgres._resolve_artifacts_batch(obj: Any, lookup: dict[str, numpy.ndarray]) Any[source]¶
Recursively replace artifact_ref dicts using a pre-fetched id->array lookup.
- Parameters:
obj – Dict, list, or scalar to walk.
lookup – Mapping of artifact_id strings to numpy arrays.
- Returns:
Object with all artifact_ref dicts replaced by numpy arrays.
- class herostools.actor.archiver.postgres.PostgresDatabase(db_url: str, identifier_key: str = 'identifier', allow_purge: bool = False, retention_days: int = 0)[source]¶
Pure SQL layer for the archiver schema — stores and retrieves clean JSON.
Can be used independently of HEROS to insert, query, and delete records. Payloads must be clean JSON dicts (no numpy arrays). Artifact references stored in
data_jsonare returned as plain dicts byget()without resolution; usePostgresArchiverfor the full artifact pipeline.Each identifier maps to exactly one row in
records. Successive payloads for the same identifier are shallow-merged (||) at write time; later events win on key collision.- Parameters:
db_url – psycopg connection string.
identifier_key – Key used to look up the record identifier in the payload.
allow_purge – Enable the
purge()method (disabled by default).retention_days – Default retention window in days used by
_find_expired().0means keep forever.
- _db_url¶
- _local¶
- _identifier_key = 'identifier'¶
- _allow_purge = False¶
- _retention_days = 0¶
- _get_conn() psycopg.Connection[source]¶
Return a per-thread connection, opening one if necessary.
psycopg connections are not thread-safe; each thread owns its own connection.
- _store(payload: dict) str | None[source]¶
Upsert a clean JSON payload into records, merging into any existing row.
The identifier is extracted from the payload using
_identifier_key. ReturnsNoneand logs an error when the identifier key is missing.- Parameters:
payload – Clean JSON dict to store. Must not contain numpy arrays.
- Returns:
The identifier string, or
Noneif the identifier key is absent.
- get_ids(tag: str, values: list[Any]) list[str][source]¶
Return identifiers matching the given tag/key in either source.
Searches
identifier_groups(post-hoc tags) andrecords.data_json(payload fields). Results are deduplicated and sorted.- Parameters:
tag – Tag key (
identifier_groups) or top-leveldata_jsonkey.values – Accepted values (cast to str for comparison).
- Returns:
Sorted, deduplicated list of matching identifiers.
- get(ids: list[str]) dict[str, dict][source]¶
Fetch the merged record for each identifier, including post-hoc tags.
Tags stored via
tag_ids()are merged into the returned dict alongsidedata_jsonfields. Tags overwritedata_jsonvalues on key collision. Artifact references remain as plain dicts in the result.- Parameters:
ids – Identifiers to fetch.
- Returns:
Dict mapping identifier to the merged record dict. Identifiers with no record are absent from the result.
- tag_ids(ids: list[str], tag: str, value: str) None[source]¶
Attach a tag to a list of identifiers, overwriting any existing value for that tag.
Inserts
(identifier, tag, value)intoidentifier_groupsfor each identifier. If the(identifier, tag)pair already exists the value is updated in place.- Parameters:
ids – Identifiers to tag. Each must already exist in
records.tag – Tag key.
value – Tag value to assign.
- Raises:
psycopg.errors.ForeignKeyViolation – If any identifier is not present in
records.
- purge(ids: list[str]) None[source]¶
Delete DB rows for the given identifiers from records and identifier_groups.
Requires
allow_purge=Trueat instantiation time. Does not touch artifact storage; usePostgresArchiver.purge()for that.- Parameters:
ids – Identifiers to delete.
- Raises:
PermissionError – If
allow_purgewas not set to True.
- class herostools.actor.archiver.postgres.PostgresRecordChangeListener(db_url: str)[source]¶
Yield batches of changed record identifiers from PostgreSQL notifications.
- Parameters:
db_url – Psycopg connection string.
- _db_url¶
- herostools.actor.archiver.postgres._USE_DEFAULT_S3¶
- class herostools.actor.archiver.postgres.PostgresRecordStore(db_url: str, identifier_key: str = 'identifier', allow_purge: bool = False, artifact_storage_kwargs: dict | None = _USE_DEFAULT_S3, array_key_template: str = '{{ source_name }}', use_single_bucket: bool = False, retention_days: int = 0, artifact_storage: herostools.actor.archiver.artifact_storage.ArtifactStorage | None = None)[source]¶
Bases:
PostgresDatabaseHEROS-independent PostgreSQL record store with artifact handling.
Owns the full artifact pipeline: numpy array detection, artifact storage writes and reads, per-bucket resolution, and artifact deletion on purge.
PostgresDatabasehandles only SQL; this class adds the artifact layer. It does not subscribe to HEROS events or PostgreSQL notifications.Each identifier maps to one row in
records. Successive payloads are shallow-merged at write time (||); later events win on key collision. Numpy arrays are replaced by artifact_ref dicts indata_jsonand stored in the configured artifact storage. Each ref embeds the bucket it was written to, enabling transparent multi-bucket resolution and deletion.- Parameters:
db_url – psycopg connection string.
identifier_key – Key used to look up the record identifier.
allow_purge – Enable the
purge()method.artifact_storage_kwargs – Passed as
S3ArtifactStorage(**kwargs). PassNoneto useInMemoryArtifactStorage(no persistence, useful for dev/test). Omit to use local RustFS defaults.array_key_template – Jinja2 template rendered against per-event metadata to form the dict key for bare numpy-array payloads.
source_nameis always available in the template context alongside all metadata fields. Defaults to"{{ source_name }}"which preserves existing behaviour.use_single_bucket – Store new artifacts in the configured static bucket instead of UTC daily buckets. Defaults to False.
retention_days – Default retention window in days used by
cleanup().0(default) means keep forever.artifact_storage – Explicit storage instance. Mutually exclusive with
artifact_storage_kwargs.
- _artifact_storage¶
- _array_key_template¶
- _use_single_bucket = False¶
- store(source_name: str, payload: Any, metadata: dict) None[source]¶
Store one payload after extracting any numpy-array artifacts.
A bare numpy array payload is wrapped as
{template_key: array}first.metadatais shallow-merged into the payload (payload wins on key collision). Arrays are replaced by artifact_ref dicts with the bucket embedded; artifacts are uploaded to storage before the DB write.- Parameters:
source_name – Name of the event source.
payload – The actual data (dict or numpy array).
metadata – Incoming metadata merged with
default_metadata.
- _store(source_name: str, payload: Any, metadata: dict) None[source]¶
Store one payload through the synchronous record-store API.
- Parameters:
source_name – Name of the event source.
payload – The data to store.
metadata – Metadata merged into the payload.
- get(ids: list[str], resolve_artifacts: bool = True, size_limit_bytes: int | None = None) dict[str, dict][source]¶
Fetch merged records and optionally resolve artifact_refs to numpy arrays.
Tags stored via
tag_ids()are merged into the returned dict alongsidedata_jsonfields. Tags overwritedata_jsonvalues on key collision (post-hoc annotations take priority).Artifact_refs are resolved per-bucket: each ref’s stored
bucketfield determines which bucket the array is fetched from. Refs without abucketfield (legacy records) fall back to the storagedefault_bucket.- Parameters:
ids – Identifiers to fetch.
resolve_artifacts – Replace artifact_ref dicts with numpy arrays.
size_limit_bytes – When set, only artifacts whose stored
size_bytesis at or below this threshold are fetched. Larger artifacts remain as artifact_ref dicts. Refs withoutsize_bytesare always resolved.Noneresolves all regardless of size.
- Returns:
Dict mapping identifier to the merged record dict. Identifiers with no record are absent from the result.
- get_artifact(artifact_id: str, bucket: str | None = None) numpy.ndarray[source]¶
Retrieve one decoded artifact from the configured artifact storage.
- Parameters:
artifact_id – Identifier of the artifact to retrieve.
bucket – Bucket override; uses the storage default when None.
- Returns:
The decoded numpy array.
- get_artifacts(locations: collections.abc.Sequence[tuple[str, str | None]]) collections.abc.Mapping[str, numpy.ndarray][source]¶
Retrieve a bounded set of artifacts from the configured storage.
- Parameters:
locations – Artifact ID and optional bucket pairs.
- Returns:
Mapping of artifact IDs to decoded numpy arrays.
- purge(ids: list[str], remove_artifacts: bool = True) None[source]¶
Delete all data for the given identifiers, including stored artifacts.
Artifacts are removed per-bucket using each ref’s stored
bucketfield. Refs without abucketfield fall back todefault_bucket. DB rows are deleted after artifact removal.Requires
allow_purge=Trueat instantiation time.- Parameters:
ids – Identifiers to purge.
remove_artifacts – Also remove referenced artifacts from storage.
- Raises:
PermissionError – If
allow_purgewas not set to True.
- cleanup(retention_days: int | None = None, remove_artifacts: bool = True, dry_run: bool = True) list[str][source]¶
Delete records older than the retention window, skipping saved ones.
Requires
allow_purge=True. Records tagged with_saveset to a truthy value ('1','true','True') are never deleted. Returns the list of expired identifiers (deleted or would-be-deleted).- Parameters:
retention_days – Purge records whose
updated_atis older than this many days.Noneuses the instance default set at construction.0is a no-op (keep forever).remove_artifacts – Also remove referenced artifacts from storage.
dry_run – When True (default), only return the expired identifiers without deleting anything.
- Returns:
List of expired identifiers.
- Raises:
PermissionError – If
allow_purgewas not set to True.
- class herostools.actor.archiver.postgres.PostgresArchiver(db_url: str, identifier_key: str = 'identifier', allow_purge: bool = False, artifact_storage_kwargs: dict | None = _USE_DEFAULT_S3, array_key_template: str = '{{ source_name }}', use_single_bucket: bool = False, retention_days: int = 0, artifact_storage: herostools.actor.archiver.artifact_storage.ArtifactStorage | None = None, *args, **kwargs)[source]¶
Bases:
herostools.actor.archiver.base.HERODataArchiver,PostgresRecordStoreHEROS event adapter for
PostgresRecordStore.Subscribes to incoming data events, queues and retries writes, and publishes
record_changednotifications. UsePostgresRecordStorefor local current-state queries without HEROS or notification connections.- _change_listener¶
- _listener_thread¶
- record_changed(identifiers: list[str]) list[str]¶
Publish changed record identifiers to HEROS subscribers.
- Parameters:
identifiers – Deduplicated identifiers from PostgreSQL notifications.
- Returns:
The published identifiers.