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]

Reply via email to