Files
Labyricorn ddf7234c9d
Deploy production / deploy (push) Successful in 5s
Add project management and GitHub source support
2026-08-13 17:47:26 -07:00

105 lines
3.5 KiB
Python

"""Provider-specific URL derivation for public Labyricorn project repositories."""
from __future__ import annotations
from dataclasses import dataclass
import ipaddress
import re
from urllib.parse import quote, urlsplit, urlunsplit
class ProjectProviderError(ValueError):
"""Raised when a public repository URL cannot be derived safely."""
@dataclass(frozen=True)
class RepositoryUrls:
provider: str
repository: str
web_url: str
api_url: str
def validate_public_https_url(value: str, field: str) -> str:
value = value.strip().rstrip("/")
parsed = urlsplit(value)
if (
parsed.scheme != "https"
or not parsed.hostname
or parsed.username
or parsed.password
):
raise ProjectProviderError(f"{field} must be a credential-free HTTPS URL")
if parsed.hostname.lower() == "localhost":
raise ProjectProviderError(f"{field} must not target localhost")
try:
address = ipaddress.ip_address(parsed.hostname)
except ValueError:
pass
else:
if not address.is_global:
raise ProjectProviderError(f"{field} must not target a private or local address")
return value
def repository_provider(web_url: str) -> str:
hostname = (urlsplit(web_url).hostname or "").lower()
return "github" if hostname == "github.com" else "gitea"
def derive_repository_urls(value: str) -> RepositoryUrls:
"""Derive Git and API URLs from one GitHub or Gitea repository web URL."""
web_url = validate_public_https_url(value, "Repository URL")
parsed = urlsplit(web_url)
hostname = (parsed.hostname or "").lower()
if hostname == "gitlab.com":
raise ProjectProviderError(
"GitLab repositories are not supported; use a public GitHub or Gitea URL"
)
if parsed.query or parsed.fragment:
raise ProjectProviderError("Repository URL must not contain a query or fragment")
path = parsed.path.rstrip("/")
if path.lower().endswith(".git"):
path = path[:-4]
parts = [part for part in path.split("/") if part]
if len(parts) != 2:
raise ProjectProviderError(
"Repository URL must identify one owner and repository, such as "
"https://github.com/owner/repository"
)
owner, repository_name = parts
if any(
part in {".", ".."} or not re.fullmatch(r"[A-Za-z0-9_.-]+", part)
for part in (owner, repository_name)
):
raise ProjectProviderError(
"Repository owner and name contain unsupported URL characters"
)
normalized_path = f"/{owner}/{repository_name}"
normalized_web = urlunsplit((parsed.scheme, parsed.netloc, normalized_path, "", ""))
provider = repository_provider(normalized_web)
if provider == "github":
api_url = (
"https://api.github.com/repos/"
f"{quote(owner, safe='')}/{quote(repository_name, safe='')}"
)
else:
api_url = urlunsplit(
(
parsed.scheme,
parsed.netloc,
f"/api/v1/repos/{quote(owner, safe='')}/{quote(repository_name, safe='')}",
"",
"",
)
)
return RepositoryUrls(provider, f"{normalized_web}.git", normalized_web, api_url)
def repository_readme_url(web_url: str, branch: str) -> str:
encoded_branch = quote(branch, safe="")
if repository_provider(web_url) == "github":
return f"{web_url}/blob/{encoded_branch}/README.md"
return f"{web_url}/src/branch/{encoded_branch}/README.md"