Skip to content

Commit 385b287

Browse files
committed
feat(grz-db,grz-common): clean up populate API surface
Four related changes to the populate-side public surface: - Split 'force' from 'on_missing' on SubmissionDb.populate. 'force' previously double-duty'd as 'allow destructive overwrites' AND 'silently treat a missing submission as fresh-create'. Add explicit 'on_missing: Literal["create", "error"]' defaulting to 'error' (raises SubmissionNotFoundError, matching grzctl 'db submission populate'). The download path opts into 'create' explicitly. - Drop the now-redundant implicit 'force=True' after auto-create. Verified: 'Diff.classify' produces only NEW states when old_value is None, so 'has_pending_destructive' is always False for a fresh row. - Drop the 'log: logging.Logger | None' parameter. Use the module logger throughout. Idiomatic Python; callers configure via 'logging.getLogger("grz_db.models.submission")'. - Drop Worker.load_metadata_with_submission_date. It bundled an S3 head_object and a local file read into one tuple-returning method to keep the download.py call site short, but those are two distinct concerns. download.py now calls 'get_metadata_upload_timestamp' and 'worker_inst.parse_submission().metadata.content' separately.
1 parent 0205cc4 commit 385b287

4 files changed

Lines changed: 50 additions & 57 deletions

File tree

packages/grz-common/src/grz_common/workers/worker.py

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
from __future__ import annotations
44

5-
import datetime
65
import logging
76
from os import PathLike
87
from pathlib import Path
@@ -12,12 +11,10 @@
1211
EncryptionError,
1312
IncompleteSubmissionError,
1413
)
15-
from grz_pydantic_models.submission.metadata import GrzSubmissionMetadata
1614

