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

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


The following commit(s) were added to refs/heads/safe_committee_type by this 
push:
     new a1d8b129 #840: rename types to *Key
a1d8b129 is described below

commit a1d8b129350fd7b0273a4504753d9b6f54eaf8a2
Author: Alastair McFarlane <[email protected]>
AuthorDate: Mon Mar 16 15:27:59 2026 +0000

    #840: rename types to *Key
---
 atr/admin/__init__.py                 | 12 +++----
 atr/api/__init__.py                   | 32 ++++++++---------
 atr/attestable.py                     | 32 ++++++++---------
 atr/blueprints/common.py              | 12 +++----
 atr/blueprints/post.py                |  2 +-
 atr/cache.py                          |  2 +-
 atr/construct.py                      | 18 +++++-----
 atr/datasources/apache.py             |  2 +-
 atr/db/__init__.py                    |  2 +-
 atr/db/interaction.py                 | 16 ++++-----
 atr/get/announce.py                   |  6 ++--
 atr/get/checklist.py                  |  4 +--
 atr/get/checks.py                     |  6 ++--
 atr/get/compose.py                    |  4 +--
 atr/get/distribution.py               | 28 +++++++--------
 atr/get/download.py                   | 26 +++++++-------
 atr/get/draft.py                      |  4 +--
 atr/get/file.py                       |  8 ++---
 atr/get/finish.py                     |  6 ++--
 atr/get/ignores.py                    |  2 +-
 atr/get/manual.py                     |  8 ++---
 atr/get/projects.py                   |  2 +-
 atr/get/release.py                    |  4 +--
 atr/get/report.py                     |  4 +--
 atr/get/result.py                     |  4 +--
 atr/get/revisions.py                  |  4 +--
 atr/get/sbom.py                       |  6 ++--
 atr/get/start.py                      |  2 +-
 atr/get/test.py                       |  8 ++---
 atr/get/upload.py                     |  4 +--
 atr/get/vote.py                       |  6 ++--
 atr/get/voting.py                     |  4 +--
 atr/merge.py                          | 16 ++++-----
 atr/models/api.py                     | 68 +++++++++++++++++------------------
 atr/models/distribution.py            |  2 +-
 atr/models/safe.py                    |  8 ++---
 atr/models/sql.py                     | 32 ++++++++---------
 atr/paths.py                          |  4 +--
 atr/post/announce.py                  |  4 +--
 atr/post/distribution.py              | 28 +++++++--------
 atr/post/draft.py                     | 28 +++++++--------
 atr/post/finish.py                    | 18 +++++-----
 atr/post/ignores.py                   |  8 ++---
 atr/post/keys.py                      |  4 +--
 atr/post/manual.py                    |  8 ++---
 atr/post/projects.py                  |  2 +-
 atr/post/resolve.py                   | 10 +++---
 atr/post/revisions.py                 | 12 +++----
 atr/post/sbom.py                      |  8 ++---
 atr/post/start.py                     |  4 +--
 atr/post/upload.py                    | 20 +++++------
 atr/post/vote.py                      |  4 +--
 atr/post/voting.py                    |  8 ++---
 atr/shared/distribution.py            | 20 +++++------
 atr/shared/projects.py                | 18 +++++-----
 atr/shared/web.py                     |  2 +-
 atr/ssh.py                            | 24 ++++++-------
 atr/storage/__init__.py               | 14 ++++----
 atr/storage/readers/checks.py         |  4 +--
 atr/storage/writers/announce.py       |  4 +--
 atr/storage/writers/checks.py         |  2 +-
 atr/storage/writers/distributions.py  | 18 +++++-----
 atr/storage/writers/keys.py           |  2 +-
 atr/storage/writers/policy.py         |  2 +-
 atr/storage/writers/project.py        |  2 +-
 atr/storage/writers/release.py        | 34 +++++++++---------
 atr/storage/writers/revision.py       | 28 +++++++--------
 atr/storage/writers/sbom.py           | 12 +++----
 atr/storage/writers/ssh.py            |  2 +-
 atr/storage/writers/vote.py           | 14 ++++----
 atr/storage/writers/workflowstatus.py |  2 +-
 atr/tasks/__init__.py                 | 12 +++----
 atr/tasks/checks/__init__.py          | 18 +++++-----
 atr/tasks/checks/compare.py           |  2 +-
 atr/tasks/gha.py                      |  4 +--
 atr/tasks/keys.py                     |  4 +--
 atr/tasks/quarantine.py               |  4 +--
 atr/tasks/sbom.py                     | 16 ++++-----
 atr/tasks/svn.py                      |  4 +--
 atr/tasks/vote.py                     |  2 +-
 atr/web.py                            |  6 ++--
 atr/worker.py                         |  4 +--
 tests/unit/test_create_revision.py    |  2 +-
 tests/unit/test_ignores_api_models.py | 10 +++---
 tests/unit/test_quarantine_task.py    | 12 +++----
 tests/unit/test_safe_types.py         | 10 +++---
 86 files changed, 445 insertions(+), 445 deletions(-)

diff --git a/atr/admin/__init__.py b/atr/admin/__init__.py
index 7538287f..bd1fa57f 100644
--- a/atr/admin/__init__.py
+++ b/atr/admin/__init__.py
@@ -599,8 +599,8 @@ async def logs(session: web.Committer) -> web.QuartResponse:
 async def ongoing_tasks_get(
     session: web.Committer, project_name: str, version_name: str, revision: str
 ) -> web.QuartResponse:
-    project = safe.ProjectName(project_name)
-    version = safe.VersionName(version_name)
+    project = safe.ProjectKey(project_name)
+    version = safe.VersionKey(version_name)
     revision_number = safe.RevisionNumber(revision)
     return await _ongoing_tasks(session, project, version, revision_number)
 
