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
The following commit(s) were added to refs/heads/arm by this push:
new 81ee214c #1010 - move distribution web validation to work on API calls
as well, and support field-level errors in model_validator functions.
81ee214c is described below
commit 81ee214c02b7d436300c22a555521357498c2c19
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]