blob: 3204811b21b8d545891b5dbd9270aab5d630297b [file] [log] [blame]
# SPDX-FileCopyrightText: StorPool <support@storpool.com>
# SPDX-License-Identifier: BSD-2-Clause
"""Parse base/URL repository location pairs."""
from __future__ import annotations
import dataclasses
import os
import re
import urllib.parse as uparse
from . import defs
from . import util
_RE_ENV_BASE = re.compile(r"^ [A-Z][A-Z0-9_]* $", re.X)
@dataclasses.dataclass(frozen=True)
class RepoURLResult:
"""Base class for the OK/error parsed URL dichotomy."""
@dataclasses.dataclass(frozen=True)
class RepoURLOK(RepoURLResult):
"""Successfully parsed a base URL for repositories."""
url: defs.RepoURL
@dataclasses.dataclass(frozen=True)
class RepoURLError(RepoURLResult):
"""Could not parse a base URL for repositories."""
name: str
value: str
err: ValueError
@dataclasses.dataclass(frozen=True)
class RepoURLPair:
"""A base/URL pair for an URL obtained from the command line."""
base: str
url: defs.RepoURL
def _slash_extend(url: uparse.ParseResult) -> uparse.ParseResult:
"""Add a / at the end of the path if there is none."""
if url.path.endswith("/"):
return url
return url._replace(path=url.path + "/")
def _validate_file(name: str, value: str, url: uparse.ParseResult) -> RepoURLResult:
"""Make sure a file:// URL has no host and an absolute path."""
if url.netloc:
return RepoURLError(name, value, ValueError("No hostname expected for a 'file' URL"))
if not url.path.startswith("/"):
return RepoURLError(name, value, ValueError("Expected an absolute path for a 'file' URL"))
url = _slash_extend(url)
return RepoURLOK(defs.RepoURL(url))
def _validate_http(name: str, value: str, url: uparse.ParseResult) -> RepoURLResult:
"""Make sure a http(s):// URL has a host, slash-terminate the path."""
if not url.netloc:
return RepoURLError(
name, value, ValueError("Expected a hostname for 'http' or 'https' URLs")
)
url = _slash_extend(url)
return RepoURLOK(defs.RepoURL(url))
_SCHEME_VALIDATORS = {
"file": _validate_file,
"http": _validate_http,
"https": _validate_http,
}
def parse_url(name: str, base: str, value: str) -> RepoURLResult:
"""Parse and validate a single base/URL pair."""
if not _RE_ENV_BASE.match(base):
return RepoURLError(name, value, ValueError(f"Invalid URL base {base!r}"))
try:
url = uparse.urlparse(value)
except ValueError as err:
return RepoURLError(name, value, err)
validator = _SCHEME_VALIDATORS.get(url.scheme)
if validator is None:
return RepoURLError(
name, value, ValueError("Expected 'http', 'https', or 'file' as the URL scheme")
)
return validator(name, value, url)
def get_env_repo_urls(environ: dict[str, str] | None = None) -> dict[str, RepoURLResult]:
"""Parse the REPO_URL_<base> environment variables."""
if environ is None:
environ = dict(os.environ)
res: dict[str, RepoURLResult] = {
"OPENSTACK": RepoURLOK(defs.RepoURL(uparse.urlparse("https://github.com/openstack")))
}
for name, value in environ.items():
base = util.str_removeprefix(name, "REPO_URL_")
if base == name:
continue
res[base] = parse_url(name, base, value)
return res
def parse_base_url_pair(arg: str) -> RepoURLPair:
"""Parse a `--repo-url base=url` command-line argument."""
raise NotImplementedError(repr(arg))