This is an automated email from the ASF dual-hosted git repository.

arm pushed a commit to branch arm
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git

commit 8c7de3418e343fc8352fcaa8622e3401712c3ed2
Author: Alastair McFarlane <[email protected]>
AuthorDate: Fri Apr 3 13:30:21 2026 +0100

    #1010 - move distribution web validation to work on API calls as well, and 
support field-level errors in model_validator functions.
---
 atr/api/__init__.py        |  4 ++++
 atr/form.py                | 18 +++++++++++++++---
 atr/models/safe.py         |  5 +++++
 atr/shared/distribution.py | 20 ++++----------------
 atr/util.py                | 11 +++++++++++
 5 files changed, 39 insertions(+), 19 deletions(-)

diff --git a/atr/api/__init__.py b/atr/api/__init__.py
index 9cca2570..71756c40 100644
--- a/atr/api/__init__.py
+++ b/atr/api/__init__.py
@@ -323,6 +323,7 @@ async def distribution_record(
     Record a manual distribution.
     """
     asf_uid = _jwt_asf_uid()
+    util.validate_distribution_owner_namespace(data.platform, 
data.distribution_owner_namespace)
     async with db.session() as db_data:
         release = await db_data.release(
             project_key=str(data.project),
@@ -364,6 +365,7 @@ async def distribution_record_from_workflow(
     Record the result of an automated distribution from the GH tooling-actions 
workflow.
     Validates the caller is a Github workflow, triggered by ATR itself
     """
+
     _payload, _asf_uid, _project, release = await 
interaction.trusted_jwt_for_dist(
         data.publisher,
         data.jwt,
@@ -372,6 +374,7 @@ async def distribution_record_from_workflow(
         data.project,
         data.version,
     )
+    util.validate_distribution_owner_namespace(data.platform, 
data.distribution_owner_namespace)
     # TODO: Split the below code into a new function and reuse in /publisher 
and /distribution / record.
     if release.committee is None:
         raise exceptions.NotFound(f"Release {release.key} has no committee")
@@ -838,6 +841,7 @@ async def publisher_distribution_record(
             data.jwt,
             interaction.TrustedProjectPhase.COMPOSE,
         )
+    util.validate_distribution_owner_namespace(data.platform, 
data.distribution_owner_namespace)
     async with db.session() as db_data:
         release = await db_data.release(
             project_key=project.key,
diff --git a/atr/form.py b/atr/form.py
index f84a7e41..507e12bf 100644
--- a/atr/form.py
+++ b/atr/form.py
@@ -99,12 +99,24 @@ def flash_error_data(
 
     for i, error in enumerate(errors):
         loc = error["loc"]
-        kind = error["type"]
         msg = error["msg"]
-        msg = msg.replace(": An email address", " because an email address")
-        msg = msg.replace("Value error, ", "")
+        kind = error["type"]
         original = error["input"]
         field_name, field_label = name_and_label(concrete_cls, i, loc)
+        if not loc:
+            ctx_error = error.get("ctx", {}).get("error")
+            # If we have a field name in a ValueError, make sure we get the 
right message and original value
+            # This comes from raising a `ValueError` in a 
`pydantic.model_validator`
+            if isinstance(ctx_error, ValueError) and len(ctx_error.args) >= 2 
and isinstance(ctx_error.args[1], str):
+                loc = (ctx_error.args[1],)
+                msg = ctx_error.args[0]
+            # Now reconstruct the field_name and label
+            field_name, field_label = name_and_label(concrete_cls, i, loc)
+            if isinstance(original, dict):
+                original = original[field_name]
+        msg = msg.replace(": An email address", " because an email address")
+        msg = msg.replace("Value error, ", "")
+
         flash_data[field_name] = {
             "label": field_label,
             "original": json_suitable(original),
diff --git a/atr/models/safe.py b/atr/models/safe.py
index 61ba28f5..7996be87 100644
--- a/atr/models/safe.py
+++ b/atr/models/safe.py
@@ -186,6 +186,11 @@ def _strip_slashes_or_none(v: object) -> object:
     return v
 
 
+type OptionalAlphanumeric = Annotated[
+    Alphanumeric | None,
+    pydantic.BeforeValidator(_strip_slashes_or_none),
+]
+
 type OptionalRelPath = Annotated[
     RelPath | None,
     pydantic.BeforeValidator(_strip_slashes_or_none),
diff --git a/atr/shared/distribution.py b/atr/shared/distribution.py
index cc7d41bf..f8046a3d 100644
--- a/atr/shared/distribution.py
+++ b/atr/shared/distribution.py
@@ -126,7 +126,7 @@ class DistributionAutomateForm(form.Form):
     platform: form.Enum[DistributionPlatform] = form.label(
         "Platform", widget=form.Widget.SELECT, 
enum_filter_include=[DistributionPlatform.MAVEN.value]
     )
-    owner_namespace: safe.Alphanumeric = form.label(
+    owner_namespace: safe.OptionalAlphanumeric = form.label(
         "Owner or Namespace",
         "Who owns or names the package (Maven groupId, npm @scope, Docker 
namespace, "
         "GitHub owner, ArtifactHub repo). Leave blank if not used.",
@@ -140,26 +140,20 @@ class DistributionAutomateForm(form.Form):
 
     @pydantic.model_validator(mode="after")
     def validate_owner_namespace(self) -> DistributionAutomateForm:
-        platform_name: str = self.platform.name  # type: ignore[attr-defined]
         sql_platform = self.platform.to_sql()  # type: ignore[attr-defined]
         default_owner_namespace = sql_platform.value.default_owner_namespace
-        requires_owner_namespace = sql_platform.value.requires_owner_namespace
 
         if default_owner_namespace and (not self.owner_namespace):
             self.owner_namespace = default_owner_namespace
 
-        if requires_owner_namespace and (not self.owner_namespace):
-            raise ValueError(f'Platform "{platform_name}" requires an owner or 
namespace.')
-
-        if (not requires_owner_namespace) and (not default_owner_namespace) 
and self.owner_namespace:
-            raise ValueError(f'Platform "{platform_name}" does not require an 
owner or namespace.')
+        util.validate_distribution_owner_namespace(sql_platform, 
self.owner_namespace)
 
         return self
 
 
 class DistributionRecordForm(form.Form):
     platform: form.Enum[DistributionPlatform] = form.label("Platform", 
widget=form.Widget.SELECT)
-    owner_namespace: safe.Alphanumeric = form.label(
+    owner_namespace: safe.OptionalAlphanumeric = form.label(
         "Owner or Namespace",
         "Who owns or names the package (Maven groupId, npm @scope, Docker 
namespace, "
         "GitHub owner, ArtifactHub repo). Leave blank if not used.",
@@ -173,19 +167,13 @@ class DistributionRecordForm(form.Form):
 
     @pydantic.model_validator(mode="after")
     def validate_owner_namespace(self) -> DistributionRecordForm:
-        platform_name: str = self.platform.name  # type: ignore[attr-defined]
         sql_platform = self.platform.to_sql()  # type: ignore[attr-defined]
         default_owner_namespace = sql_platform.value.default_owner_namespace
-        requires_owner_namespace = sql_platform.value.requires_owner_namespace
 
         if default_owner_namespace and (not self.owner_namespace):
             self.owner_namespace = default_owner_namespace
 
-        if requires_owner_namespace and (not self.owner_namespace):
-            raise ValueError(f'Platform "{platform_name}" requires an owner or 
namespace.')
-
-        if (not requires_owner_namespace) and (not default_owner_namespace) 
and self.owner_namespace:
-            raise ValueError(f'Platform "{platform_name}" does not require an 
owner or namespace.')
+        util.validate_distribution_owner_namespace(sql_platform, 
self.owner_namespace)
 
         return self
 
diff --git a/atr/util.py b/atr/util.py
index b716a98c..e8088cea 100644
--- a/atr/util.py
+++ b/atr/util.py
@@ -1154,6 +1154,17 @@ def validate_as_type[T](value: Any, t: type[T]) -> T:
     return value
 
 
+def validate_distribution_owner_namespace(platform: sql.DistributionPlatform, 
namespace: Any):
+    default_owner_namespace = platform.value.default_owner_namespace
+    requires_owner_namespace = platform.value.requires_owner_namespace
+
+    if requires_owner_namespace and (not namespace):
+        raise ValueError(f'Platform "{platform.value.name}" requires an owner 
or namespace.', "owner_namespace")
+
+    if (not requires_owner_namespace) and (not default_owner_namespace) and 
namespace:
+        raise ValueError(f'Platform "{platform.value.name}" does not require 
an owner or namespace.', "owner_namespace")
+
+
 def validate_email_recipients(recipients: EmailRecipients) -> None:
     if not recipients.email_to:
         raise ValueError("At least one To recipient is required")


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to