alitheg commented on code in PR #1376: URL: https://github.com/apache/tooling-trusted-releases/pull/1376#discussion_r3578361049
########## atr/shared/catalogue_diff.py: ########## @@ -0,0 +1,494 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import dataclasses +import enum + +import pydantic + +import atr.shared.catalogue_rows as catalogue_rows + + +class Mode(enum.StrEnum): + # REPLACE clears the committee and re-adds from the files, so every row is an add. ADDITIVE + # matches each row against what is already there, so a row can also move, re-home, or say + # nothing new. The two share their types and their parsing, but not a code path + REPLACE = "replace" + ADDITIVE = "additive" + + [email protected](frozen=True) +class DbSnapshot: + project_committee: dict[str, str] + release_project: dict[str, str] + artifact_by_dist: dict[tuple[str, str], tuple[str, str, str]] + managed_project_keys: frozenset[str] + # Every release key foundation-wide, unlike the workspace-scoped release_project + release_keys: frozenset[str] = dataclasses.field(default_factory=frozenset) + # The workspace committee's artifact primary keys, (project_key, version, artifact_path) + artifact_pks: frozenset[tuple[str, str, str]] = dataclasses.field(default_factory=frozenset) + + [email protected](frozen=True) +class Conflict: + table: str + key: str + reason: str + + [email protected](frozen=True) +class ArtifactRehome: + dist: tuple[str, str] + from_pk: tuple[str, str, str] + to_row: catalogue_rows.ArtifactRow + + [email protected](frozen=True) +class ReleaseMove: + key: str + from_project: str + to_project: str + row: catalogue_rows.ReleaseRow + + [email protected] +class CatalogueDiff: + adds: list[catalogue_rows.Row] = dataclasses.field(default_factory=list) + artifact_rehomes: list[ArtifactRehome] = dataclasses.field(default_factory=list) + release_moves: list[ReleaseMove] = dataclasses.field(default_factory=list) + conflicts: list[Conflict] = dataclasses.field(default_factory=list) + project_deletions: list[str] = dataclasses.field(default_factory=list) + release_deletions: list[str] = dataclasses.field(default_factory=list) + artifact_deletions: list[tuple[str, str, str]] = dataclasses.field(default_factory=list) + warnings: list[str] = dataclasses.field(default_factory=list) + unchanged: int = 0 + # A replace applies as a whole or not at all, so a conflict leaves the committee untouched + refused: bool = False + + @property + def counts(self) -> dict[str, int]: + return { + "add": len(self.adds), + "artifact_rehome": len(self.artifact_rehomes), + "release_move": len(self.release_moves), + "delete": len(self.project_deletions) + len(self.release_deletions) + len(self.artifact_deletions), + "conflict": len(self.conflicts), + "unchanged": self.unchanged, + } + + +def classify( + rows_by_table: dict[str, list[dict[str, str]]], snapshot: DbSnapshot, committee_key: str, mode: Mode +) -> CatalogueDiff: + match mode: + case Mode.REPLACE: + return classify_replace(rows_by_table, snapshot, committee_key) + case Mode.ADDITIVE: + return classify_additive(rows_by_table, snapshot, committee_key) + + +def classify_additive( + rows_by_table: dict[str, list[dict[str, str]]], snapshot: DbSnapshot, committee_key: str +) -> CatalogueDiff: + # Rows are matched against what the committee already holds. A row that matches nothing is an + # add, one that matches somewhere else is a move or a re-home, and one that matches where it + # says it is is unchanged. Nothing is deleted, so a bad row is skipped and the rest still apply + diff = CatalogueDiff() + projects, releases, artifacts = _parse_all(rows_by_table, diff) + _additive_projects(projects, snapshot, diff, committee_key) + _additive_releases(releases, snapshot, diff, committee_key) + _additive_artifacts(artifacts, snapshot, diff, committee_key) + return diff + + +def classify_replace( + rows_by_table: dict[str, list[dict[str, str]]], snapshot: DbSnapshot, committee_key: str +) -> CatalogueDiff: + # The files *are* the committee's catalogue. The deepest table uploaded decides what is deleted, + # which cascades downwards, and every uploaded row becomes a plain add + diff = CatalogueDiff() + snapshot = _clear(rows_by_table, snapshot, diff, committee_key) + projects, releases, artifacts = _parse_all(rows_by_table, diff) + _replace_projects(projects, snapshot, diff, committee_key) + _replace_releases(releases, snapshot, diff, committee_key) + _replace_artifacts(artifacts, snapshot, diff, committee_key) + if diff.conflicts: + # A row that cannot be applied would be deleted and not restored, so the files only describe + # a complete catalogue if every row in them is good + return _refused(diff) + return diff + + +def _additive_artifacts( + rows: list[catalogue_rows.ArtifactRow], snapshot: DbSnapshot, diff: CatalogueDiff, committee_key: str +) -> None: + # An artifact stays within its own committee; a cross-committee correction moves the whole + # release, which carries its artifacts + known_projects = _known_projects(snapshot, diff, committee_key) + known_releases = ( + set(snapshot.release_project) + | {str(add.key) for add in diff.adds if isinstance(add, catalogue_rows.ReleaseRow)} + | {f"{move.to_project}-{move.row.version}" for move in diff.release_moves} + ) + seen_dists: set[tuple[str, str]] = set() + seen_pks: set[tuple[str, str, str]] = set() Review Comment: Thanks for the test, and good catch! It now tracks the resulting primary keys as it goes along, meaning we should catch duplicates that are both in the uploaded files. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
