blob: 3204811b21b8d545891b5dbd9270aab5d630297b [file] [log] [blame]
Peter Pentchevb91fc932023-01-19 15:27:13 +02001# SPDX-FileCopyrightText: StorPool <support@storpool.com>
2# SPDX-License-Identifier: BSD-2-Clause
3"""Parse base/URL repository location pairs."""
4
5from __future__ import annotations
6
7import dataclasses
8import os
9import re
10import urllib.parse as uparse
11
12from . import defs
13from . import util
14
15
16_RE_ENV_BASE = re.compile(r"^ [A-Z][A-Z0-9_]* $", re.X)
17
18
19@dataclasses.dataclass(frozen=True)
20class RepoURLResult:
21 """Base class for the OK/error parsed URL dichotomy."""
22
23
24@dataclasses.dataclass(frozen=True)
25class RepoURLOK(RepoURLResult):
26 """Successfully parsed a base URL for repositories."""
27
28 url: defs.RepoURL
29
30
31@dataclasses.dataclass(frozen=True)
32class RepoURLError(RepoURLResult):
33 """Could not parse a base URL for repositories."""
34
35 name: str
36 value: str
37 err: ValueError
38
39
40@dataclasses.dataclass(frozen=True)
41class RepoURLPair:
42 """A base/URL pair for an URL obtained from the command line."""
43
44 base: str
45 url: defs.RepoURL
46
47
48def _slash_extend(url: uparse.ParseResult) -> uparse.ParseResult:
49 """Add a / at the end of the path if there is none."""
50 if url.path.endswith("/"):
51 return url
52
53 return url._replace(path=url.path + "/")
54
55
56def _validate_file(name: str, value: str, url: uparse.ParseResult) -> RepoURLResult:
57 """Make sure a file:// URL has no host and an absolute path."""
58 if url.netloc:
59 return RepoURLError(name, value, ValueError("No hostname expected for a 'file' URL"))
60 if not url.path.startswith("/"):
61 return RepoURLError(name, value, ValueError("Expected an absolute path for a 'file' URL"))
62
63 url = _slash_extend(url)
64 return RepoURLOK(defs.RepoURL(url))
65
66
67def _validate_http(name: str, value: str, url: uparse.ParseResult) -> RepoURLResult:
68 """Make sure a http(s):// URL has a host, slash-terminate the path."""
69 if not url.netloc:
70 return RepoURLError(
71 name, value, ValueError("Expected a hostname for 'http' or 'https' URLs")
72 )
73
74 url = _slash_extend(url)
75 return RepoURLOK(defs.RepoURL(url))
76
77
78_SCHEME_VALIDATORS = {
79 "file": _validate_file,
80 "http": _validate_http,
81 "https": _validate_http,
82}
83
84
85def parse_url(name: str, base: str, value: str) -> RepoURLResult:
86 """Parse and validate a single base/URL pair."""
87 if not _RE_ENV_BASE.match(base):
88 return RepoURLError(name, value, ValueError(f"Invalid URL base {base!r}"))
89
90 try:
91 url = uparse.urlparse(value)
92 except ValueError as err:
93 return RepoURLError(name, value, err)
94
95 validator = _SCHEME_VALIDATORS.get(url.scheme)
96 if validator is None:
97 return RepoURLError(
98 name, value, ValueError("Expected 'http', 'https', or 'file' as the URL scheme")
99 )
100 return validator(name, value, url)
101
102
103def get_env_repo_urls(environ: dict[str, str] | None = None) -> dict[str, RepoURLResult]:
104 """Parse the REPO_URL_<base> environment variables."""
105 if environ is None:
106 environ = dict(os.environ)
107
108 res: dict[str, RepoURLResult] = {
109 "OPENSTACK": RepoURLOK(defs.RepoURL(uparse.urlparse("https://github.com/openstack")))
110 }
111 for name, value in environ.items():
112 base = util.str_removeprefix(name, "REPO_URL_")
113 if base == name:
114 continue
115 res[base] = parse_url(name, base, value)
116
117 return res
118
119
120def parse_base_url_pair(arg: str) -> RepoURLPair:
121 """Parse a `--repo-url base=url` command-line argument."""
122 raise NotImplementedError(repr(arg))