1715
from ..models.identifiers import IdentifiersModel
1816
from ..models.s3 import S3Options
1917
from ..progress import EncryptionState, FileProgressLogger, ValidationState
20-
from ..transfer import get_metadata_upload_timestamp, init_s3_client
2118
from .download import S3BotoDownloadWorker
2219
from .submission import EncryptedSubmission, Submission, SubmissionValidationError
2320
from .upload import S3BotoUploadWorker, UploadError
@@ -312,25 +309,3 @@ def download(self, s3_options: S3Options, submission_id: str, force: bool = Fals
312309

313310
self.__log.info("Downloading encrypted files...")
314311
download_worker.download(submission_id, EncryptedSubmission(self.metadata_dir, self.encrypted_files_dir))
315-
316-
def load_metadata_with_submission_date(
317-
self, s3_options: S3Options, submission_id: str
318-
) -> tuple[GrzSubmissionMetadata, datetime.date]:
319-
"""Load the downloaded metadata.json and fetch its S3 last-modified date.
320-
321-
:param s3_options: S3 connection options for the inbox bucket.
322-
:param submission_id: Submission identifier (the top-level S3 prefix).
323-
:returns: The parsed :class:`GrzSubmissionMetadata` plus the inbox upload
324-
date derived from
325-
:func:`grz_common.transfer.get_metadata_upload_timestamp`, suitable
326-
for feeding into
327-
:meth:`grz_db.models.submission.SubmissionDb.populate`.
328-
"""
329-
s3_client = init_s3_client(s3_options)
330-
submission_date = get_metadata_upload_timestamp(s3_client, s3_options.bucket, submission_id).date()
331-
332-
metadata_file_path = self.metadata_dir / "metadata.json"
333-
with open(metadata_file_path) as fd:
334-
metadata = GrzSubmissionMetadata.model_validate_json(fd.read())
335-
336-
return metadata, submission_date

packages/grz-db/src/grz_db/models/submission/__init__.py

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from collections.abc import Generator, Sequence
88
from contextlib import contextmanager
99
from operator import attrgetter
10-
from typing import Any, ClassVar, Optional, Self
10+
from typing import Any, ClassVar, Literal, Optional, Self
1111

1212
import sqlalchemy as sa
1313
import sqlalchemy.dialects.postgresql as sa_psql
@@ -1251,36 +1251,35 @@ def _log_pending_changes(
12511251
submission_id: str,
12521252
submission_diff: SubmissionDiffCollection,
12531253
donors_diff: DonorsDiffCollection,
1254-
log: logging.Logger,
12551254
) -> None:
12561255
"""Emit info-level log lines summarising what is about to be committed."""
12571256
sid = f"Submission: {submission_id}"
12581257

12591258
pending_keys = [d.key for d in submission_diff.pending]
12601259
unchanged_keys = [d.key for d in submission_diff.unchanged]
12611260
if pending_keys:
1262-
log.info("%s - Updating fields: %s in database", sid, ", ".join(f'"{k}"' for k in pending_keys))
1261+
logger.info("%s - Updating fields: %s in database", sid, ", ".join(f'"{k}"' for k in pending_keys))
12631262
if unchanged_keys:
1264-
log.info("%s - Not updating fields: %s in database", sid, ", ".join(f'"{k}"' for k in unchanged_keys))
1263+
logger.info("%s - Not updating fields: %s in database", sid, ", ".join(f'"{k}"' for k in unchanged_keys))
12651264

12661265
if donors_diff.unchanged:
1267-
log.info(
1266+
logger.info(
12681267
"%s - Keep existing donor(s): %s", sid, ", ".join(f'"{d.pseudonym}"' for d in donors_diff.unchanged)
12691268
)
12701269
if donors_diff.added:
1271-
log.info(
1270+
logger.info(
12721271
"%s - Adding new donor(s): %s",
12731272
sid,
12741273
", ".join(f'"{d.pseudonym}"' for d in donors_diff.added),
12751274
)
12761275
if donors_diff.updated:
1277-
log.info(
1276+
logger.info(
12781277
"%s - Modifying existing donor(s): %s",
12791278
sid,
12801279
", ".join(f'"{d.pseudonym}"' for d in donors_diff.updated),
12811280
)
12821281
if donors_diff.deleted:
1283-
log.info("%s - Dropping donor(s): %s", sid, ", ".join(f'"{d.pseudonym}"' for d in donors_diff.deleted))
1282+
logger.info("%s - Dropping donor(s): %s", sid, ", ".join(f'"{d.pseudonym}"' for d in donors_diff.deleted))
12841283

12851284
@staticmethod
12861285
def assert_metadata_not_redacted(
@@ -1317,38 +1316,44 @@ def populate( # noqa: PLR0913
13171316
submission_date: datetime.date | None,
13181317
*,
13191318
force: bool = False,
1319+
on_missing: Literal["create", "error"] = "error",
13201320
ignore_fields: set[str] | None = None,
1321-
log: logging.Logger | None = None,
13221321
) -> None:
13231322
"""Reconcile DB state for ``submission_id`` with ``metadata``.
13241323
1325-
Creates the submission row if absent (then forces overwrite). Rejects
1326-
redacted ``tan_g`` or missing/redacted ``local_case_id`` via
1324+
Rejects redacted ``tan_g`` or missing/redacted ``local_case_id`` via
13271325
:meth:`assert_metadata_not_redacted` unless the corresponding key
13281326
(``"tan_g"`` or ``"pseudonym"``) is in ``ignore_fields``. Computes diffs
13291327
via :meth:`diff`, rejects destructive changes unless ``force``, and
1330-
commits via :meth:`commit_changes`.
1328+
commits via :meth:`commit_changes`. Operational progress is logged via
1329+
the module-level logger; callers configure verbosity through
1330+
``logging.getLogger("grz_db.models.submission")``.
13311331
13321332
:param submission_id: ID of the submission being populated.
13331333
:param metadata: Parsed submission metadata.
13341334
:param submission_date: S3 last-modified date of the metadata file, if known.
1335-
:param force: If ``True``, allow destructive updates/deletes.
1335+
:param force: If ``True``, allow destructive updates/deletes of existing fields.
1336+
:param on_missing: What to do when ``submission_id`` is not yet in the
1337+
database. ``"error"`` (default) raises :class:`SubmissionNotFoundError`.
1338+
``"create"`` calls :meth:`add_submission` first and then proceeds; the
1339+
resulting diff against the fresh row contains only additive changes,
1340+
so ``force`` does not need to be set.
13361341
:param ignore_fields: Field keys to skip during diff and redaction
13371342
validation. See :meth:`assert_metadata_not_redacted`.
1338-
:param log: Logger for info/warning summaries; defaults to module logger.
1343+
:raises SubmissionNotFoundError: if the submission row is absent and
1344+
``on_missing`` is ``"error"``.
13391345
:raises ValueError: if ``tan_g`` or ``local_case_id`` is redacted/missing
13401346
and the corresponding key is not in ``ignore_fields``.
13411347
:raises RuntimeError: if pending changes are destructive and ``force`` is False.
13421348
"""
1343-
if log is None:
1344-
log = logger
13451349
ignore_fields = ignore_fields or set()
13461350

13471351
if not self.get_submission(submission_id):
1348-
log.warning("Submission %s does not exist. Creating ...", submission_id)
1352+
if on_missing == "error":
1353+
raise SubmissionNotFoundError(submission_id)
1354+
logger.warning("Submission %s does not exist. Creating ...", submission_id)
13491355
self.add_submission(submission_id)
1350-
log.debug("Submission %s added to database. Force populate", submission_id)
1351-
force = True
1356+
logger.debug("Submission %s added to database.", submission_id)
13521357

13531358
self.assert_metadata_not_redacted(metadata, submission_id, ignore_fields)
13541359

@@ -1367,8 +1372,8 @@ def populate( # noqa: PLR0913
13671372
)
13681373

13691374
if submission_diff.has_pending or donors_diff.has_pending:
1370-
log.info("Submission: %s - Updating...", submission_id)
1371-
self._log_pending_changes(submission_id, submission_diff, donors_diff, log)
1375+
logger.info("Submission: %s - Updating...", submission_id)
1376+
self._log_pending_changes(submission_id, submission_diff, donors_diff)
13721377
self.commit_changes(submission_id, submission_diff, donors_diff)
13731378
else:
1374-
log.info("Submission: %s - No updates necessary.", submission_id)
1379+
logger.info("Submission: %s - No updates necessary.", submission_id)

packages/grzctl/src/grzctl/commands/download.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import click
88
import grz_common.cli as grzcli
9+
from grz_common.transfer import get_metadata_upload_timestamp, init_s3_client
910
from grz_common.workers.worker import Worker
1011
from grz_db.models.submission import SubmissionStateEnum
1112

@@ -72,7 +73,15 @@ def download( # noqa: PLR0913
7273
if not db_context.db:
7374
log.warning("Database context is not available, skipping population of submission metadata in DB.")
7475
else:
75-
metadata, submission_date = worker_inst.load_metadata_with_submission_date(config.s3, submission_id)
76-
db_context.db.populate(submission_id, metadata, submission_date, force=force, log=log)
76+
s3_client = init_s3_client(config.s3)
77+
submission_date = get_metadata_upload_timestamp(s3_client, config.s3.bucket, submission_id).date()
78+
metadata = worker_inst.parse_submission().metadata.content
79+
db_context.db.populate(
80+
submission_id,
81+
metadata,
82+
submission_date,
83+
force=force,
84+
on_missing="create",
85+
)
7786

7887
log.info("Download finished!")

tests/cli/test_db_context.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -270,15 +270,19 @@ def test_db_wrappers(
270270
)
271271

272272
with mock_command(command_spec, submission_id) as (mock_worker, mock_extra):
273+
# download.py's --populate path calls `worker.parse_submission().metadata.content`
274+
# plus `get_metadata_upload_timestamp(...)`. Configure the mocked Worker and patch
275+
# the S3 helpers so the populate runs end-to-end against the real db. Harmless for
276+
# commands that never call these.
273277
if mock_worker is not None:
274-
# download.py's --populate path calls Worker.load_metadata_with_submission_date
275-
# and unpacks (metadata, submission_date). Provide a usable return value so the
276-
# mocked Worker plays along; harmless for commands that never call the method.
277-
mock_worker.load_metadata_with_submission_date.return_value = (
278-
parsed_metadata,
279-
datetime.date.today(),
280-
)
281-
result = runner.invoke(cli, args)
278+
mock_worker.parse_submission.return_value.metadata.content = parsed_metadata
279+
with (
280+
patch("grzctl.commands.download.init_s3_client") as mock_init_s3,
281+
patch("grzctl.commands.download.get_metadata_upload_timestamp") as mock_get_ts,
282+
):
283+
mock_init_s3.return_value = MagicMock()
284+
mock_get_ts.return_value.date.return_value = datetime.date.today()
285+
result = runner.invoke(cli, args)
282286

283287
assert result.exit_code == 0, f"Command failed: {result.output}"
284288

0 commit comments

Comments
 (0)