@@ -609,8 +609,8 @@ async def ongoing_tasks_get(
 async def ongoing_tasks_post(
     session: web.Committer, project_name: str, version_name: str, revision: str
 ) -> web.QuartResponse:
-    project = safe.ProjectName(project_name)
-    version = safe.VersionName(version_name)
+    project = safe.ProjectKey(project_name)
+    version = safe.VersionKey(version_name)
     revision_number = safe.RevisionNumber(revision)
     return await _ongoing_tasks(session, project, version, revision_number)
 
@@ -1177,8 +1177,8 @@ async def 
_get_filesystem_dirs_unfinished(filesystem_dirs: list[str]) -> None:
 
 async def _ongoing_tasks(
     session: web.Committer,
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     revision: safe.RevisionNumber,
 ) -> web.QuartResponse:
     try:
diff --git a/atr/api/__init__.py b/atr/api/__init__.py
index 820f9b60..7ab2d1bd 100644
--- a/atr/api/__init__.py
+++ b/atr/api/__init__.py
@@ -65,8 +65,8 @@ ROUTES_MODULE: Final[Literal[True]] = True
 @quart_schema.validate_response(models.api.ChecksListResults, 200)
 async def checks_list(
     _checks_list: Literal["checks/list"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
 ) -> DictResponse:
     """
     URL: GET /checks/list/<project_name>/<version_name>
@@ -99,8 +99,8 @@ async def checks_list(
 @quart_schema.validate_response(models.api.ChecksListResults, 200)
 async def checks_list_revision(
     _checks_list: Literal["checks/list"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     revision: safe.RevisionNumber,
 ) -> DictResponse:
     """
@@ -140,8 +140,8 @@ async def checks_list_revision(
 @quart_schema.validate_response(models.api.ChecksOngoingResults, 200)
 async def checks_ongoing(
     _checks_ongoing: Literal["checks/ongoing"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     revision: safe.RevisionNumber | None = None,
 ) -> DictResponse:
     """
@@ -462,7 +462,7 @@ async def ignore_delete(
 @quart_schema.validate_response(models.api.IgnoreListResults, 200)
 async def ignore_list(
     _ignore_list: Literal["ignore/list"],
-    project_name: safe.ProjectName,
+    project_name: safe.ProjectKey,
 ) -> DictResponse:
     """
     URL: GET /ignore/list/<project_name>
@@ -692,7 +692,7 @@ async def keys_user(
 @quart_schema.validate_response(models.api.ProjectGetResults, 200)
 async def project_get(
     _project_get: Literal["project/get"],
-    project_name: safe.ProjectName,
+    project_name: safe.ProjectKey,
 ) -> DictResponse:
     """
     URL: GET /project/get/<project_name>
@@ -711,7 +711,7 @@ async def project_get(
 @quart_schema.validate_response(models.api.ProjectPolicyResults, 200)
 async def project_policy(
     _project_policy: Literal["project/policy"],
-    project_name: safe.ProjectName,
+    project_name: safe.ProjectKey,
 ) -> DictResponse:
     """
     URL: GET /project/policy/<project_name>
@@ -755,7 +755,7 @@ async def project_policy(
 @quart_schema.validate_response(models.api.ProjectReleasesResults, 200)
 async def project_releases(
     _project_releases: Literal["project/releases"],
-    project_name: safe.ProjectName,
+    project_name: safe.ProjectKey,
 ) -> DictResponse:
     """
     URL: GET /project/releases/<project_name>
@@ -1049,8 +1049,8 @@ async def release_delete(
 @quart_schema.validate_response(models.api.ReleaseGetResults, 200)
 async def release_get(
     _release_get: Literal["release/get"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
 ) -> DictResponse:
     """
     URL: GET /release/get/<project_name>/<version_name>
@@ -1070,8 +1070,8 @@ async def release_get(
 @quart_schema.validate_response(models.api.ReleasePathsResults, 200)
 async def release_paths(
     _release_paths: Literal["release/paths"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     revision: safe.RevisionNumber | None = None,
 ) -> DictResponse:
     """
@@ -1101,8 +1101,8 @@ async def release_paths(
 @quart_schema.validate_response(models.api.ReleaseRevisionsResults, 200)
 async def release_revisions(
     _release_revisions: Literal["release/revisions"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
 ) -> DictResponse:
     """
     URL: GET /release/revisions/<project_name>/<version_name>
diff --git a/atr/attestable.py b/atr/attestable.py
index a020776d..d53feb34 100644
--- a/atr/attestable.py
+++ b/atr/attestable.py
@@ -36,32 +36,32 @@ if TYPE_CHECKING:
 
 
 def attestable_checks_path(
-    project_name: safe.ProjectName, version_name: safe.VersionName, 
revision_number: safe.RevisionNumber
+    project_name: safe.ProjectKey, version_name: safe.VersionKey, 
revision_number: safe.RevisionNumber
 ) -> pathlib.Path:
     return paths.get_attestable_dir() / str(project_name) / str(version_name) 
/ f"{revision_number!s}.checks.json"
 
 
 def attestable_path(
-    project_name: safe.ProjectName, version_name: safe.VersionName, 
revision_number: safe.RevisionNumber
+    project_name: safe.ProjectKey, version_name: safe.VersionKey, 
revision_number: safe.RevisionNumber
 ) -> pathlib.Path:
     return paths.get_attestable_dir() / str(project_name) / str(version_name) 
/ f"{revision_number!s}.json"
 
 
 def attestable_paths_path(
-    project_name: safe.ProjectName, version_name: safe.VersionName, 
revision_number: safe.RevisionNumber
+    project_name: safe.ProjectKey, version_name: safe.VersionKey, 
revision_number: safe.RevisionNumber
 ) -> pathlib.Path:
     return paths.get_attestable_dir() / str(project_name) / str(version_name) 
/ f"{revision_number!s}.paths.json"
 
 
 def github_tp_payload_path(
-    project_name: safe.ProjectName, version_name: safe.VersionName, 
revision_number: safe.RevisionNumber
+    project_name: safe.ProjectKey, version_name: safe.VersionKey, 
revision_number: safe.RevisionNumber
 ) -> pathlib.Path:
     return paths.get_attestable_dir() / str(project_name) / str(version_name) 
/ f"{revision_number!s}.github-tp.json"
 
 
 async def github_tp_payload_write(
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     revision_number: safe.RevisionNumber,
     github_payload: dict[str, Any],
 ) -> None:
@@ -70,8 +70,8 @@ async def github_tp_payload_write(
 
 
 async def load(
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     revision_number: safe.RevisionNumber,
 ) -> models.AttestableV1 | None:
     file_path = attestable_path(project_name, version_name, revision_number)
@@ -87,8 +87,8 @@ async def load(
 
 
 async def load_checks(
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     revision_number: safe.RevisionNumber,
 ) -> dict[str, dict[str, str]]:
     file_path = attestable_checks_path(project_name, version_name, 
revision_number)
@@ -109,8 +109,8 @@ async def load_checks(
 
 
 async def load_paths(
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     revision_number: safe.RevisionNumber,
 ) -> dict[str, str] | None:
     file_path = attestable_paths_path(project_name, version_name, 
revision_number)
@@ -175,8 +175,8 @@ async def paths_to_hashes_and_sizes(directory: 
pathlib.Path) -> tuple[dict[str,
 
 
 async def write_checks_data(
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     revision_number: safe.RevisionNumber,
     rel_path: str,
     checks: dict[str, str],
@@ -199,8 +199,8 @@ async def write_checks_data(
 
 
 async def write_files_data(
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     revision_number: safe.RevisionNumber,
     release_policy: dict[str, Any] | None,
     uploader_uid: str,
diff --git a/atr/blueprints/common.py b/atr/blueprints/common.py
index 1382e90c..6801ce9c 100644
--- a/atr/blueprints/common.py
+++ b/atr/blueprints/common.py
@@ -41,9 +41,9 @@ QUART_CONVERTERS: dict[Any, str] = {
 VALIDATED_TYPES: set[Any] = {
     safe.Alphanumeric,
     safe.CommitteeKey,
-    safe.ProjectName,
+    safe.ProjectKey,
     safe.RevisionNumber,
-    safe.VersionName,
+    safe.VersionKey,
     unsafe.UnsafeStr,
 }
 
@@ -196,14 +196,14 @@ async def validate_params(kwargs: dict[str, Any], 
known_params: list[tuple[str,
     """Validate URL parameters in order, using the type-specific validators."""
     for param_name, param_type in known_params:
         raw = kwargs[param_name]
-        if param_type is safe.ProjectName:
+        if param_type is safe.ProjectKey:
             try:
-                kwargs[param_name] = safe.ProjectName(raw)
+                kwargs[param_name] = safe.ProjectKey(raw)
             except ValueError:
                 raise base.ASFQuartException(f"Project name {param_name!r} is 
invalid. ")
-        elif param_type is safe.VersionName:
+        elif param_type is safe.VersionKey:
             try:
-                kwargs[param_name] = safe.VersionName(raw)
+                kwargs[param_name] = safe.VersionKey(raw)
             except ValueError:
                 raise base.ASFQuartException(f"Version name {param_name!r} is 
invalid. ")
         elif param_type is safe.RevisionNumber:
diff --git a/atr/blueprints/post.py b/atr/blueprints/post.py
index 6c585507..0d08f5db 100644
--- a/atr/blueprints/post.py
+++ b/atr/blueprints/post.py
@@ -64,7 +64,7 @@ def typed(func: Callable[..., Any]) -> web.RouteFunction[Any]:
     - check_access is called automatically for committer routes with 
project_name
     """
     path, validated_params, literal_params, form_param, public = 
common.build_path(func)
-    project_name_var = next((name for name, t in validated_params if t is 
safe.ProjectName), None)
+    project_name_var = next((name for name, t in validated_params if t is 
safe.ProjectKey), None)
     check_access = (not public) and (project_name_var is not None)
     form_safe_params = common.safe_params_for_type(form_param[1]) if 
form_param is not None else []
 
diff --git a/atr/cache.py b/atr/cache.py
index e578f0aa..354411b4 100644
--- a/atr/cache.py
+++ b/atr/cache.py
@@ -105,7 +105,7 @@ def project_version_has_project(project_name: str) -> bool:
     return project_name in project_version_get()
 
 
-def project_version_has_version(project_name: safe.ProjectName, version_name: 
str) -> bool:
+def project_version_has_version(project_name: safe.ProjectKey, version_name: 
str) -> bool:
     projects = project_version_get()
     if str(project_name) not in projects:
         return False
diff --git a/atr/construct.py b/atr/construct.py
index 32a18770..e7e1ac80 100644
--- a/atr/construct.py
+++ b/atr/construct.py
@@ -54,8 +54,8 @@ TEMPLATE_VARIABLES: list[tuple[str, str, set[Context]]] = [
 class AnnounceReleaseOptions:
     asfuid: str
     fullname: str
-    project_name: safe.ProjectName
-    version_name: safe.VersionName
+    project_name: safe.ProjectKey
+    version_name: safe.VersionKey
     revision_number: safe.RevisionNumber
 
 
@@ -63,13 +63,13 @@ class AnnounceReleaseOptions:
 class StartVoteOptions:
     asfuid: str
     fullname: str
-    project_name: safe.ProjectName
-    version_name: safe.VersionName
+    project_name: safe.ProjectKey
+    version_name: safe.VersionKey
     revision_number: safe.RevisionNumber
     vote_duration: int
 
 
-async def announce_release_default(project_name: safe.ProjectName) -> str:
+async def announce_release_default(project_name: safe.ProjectKey) -> str:
     async with db.session() as data:
         project = await data.project(
             name=str(project_name), status=sql.ProjectStatus.ACTIVE, 
_release_policy=True
@@ -132,7 +132,7 @@ async def announce_release_subject_and_body(
     return subject, body
 
 
-async def announce_release_subject_default(project_name: safe.ProjectName) -> 
str:
+async def announce_release_subject_default(project_name: safe.ProjectKey) -> 
str:
     async with db.session() as data:
         project = await data.project(
             name=str(project_name), status=sql.ProjectStatus.ACTIVE, 
_release_policy=True
@@ -152,7 +152,7 @@ def announce_template_variables() -> list[tuple[str, str]]:
 def checklist_body(
     markdown: str,
     project: sql.Project,
-    version_name: safe.VersionName,
+    version_name: safe.VersionKey,
     committee: sql.Committee,
     revision: sql.Revision | None,
 ) -> str:
@@ -181,7 +181,7 @@ def checklist_template_variables() -> list[tuple[str, str]]:
     return [(name, desc) for (name, desc, contexts) in TEMPLATE_VARIABLES if 
"checklist" in contexts]
 
 
-async def start_vote_default(project_name: safe.ProjectName) -> str:
+async def start_vote_default(project_name: safe.ProjectKey) -> str:
     async with db.session() as data:
         project = await data.project(
             name=str(project_name), status=sql.ProjectStatus.ACTIVE, 
_release_policy=True
@@ -280,7 +280,7 @@ async def start_vote_subject_and_body(subject: str, body: 
str, options: StartVot
     return subject, body
 
 
-async def start_vote_subject_default(project_name: safe.ProjectName) -> str:
+async def start_vote_subject_default(project_name: safe.ProjectKey) -> str:
     async with db.session() as data:
         project = await data.project(
             name=str(project_name), status=sql.ProjectStatus.ACTIVE, 
_release_policy=True
diff --git a/atr/datasources/apache.py b/atr/datasources/apache.py
index 674cd912..0045ae92 100644
--- a/atr/datasources/apache.py
+++ b/atr/datasources/apache.py
@@ -484,7 +484,7 @@ async def _update_projects(data: db.Session, projects: 
ProjectsData) -> tuple[in
             updated_count += 1
 
         # Pass the project name through the validator
-        safe.ProjectName(project_model.name)
+        safe.ProjectKey(project_model.name)
         project_model.full_name = str(project_status.name)
         project_model.category = project_status.category
         project_model.description = project_status.description
diff --git a/atr/db/__init__.py b/atr/db/__init__.py
index 3236e117..b17ecfa5 100644
--- a/atr/db/__init__.py
+++ b/atr/db/__init__.py
@@ -870,7 +870,7 @@ def ensure_session(caller_data: Session | None) -> Session 
| contextlib.nullcont
     return contextlib.nullcontext(caller_data)
 
 
-async def get_project_release_policy(data: Session, project_name: 
safe.ProjectName) -> sql.ReleasePolicy | None:
+async def get_project_release_policy(data: Session, project_name: 
safe.ProjectKey) -> sql.ReleasePolicy | None:
     """Fetch the ReleasePolicy for a project."""
     project = await data.project(name=str(project_name), 
status=sql.ProjectStatus.ACTIVE, _release_policy=True).demand(
         RuntimeError(f"Project {project_name} not found")
diff --git a/atr/db/interaction.py b/atr/db/interaction.py
index 35f86a8b..87259277 100644
--- a/atr/db/interaction.py
+++ b/atr/db/interaction.py
@@ -250,7 +250,7 @@ async def has_failing_checks(
 
 
 async def latest_info(
-    project_name: safe.ProjectName, version_name: safe.VersionName
+    project_name: safe.ProjectKey, version_name: safe.VersionKey
 ) -> tuple[safe.RevisionNumber, str, datetime.datetime] | None:
     """Get the name, editor, and timestamp of the latest revision."""
     release_name = sql.release_name(project_name, version_name)
@@ -303,8 +303,8 @@ async def release_latest_vote_task(release: sql.Release, 
caller_data: db.Session
 
 async def release_ready_for_vote(  # noqa: C901
     session: web.Committer,
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     revision: safe.RevisionNumber,
     data: db.Session,
     manual_vote: bool = False,
@@ -404,7 +404,7 @@ def task_recipient_get(latest_vote_task: sql.Task) -> str | 
None:
 
 
 async def tasks_ongoing(
-    project_name: safe.ProjectName, version_name: safe.VersionName, 
revision_number: safe.RevisionNumber | None = None
+    project_name: safe.ProjectKey, version_name: safe.VersionKey, 
revision_number: safe.RevisionNumber | None = None
 ) -> int:
     tasks = sqlmodel.select(sqlalchemy.func.count()).select_from(sql.Task)
     async with db.session() as data:
@@ -420,8 +420,8 @@ async def tasks_ongoing(
 
 
 async def tasks_ongoing_revision(
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     revision_number: safe.RevisionNumber | None = None,
 ) -> tuple[int, str | None]:
     via = sql.validate_instrumented_attribute
@@ -471,8 +471,8 @@ async def trusted_jwt_for_dist(
     jwt: str,
     asf_uid: str,
     phase: TrustedProjectPhase,
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
 ) -> tuple[dict[str, Any], str, sql.Project, sql.Release]:
     payload, asf_uid_from_jwt = await validate_trusted_jwt(publisher, jwt)
     if asf_uid_from_jwt is not None:
diff --git a/atr/get/announce.py b/atr/get/announce.py
index 5054f68d..8035b927 100644
--- a/atr/get/announce.py
+++ b/atr/get/announce.py
@@ -40,8 +40,8 @@ import atr.web as web
 async def selected(
     session: web.Committer,
     _announce: Literal["announce"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
 ) -> str | web.WerkzeugResponse:
     """
     URL: /announce/<project_name>/<version_name>
@@ -111,7 +111,7 @@ async def selected(
 
 
 async def _get_page_data(
-    session: web.Committer, project_name: safe.ProjectName, version_name: 
safe.VersionName
+    session: web.Committer, project_name: safe.ProjectKey, version_name: 
safe.VersionKey
 ) -> sql.Release:
     release = await session.release(
         project_name,
diff --git a/atr/get/checklist.py b/atr/get/checklist.py
index d5ba2e3c..087a1573 100644
--- a/atr/get/checklist.py
+++ b/atr/get/checklist.py
@@ -37,8 +37,8 @@ import atr.web as web
 async def selected(
     _session: web.Public,
     _checklist: Literal["checklist"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
 ) -> str:
     async with db.session() as data:
         release = await data.release(
diff --git a/atr/get/checks.py b/atr/get/checks.py
index 52ef3a6d..b601d808 100644
--- a/atr/get/checks.py
+++ b/atr/get/checks.py
@@ -100,7 +100,7 @@ async def get_file_totals(release: sql.Release, session: 
web.Committer | None) -
 
 @get.typed
 async def selected(
-    session: web.Public, _checks: Literal["checks"], project_name: 
safe.ProjectName, version_name: safe.VersionName
+    session: web.Public, _checks: Literal["checks"], project_name: 
safe.ProjectKey, version_name: safe.VersionKey
 ) -> str:
     """
     URL: /checks/<project_name>/<version_name>
@@ -144,8 +144,8 @@ async def selected(
 async def selected_revision(
     session: web.Committer,
     _checks: Literal["checks"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     revision_number: safe.RevisionNumber,
 ) -> web.QuartResponse:
     """
diff --git a/atr/get/compose.py b/atr/get/compose.py
index 38d2e1fb..28b329cc 100644
--- a/atr/get/compose.py
+++ b/atr/get/compose.py
@@ -32,8 +32,8 @@ import atr.web as web
 async def selected(
     session: web.Committer,
     _compose: Literal["compose"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
 ) -> web.WerkzeugResponse | str:
     """
     URL: /compose/<project_name>/<version_name>
diff --git a/atr/get/distribution.py b/atr/get/distribution.py
index a8742e58..fae6eed2 100644
--- a/atr/get/distribution.py
+++ b/atr/get/distribution.py
@@ -39,8 +39,8 @@ from atr.tasks import gha
 async def automate(
     session: web.Committer,
     _distribution: Literal["distribution/automate"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
 ) -> str:
     """
     URL: /distribution/automate/<project_name>/<version>
@@ -53,8 +53,8 @@ async def automate(
 async def list_get(
     _session: web.Committer,
     _distribution: Literal["distribution/list"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
 ) -> str:
     """
     URL: /distribution/list/<project_name>/<version_name>
@@ -147,8 +147,8 @@ async def list_get(
 async def record(
     session: web.Committer,
     _distribution: Literal["distribution/record"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
 ) -> str:
     """
     URL: /distribution/record/<project_name>/<version_name>
@@ -161,8 +161,8 @@ async def record(
 async def stage_automate(
     session: web.Committer,
     _distribution: Literal["distribution/stage/automate"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
 ) -> str:
     """
     URL: /distribution/stage/automate/<project_name>/<version_name>
@@ -175,8 +175,8 @@ async def stage_automate(
 async def stage_record(
     session: web.Committer,
     _distribution: Literal["distribution/stage/record"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
 ) -> str:
     """
     URL: /distribution/stage/record/<project_name>/<version_name>
@@ -185,7 +185,7 @@ async def stage_record(
     return await _record_form_page(project_name, version_name, staging=True)
 
 
-async def _automate_form_page(project: safe.ProjectName, version: 
safe.VersionName, staging: bool) -> str:
+async def _automate_form_page(project: safe.ProjectKey, version: 
safe.VersionKey, staging: bool) -> str:
     """Helper to render the distribution automation form page."""
     await shared.distribution.release_validated(project, version, 
staging=staging)
 
@@ -228,7 +228,7 @@ async def _automate_form_page(project: safe.ProjectName, 
version: safe.VersionNa
 
 
 async def _get_page_data(
-    project_name: safe.ProjectName, version_name: safe.VersionName
+    project_name: safe.ProjectKey, version_name: safe.VersionKey
 ) -> tuple[Sequence[sql.Distribution], Sequence[sql.Task]]:
     """Get all the data needed to render the finish page."""
     async with db.session() as data:
@@ -261,7 +261,7 @@ async def _get_page_data(
     return distributions, tasks
 
 
-async def _record_form_page(project: safe.ProjectName, version: 
safe.VersionName, staging: bool) -> str:
+async def _record_form_page(project: safe.ProjectKey, version: 
safe.VersionKey, staging: bool) -> str:
     """Helper to render the distribution recording form page."""
     await shared.distribution.release_validated(project, version, 
staging=staging)
 
@@ -304,7 +304,7 @@ async def _record_form_page(project: safe.ProjectName, 
version: safe.VersionName
 
 
 def _render_distribution_tasks(
-    tasks: Sequence[sql.Task], block: htm.Block, project_name: 
safe.ProjectName, version_name: safe.VersionName
+    tasks: Sequence[sql.Task], block: htm.Block, project_name: 
safe.ProjectKey, version_name: safe.VersionKey
 ):
     failed_tasks = [
         t for t in tasks if (t.status == sql.TaskStatus.FAILED) or (t.workflow 
and (t.workflow.status == "failed"))
diff --git a/atr/get/download.py b/atr/get/download.py
index 801adcd7..97f4419c 100644
--- a/atr/get/download.py
+++ b/atr/get/download.py
@@ -44,8 +44,8 @@ import atr.web as web
 async def all_selected(
     session: web.Committer,
     _download_all: Literal["download/all"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
 ) -> web.WerkzeugResponse | str:
     """
     URL: /download/all/<project_name>/<version_name>
@@ -83,8 +83,8 @@ async def all_selected(
 async def path(
     _session: web.Public,
     _download_path: Literal["download/path"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     file_path: unsafe.Path,
 ) -> web.Response:
     """
@@ -98,8 +98,8 @@ async def path(
 async def path_empty(
     _session: web.Public,
     _download_path: Literal["download/path"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
 ) -> web.Response:
     """
     URL: /download/path/<project_name>/<version_name>/
@@ -112,8 +112,8 @@ async def path_empty(
 async def sh_selected(
     _session: web.Public,
     _download_sh: Literal["download/sh"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
 ) -> web.Response:
     """
     URL: /download/sh/<project_name>/<version_name>
@@ -137,8 +137,8 @@ async def sh_selected(
 async def urls_selected(
     _session: web.Public,
     _download_urls: Literal["download/urls"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
 ) -> web.Response:
     """
     URL: /download/urls/<project_name>/<version_name>
@@ -160,8 +160,8 @@ async def urls_selected(
 async def zip_selected(
     session: web.Committer,
     _download_zip: Literal["download/zip"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
 ) -> web.Response:
     """
     URL: /download/zip/<project_name>/<version_name>
@@ -196,7 +196,7 @@ async def zip_selected(
 
 
 async def _download_or_list(
-    project_name: safe.ProjectName, version_name: safe.VersionName, file_path: 
str
+    project_name: safe.ProjectKey, version_name: safe.VersionKey, file_path: 
str
 ) -> web.Response:
     """Download a file or list a directory from a release in any phase."""
     import atr.get.root as root
diff --git a/atr/get/draft.py b/atr/get/draft.py
index 64a5a94a..e565fbf4 100644
--- a/atr/get/draft.py
+++ b/atr/get/draft.py
@@ -39,8 +39,8 @@ import atr.web as web
 async def tools(
     session: web.Committer,
     _draft_tools: Literal["draft/tools"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     file_path: unsafe.Path,
 ) -> str:
     """
diff --git a/atr/get/file.py b/atr/get/file.py
index 2cad45f1..9e9f56e8 100644
--- a/atr/get/file.py
+++ b/atr/get/file.py
@@ -39,8 +39,8 @@ type Phase = Literal["COMPOSE", "VOTE", "FINISH"]
 async def selected(
     session: web.Committer,
     _file: Literal["file"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
 ) -> str:
     """
     URL: /file/<project_name>/<version_name>
@@ -141,8 +141,8 @@ async def selected(
 async def selected_path(
     session: web.Committer,
     _file: Literal["file"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     file_path: unsafe.Path,
 ) -> str:
     """
diff --git a/atr/get/finish.py b/atr/get/finish.py
index 3690c739..13e1ac43 100644
--- a/atr/get/finish.py
+++ b/atr/get/finish.py
@@ -62,8 +62,8 @@ class RCTagAnalysisResult:
 async def selected(
     session: web.Committer,
     _finish: Literal["finish"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
 ) -> tuple[web.QuartResponse, int] | web.WerkzeugResponse | str:
     """
     URL: /finish/<project_name>/<version_name>
@@ -155,7 +155,7 @@ async def _deletable_choices(
 
 
 async def _get_page_data(
-    project_name: safe.ProjectName, version_name: safe.VersionName
+    project_name: safe.ProjectKey, version_name: safe.VersionKey
 ) -> tuple[
     sql.Release, list[pathlib.Path], set[pathlib.Path], list[tuple[str, str]], 
RCTagAnalysisResult, Sequence[sql.Task]
 ]:
diff --git a/atr/get/ignores.py b/atr/get/ignores.py
index fd07bd53..9e70d464 100644
--- a/atr/get/ignores.py
+++ b/atr/get/ignores.py
@@ -34,7 +34,7 @@ import atr.web as web
 async def ignores(
     session: web.Committer,
     _ignores: Literal["ignores"],
-    project_name: safe.ProjectName,
+    project_name: safe.ProjectKey,
 ) -> str | web.WerkzeugResponse:
     """
     URL: /ignores/<project_name>
diff --git a/atr/get/manual.py b/atr/get/manual.py
index e9ba1162..61e8d47a 100644
--- a/atr/get/manual.py
+++ b/atr/get/manual.py
@@ -38,8 +38,8 @@ import atr.web as web
 async def resolve_selected(
     session: web.Committer,
     _manual_resolve: Literal["manual/resolve"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
 ) -> str:
     """
     URL: /manual/resolve/<project_name>/<version_name>
@@ -69,8 +69,8 @@ async def resolve_selected(
 async def start_selected_revision(
     session: web.Committer,
     _manual_start: Literal["manual/start"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     revision: safe.RevisionNumber,
 ) -> web.WerkzeugResponse | str:
     """
diff --git a/atr/get/projects.py b/atr/get/projects.py
index 20cb2a77..63cc2c7a 100644
--- a/atr/get/projects.py
+++ b/atr/get/projects.py
@@ -149,7 +149,7 @@ async def select(session: web.Committer, _project_select: 
Literal["project/selec
 
 @get.typed
 async def view(
-    session: web.Committer, _projects: Literal["projects"], project_name: 
safe.ProjectName
+    session: web.Committer, _projects: Literal["projects"], project_name: 
safe.ProjectKey
 ) -> web.WerkzeugResponse | str:
     """
     URL: /projects/<project_name>
diff --git a/atr/get/release.py b/atr/get/release.py
index cf610872..bb031be1 100644
--- a/atr/get/release.py
+++ b/atr/get/release.py
@@ -32,7 +32,7 @@ import atr.web as web
 
 @get.typed
 async def finished(
-    _session: web.Public, _releases_finished: Literal["releases/finished"], 
project_name: safe.ProjectName
+    _session: web.Public, _releases_finished: Literal["releases/finished"], 
project_name: safe.ProjectKey
 ) -> str:
     """
     URL: /releases/finished/<project_name>
@@ -89,7 +89,7 @@ async def releases(_session: web.Public, _releases: 
Literal["releases"]) -> str:
 
 @get.typed
 async def select(
-    session: web.Committer, _release_select: Literal["release/select"], 
project_name: safe.ProjectName
+    session: web.Committer, _release_select: Literal["release/select"], 
project_name: safe.ProjectKey
 ) -> str:
     """
     URL: /release/select/<project_name>
diff --git a/atr/get/report.py b/atr/get/report.py
index 15d81be6..e074afe9 100644
--- a/atr/get/report.py
+++ b/atr/get/report.py
@@ -37,8 +37,8 @@ import atr.web as web
 async def selected_path(
     session: web.Committer,
     _report: Literal["report"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     rel_path: unsafe.Path,
 ) -> str:
     """
diff --git a/atr/get/result.py b/atr/get/result.py
index eaf52f8a..7c82a889 100644
--- a/atr/get/result.py
+++ b/atr/get/result.py
@@ -31,8 +31,8 @@ import atr.web as web
 async def data(
     session: web.Committer,
     _result_data: Literal["result/data"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     check_id: int,
 ) -> web.TextResponse:
     """
diff --git a/atr/get/revisions.py b/atr/get/revisions.py
index c75e93bc..8fddc451 100644
--- a/atr/get/revisions.py
+++ b/atr/get/revisions.py
@@ -53,8 +53,8 @@ class FilesDiff(schema.Strict):
 async def selected(
     session: web.Committer,
     _revisions: Literal["revisions"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
 ) -> str:
     """
     URL: /revisions/<project_name>/<version_name>
diff --git a/atr/get/sbom.py b/atr/get/sbom.py
index a8ef42cc..986bd2a5 100644
--- a/atr/get/sbom.py
+++ b/atr/get/sbom.py
@@ -49,8 +49,8 @@ if TYPE_CHECKING:
 async def report(
     session: web.Committer,
     _sbom_report: Literal["sbom/report"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     file_path: unsafe.Path,
 ) -> str:
     """
@@ -245,7 +245,7 @@ def _extract_vulnerability_severity(vuln: 
results.VulnerabilityDetails) -> str:
 
 
 async def _fetch_tasks(
-    file_path: str, project: safe.ProjectName, release: sql.Release, version: 
safe.VersionName
+    file_path: str, project: safe.ProjectKey, release: sql.Release, version: 
safe.VersionKey
 ) -> tuple[sql.Task | None, Sequence[sql.Task], Sequence[sql.Task]]:
     # TODO: Abstract this code and the sbomtool.MissingAdapter validators
     async with db.session() as data:
diff --git a/atr/get/start.py b/atr/get/start.py
index 7e33a0fb..7150db9a 100644
--- a/atr/get/start.py
+++ b/atr/get/start.py
@@ -35,7 +35,7 @@ import atr.web as web
 
 
 @get.typed
-async def selected(session: web.Committer, _start: Literal["start"], 
project_name: safe.ProjectName) -> str:
+async def selected(session: web.Committer, _start: Literal["start"], 
project_name: safe.ProjectKey) -> str:
     """
     URL: /start/<project_name>
     """
diff --git a/atr/get/test.py b/atr/get/test.py
index a8075c23..88d4f5c9 100644
--- a/atr/get/test.py
+++ b/atr/get/test.py
@@ -89,8 +89,8 @@ async def test_login(_session: web.Public, _test_login: 
Literal["test/login"]) -
 async def test_merge(
     session: web.Committer,
     _test_merge: Literal["test/merge"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
 ) -> web.WerkzeugResponse:
     """
     URL: /test/merge/<project_name>/<version_name>
@@ -206,8 +206,8 @@ async def test_vote(
     session: web.Public,
     _test_vote: Literal["test/vote"],
     category: unsafe.UnsafeStr,
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
 ) -> str:
     """
     URL: /test/vote/<category>/<project_name>/<version_name>
diff --git a/atr/get/upload.py b/atr/get/upload.py
index 3ff14a15..823fb901 100644
--- a/atr/get/upload.py
+++ b/atr/get/upload.py
@@ -41,8 +41,8 @@ import atr.web as web
 async def selected(
     session: web.Committer,
     _upload: Literal["upload"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
 ) -> str:
     """
     URL: /upload/<project_name>/<version_name>
diff --git a/atr/get/vote.py b/atr/get/vote.py
index 7adc5aaf..24fe552c 100644
--- a/atr/get/vote.py
+++ b/atr/get/vote.py
@@ -59,7 +59,7 @@ class UserCategory(enum.StrEnum):
 
 
 async def category_and_release(
-    session: web.Committer | None, project_name: safe.ProjectName, 
version_name: safe.VersionName
+    session: web.Committer | None, project_name: safe.ProjectKey, 
version_name: safe.VersionKey
 ) -> tuple[UserCategory, sql.Release, sql.Task | None]:
     async with db.session() as data:
         release = await data.release(
@@ -176,8 +176,8 @@ async def render_vote_closed_page(release: sql.Release) -> 
str:
 async def selected(
     session: web.Public,
     _vote: Literal["vote"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
 ) -> web.WerkzeugResponse | str:
     """
     URL: /vote/<project_name>/<version_name>
diff --git a/atr/get/voting.py b/atr/get/voting.py
index 90db36ff..ad0c1de1 100644
--- a/atr/get/voting.py
+++ b/atr/get/voting.py
@@ -45,8 +45,8 @@ import atr.web as web
 async def selected_revision(
     session: web.Committer,
     _voting: Literal["voting"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     revision: safe.RevisionNumber,
 ) -> web.WerkzeugResponse | str:
     """
diff --git a/atr/merge.py b/atr/merge.py
index 84e7a050..0e108fb3 100644
--- a/atr/merge.py
+++ b/atr/merge.py
@@ -36,8 +36,8 @@ async def merge(
     base_inodes: dict[str, int],
     base_hashes: dict[str, str],
     prior_dir: pathlib.Path,
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     prior_revision_number: safe.RevisionNumber,
     temp_dir: pathlib.Path,
     n_inodes: dict[str, int],
@@ -120,8 +120,8 @@ async def _add_from_prior(
     n_hashes: dict[str, str],
     n_sizes: dict[str, int],
     prior_hashes: dict[str, str] | None,
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     prior_revision_number: safe.RevisionNumber,
 ) -> dict[str, str] | None:
     target = temp_dir / path
@@ -177,8 +177,8 @@ async def _merge_all_present(
     n_hashes: dict[str, str],
     n_sizes: dict[str, int],
     prior_hashes: dict[str, str] | None,
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     prior_revision_number: safe.RevisionNumber,
 ) -> dict[str, str] | None:
     # Cases 6, 8: prior and new share an inode so they already agree
@@ -238,8 +238,8 @@ async def _replace_with_prior(
     n_hashes: dict[str, str],
     n_sizes: dict[str, int],
     prior_hashes: dict[str, str] | None,
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     prior_revision_number: safe.RevisionNumber,
 ) -> dict[str, str] | None:
     await aiofiles.os.remove(temp_dir / path)
diff --git a/atr/models/api.py b/atr/models/api.py
index 514a7f6d..3c38ec3d 100644
--- a/atr/models/api.py
+++ b/atr/models/api.py
@@ -73,14 +73,14 @@ class DistributeSshRegisterArgs(schema.Strict):
     ssh_key: str = schema.example("ssh-ed25519 
AAAAC3NzaC1lZDI1NTEgH5C9okWi0dh25AAAAIOMqqnkVzrm0SdG6UOoqKLsabl9GKJl")
     phase: str = schema.Field(strict=False, default="compose", 
json_schema_extra={"examples": ["compose", "finish"]})
     asf_uid: str = schema.example("user")
-    project_name: safe.ProjectName = schema.example("tooling")
-    version: safe.VersionName = schema.example("0.0.1")
+    project_name: safe.ProjectKey = schema.example("tooling")
+    version: safe.VersionKey = schema.example("0.0.1")
 
 
 class DistributeSshRegisterResults(schema.Strict):
     endpoint: Literal["/distribute/ssh/register"] = schema.alias("endpoint")
     fingerprint: str = 
schema.example("SHA256:0123456789abcdef0123456789abcdef01234567")
-    project: safe.ProjectName = schema.example("example")
+    project: safe.ProjectKey = schema.example("example")
     expires: int = schema.example(1713547200)
 
 
@@ -89,7 +89,7 @@ class DistributeStatusUpdateArgs(schema.Strict):
     jwt: str = schema.example("eyJhbGciOiJIUzI1[...]mMjLiuyu5CSpyHI=")
     workflow: str = schema.description("Workflow name")
     run_id: str = schema.description("Workflow run ID")
-    project_name: safe.ProjectName = schema.description("Project name in ATR")
+    project_name: safe.ProjectKey = schema.description("Project name in ATR")
     status: str = schema.description("Workflow status")
     message: str = schema.description("Workflow message")
 
@@ -100,12 +100,12 @@ class DistributeStatusUpdateResults(schema.Strict):
 
 
 class DistributionRecordArgs(schema.Strict):
-    project: safe.ProjectName = schema.example("example")
-    version: safe.VersionName = schema.example("0.0.1")
+    project: safe.ProjectKey = schema.example("example")
+    version: safe.VersionKey = schema.example("0.0.1")
     platform: sql.DistributionPlatform = 
schema.example(sql.DistributionPlatform.ARTIFACT_HUB)
     distribution_owner_namespace: safe.Alphanumeric | None = 
schema.default_example(None, "example")
     distribution_package: safe.Alphanumeric = schema.example("example")
-    distribution_version: safe.VersionName = schema.example("0.0.1")
+    distribution_version: safe.VersionKey = schema.example("0.0.1")
     staging: bool = schema.example(False)
     details: bool = schema.example(False)
 
@@ -128,12 +128,12 @@ class DistributionRecordFromWorkflowArgs(schema.Strict):
     asf_uid: str = schema.example("user")
     publisher: str = schema.example("user")
     jwt: str = schema.example("eyJhbGciOiJIUzI1[...]mMjLiuyu5CSpyHI=")
-    project: safe.ProjectName = schema.example("example")
-    version: safe.VersionName = schema.example("0.0.1")
+    project: safe.ProjectKey = schema.example("example")
+    version: safe.VersionKey = schema.example("0.0.1")
     platform: sql.DistributionPlatform = 
schema.example(sql.DistributionPlatform.ARTIFACT_HUB)
     distribution_owner_namespace: safe.Alphanumeric | None = 
schema.default_example(None, "example")
     distribution_package: safe.Alphanumeric = schema.example("example")
-    distribution_version: safe.VersionName = schema.example("0.0.1")
+    distribution_version: safe.VersionKey = schema.example("0.0.1")
     phase: str = schema.Field(strict=False, default="compose", 
json_schema_extra={"examples": ["compose", "finish"]})
     staging: bool = schema.example(False)
     details: bool = schema.example(False)
@@ -164,7 +164,7 @@ class DistributionRecordResults(schema.Strict):
 
 
 class IgnoreAddArgs(schema.Strict):
-    project_name: safe.ProjectName = schema.example("example")
+    project_name: safe.ProjectKey = schema.example("example")
     release_glob: str | None = schema.default_example(None, "example-0.0.*")
     revision_number: safe.RevisionNumber | None = schema.default_example(None, 
"00001")
     checker_glob: str | None = schema.default_example(None, 
"atr.tasks.checks.license.files")
@@ -194,7 +194,7 @@ class IgnoreAddResults(schema.Strict):
 
 
 class IgnoreDeleteArgs(schema.Strict):
-    project_name: safe.ProjectName = schema.example("example")
+    project_name: safe.ProjectKey = schema.example("example")
     id: int = schema.example(1)
 
 
@@ -289,7 +289,7 @@ class ProjectGetResults(schema.Strict):
 
 class ProjectPolicyResults(schema.Strict):
     endpoint: Literal["/project/policy"] = schema.alias("endpoint")
-    project_name: safe.ProjectName
+    project_name: safe.ProjectKey
     policy_announce_release_subject: str
     policy_announce_release_template: str
     policy_binary_artifact_paths: list[str]
@@ -325,11 +325,11 @@ class ProjectsListResults(schema.Strict):
 class PublisherDistributionRecordArgs(schema.Strict):
     publisher: str = schema.example("user")
     jwt: str = schema.example("eyJhbGciOiJIUzI1[...]mMjLiuyu5CSpyHI=")
-    version: safe.VersionName = schema.example("0.0.1")
+    version: safe.VersionKey = schema.example("0.0.1")
     platform: sql.DistributionPlatform = 
schema.example(sql.DistributionPlatform.ARTIFACT_HUB)
     distribution_owner_namespace: safe.Alphanumeric | None = 
schema.default_example(None, "example")
     distribution_package: safe.Alphanumeric = schema.example("example")
-    distribution_version: safe.VersionName = schema.example("0.0.1")
+    distribution_version: safe.VersionKey = schema.example("0.0.1")
     staging: bool = schema.example(False)
     details: bool = schema.example(False)
 
@@ -356,7 +356,7 @@ class PublisherDistributionRecordResults(schema.Strict):
 class PublisherReleaseAnnounceArgs(schema.Strict):
     publisher: str = schema.example("user")
     jwt: str = schema.example("eyJhbGciOiJIUzI1[...]mMjLiuyu5CSpyHI=")
-    version: safe.VersionName = schema.example("0.0.1")
+    version: safe.VersionKey = schema.example("0.0.1")
     revision: safe.RevisionNumber = schema.example("00005")
     email_to: str = schema.example("[email protected]")
     body: str = schema.example("The Apache Example team is pleased to announce 
the release of Example 1.0.0...")
@@ -377,14 +377,14 @@ class PublisherSshRegisterArgs(schema.Strict):
 class PublisherSshRegisterResults(schema.Strict):
     endpoint: Literal["/publisher/ssh/register"] = schema.alias("endpoint")
     fingerprint: str = 
schema.example("SHA256:0123456789abcdef0123456789abcdef01234567")
-    project: safe.ProjectName = schema.example("example")
+    project: safe.ProjectKey = schema.example("example")
     expires: int = schema.example(1713547200)
 
 
 class PublisherVoteResolveArgs(schema.Strict):
     publisher: str = schema.example("user")
     jwt: str = schema.example("eyJhbGciOiJIUzI1[...]mMjLiuyu5CSpyHI=")
-    version: safe.VersionName = schema.example("0.0.1")
+    version: safe.VersionKey = schema.example("0.0.1")
     resolution: Literal["passed", "failed"] = schema.example("passed")
 
 
@@ -394,8 +394,8 @@ class PublisherVoteResolveResults(schema.Strict):
 
 
 class ReleaseAnnounceArgs(schema.Strict):
-    project: safe.ProjectName = schema.example("example")
-    version: safe.VersionName = schema.example("1.0.0")
+    project: safe.ProjectKey = schema.example("example")
+    version: safe.VersionKey = schema.example("1.0.0")
     revision: safe.RevisionNumber = schema.example("00005")
     email_to: str = schema.example("[email protected]")
     body: str = schema.example("The Apache Example team is pleased to announce 
the release of Example 1.0.0...")
@@ -408,8 +408,8 @@ class ReleaseAnnounceResults(schema.Strict):
 
 
 class ReleaseDraftDeleteArgs(schema.Strict):
-    project: safe.ProjectName = schema.example("example")
-    version: safe.VersionName = schema.example("0.0.1")
+    project: safe.ProjectKey = schema.example("example")
+    version: safe.VersionKey = schema.example("0.0.1")
 
 
 class ReleaseDraftDeleteResults(schema.Strict):
@@ -418,8 +418,8 @@ class ReleaseDraftDeleteResults(schema.Strict):
 
 
 class ReleaseCreateArgs(schema.Strict):
-    project: safe.ProjectName = schema.example("example")
-    version: safe.VersionName = schema.example("0.0.1")
+    project: safe.ProjectKey = schema.example("example")
+    version: safe.VersionKey = schema.example("0.0.1")
 
 
 class ReleaseCreateResults(schema.Strict):
@@ -428,8 +428,8 @@ class ReleaseCreateResults(schema.Strict):
 
 
 class ReleaseDeleteArgs(schema.Strict):
-    project: safe.ProjectName = schema.example("example")
-    version: safe.VersionName = schema.example("0.0.1")
+    project: safe.ProjectKey = schema.example("example")
+    version: safe.VersionKey = schema.example("0.0.1")
 
 
 class ReleaseDeleteResults(schema.Strict):
@@ -466,8 +466,8 @@ class ReleaseRevisionsResults(schema.Strict):
 
 
 class ReleaseUploadArgs(schema.Strict):
-    project: safe.ProjectName = schema.example("example")
-    version: safe.VersionName = schema.example("0.0.1")
+    project: safe.ProjectKey = schema.example("example")
+    version: safe.VersionKey = schema.example("0.0.1")
     relpath: str = schema.example("example/0.0.1/example-0.0.1-bin.tar.gz")
     content: str = schema.example("This is the content of the file.")
 
@@ -568,8 +568,8 @@ class UsersListResults(schema.Strict):
 
 
 class VoteResolveArgs(schema.Strict):
-    project: safe.ProjectName = schema.example("example")
-    version: safe.VersionName = schema.example("0.0.1")
+    project: safe.ProjectKey = schema.example("example")
+    version: safe.VersionKey = schema.example("0.0.1")
     resolution: Literal["passed", "failed"] = schema.example("passed")
 
 
@@ -579,8 +579,8 @@ class VoteResolveResults(schema.Strict):
 
 
 class VoteStartArgs(schema.Strict):
-    project: safe.ProjectName = schema.example("example")
-    version: safe.VersionName = schema.example("0.0.1")
+    project: safe.ProjectKey = schema.example("example")
+    version: safe.VersionKey = schema.example("0.0.1")
     revision: safe.RevisionNumber = schema.example("00005")
     email_to: str = schema.example("[email protected]")
     vote_duration: int = schema.example(10)
@@ -594,8 +594,8 @@ class VoteStartResults(schema.Strict):
 
 
 class VoteTabulateArgs(schema.Strict):
-    project: safe.ProjectName = schema.example("example")
-    version: safe.VersionName = schema.example("0.0.1")
+    project: safe.ProjectKey = schema.example("example")
+    version: safe.VersionKey = schema.example("0.0.1")
 
 
 class VoteTabulateResults(schema.Strict):
diff --git a/atr/models/distribution.py b/atr/models/distribution.py
index 7e5f3149..c805a894 100644
--- a/atr/models/distribution.py
+++ b/atr/models/distribution.py
@@ -95,7 +95,7 @@ class Data(schema.Subset):
     platform: sql.DistributionPlatform
     owner_namespace: safe.Alphanumeric | None = None
     package: safe.Alphanumeric
-    version: safe.VersionName
+    version: safe.VersionKey
     details: bool
 
     @pydantic.field_validator("owner_namespace", mode="before")
diff --git a/atr/models/safe.py b/atr/models/safe.py
index e72cfb21..a69750dd 100644
--- a/atr/models/safe.py
+++ b/atr/models/safe.py
@@ -102,12 +102,12 @@ class Numeric(SafeType):
         return _NUMERIC
 
 
-class ProjectName(Alphanumeric):
+class ProjectKey(Alphanumeric):
     """A project name that has been validated for safety."""
 
 
-class ReleaseName(Alphanumeric):
-    """A release name composed from a validated ProjectName and VersionName."""
+class ReleaseKey(Alphanumeric):
+    """A release name composed from a validated ProjectKey and VersionKey."""
 
     @classmethod
     def _valid_chars(cls) -> frozenset[str]:
@@ -118,7 +118,7 @@ class RevisionNumber(Numeric):
     """A revision number that has been validated for safety."""
 
 
-class VersionName(Alphanumeric):
+class VersionKey(Alphanumeric):
     """A version name that has been validated for safety"""
 
     @classmethod
diff --git a/atr/models/sql.py b/atr/models/sql.py
index 7b24582f..b13bfb45 100644
--- a/atr/models/sql.py
+++ b/atr/models/sql.py
@@ -613,9 +613,9 @@ class Project(sqlmodel.SQLModel, table=True):
         return base
 
     @property
-    def safe_name(self) -> safe.ProjectName:
+    def safe_name(self) -> safe.ProjectKey:
         """Get the typesafe validated name for the Project"""
-        return safe.ProjectName(self.name)
+        return safe.ProjectKey(self.name)
 
     @property
     def short_display_name(self) -> str:
@@ -942,19 +942,19 @@ class Release(sqlmodel.SQLModel, table=True):
         return safe.RevisionNumber(self.unwrap_revision_number)
 
     @property
-    def safe_name(self) -> safe.ReleaseName:
+    def safe_name(self) -> safe.ReleaseKey:
         """Get the typesafe validated name for the Release"""
-        return safe.ReleaseName(self.name)
+        return safe.ReleaseKey(self.name)
 
     @property
-    def safe_project_name(self) -> safe.ProjectName:
+    def safe_project_name(self) -> safe.ProjectKey:
         """Get the typesafe validated name for the release project"""
-        return safe.ProjectName(self.project_name)
+        return safe.ProjectKey(self.project_name)
 
     @property
-    def safe_version_name(self) -> safe.VersionName:
+    def safe_version_name(self) -> safe.VersionKey:
         """Get the typesafe validated name for the release version"""
-        return safe.VersionName(self.version)
+        return safe.VersionKey(self.version)
 
     @property
     def short_display_name(self) -> str:
@@ -1087,7 +1087,7 @@ class Distribution(sqlmodel.SQLModel, table=True):
             platform=self.platform,
             owner_namespace=safe.Alphanumeric(self.owner_namespace),
             package=safe.Alphanumeric(self.package),
-            version=safe.VersionName(self.version),
+            version=safe.VersionKey(self.version),
             details=details,
         )
 
@@ -1102,9 +1102,9 @@ class Distribution(sqlmodel.SQLModel, table=True):
         return f"{name}-{package}-{version}"
 
     @property
-    def safe_release_name(self) -> safe.ReleaseName:
+    def safe_release_name(self) -> safe.ReleaseKey:
         """Get the typesafe validated name for the distribution release"""
-        return safe.ReleaseName(self.release_name)
+        return safe.ReleaseKey(self.release_name)
 
     @property
     def title(self) -> str:
@@ -1381,7 +1381,7 @@ class WorkflowStatus(sqlmodel.SQLModel, table=True):
     message: str | None = sqlmodel.Field(default=None)
 
 
-def revision_name(release_name: safe.ReleaseName | str, number: str) -> str:
+def revision_name(release_name: safe.ReleaseKey | str, number: str) -> str:
     return f"{release_name} {number}"
 
 
@@ -1453,18 +1453,18 @@ def latest_revision_number_query(release_name: str | 
None = None) -> expression.
 
 
 @overload
-def release_name(project_name: safe.ProjectName, version_name: 
safe.VersionName) -> safe.ReleaseName: ...
+def release_name(project_name: safe.ProjectKey, version_name: safe.VersionKey) 
-> safe.ReleaseKey: ...
 
 
 @overload
 def release_name(project_name: str, version_name: str) -> str: ...
 
 
-def release_name(project_name: safe.ProjectName | str, version_name: 
safe.VersionName | str) -> safe.ReleaseName | str:
+def release_name(project_name: safe.ProjectKey | str, version_name: 
safe.VersionKey | str) -> safe.ReleaseKey | str:
     """Return the release name for a given project and version."""
     name = f"{project_name}-{version_name}"
-    if isinstance(project_name, safe.ProjectName) and isinstance(version_name, 
safe.VersionName):
-        return safe.ReleaseName(name)
+    if isinstance(project_name, safe.ProjectKey) and isinstance(version_name, 
safe.VersionKey):
+        return safe.ReleaseKey(name)
     return name
 
 
diff --git a/atr/paths.py b/atr/paths.py
index 38ced9e8..b19dae61 100644
--- a/atr/paths.py
+++ b/atr/paths.py
@@ -23,7 +23,7 @@ import atr.models.sql as sql
 
 
 def base_path_for_revision(
-    project_name: safe.ProjectName, version_name: safe.VersionName, revision: 
safe.RevisionNumber
+    project_name: safe.ProjectKey, version_name: safe.VersionKey, revision: 
safe.RevisionNumber
 ) -> pathlib.Path:
     return pathlib.Path(get_unfinished_dir(), str(project_name), 
str(version_name), str(revision))
 
@@ -135,6 +135,6 @@ def release_directory_version(release: sql.Release) -> 
pathlib.Path:
 
 
 def revision_path_for_file(
-    project_name: safe.ProjectName, version_name: safe.VersionName, revision: 
safe.RevisionNumber, file_name: str
+    project_name: safe.ProjectKey, version_name: safe.VersionKey, revision: 
safe.RevisionNumber, file_name: str
 ) -> pathlib.Path:
     return base_path_for_revision(project_name, version_name, revision) / 
file_name
diff --git a/atr/post/announce.py b/atr/post/announce.py
index 23ea535f..a65f47f9 100644
--- a/atr/post/announce.py
+++ b/atr/post/announce.py
@@ -34,8 +34,8 @@ import atr.web as web
 async def selected(
     session: web.Committer,
     _announce: Literal["announce"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     announce_form: shared.announce.AnnounceForm,
 ) -> web.WerkzeugResponse:
     """
diff --git a/atr/post/distribution.py b/atr/post/distribution.py
index 5680c02f..d58633b8 100644
--- a/atr/post/distribution.py
+++ b/atr/post/distribution.py
@@ -41,8 +41,8 @@ _AUTOMATED_PLATFORMS_STAGE: 
Final[tuple[shared.distribution.DistributionPlatform
 async def automate_form_process_page(
     session: web.Committer,
     form_data: shared.distribution.DistributionAutomateForm,
-    project: safe.ProjectName,
-    version: safe.VersionName,
+    project: safe.ProjectKey,
+    version: safe.VersionKey,
     /,
     staging: bool = False,
 ) -> web.WerkzeugResponse:
@@ -112,8 +112,8 @@ async def automate_form_process_page(
 async def automate_selected(
     session: web.Committer,
     _distribution_automate: Literal["distribution/automate"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     distribute_form: shared.distribution.DistributionAutomateForm,
 ) -> web.WerkzeugResponse:
     """
@@ -127,8 +127,8 @@ async def automate_selected(
 async def delete(
     session: web.Committer,
     _distribution_delete: Literal["distribution/delete"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     delete_form: shared.distribution.DeleteForm,
 ) -> web.WerkzeugResponse:
     """
@@ -170,8 +170,8 @@ async def delete(
 async def record_form_process_page(
     session: web.Committer,
     form_data: shared.distribution.DistributionRecordForm,
-    project: safe.ProjectName,
-    version: safe.VersionName,
+    project: safe.ProjectKey,
+    version: safe.VersionKey,
     /,
     staging: bool = False,
 ) -> web.WerkzeugResponse:
@@ -220,8 +220,8 @@ async def record_form_process_page(
 async def record_selected(
     session: web.Committer,
     _distribution_record: Literal["distribution/record"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     distribute_form: shared.distribution.DistributionRecordForm,
 ) -> web.WerkzeugResponse:
     """
@@ -234,8 +234,8 @@ async def record_selected(
 async def stage_automate_selected(
     session: web.Committer,
     _distribution_stage_automate: Literal["distribution/stage/automate"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     distribute_form: shared.distribution.DistributionAutomateForm,
 ) -> web.WerkzeugResponse:
     """
@@ -248,8 +248,8 @@ async def stage_automate_selected(
 async def stage_record_selected(
     session: web.Committer,
     _distribution_stage_record: Literal["distribution/stage/record"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     distribute_form: shared.distribution.DistributionRecordForm,
 ) -> web.WerkzeugResponse:
     """
diff --git a/atr/post/draft.py b/atr/post/draft.py
index dcfa6add..afbe408f 100644
--- a/atr/post/draft.py
+++ b/atr/post/draft.py
@@ -44,8 +44,8 @@ if TYPE_CHECKING:
 async def cache_reset(
     session: web.Committer,
     _draft_reset: Literal["draft/reset"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     _form: form.Empty,
 ) -> web.WerkzeugResponse:
     """
@@ -81,8 +81,8 @@ async def cache_reset(
 async def delete(
     session: web.Committer,
     _compose: Literal["compose"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     _form: form.Empty,
 ) -> web.WerkzeugResponse:
     """
@@ -112,8 +112,8 @@ async def delete(
 async def delete_file(
     session: web.Committer,
     _draft_delete_file: Literal["draft/delete-file"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     delete_file_form: shared.draft.DeleteFileForm,
 ) -> web.WerkzeugResponse:
     """
@@ -150,8 +150,8 @@ async def delete_file(
 async def hashgen(
     session: web.Committer,
     _draft_hashgen: Literal["draft/hashgen"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     file_path: unsafe.Path,
     empty_form: form.Empty,
 ) -> web.WerkzeugResponse:
@@ -190,8 +190,8 @@ async def hashgen(
 async def quarantine_clear(
     session: web.Committer,
     _draft_quarantine_clear: Literal["draft/quarantine/clear"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     clear_form: shared.draft.ClearQuarantineForm,
 ) -> web.WerkzeugResponse:
     """URL: /draft/quarantine/clear/<project_name>/<version_name>"""
@@ -211,8 +211,8 @@ async def quarantine_clear(
 async def recheck(
     session: web.Committer,
     _draft_recheck: Literal["draft/recheck"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     empty_form: form.Empty,
 ) -> web.WerkzeugResponse:
     """
@@ -248,8 +248,8 @@ async def recheck(
 async def sbomgen(
     session: web.Committer,
     _draft_sbomgen: Literal["draft/sbomgen"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     file_path: unsafe.Path,
     empty_form: form.Empty,
 ) -> web.WerkzeugResponse:
diff --git a/atr/post/finish.py b/atr/post/finish.py
index 74c26a13..115ad85b 100644
--- a/atr/post/finish.py
+++ b/atr/post/finish.py
@@ -32,8 +32,8 @@ import atr.web as web
 async def selected(
     session: web.Committer,
     _finish: Literal["finish"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     finish_form: shared.finish.FinishForm,
 ) -> tuple[web.QuartResponse, int] | web.WerkzeugResponse:
     """
@@ -54,8 +54,8 @@ async def selected(
 async def _delete_empty_directory(
     delete_form: shared.finish.DeleteEmptyDirectoryForm,
     session: web.Committer,
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     respond: shared.finish.Respond,
 ) -> tuple[web.QuartResponse, int] | web.WerkzeugResponse:
     dir_to_delete_rel = delete_form.directory_to_delete
@@ -77,8 +77,8 @@ async def _delete_empty_directory(
 async def _move_file_to_revision(
     move_form: shared.finish.MoveFileForm,
     session: web.Committer,
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     respond: shared.finish.Respond,
 ) -> tuple[web.QuartResponse, int] | web.WerkzeugResponse:
     source_files_rel = move_form.source_files
@@ -122,8 +122,8 @@ async def _move_file_to_revision(
 
 async def _remove_rc_tags(
     session: web.Committer,
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     respond: shared.finish.Respond,
 ) -> tuple[web.QuartResponse, int] | web.WerkzeugResponse:
     try:
@@ -154,7 +154,7 @@ async def _remove_rc_tags(
 
 
 def _respond_helper(
-    session: web.Committer, project_name: safe.ProjectName, version_name: 
safe.VersionName, wants_json: bool
+    session: web.Committer, project_name: safe.ProjectKey, version_name: 
safe.VersionKey, wants_json: bool
 ) -> shared.finish.Respond:
     """Create a response helper function for the finish route."""
     import atr.get as get
diff --git a/atr/post/ignores.py b/atr/post/ignores.py
index 92f37734..e5384eec 100644
--- a/atr/post/ignores.py
+++ b/atr/post/ignores.py
@@ -30,7 +30,7 @@ import atr.web as web
 async def ignores(
     session: web.Committer,
     _ignores: Literal["ignores"],
-    project_name: safe.ProjectName,
+    project_name: safe.ProjectKey,
     ignore_form: shared.ignores.IgnoreForm,
 ) -> web.WerkzeugResponse:
     """
@@ -50,7 +50,7 @@ async def ignores(
 
 
 async def _add_ignore(
-    session: web.Committer, add_form: shared.ignores.AddIgnoreForm, 
project_name: safe.ProjectName
+    session: web.Committer, add_form: shared.ignores.AddIgnoreForm, 
project_name: safe.ProjectKey
 ) -> web.WerkzeugResponse:
     """Add a new ignore."""
     status = shared.ignores.ignore_status_to_sql(add_form.status)  # pyright: 
ignore[reportArgumentType]
@@ -76,7 +76,7 @@ async def _add_ignore(
 
 
 async def _delete_ignore(
-    session: web.Committer, delete_form: shared.ignores.DeleteIgnoreForm, 
project_name: safe.ProjectName
+    session: web.Committer, delete_form: shared.ignores.DeleteIgnoreForm, 
project_name: safe.ProjectKey
 ) -> web.WerkzeugResponse:
     """Delete an ignore."""
     async with storage.write() as write:
@@ -91,7 +91,7 @@ async def _delete_ignore(
 
 
 async def _update_ignore(
-    session: web.Committer, update_form: shared.ignores.UpdateIgnoreForm, 
project_name: safe.ProjectName
+    session: web.Committer, update_form: shared.ignores.UpdateIgnoreForm, 
project_name: safe.ProjectKey
 ) -> web.WerkzeugResponse:
     """Update an ignore."""
     status = shared.ignores.ignore_status_to_sql(update_form.status)  # 
pyright: ignore[reportArgumentType]
diff --git a/atr/post/keys.py b/atr/post/keys.py
index 92b678a3..478c4d72 100644
--- a/atr/post/keys.py
+++ b/atr/post/keys.py
@@ -146,8 +146,8 @@ async def details(
 async def import_selected_revision(
     session: web.Committer,
     _keys_import: Literal["keys/import"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     _form: form.Empty,
 ) -> web.WerkzeugResponse:
     """
diff --git a/atr/post/manual.py b/atr/post/manual.py
index 591b323d..2643518e 100644
--- a/atr/post/manual.py
+++ b/atr/post/manual.py
@@ -34,8 +34,8 @@ import atr.web as web
 async def resolve_selected(
     session: web.Committer,
     _manual_resolve: Literal["manual/resolve"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     resolve_vote_form: shared.manual.ResolveVoteForm,
 ) -> web.WerkzeugResponse | str:
     """
@@ -85,8 +85,8 @@ async def resolve_selected(
 async def start_selected_revision(
     session: web.Committer,
     _manual_start: Literal["manual/start"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     revision: safe.RevisionNumber,
     _form: form.Empty,
 ) -> web.WerkzeugResponse | str:
diff --git a/atr/post/projects.py b/atr/post/projects.py
index 9896834a..868db28c 100644
--- a/atr/post/projects.py
+++ b/atr/post/projects.py
@@ -47,7 +47,7 @@ async def add_project(
     label = project_form.label
 
     async with storage.write(session) as write:
-        wacm = await 
write.as_project_committee_member(safe.ProjectName(str(committee_name)))
+        wacm = await 
write.as_project_committee_member(safe.ProjectKey(str(committee_name)))
         try:
             await wacm.project.create(committee_name, display_name, label)
         except storage.AccessError as e:
diff --git a/atr/post/resolve.py b/atr/post/resolve.py
index dee53b50..ae7ae1af 100644
--- a/atr/post/resolve.py
+++ b/atr/post/resolve.py
@@ -37,8 +37,8 @@ import atr.web as web
 async def selected(
     session: web.Committer,
     _resolve: Literal["resolve"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     resolve_form: shared.resolve.ResolveForm,
 ) -> web.WerkzeugResponse | str:
     """
@@ -55,8 +55,8 @@ async def selected(
 async def _submit(
     session: web.Committer,
     submit_form: shared.resolve.SubmitForm,
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
 ) -> web.WerkzeugResponse:
     email_body = submit_form.email_body
     vote_result = submit_form.vote_result
@@ -85,7 +85,7 @@ async def _submit(
     )
 
 
-async def _tabulate(session: web.Committer, project_name: safe.ProjectName, 
version_name: safe.VersionName) -> str:
+async def _tabulate(session: web.Committer, project_name: safe.ProjectKey, 
version_name: safe.VersionKey) -> str:
     asf_uid = session.uid
     full_name = session.fullname
 
diff --git a/atr/post/revisions.py b/atr/post/revisions.py
index 1acd8d9c..992914a0 100644
--- a/atr/post/revisions.py
+++ b/atr/post/revisions.py
@@ -32,8 +32,8 @@ import atr.web as web
 async def selected_post(
     session: web.Committer,
     _revisions: Literal["revisions"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     revision_form: shared.revisions.RevisionForm,
 ) -> web.WerkzeugResponse:
     """
@@ -49,8 +49,8 @@ async def selected_post(
 async def _set_revision(
     session: web.Committer,
     set_revision_form: shared.revisions.SetRevisionForm,
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
 ) -> web.WerkzeugResponse:
     """Set a specific revision as the latest for a candidate draft or release 
preview."""
     selected_revision_number = set_revision_form.revision_number
@@ -91,8 +91,8 @@ async def _set_revision(
 async def _set_tag(
     session: web.Committer,
     set_tag_form: shared.revisions.SetTagForm,
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
 ) -> web.WerkzeugResponse:
     """Set a tag on a specific revision."""
     revision_number = set_tag_form.revision_number
diff --git a/atr/post/sbom.py b/atr/post/sbom.py
index d3aeb28b..8dcf1014 100644
--- a/atr/post/sbom.py
+++ b/atr/post/sbom.py
@@ -42,8 +42,8 @@ if TYPE_CHECKING:
 async def report(
     session: web.Committer,
     _sbom_report: Literal["sbom/report"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     file_path: unsafe.Path,
     sbom_form: shared.sbom.SBOMForm,
 ) -> web.WerkzeugResponse:
@@ -64,7 +64,7 @@ async def report(
 
 
 async def _augment(
-    session: web.Committer, project_name: safe.ProjectName, version_name: 
safe.VersionName, rel_path: pathlib.Path
+    session: web.Committer, project_name: safe.ProjectKey, version_name: 
safe.VersionKey, rel_path: pathlib.Path
 ) -> web.WerkzeugResponse:
     """Augment a CycloneDX SBOM file."""
     # Check that the file is a .cdx.json archive before creating a revision
@@ -108,7 +108,7 @@ async def _augment(
 
 
 async def _scan(
-    session: web.Committer, project_name: safe.ProjectName, version_name: 
safe.VersionName, rel_path: pathlib.Path
+    session: web.Committer, project_name: safe.ProjectKey, version_name: 
safe.VersionKey, rel_path: pathlib.Path
 ) -> web.WerkzeugResponse:
     """Scan a CycloneDX SBOM file for vulnerabilities using OSV."""
     if not (rel_path.name.endswith(".cdx.json")):
diff --git a/atr/post/start.py b/atr/post/start.py
index b4c7fcde..26af3d4c 100644
--- a/atr/post/start.py
+++ b/atr/post/start.py
@@ -31,7 +31,7 @@ import atr.web as web
 async def selected(
     session: web.Committer,
     _start: Literal["start"],
-    project_name: safe.ProjectName,
+    project_name: safe.ProjectKey,
     start_release_form: shared.start.StartReleaseForm,
 ) -> web.WerkzeugResponse:
     """
@@ -43,7 +43,7 @@ async def selected(
             wacp = await write.as_project_committee_participant(project_name)
             new_release, _project = await wacp.release.start(
                 project_name,
-                safe.VersionName(start_release_form.version_name),
+                safe.VersionKey(start_release_form.version_name),
             )
 
         return await session.redirect(
diff --git a/atr/post/upload.py b/atr/post/upload.py
index 9e914cdf..ba46aa2f 100644
--- a/atr/post/upload.py
+++ b/atr/post/upload.py
@@ -46,8 +46,8 @@ import atr.web as web
 async def finalise(
     session: web.Committer,
     _upload_finalise: Literal["upload/finalise"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     upload_session: unsafe.UnsafeStr,
 ) -> web.WerkzeugResponse:
     """
@@ -120,8 +120,8 @@ async def finalise(
 async def selected(
     session: web.Committer,
     _upload: Literal["upload"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     upload_form: shared.upload.UploadForm,
 ) -> web.WerkzeugResponse:
     """
@@ -140,8 +140,8 @@ async def selected(
 async def stage(
     _session: web.Committer,
     _upload_stage: Literal["upload/stage"],
-    _project_name: safe.ProjectName,
-    _version_name: safe.VersionName,
+    _project_name: safe.ProjectKey,
+    _version_name: safe.VersionKey,
     upload_session: unsafe.UnsafeStr,
 ) -> web.WerkzeugResponse:
     """
@@ -188,8 +188,8 @@ async def stage(
 async def _add_files(
     session: web.Committer,
     add_form: shared.upload.AddFilesForm,
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
 ) -> web.WerkzeugResponse:
     try:
         file_data = add_form.file_data
@@ -251,8 +251,8 @@ def _json_success(data: dict[str, str], status: int = 200) 
-> web.WerkzeugRespon
 async def _svn_import(
     session: web.Committer,
     svn_form: shared.upload.SvnImportForm,
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
 ) -> web.WerkzeugResponse:
     # audit_guidance any file uploads are from known and managed repositories 
so file size is not an issue
     try:
diff --git a/atr/post/vote.py b/atr/post/vote.py
index b6d99c87..003f1bfb 100644
--- a/atr/post/vote.py
+++ b/atr/post/vote.py
@@ -32,8 +32,8 @@ import atr.web as web
 async def selected_post(
     session: web.Committer,
     _vote: Literal["vote"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     cast_vote_form: shared.vote.CastVoteForm,
 ) -> web.WerkzeugResponse:
     """
diff --git a/atr/post/voting.py b/atr/post/voting.py
index cac23325..3a0ea8b5 100644
--- a/atr/post/voting.py
+++ b/atr/post/voting.py
@@ -41,8 +41,8 @@ class BodyPreviewForm(form.Form):
 async def body_preview(
     session: web.Committer,
     _voting_body_preview: Literal["voting/body/preview"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     revision_number: safe.RevisionNumber,
     preview_form: BodyPreviewForm,
 ) -> web.QuartResponse:
@@ -70,8 +70,8 @@ async def body_preview(
 async def selected_revision(
     session: web.Committer,
     _voting: Literal["voting"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     revision: safe.RevisionNumber,
     start_voting_form: shared.voting.StartVotingForm,
 ) -> web.WerkzeugResponse | str:
diff --git a/atr/shared/distribution.py b/atr/shared/distribution.py
index 922e02db..d83a8c44 100644
--- a/atr/shared/distribution.py
+++ b/atr/shared/distribution.py
@@ -115,7 +115,7 @@ class DistributionPlatform(enum.Enum):
 
 
 class DeleteForm(form.Form):
-    release_name: safe.ReleaseName = form.label("Release name", 
widget=form.Widget.HIDDEN)
+    release_name: safe.ReleaseKey = form.label("Release name", 
widget=form.Widget.HIDDEN)
     platform: form.Enum[DistributionPlatform] = form.label("Platform", 
widget=form.Widget.HIDDEN)
     owner_namespace: str = form.label("Owner namespace", 
widget=form.Widget.HIDDEN)
     package: str = form.label("Package", widget=form.Widget.HIDDEN)
@@ -132,7 +132,7 @@ class DistributionAutomateForm(form.Form):
         "GitHub owner, ArtifactHub repo). Leave blank if not used.",
     )
     package: safe.Alphanumeric = form.label("Package")
-    version: safe.VersionName = form.label("Version")
+    version: safe.VersionKey = form.label("Version")
     details: form.Bool = form.label(
         "Include details",
         "Include the details of the distribution in the response",
@@ -165,7 +165,7 @@ class DistributionRecordForm(form.Form):
         "GitHub owner, ArtifactHub repo). Leave blank if not used.",
     )
     package: safe.Alphanumeric = form.label("Package")
-    version: safe.VersionName = form.label("Version")
+    version: safe.VersionKey = form.label("Version")
     details: form.Bool = form.label(
         "Include details",
         "Include the details of the distribution in the response",
@@ -193,7 +193,7 @@ class DistributionRecordForm(form.Form):
 def distribution_upload_date(  # noqa: C901
     platform: sql.DistributionPlatform,
     data: basic.JSON,
-    version_name: safe.VersionName,
+    version_name: safe.VersionKey,
 ) -> datetime.datetime | None:
     version = str(version_name)
     match platform:
@@ -237,7 +237,7 @@ def distribution_upload_date(  # noqa: C901
 def distribution_web_url(  # noqa: C901
     platform: sql.DistributionPlatform,
     data: basic.JSON,
-    version: safe.VersionName,
+    version: safe.VersionKey,
 ) -> str | None:
     match platform:
         case sql.DistributionPlatform.ARTIFACT_HUB:
@@ -319,7 +319,7 @@ def html_tr_a(label: str, value: str | None) -> htm.Element:
 
 
 async def json_from_distribution_platform(
-    api_url: str, platform: sql.DistributionPlatform, version_name: 
safe.VersionName
+    api_url: str, platform: sql.DistributionPlatform, version_name: 
safe.VersionKey
 ) -> outcome.Outcome[basic.JSON]:
     version = str(version_name)
     try:
@@ -338,7 +338,7 @@ async def json_from_distribution_platform(
     return outcome.Result(result)
 
 
-async def json_from_maven_xml(api_url: str, version_name: safe.VersionName) -> 
outcome.Outcome[basic.JSON]:
+async def json_from_maven_xml(api_url: str, version_name: safe.VersionKey) -> 
outcome.Outcome[basic.JSON]:
     import datetime
 
     import defusedxml.ElementTree as ElementTree
@@ -404,8 +404,8 @@ async def json_from_maven_xml(api_url: str, version_name: 
safe.VersionName) -> o
 
 
 async def release_validated(
-    project: safe.ProjectName,
-    version: safe.VersionName,
+    project: safe.ProjectKey,
+    version: safe.VersionKey,
     committee: bool = False,
     staging: bool | None = None,
     release_policy: bool = False,
@@ -432,7 +432,7 @@ async def release_validated(
 
 
 async def release_validated_and_committee(
-    project: safe.ProjectName, version: safe.VersionName, *, staging: bool | 
None = None, release_policy: bool = False
+    project: safe.ProjectKey, version: safe.VersionKey, *, staging: bool | 
None = None, release_policy: bool = False
 ) -> tuple[sql.Release, sql.Committee]:
     release = await release_validated(project, version, committee=True, 
staging=staging, release_policy=release_policy)
     committee = release.committee
diff --git a/atr/shared/projects.py b/atr/shared/projects.py
index f09f0d81..c8e6ff84 100644
--- a/atr/shared/projects.py
+++ b/atr/shared/projects.py
@@ -106,7 +106,7 @@ class AddProjectForm(form.Form):
 
 class ComposePolicyForm(form.Form):
     variant: COMPOSE = form.value(COMPOSE)
-    project_name: safe.ProjectName = form.label("Project name", 
widget=form.Widget.HIDDEN)
+    project_name: safe.ProjectKey = form.label("Project name", 
widget=form.Widget.HIDDEN)
     source_artifact_paths: str = form.label(
         "Source artifact paths",
         "Paths to source artifacts to be included in the release.",
@@ -182,7 +182,7 @@ class ComposePolicyForm(form.Form):
 
 class VotePolicyForm(form.Form):
     variant: VOTE = form.value(VOTE)
-    project_name: safe.ProjectName = form.label("Project name", 
widget=form.Widget.HIDDEN)
+    project_name: safe.ProjectKey = form.label("Project name", 
widget=form.Widget.HIDDEN)
     github_vote_workflow_path: str = form.label(
         "GitHub vote workflow paths",
         "The full paths to the GitHub workflows to use for the release, 
including the .github/workflows/ prefix.",
@@ -244,7 +244,7 @@ class VotePolicyForm(form.Form):
 
 class FinishPolicyForm(form.Form):
     variant: FINISH = form.value(FINISH)
-    project_name: safe.ProjectName = form.label("Project name", 
widget=form.Widget.HIDDEN)
+    project_name: safe.ProjectKey = form.label("Project name", 
widget=form.Widget.HIDDEN)
     github_finish_workflow_path: str = form.label(
         "GitHub finish workflow paths",
         "The full paths to the GitHub workflows to use for the release, 
including the .github/workflows/ prefix.",
@@ -278,35 +278,35 @@ class FinishPolicyForm(form.Form):
 
 class AddCategoryForm(form.Form):
     variant: ADD_CATEGORY = form.value(ADD_CATEGORY)
-    project_name: safe.ProjectName = form.label("Project name", 
widget=form.Widget.HIDDEN)
+    project_name: safe.ProjectKey = form.label("Project name", 
widget=form.Widget.HIDDEN)
     category_to_add: str = form.label("New category name")
 
 
 class RemoveCategoryForm(form.Form):
     variant: REMOVE_CATEGORY = form.value(REMOVE_CATEGORY)
-    project_name: safe.ProjectName = form.label("Project name", 
widget=form.Widget.HIDDEN)
+    project_name: safe.ProjectKey = form.label("Project name", 
widget=form.Widget.HIDDEN)
     category_to_remove: str = form.label("Category to remove", 
widget=form.Widget.HIDDEN)
 
 
 class AddLanguageForm(form.Form):
     variant: ADD_LANGUAGE = form.value(ADD_LANGUAGE)
-    project_name: safe.ProjectName = form.label("Project name", 
widget=form.Widget.HIDDEN)
+    project_name: safe.ProjectKey = form.label("Project name", 
widget=form.Widget.HIDDEN)
     language_to_add: str = form.label("New language name")
 
 
 class RemoveLanguageForm(form.Form):
     variant: REMOVE_LANGUAGE = form.value(REMOVE_LANGUAGE)
-    project_name: safe.ProjectName = form.label("Project name", 
widget=form.Widget.HIDDEN)
+    project_name: safe.ProjectKey = form.label("Project name", 
widget=form.Widget.HIDDEN)
     language_to_remove: str = form.label("Language to remove", 
widget=form.Widget.HIDDEN)
 
 
 class DeleteProjectForm(form.Form):
     variant: DELETE_PROJECT = form.value(DELETE_PROJECT)
-    project_name: safe.ProjectName = form.label("Project name", 
widget=form.Widget.HIDDEN)
+    project_name: safe.ProjectKey = form.label("Project name", 
widget=form.Widget.HIDDEN)
 
 
 class DeleteSelectedProject(form.Form):
-    project_name: safe.ProjectName = form.label("Project name", 
widget=form.Widget.HIDDEN)
+    project_name: safe.ProjectKey = form.label("Project name", 
widget=form.Widget.HIDDEN)
 
 
 type ProjectViewForm = Annotated[
diff --git a/atr/shared/web.py b/atr/shared/web.py
index 93089cf3..21f047d9 100644
--- a/atr/shared/web.py
+++ b/atr/shared/web.py
@@ -214,7 +214,7 @@ async def check(
 
 
 def render_checks_summary(
-    info: types.PathInfo | None, project_name: safe.ProjectName, version_name: 
safe.VersionName
+    info: types.PathInfo | None, project_name: safe.ProjectKey, version_name: 
safe.VersionKey
 ) -> htm.Element | None:
     if (info is None) or (not info.checker_stats):
         return None
diff --git a/atr/ssh.py b/atr/ssh.py
index 9ca7a650..7ad0be81 100644
--- a/atr/ssh.py
+++ b/atr/ssh.py
@@ -341,7 +341,7 @@ def _step_03_command_simple_validate(argv: list[str]) -> 
bool:
 
 async def _step_04_command_validate(
     process: asyncssh.SSHServerProcess, argv: list[str], is_read_request: 
bool, server: SSHServer
-) -> tuple[safe.ProjectName, safe.VersionName, list[str] | None, sql.Release | 
None]:
+) -> tuple[safe.ProjectKey, safe.VersionKey, list[str] | None, sql.Release | 
None]:
     """Validate the path and user permissions for read or write."""
     ############################################
     ### Calls _step_05a/b_command_path_validate ###
@@ -377,7 +377,7 @@ async def _step_04_command_validate(
     return project_name, version_name, None, None
 
 
-async def _step_05a_command_path_validate_read(path: str) -> 
tuple[safe.ProjectName, safe.VersionName, str | None]:
+async def _step_05a_command_path_validate_read(path: str) -> 
tuple[safe.ProjectKey, safe.VersionKey, str | None]:
     """Validate the path argument for rsync read commands, returning safe 
types."""
     # READ: rsync --server --sender -vlogDtpre.iLsfxCIvu . /proj/v1/
     # Validating path: /proj/v1/
@@ -388,11 +388,11 @@ async def _step_05a_command_path_validate_read(path: str) 
-> tuple[safe.ProjectN
     path_project, path_version, *rest = path.strip("/").split("/", 2)
     tag = rest[0] if rest else None
     try:
-        project_name = safe.ProjectName(path_project)
+        project_name = safe.ProjectKey(path_project)
     except ValueError:
         raise RsyncArgsError("Project is invalid")
     try:
-        version_name = safe.VersionName(path_version)
+        version_name = safe.VersionKey(path_version)
     except ValueError:
         raise RsyncArgsError("Version is invalid")
     if tag:
@@ -408,7 +408,7 @@ async def _step_05a_command_path_validate_read(path: str) 
-> tuple[safe.ProjectN
     return project_name, version_name, tag
 
 
-async def _step_05b_command_path_validate_write(path: str) -> 
tuple[safe.ProjectName, safe.VersionName, None]:
+async def _step_05b_command_path_validate_write(path: str) -> 
tuple[safe.ProjectKey, safe.VersionKey, None]:
     """Validate the path argument for rsync write commands, returning safe 
types."""
     # WRITE: rsync --server -vlogDtpre.iLsfxCIvu . /proj/v1/
     # Validating path: /proj/v1/
@@ -418,11 +418,11 @@ async def _step_05b_command_path_validate_write(path: 
str) -> tuple[safe.Project
 
     path_project, path_version = path.strip("/").split("/", 1)
     try:
-        project_name = safe.ProjectName(path_project)
+        project_name = safe.ProjectKey(path_project)
     except ValueError:
         raise RsyncArgsError("Project is invalid")
     try:
-        version_name = safe.VersionName(path_version)
+        version_name = safe.VersionKey(path_version)
     except ValueError:
         raise RsyncArgsError("Version is invalid")
 
@@ -455,8 +455,8 @@ async def _step_06a_validate_read_permissions(
     ssh_uid: str,
     project: sql.Project,
     release: sql.Release | None,
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     tag: str | None,
 ) -> tuple[sql.Release | None, list[str] | None]:
     """Validate permissions for a read request."""
@@ -564,8 +564,8 @@ async def _step_07a_process_validated_rsync_read(
 async def _step_07b_process_validated_rsync_write(
     process: asyncssh.SSHServerProcess,
     argv: list[str],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     server: SSHServer,
 ) -> None:
     """Handle a validated rsync write request."""
@@ -636,7 +636,7 @@ async def _step_07b_process_validated_rsync_write(
 
 
 async def _step_07c_ensure_release_object_for_write(
-    project_name: safe.ProjectName, version_name: safe.VersionName
+    project_name: safe.ProjectKey, version_name: safe.VersionKey
 ) -> None:
     """Ensure the release object exists or create it for a write operation."""
     release_name = sql.release_name(str(project_name), str(version_name))
diff --git a/atr/storage/__init__.py b/atr/storage/__init__.py
index e0b5852e..4979fb6e 100644
--- a/atr/storage/__init__.py
+++ b/atr/storage/__init__.py
@@ -333,12 +333,12 @@ class Write:
     # async def as_key_owner(self) -> types.Outcome[WriteAsKeyOwner]:
     #     ...
 
-    async def as_project_committee_admin(self, project_name: safe.ProjectName) 
-> WriteAsCommitteeAdmin:
+    async def as_project_committee_admin(self, project_name: safe.ProjectKey) 
-> WriteAsCommitteeAdmin:
         write_as_outcome = await 
self.as_project_committee_admin_outcome(project_name)
         return write_as_outcome.result_or_raise()
 
     async def as_project_committee_admin_outcome(
-        self, project_name: safe.ProjectName
+        self, project_name: safe.ProjectKey
     ) -> outcome.Outcome[WriteAsCommitteeAdmin]:
         project = await self.__data.project(str(project_name), 
_committee=True).demand(
             AccessError(f"Project not found: {project_name}")
@@ -355,12 +355,12 @@ class Write:
             return outcome.Error(e)
         return outcome.Result(waca)
 
-    async def as_project_committee_member(self, project_name: 
safe.ProjectName) -> WriteAsCommitteeMember:
+    async def as_project_committee_member(self, project_name: safe.ProjectKey) 
-> WriteAsCommitteeMember:
         write_as_outcome = await 
self.as_project_committee_member_outcome(project_name)
         return write_as_outcome.result_or_raise()
 
     async def as_project_committee_member_outcome(
-        self, project_name: safe.ProjectName
+        self, project_name: safe.ProjectKey
     ) -> outcome.Outcome[WriteAsCommitteeMember]:
         project = await self.__data.project(str(project_name), 
_committee=True).demand(
             AccessError(f"Project not found: {project_name}")
@@ -377,12 +377,12 @@ class Write:
             return outcome.Error(e)
         return outcome.Result(wacm)
 
-    async def as_project_committee_participant(self, project_name: 
safe.ProjectName) -> WriteAsCommitteeParticipant:
+    async def as_project_committee_participant(self, project_name: 
safe.ProjectKey) -> WriteAsCommitteeParticipant:
         write_as_outcome = await 
self.as_project_committee_participant_outcome(project_name)
         return write_as_outcome.result_or_raise()
 
     async def as_project_committee_participant_outcome(
-        self, project_name: safe.ProjectName
+        self, project_name: safe.ProjectKey
     ) -> outcome.Outcome[WriteAsCommitteeParticipant]:
         project = await self.__data.project(str(project_name), 
_committee=True).demand(
             AccessError(f"Project not found: {project_name!s}")
@@ -514,7 +514,7 @@ async def write_as_committee_participant(
 
 @contextlib.asynccontextmanager
 async def write_as_project_committee_member(
-    project_name: safe.ProjectName,
+    project_name: safe.ProjectKey,
     asf_uid: principal.UID = principal.ArgumentNone,
 ) -> AsyncGenerator[WriteAsCommitteeMember]:
     async with write(asf_uid) as w:
diff --git a/atr/storage/readers/checks.py b/atr/storage/readers/checks.py
index 466e2fc7..b87777d8 100644
--- a/atr/storage/readers/checks.py
+++ b/atr/storage/readers/checks.py
@@ -79,7 +79,7 @@ class GeneralPublic:
             member_results_list[member_rel_path].sort(key=lambda r: r.checker)
         return types.CheckResults(primary_results_list, member_results_list, 
ignored_checks)
 
-    async def ignores(self, project_name: safe.ProjectName) -> 
list[sql.CheckResultIgnore]:
+    async def ignores(self, project_name: safe.ProjectKey) -> 
list[sql.CheckResultIgnore]:
         results = await self.__data.check_result_ignore(
             project_name=str(project_name),
         ).all()
@@ -87,7 +87,7 @@ class GeneralPublic:
 
     async def ignores_matcher(
         self,
-        project_name: safe.ProjectName,
+        project_name: safe.ProjectKey,
     ) -> Callable[[sql.CheckResult], bool]:
         ignores = await self.__data.check_result_ignore(
             project_name=str(project_name),
diff --git a/atr/storage/writers/announce.py b/atr/storage/writers/announce.py
index 69f10aa5..3124fda4 100644
--- a/atr/storage/writers/announce.py
+++ b/atr/storage/writers/announce.py
@@ -104,8 +104,8 @@ class CommitteeMember(CommitteeParticipant):
 
     async def release(  # noqa: C901
         self,
-        project_name: safe.ProjectName,
-        version_name: safe.VersionName,
+        project_name: safe.ProjectKey,
+        version_name: safe.VersionKey,
         preview_revision_number: safe.RevisionNumber,
         recipient: str,
         body: str,
diff --git a/atr/storage/writers/checks.py b/atr/storage/writers/checks.py
index 36094fdd..c4016e06 100644
--- a/atr/storage/writers/checks.py
+++ b/atr/storage/writers/checks.py
@@ -93,7 +93,7 @@ class CommitteeMember(CommitteeParticipant):
 
     async def ignore_add(
         self,
-        project_name: safe.ProjectName,
+        project_name: safe.ProjectKey,
         release_glob: str | None = None,
         revision_number: safe.RevisionNumber | None = None,
         checker_glob: str | None = None,
diff --git a/atr/storage/writers/distributions.py 
b/atr/storage/writers/distributions.py
index f9707c83..21f2ef7d 100644
--- a/atr/storage/writers/distributions.py
+++ b/atr/storage/writers/distributions.py
@@ -94,16 +94,16 @@ class CommitteeMember(CommitteeParticipant):
 
     async def automate(
         self,
-        release_name: models.safe.ReleaseName,
+        release_name: models.safe.ReleaseKey,
         platform: models.sql.DistributionPlatform,
         committee_name: str,
         owner_namespace: models.safe.Alphanumeric | None,
-        project_name: models.safe.ProjectName,
-        version_name: models.safe.VersionName,
+        project_name: models.safe.ProjectKey,
+        version_name: models.safe.VersionKey,
         phase: str,
         revision_number: str | None,
         package: models.safe.Alphanumeric,
-        version: models.safe.VersionName,
+        version: models.safe.VersionKey,
         staging: bool,
     ) -> models.sql.Task:
         dist_task = models.sql.Task(
@@ -136,11 +136,11 @@ class CommitteeMember(CommitteeParticipant):
 
     async def record(
         self,
-        release_name: models.safe.ReleaseName,
+        release_name: models.safe.ReleaseKey,
         platform: models.sql.DistributionPlatform,
         owner_namespace: models.safe.Alphanumeric | None,
         package: models.safe.Alphanumeric,
-        version: models.safe.VersionName,
+        version: models.safe.VersionKey,
         staging: bool,
         pending: bool,
         upload_date: datetime.datetime | None,
@@ -197,7 +197,7 @@ class CommitteeMember(CommitteeParticipant):
 
     async def record_from_data(
         self,
-        release_name: models.safe.ReleaseName,
+        release_name: models.safe.ReleaseKey,
         staging: bool,
         dd: models.distribution.Data,
         allow_retries: bool = False,
@@ -253,7 +253,7 @@ class CommitteeMember(CommitteeParticipant):
 
     async def __upgrade_staging_to_final(
         self,
-        release_name: models.safe.ReleaseName,
+        release_name: models.safe.ReleaseKey,
         platform: models.sql.DistributionPlatform,
         owner_namespace: str | None,
         package: str,
@@ -284,7 +284,7 @@ class CommitteeMember(CommitteeParticipant):
 
     async def delete_distribution(
         self,
-        release_name: models.safe.ReleaseName,
+        release_name: models.safe.ReleaseKey,
         platform: models.sql.DistributionPlatform,
         owner_namespace: str,
         package: str,
diff --git a/atr/storage/writers/keys.py b/atr/storage/writers/keys.py
index cfc07d5e..32df96b6 100644
--- a/atr/storage/writers/keys.py
+++ b/atr/storage/writers/keys.py
@@ -467,7 +467,7 @@ class CommitteeParticipant(FoundationCommitter):
         return outcomes
 
     async def import_keys_file(
-        self, project_name: safe.ProjectName, version_name: safe.VersionName
+        self, project_name: safe.ProjectKey, version_name: safe.VersionKey
     ) -> outcome.List[types.Key]:
         release = await self.__data.release(
             project_name=str(project_name),
diff --git a/atr/storage/writers/policy.py b/atr/storage/writers/policy.py
index 79f0f6f8..d243d1dd 100644
--- a/atr/storage/writers/policy.py
+++ b/atr/storage/writers/policy.py
@@ -170,7 +170,7 @@ class CommitteeMember(CommitteeParticipant):
         )
 
     async def __get_or_create_policy(
-        self, project_name: models.safe.ProjectName
+        self, project_name: models.safe.ProjectKey
     ) -> tuple[models.sql.Project, models.sql.ReleasePolicy]:
         project = await self.__data.project(
             name=str(project_name), status=models.sql.ProjectStatus.ACTIVE, 
_release_policy=True, _committee=True
diff --git a/atr/storage/writers/project.py b/atr/storage/writers/project.py
index 031be6d7..3bcb471b 100644
--- a/atr/storage/writers/project.py
+++ b/atr/storage/writers/project.py
@@ -175,7 +175,7 @@ class CommitteeMember(CommitteeParticipant):
             project_name=label,
         )
 
-    async def delete(self, project_name: safe.ProjectName) -> None:
+    async def delete(self, project_name: safe.ProjectKey) -> None:
         project = await self.__data.project(
             name=str(project_name), status=sql.ProjectStatus.ACTIVE, 
_releases=True, _distribution_channels=True
         ).get()
diff --git a/atr/storage/writers/release.py b/atr/storage/writers/release.py
index 8503e1c3..2aee86fc 100644
--- a/atr/storage/writers/release.py
+++ b/atr/storage/writers/release.py
@@ -96,8 +96,8 @@ class CommitteeParticipant(FoundationCommitter):
 
     async def delete(
         self,
-        project_name: safe.ProjectName,
-        version: safe.VersionName,
+        project_name: safe.ProjectKey,
+        version: safe.VersionKey,
         phase: db.Opt[sql.ReleasePhase] = db.NOT_SET,
         include_downloads: bool = True,
     ) -> str | None:
@@ -158,7 +158,7 @@ class CommitteeParticipant(FoundationCommitter):
         return error
 
     async def delete_empty_directory(
-        self, project_name: safe.ProjectName, version_name: safe.VersionName, 
dir_to_delete_rel: pathlib.Path
+        self, project_name: safe.ProjectKey, version_name: safe.VersionKey, 
dir_to_delete_rel: pathlib.Path
     ) -> str | None:
         description = f"Delete empty directory {dir_to_delete_rel} via web 
interface"
 
@@ -181,7 +181,7 @@ class CommitteeParticipant(FoundationCommitter):
         return None
 
     async def delete_file(
-        self, project_name: safe.ProjectName, version: safe.VersionName, 
rel_path_to_delete: pathlib.Path
+        self, project_name: safe.ProjectKey, version: safe.VersionKey, 
rel_path_to_delete: pathlib.Path
     ) -> int:
         metadata_files_deleted = 0
         description = "File deletion through web interface"
@@ -221,7 +221,7 @@ class CommitteeParticipant(FoundationCommitter):
         return metadata_files_deleted
 
     async def generate_hash_file(
-        self, project_name: safe.ProjectName, version_name: safe.VersionName, 
rel_path: pathlib.Path
+        self, project_name: safe.ProjectKey, version_name: safe.VersionKey, 
rel_path: pathlib.Path
     ) -> None:
         description = "Hash generation through web interface"
 
@@ -262,8 +262,8 @@ class CommitteeParticipant(FoundationCommitter):
 
     async def import_from_svn(
         self,
-        project_name: safe.ProjectName,
-        version_name: safe.VersionName,
+        project_name: safe.ProjectKey,
+        version_name: safe.VersionKey,
         svn_url: str,
         revision: str,
         target_subdirectory: str | None,
@@ -292,8 +292,8 @@ class CommitteeParticipant(FoundationCommitter):
 
     async def move_file(
         self,
-        project_name: safe.ProjectName,
-        version_name: safe.VersionName,
+        project_name: safe.ProjectKey,
+        version_name: safe.VersionKey,
         source_files_rel: list[pathlib.Path],
         target_dir_rel: pathlib.Path,
     ) -> tuple[str | None, list[str], list[str]]:
@@ -320,7 +320,7 @@ class CommitteeParticipant(FoundationCommitter):
 
     async def promote_to_candidate(
         self,
-        release_name: safe.ReleaseName,
+        release_name: safe.ReleaseKey,
         selected_revision_number: safe.RevisionNumber,
         vote_manual: bool = False,
     ) -> str | None:
@@ -384,7 +384,7 @@ class CommitteeParticipant(FoundationCommitter):
         return None
 
     async def remove_rc_tags(
-        self, project_name: safe.ProjectName, version_name: safe.VersionName
+        self, project_name: safe.ProjectKey, version_name: safe.VersionKey
     ) -> tuple[str | None, int, list[str]]:
         description = "Remove RC tags from paths via web interface"
         error_messages: list[str] = []
@@ -402,7 +402,7 @@ class CommitteeParticipant(FoundationCommitter):
             return str(e), renamed_count, error_messages
         return None, renamed_count, error_messages
 
-    async def start(self, project_name: safe.ProjectName, version: 
safe.VersionName) -> tuple[sql.Release, sql.Project]:  # noqa: C901
+    async def start(self, project_name: safe.ProjectKey, version: 
safe.VersionKey) -> tuple[sql.Release, sql.Project]:  # noqa: C901
         """Creates the initial release draft record and revision directory."""
         # Get the project from the project name
         project = await self.__data.project(
@@ -503,8 +503,8 @@ class CommitteeParticipant(FoundationCommitter):
 
     async def upload_files(
         self,
-        project_name: safe.ProjectName,
-        version_name: safe.VersionName,
+        project_name: safe.ProjectKey,
+        version_name: safe.VersionKey,
         files: Sequence[datastructures.FileStorage],
     ) -> tuple[str | None, int, bool]:
         """Process and save the uploaded files into a new draft revision."""
@@ -565,7 +565,7 @@ class CommitteeParticipant(FoundationCommitter):
                         continue
 
     async def __delete_release_data_filesystem(
-        self, release_dirs: Sequence[pathlib.Path], project_name: 
safe.ProjectName, version: safe.VersionName
+        self, release_dirs: Sequence[pathlib.Path], project_name: 
safe.ProjectKey, version: safe.VersionKey
     ) -> str | None:
         delete_errors: list[str] = []
         for release_dir in release_dirs:
@@ -756,8 +756,8 @@ class CommitteeParticipant(FoundationCommitter):
 
     async def __tasks_ongoing(
         self,
-        project_name: safe.ProjectName,
-        version_name: safe.VersionName,
+        project_name: safe.ProjectKey,
+        version_name: safe.VersionKey,
         revision_number: safe.RevisionNumber | None = None,
     ) -> int:
         tasks = sqlmodel.select(sqlalchemy.func.count()).select_from(sql.Task)
diff --git a/atr/storage/writers/revision.py b/atr/storage/writers/revision.py
index d5c02dfa..e8265201 100644
--- a/atr/storage/writers/revision.py
+++ b/atr/storage/writers/revision.py
@@ -83,12 +83,12 @@ async def finalise_revision(
     path_to_hash: dict[str, str],
     path_to_size: dict[str, int],
     previous_attestable: atr.models.attestable.AttestableV1 | None,
-    project_name: safe.ProjectName,
+    project_name: safe.ProjectKey,
     release: sql.Release,
-    release_name: safe.ReleaseName,
+    release_name: safe.ReleaseKey,
     temp_dir: str,
     temp_dir_path: pathlib.Path,
-    version_name: safe.VersionName,
+    version_name: safe.VersionKey,
     was_quarantined: bool = False,
 ) -> sql.Revision:
     try:
@@ -138,11 +138,11 @@ async def _commit_new_revision(
     path_to_hash: dict[str, str],
     path_to_size: dict[str, int],
     previous_attestable: atr.models.attestable.AttestableV1 | None,
-    project_name: safe.ProjectName,
+    project_name: safe.ProjectKey,
     release: sql.Release,
     release_name: str,
     temp_dir: str,
-    version_name: safe.VersionName,
+    version_name: safe.VersionKey,
     was_quarantined: bool = False,
 ) -> sql.Revision:
     try:
@@ -242,11 +242,11 @@ async def _lock_and_merge(
     path_to_hash: dict[str, str],
     path_to_size: dict[str, int],
     previous_attestable: atr.models.attestable.AttestableV1 | None,
-    project_name: safe.ProjectName,
+    project_name: safe.ProjectKey,
     release: sql.Release,
-    _release_name: safe.ReleaseName,
+    _release_name: safe.ReleaseKey,
     temp_dir_path: pathlib.Path,
-    version_name: safe.VersionName,
+    version_name: safe.VersionKey,
 ) -> tuple[atr.models.attestable.AttestableV1 | None, str | None, str | None, 
sql.Release]:
     # Acquire the write lock
     # We need this write lock for moving the directory afterwards atomically
@@ -333,8 +333,8 @@ class CommitteeParticipant(FoundationCommitter):
 
     async def clear_quarantine(
         self,
-        project_name: safe.ProjectName,
-        version_name: safe.VersionName,
+        project_name: safe.ProjectKey,
+        version_name: safe.VersionKey,
         quarantined_id: int,
     ) -> None:
         release_name = sql.release_name(str(project_name), str(version_name))
@@ -354,8 +354,8 @@ class CommitteeParticipant(FoundationCommitter):
 
     async def create_revision_with_quarantine(  # noqa: C901
         self,
-        project_name: safe.ProjectName,
-        version_name: safe.VersionName,
+        project_name: safe.ProjectKey,
+        version_name: safe.VersionKey,
         asf_uid: str,
         description: str | None = None,
         set_local_cache: bool = False,
@@ -513,10 +513,10 @@ class CommitteeParticipant(FoundationCommitter):
         description: str | None,
         path_to_size: dict[str, int],
         prior_revision_name: str | None,
-        project_name: safe.ProjectName,
+        project_name: safe.ProjectKey,
         release_name: str,
         temp_dir: str,
-        version_name: safe.VersionName,
+        version_name: safe.VersionKey,
     ) -> sql.Quarantined:
         file_metadata = [
             sql.QuarantineFileEntryV1(
diff --git a/atr/storage/writers/sbom.py b/atr/storage/writers/sbom.py
index 91088e1b..13a6f7bd 100644
--- a/atr/storage/writers/sbom.py
+++ b/atr/storage/writers/sbom.py
@@ -78,8 +78,8 @@ class CommitteeParticipant(FoundationCommitter):
 
     async def augment_cyclonedx(
         self,
-        project_name: safe.ProjectName,
-        version_name: safe.VersionName,
+        project_name: safe.ProjectKey,
+        version_name: safe.VersionKey,
         revision_number: str,
         rel_path: pathlib.Path,
     ) -> sql.Task:
@@ -107,8 +107,8 @@ class CommitteeParticipant(FoundationCommitter):
 
     async def generate_cyclonedx(
         self,
-        project_name: safe.ProjectName,
-        version_name: safe.VersionName,
+        project_name: safe.ProjectKey,
+        version_name: safe.VersionKey,
         revision_number: str,
         path_in_new_revision: pathlib.Path,
         sbom_path_in_new_revision: pathlib.Path,
@@ -137,8 +137,8 @@ class CommitteeParticipant(FoundationCommitter):
 
     async def osv_scan_cyclonedx(
         self,
-        project_name: safe.ProjectName,
-        version_name: safe.VersionName,
+        project_name: safe.ProjectKey,
+        version_name: safe.VersionKey,
         revision_number: str,
         rel_path: pathlib.Path,
     ) -> sql.Task:
diff --git a/atr/storage/writers/ssh.py b/atr/storage/writers/ssh.py
index 3354ab28..8bd2a140 100644
--- a/atr/storage/writers/ssh.py
+++ b/atr/storage/writers/ssh.py
@@ -86,7 +86,7 @@ class CommitteeParticipant(FoundationCommitter):
         self.__committee_name = committee_name
 
     async def add_workflow_key(
-        self, github_uid: str, github_nid: int, project_name: 
safe.ProjectName, key: str, github_payload: dict[str, Any]
+        self, github_uid: str, github_nid: int, project_name: safe.ProjectKey, 
key: str, github_payload: dict[str, Any]
     ) -> tuple[str, int]:
         now = int(time.time())
         # Twenty minutes to upload all files
diff --git a/atr/storage/writers/vote.py b/atr/storage/writers/vote.py
index fb8e7c59..0e8ddeaa 100644
--- a/atr/storage/writers/vote.py
+++ b/atr/storage/writers/vote.py
@@ -137,8 +137,8 @@ class CommitteeParticipant(FoundationCommitter):
     async def start(
         self,
         email_to: str,
-        project_name: safe.ProjectName,
-        version_name: safe.VersionName,
+        project_name: safe.ProjectKey,
+        version_name: safe.VersionKey,
         selected_revision_number: safe.RevisionNumber,
         vote_duration_choice: int,
         subject: str,
@@ -229,8 +229,8 @@ class CommitteeMember(CommitteeParticipant):
 
     async def resolve(
         self,
-        project_name: safe.ProjectName,
-        version_name: safe.VersionName,
+        project_name: safe.ProjectKey,
+        version_name: safe.VersionKey,
         vote_result: Literal["passed", "failed"],
         asf_fullname: str,
         resolution_body: str,
@@ -269,8 +269,8 @@ class CommitteeMember(CommitteeParticipant):
 
     async def resolve_manually(
         self,
-        project_name: safe.ProjectName,
-        version_name: safe.VersionName,
+        project_name: safe.ProjectKey,
+        version_name: safe.VersionKey,
         vote_result: Literal["passed", "failed"],
     ) -> str:
         release = await self.__data.release(
@@ -318,7 +318,7 @@ class CommitteeMember(CommitteeParticipant):
 
     async def resolve_release(
         self,
-        project_name: safe.ProjectName,
+        project_name: safe.ProjectKey,
         release: sql.Release,
         voting_round: int | None,
         vote_result: Literal["passed", "failed"],
diff --git a/atr/storage/writers/workflowstatus.py 
b/atr/storage/writers/workflowstatus.py
index 1730afe0..2783f9b3 100644
--- a/atr/storage/writers/workflowstatus.py
+++ b/atr/storage/writers/workflowstatus.py
@@ -104,7 +104,7 @@ class CommitteeMember(CommitteeParticipant):
         self,
         workflow_id: str,
         run_id: int,
-        project_name: safe.ProjectName,
+        project_name: safe.ProjectKey,
         task_id: int | None = None,
         status: str | None = None,
         message: str | None = None,
diff --git a/atr/tasks/__init__.py b/atr/tasks/__init__.py
index db144349..34106535 100644
--- a/atr/tasks/__init__.py
+++ b/atr/tasks/__init__.py
@@ -133,8 +133,8 @@ async def distribution_status_check(
 
 async def draft_checks(
     asf_uid: str,
-    project_name: safe.ProjectName,
-    release_version: safe.VersionName,
+    project_name: safe.ProjectKey,
+    release_version: safe.VersionKey,
     revision_number: safe.RevisionNumber,
     caller_data: db.Session | None = None,
 ) -> int:
@@ -206,8 +206,8 @@ async def draft_checks(
 
 async def keys_import_file(
     asf_uid: str,
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     revision_number: str,
     caller_data: db.Session | None = None,
 ) -> None:
@@ -569,9 +569,9 @@ async def _draft_file_checks(
     data: db.Session,
     path: pathlib.Path,
     previous_version: sql.Release | None,
-    project_name: safe.ProjectName,
+    project_name: safe.ProjectKey,
     release: sql.Release,
-    release_version: safe.VersionName,
+    release_version: safe.VersionKey,
     revision_number: safe.RevisionNumber,
 ):
     path_str = str(path)
diff --git a/atr/tasks/checks/__init__.py b/atr/tasks/checks/__init__.py
index 16f1bb8c..8e754b7a 100644
--- a/atr/tasks/checks/__init__.py
+++ b/atr/tasks/checks/__init__.py
@@ -52,8 +52,8 @@ import atr.util as util
 class FunctionArguments:
     recorder: Callable[[], Awaitable[Recorder]]
     asf_uid: str
-    project_name: safe.ProjectName
-    version_name: safe.VersionName
+    project_name: safe.ProjectKey
+    version_name: safe.VersionKey
     revision_number: safe.RevisionNumber
     primary_rel_path: str | None
     extra_args: dict[str, Any]
@@ -61,9 +61,9 @@ class FunctionArguments:
 
 class Recorder:
     checker: str
-    release_name: safe.ReleaseName
-    project_name: safe.ProjectName
-    version_name: safe.VersionName
+    release_name: safe.ReleaseKey
+    project_name: safe.ProjectKey
+    version_name: safe.VersionKey
     primary_rel_path: str | None
     member_rel_path: str | None
     revision_number: safe.RevisionNumber
@@ -75,8 +75,8 @@ class Recorder:
         self,
         checker: str | Callable[..., Any],
         inputs_hash: str | None,
-        project_name: safe.ProjectName,
-        version_name: safe.VersionName,
+        project_name: safe.ProjectKey,
+        version_name: safe.VersionKey,
         revision_number: safe.RevisionNumber,
         primary_rel_path: str | None = None,
         member_rel_path: str | None = None,
@@ -101,8 +101,8 @@ class Recorder:
         cls,
         checker: str | Callable[..., Any],
         inputs_hash: str,
-        project_name: safe.ProjectName,
-        version_name: safe.VersionName,
+        project_name: safe.ProjectKey,
+        version_name: safe.VersionKey,
         revision_number: safe.RevisionNumber,
         primary_rel_path: str | None = None,
         member_rel_path: str | None = None,
diff --git a/atr/tasks/checks/compare.py b/atr/tasks/checks/compare.py
index dec520a3..47f5c2a7 100644
--- a/atr/tasks/checks/compare.py
+++ b/atr/tasks/checks/compare.py
@@ -335,7 +335,7 @@ async def _find_archive_root(archive_path: pathlib.Path, 
extract_dir: pathlib.Pa
 
 
 async def _load_tp_payload(
-    project_name: safe.ProjectName, version_name: safe.VersionName, 
revision_number: safe.RevisionNumber
+    project_name: safe.ProjectKey, version_name: safe.VersionKey, 
revision_number: safe.RevisionNumber
 ) -> github_models.TrustedPublisherPayload | None:
     payload_path = attestable.github_tp_payload_path(project_name, 
version_name, revision_number)
     if not await aiofiles.os.path.isfile(payload_path):
diff --git a/atr/tasks/gha.py b/atr/tasks/gha.py
index 5fc42607..f5698db0 100644
--- a/atr/tasks/gha.py
+++ b/atr/tasks/gha.py
@@ -142,8 +142,8 @@ async def status_check(args: WorkflowStatusCheck) -> 
DistributionWorkflowStatus:
 @checks.with_model(DistributionWorkflow)
 async def trigger_workflow(args: DistributionWorkflow, *, task_id: int | None 
= None) -> results.Results | None:
     unique_id = f"atr-dist-{args.name}-{uuid.uuid4()}"
-    project = safe.ProjectName(args.project_name)
-    safe.VersionName(args.version_name)
+    project = safe.ProjectKey(args.project_name)
+    safe.VersionKey(args.version_name)
     try:
         sql_platform = sql.DistributionPlatform[args.platform]
     except KeyError:
diff --git a/atr/tasks/keys.py b/atr/tasks/keys.py
index 79c6e773..b8210fae 100644
--- a/atr/tasks/keys.py
+++ b/atr/tasks/keys.py
@@ -33,8 +33,8 @@ class ImportFile(schema.Strict):
 @checks.with_model(ImportFile)
 async def import_file(args: ImportFile) -> results.Results | None:
     """Import a KEYS file from a draft release candidate revision."""
-    project = safe.ProjectName(args.project_name)
-    version = safe.VersionName(args.version_name)
+    project = safe.ProjectKey(args.project_name)
+    version = safe.VersionKey(args.version_name)
     async with storage.write(args.asf_uid) as write:
         wacm = await write.as_project_committee_member(project)
         outcomes = await wacm.keys.import_keys_file(project, version)
diff --git a/atr/tasks/quarantine.py b/atr/tasks/quarantine.py
index 643e1af8..ff809b90 100644
--- a/atr/tasks/quarantine.py
+++ b/atr/tasks/quarantine.py
@@ -297,8 +297,8 @@ async def _mark_failed(
 
 async def _promote(
     quarantined: sql.Quarantined,
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName,
+    project_name: safe.ProjectKey,
+    version_name: safe.VersionKey,
     release_name: str,
     quarantine_dir: str,
 ) -> None:
diff --git a/atr/tasks/sbom.py b/atr/tasks/sbom.py
index 159da434..032eaa97 100644
--- a/atr/tasks/sbom.py
+++ b/atr/tasks/sbom.py
@@ -83,8 +83,8 @@ class ScoreArgs(FileArgs):
 
 @checks.with_model(FileArgs)
 async def augment(args: FileArgs) -> results.Results | None:
-    project = safe.ProjectName(args.project_name)
-    version = safe.VersionName(args.version_name)
+    project = safe.ProjectKey(args.project_name)
+    version = safe.VersionKey(args.version_name)
 
     base_dir = paths.get_unfinished_dir() / args.project_name / 
args.version_name / args.revision_number
     if not await aiofiles.os.path.isdir(base_dir):
@@ -146,8 +146,8 @@ async def generate_cyclonedx(args: GenerateCycloneDX) -> 
results.Results | None:
 
 @checks.with_model(FileArgs)
 async def osv_scan(args: FileArgs) -> results.Results | None:
-    project = safe.ProjectName(args.project_name)
-    version = safe.VersionName(args.version_name)
+    project = safe.ProjectKey(args.project_name)
+    version = safe.VersionKey(args.version_name)
 
     base_dir = paths.get_unfinished_dir() / args.project_name / 
args.version_name / args.revision_number
     if not await aiofiles.os.path.isdir(base_dir):
@@ -206,8 +206,8 @@ async def osv_scan(args: FileArgs) -> results.Results | 
None:
 
 @checks.with_model(FileArgs)
 async def score_qs(args: FileArgs) -> results.Results | None:
-    safe.ProjectName(args.project_name)
-    safe.VersionName(args.version_name)
+    safe.ProjectKey(args.project_name)
+    safe.VersionKey(args.version_name)
 
     base_dir = paths.get_unfinished_dir() / args.project_name / 
args.version_name / args.revision_number
     if not await aiofiles.os.path.isdir(base_dir):
@@ -245,8 +245,8 @@ async def score_qs(args: FileArgs) -> results.Results | 
None:
 
 @checks.with_model(ScoreArgs)
 async def score_tool(args: ScoreArgs) -> results.Results | None:
-    safe.ProjectName(args.project_name)
-    safe.VersionName(args.version_name)
+    safe.ProjectKey(args.project_name)
+    safe.VersionKey(args.version_name)
 
     base_dir = paths.get_unfinished_dir() / args.project_name / 
args.version_name / args.revision_number
     previous_base_dir = None
diff --git a/atr/tasks/svn.py b/atr/tasks/svn.py
index d5c2033e..6d4f4403 100644
--- a/atr/tasks/svn.py
+++ b/atr/tasks/svn.py
@@ -73,8 +73,8 @@ async def import_files(args: SvnImport) -> results.Results | 
None:
 async def _import_files_core(args: SvnImport) -> str:
     """Core logic to perform the SVN export."""
 
-    project = safe.ProjectName(args.project_name)
-    version = safe.VersionName(args.version_name)
+    project = safe.ProjectKey(args.project_name)
+    version = safe.VersionKey(args.version_name)
 
     log.info(f"Starting SVN import for 
{args.project_name}-{args.version_name}")
     # We have to use a temporary directory otherwise SVN thinks it's a pegged 
revision
diff --git a/atr/tasks/vote.py b/atr/tasks/vote.py
index 3f6ea9a2..997e0b3f 100644
--- a/atr/tasks/vote.py
+++ b/atr/tasks/vote.py
@@ -62,7 +62,7 @@ async def initiate(args: Initiate) -> results.Results | None:
 async def _initiate_core_logic(args: Initiate) -> results.Results | None:
     """Get arguments, create an email, and then send it to the recipient."""
     log.info("Starting initiate_core")
-    safe.ReleaseName(args.release_name)
+    safe.ReleaseKey(args.release_name)
 
     # Validate arguments
     if not (args.email_to.endswith("@apache.org") or 
args.email_to.endswith(".apache.org")):
diff --git a/atr/web.py b/atr/web.py
index 2d79589d..76c727bb 100644
--- a/atr/web.py
+++ b/atr/web.py
@@ -88,7 +88,7 @@ class Committer:
     def is_admin(self) -> bool:
         return user.is_admin(self.uid)
 
-    async def check_access(self, project_name: str | safe.ProjectName) -> None:
+    async def check_access(self, project_name: str | safe.ProjectKey) -> None:
         if not any((str(p.name) == str(project_name)) for p in (await 
self.user_projects)):
             if self.is_admin:
                 # Admins can view all projects
@@ -162,8 +162,8 @@ class Committer:
 
     async def release(
         self,
-        project_name: safe.ProjectName,
-        version_name: safe.VersionName,
+        project_name: safe.ProjectKey,
+        version_name: safe.VersionKey,
         phase: sql.ReleasePhase | db.NotSet | None = db.NOT_SET,
         latest_revision_number: safe.RevisionNumber | db.NotSet | None = 
db.NOT_SET,
         data: db.Session | None = None,
diff --git a/atr/worker.py b/atr/worker.py
index 1f65865a..56f18f56 100644
--- a/atr/worker.py
+++ b/atr/worker.py
@@ -119,8 +119,8 @@ async def _execute_check_task(
             f"Task {task_id} ({task_type}) has non-dict raw args {task_args} 
which should represent keyword_args"
         )
 
-    project_name = safe.ProjectName(task_obj.project_name)
-    version_name = safe.VersionName(task_obj.version_name)
+    project_name = safe.ProjectKey(task_obj.project_name)
+    version_name = safe.VersionKey(task_obj.version_name)
     revision_number = safe.RevisionNumber(task_obj.revision_number)
 
     async def recorder_factory() -> checks.Recorder:
diff --git a/tests/unit/test_create_revision.py 
b/tests/unit/test_create_revision.py
index 82d07ad3..1b8a7ff0 100644
--- a/tests/unit/test_create_revision.py
+++ b/tests/unit/test_create_revision.py
@@ -157,7 +157,7 @@ async def 
test_clone_from_older_revision_skips_merge_without_intervening_change(
         mock.patch.object(revision.paths, "release_directory_base", 
return_value=tmp_path / "releases"),
     ):
         await participant.create_revision_with_quarantine(
-            safe.ProjectName("proj"), safe.VersionName("1.0"), "test", 
clone_from=safe.RevisionNumber("00002")
+            safe.ProjectKey("proj"), safe.VersionKey("1.0"), "test", 
clone_from=safe.RevisionNumber("00002")
         )
 
     if merge_mock.called:
diff --git a/tests/unit/test_ignores_api_models.py 
b/tests/unit/test_ignores_api_models.py
index 8d12aa8f..d77a83aa 100644
--- a/tests/unit/test_ignores_api_models.py
+++ b/tests/unit/test_ignores_api_models.py
@@ -32,7 +32,7 @@ def test_check_result_ignore_has_project_name_field() -> None:
 
 def test_ignore_add_args_accepts_all_fields() -> None:
     args = api.IgnoreAddArgs(
-        project_name=safe.ProjectName("example"),
+        project_name=safe.ProjectKey("example"),
         release_glob="example-1.0.*",
         revision_number="00001",
         checker_glob="atr.tasks.checks.rat.*",
@@ -41,22 +41,22 @@ def test_ignore_add_args_accepts_all_fields() -> None:
         status=sql.CheckResultStatusIgnore.WARNING,
         message_glob="*warning*",
     )
-    assert args.project_name == safe.ProjectName("example")
+    assert args.project_name == safe.ProjectKey("example")
     assert args.release_glob == "example-1.0.*"
     assert args.status == sql.CheckResultStatusIgnore.WARNING
 
 
 def test_ignore_add_args_rejects_invalid_pattern() -> None:
     with pytest.raises(ValueError):
-        api.IgnoreAddArgs(project_name=safe.ProjectName("test"), 
checker_glob="^(?=lookahead)$")
+        api.IgnoreAddArgs(project_name=safe.ProjectKey("test"), 
checker_glob="^(?=lookahead)$")
 
 
 def test_ignore_add_args_requires_project_name() -> None:
-    args = api.IgnoreAddArgs(project_name=safe.ProjectName("test"), 
checker_glob="atr.tasks.*")
+    args = api.IgnoreAddArgs(project_name=safe.ProjectKey("test"), 
checker_glob="atr.tasks.*")
     assert str(args.project_name) == "test"
 
 
 def test_ignore_delete_args_requires_project_name() -> None:
-    args = api.IgnoreDeleteArgs(project_name=safe.ProjectName("test"), id=1)
+    args = api.IgnoreDeleteArgs(project_name=safe.ProjectKey("test"), id=1)
     assert str(args.project_name) == "test"
     assert args.id == 1
diff --git a/tests/unit/test_quarantine_task.py 
b/tests/unit/test_quarantine_task.py
index 74b0f8d8..e03f5376 100644
--- a/tests/unit/test_quarantine_task.py
+++ b/tests/unit/test_quarantine_task.py
@@ -41,7 +41,7 @@ async def test_clear_quarantine_raises_when_not_found():
 
     writer = _make_revision_writer(mock_data)
     with pytest.raises(RuntimeError, match="not found"):
-        await writer.clear_quarantine(safe.ProjectName("proj"), 
safe.VersionName("1.0"), 999)
+        await writer.clear_quarantine(safe.ProjectKey("proj"), 
safe.VersionKey("1.0"), 999)
 
     mock_data.commit.assert_not_awaited()
 
@@ -58,7 +58,7 @@ async def 
test_clear_quarantine_transitions_failed_to_acknowledged():
     mock_data.quarantined = mock.MagicMock(return_value=mock_query)
 
     writer = _make_revision_writer(mock_data)
-    await writer.clear_quarantine(safe.ProjectName("proj"), 
safe.VersionName("1.0"), 7)
+    await writer.clear_quarantine(safe.ProjectKey("proj"), 
safe.VersionKey("1.0"), 7)
 
     assert quarantined_row.status == sql.QuarantineStatus.ACKNOWLEDGED
     mock_data.commit.assert_awaited_once()
@@ -490,7 +490,7 @@ async def test_validate_success_calls_promote(tmp_path: 
pathlib.Path):
 
     assert result is None
     mock_promote.assert_awaited_once_with(
-        row, safe.ProjectName("proj"), safe.VersionName("1.0"), 
row.release.name, str(quarantine_dir)
+        row, safe.ProjectKey("proj"), safe.VersionKey("1.0"), 
row.release.name, str(quarantine_dir)
     )
     mock_mark.assert_not_awaited()
 
@@ -521,11 +521,11 @@ def _make_quarantined_row() -> mock.MagicMock:
     row.status = sql.QuarantineStatus.PENDING
     row.release = mock.MagicMock()
     row.release.name = "proj-1.0"
-    row.release.safe_name = safe.ReleaseName(row.release.name)
+    row.release.safe_name = safe.ReleaseKey(row.release.name)
     row.release.project_name = "proj"
-    row.release.safe_project_name = safe.ProjectName(row.release.project_name)
+    row.release.safe_project_name = safe.ProjectKey(row.release.project_name)
     row.release.version = "1.0"
-    row.release.safe_version_name = safe.VersionName(row.release.version)
+    row.release.safe_version_name = safe.VersionKey(row.release.version)
     return row
 
 
diff --git a/tests/unit/test_safe_types.py b/tests/unit/test_safe_types.py
index af24b0fb..593d8250 100644
--- a/tests/unit/test_safe_types.py
+++ b/tests/unit/test_safe_types.py
@@ -19,7 +19,7 @@ import pytest
 import atr.models.safe as safe
 
 
[email protected]("cls", [safe.Alphanumeric, safe.ProjectName, 
safe.VersionName, safe.ReleaseName])
[email protected]("cls", [safe.Alphanumeric, safe.ProjectKey, 
safe.VersionKey, safe.ReleaseKey])
 @pytest.mark.parametrize(
     "bad",
     ["\n", "\t", "\r", "\x1f", "\x7f", "\u200b", "e\u0301"],
@@ -29,7 +29,7 @@ def test_safe_types_reject_bad_bytes(cls: 
type[safe.Alphanumeric], bad: str):
         cls("abc" + bad + "def")
 
 
[email protected]("cls", [safe.Alphanumeric, safe.ProjectName, 
safe.VersionName, safe.ReleaseName])
[email protected]("cls", [safe.Alphanumeric, safe.ProjectKey, 
safe.VersionKey, safe.ReleaseKey])
 @pytest.mark.parametrize(
     "bad",
     [
@@ -44,19 +44,19 @@ def test_safe_types_reject_invalid_characters(cls: 
type[safe.Alphanumeric], bad:
         cls(bad)
 
 
[email protected]("cls", [safe.Alphanumeric, safe.ProjectName, 
safe.VersionName, safe.ReleaseName])
[email protected]("cls", [safe.Alphanumeric, safe.ProjectKey, 
safe.VersionKey, safe.ReleaseKey])
 def test_safe_types_accept_valid_alpha(cls: type[safe.Alphanumeric]):
     value = cls("abcdef")
     assert str(value) == "abcdef"
 
 
[email protected]("cls", [safe.VersionName, safe.ReleaseName])
[email protected]("cls", [safe.VersionKey, safe.ReleaseKey])
 def test_safe_version_types_accept_valid_version(cls: type[safe.Alphanumeric]):
     value = cls("0.1+def")
     assert str(value) == "0.1+def"
 
 
[email protected]("cls", [safe.Alphanumeric, safe.ProjectName])
[email protected]("cls", [safe.Alphanumeric, safe.ProjectKey])
 def test_safe_alpha_types_reject_valid_version(cls: type[safe.Alphanumeric]):
     with pytest.raises(ValueError):
         cls("0.1+def")


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

Reply via email to