ruyi 0.39.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. ruyi/__init__.py +21 -0
  2. ruyi/__main__.py +98 -0
  3. ruyi/cli/__init__.py +5 -0
  4. ruyi/cli/builtin_commands.py +14 -0
  5. ruyi/cli/cmd.py +224 -0
  6. ruyi/cli/completer.py +50 -0
  7. ruyi/cli/completion.py +26 -0
  8. ruyi/cli/config_cli.py +153 -0
  9. ruyi/cli/main.py +111 -0
  10. ruyi/cli/self_cli.py +295 -0
  11. ruyi/cli/user_input.py +127 -0
  12. ruyi/cli/version_cli.py +45 -0
  13. ruyi/config/__init__.py +401 -0
  14. ruyi/config/editor.py +92 -0
  15. ruyi/config/errors.py +76 -0
  16. ruyi/config/news.py +39 -0
  17. ruyi/config/schema.py +197 -0
  18. ruyi/device/__init__.py +0 -0
  19. ruyi/device/provision.py +591 -0
  20. ruyi/device/provision_cli.py +40 -0
  21. ruyi/log/__init__.py +272 -0
  22. ruyi/mux/.gitignore +1 -0
  23. ruyi/mux/__init__.py +0 -0
  24. ruyi/mux/runtime.py +213 -0
  25. ruyi/mux/venv/__init__.py +12 -0
  26. ruyi/mux/venv/emulator_cfg.py +41 -0
  27. ruyi/mux/venv/maker.py +782 -0
  28. ruyi/mux/venv/venv_cli.py +92 -0
  29. ruyi/mux/venv_cfg.py +214 -0
  30. ruyi/pluginhost/__init__.py +0 -0
  31. ruyi/pluginhost/api.py +206 -0
  32. ruyi/pluginhost/ctx.py +222 -0
  33. ruyi/pluginhost/paths.py +135 -0
  34. ruyi/pluginhost/plugin_cli.py +37 -0
  35. ruyi/pluginhost/unsandboxed.py +246 -0
  36. ruyi/py.typed +0 -0
  37. ruyi/resource_bundle/__init__.py +20 -0
  38. ruyi/resource_bundle/__main__.py +55 -0
  39. ruyi/resource_bundle/data.py +26 -0
  40. ruyi/ruyipkg/__init__.py +0 -0
  41. ruyi/ruyipkg/admin_checksum.py +88 -0
  42. ruyi/ruyipkg/admin_cli.py +83 -0
  43. ruyi/ruyipkg/atom.py +184 -0
  44. ruyi/ruyipkg/augmented_pkg.py +212 -0
  45. ruyi/ruyipkg/canonical_dump.py +320 -0
  46. ruyi/ruyipkg/checksum.py +39 -0
  47. ruyi/ruyipkg/cli_completion.py +42 -0
  48. ruyi/ruyipkg/distfile.py +208 -0
  49. ruyi/ruyipkg/entity.py +387 -0
  50. ruyi/ruyipkg/entity_cli.py +123 -0
  51. ruyi/ruyipkg/entity_provider.py +273 -0
  52. ruyi/ruyipkg/fetch.py +271 -0
  53. ruyi/ruyipkg/host.py +55 -0
  54. ruyi/ruyipkg/install.py +554 -0
  55. ruyi/ruyipkg/install_cli.py +150 -0
  56. ruyi/ruyipkg/list.py +126 -0
  57. ruyi/ruyipkg/list_cli.py +79 -0
  58. ruyi/ruyipkg/list_filter.py +173 -0
  59. ruyi/ruyipkg/msg.py +99 -0
  60. ruyi/ruyipkg/news.py +123 -0
  61. ruyi/ruyipkg/news_cli.py +78 -0
  62. ruyi/ruyipkg/news_store.py +183 -0
  63. ruyi/ruyipkg/pkg_manifest.py +657 -0
  64. ruyi/ruyipkg/profile.py +208 -0
  65. ruyi/ruyipkg/profile_cli.py +33 -0
  66. ruyi/ruyipkg/protocols.py +55 -0
  67. ruyi/ruyipkg/repo.py +763 -0
  68. ruyi/ruyipkg/state.py +345 -0
  69. ruyi/ruyipkg/unpack.py +369 -0
  70. ruyi/ruyipkg/unpack_method.py +91 -0
  71. ruyi/ruyipkg/update_cli.py +54 -0
  72. ruyi/telemetry/__init__.py +0 -0
  73. ruyi/telemetry/aggregate.py +72 -0
  74. ruyi/telemetry/event.py +41 -0
  75. ruyi/telemetry/node_info.py +192 -0
  76. ruyi/telemetry/provider.py +411 -0
  77. ruyi/telemetry/scope.py +43 -0
  78. ruyi/telemetry/store.py +238 -0
  79. ruyi/telemetry/telemetry_cli.py +127 -0
  80. ruyi/utils/__init__.py +0 -0
  81. ruyi/utils/ar.py +74 -0
  82. ruyi/utils/ci.py +63 -0
  83. ruyi/utils/frontmatter.py +38 -0
  84. ruyi/utils/git.py +169 -0
  85. ruyi/utils/global_mode.py +204 -0
  86. ruyi/utils/l10n.py +83 -0
  87. ruyi/utils/markdown.py +73 -0
  88. ruyi/utils/nuitka.py +33 -0
  89. ruyi/utils/porcelain.py +51 -0
  90. ruyi/utils/prereqs.py +77 -0
  91. ruyi/utils/ssl_patch.py +170 -0
  92. ruyi/utils/templating.py +34 -0
  93. ruyi/utils/toml.py +115 -0
  94. ruyi/utils/url.py +7 -0
  95. ruyi/utils/xdg_basedir.py +80 -0
  96. ruyi/version.py +67 -0
  97. ruyi-0.39.0.dist-info/LICENSE-Apache.txt +201 -0
  98. ruyi-0.39.0.dist-info/METADATA +403 -0
  99. ruyi-0.39.0.dist-info/RECORD +101 -0
  100. ruyi-0.39.0.dist-info/WHEEL +4 -0
  101. ruyi-0.39.0.dist-info/entry_points.txt +3 -0
ruyi/utils/l10n.py ADDED
@@ -0,0 +1,83 @@
1
+ import locale
2
+
3
+ from typing import Iterable, NamedTuple
4
+
5
+
6
+ class LangAndRegion(NamedTuple):
7
+ raw: str
8
+ lang: str
9
+ region: str | None
10
+
11
+
12
+ def lang_code_to_lang_region(lang_code: str, guess_region: bool) -> LangAndRegion:
13
+ if not guess_region and "_" not in lang_code:
14
+ return LangAndRegion(lang_code, lang_code, None)
15
+
16
+ lang_region_str = locale.normalize(lang_code).split(".")[0]
17
+ parts = lang_region_str.split("_", 2)
18
+ if len(parts) == 1:
19
+ return LangAndRegion(lang_code, lang_region_str, None)
20
+ return LangAndRegion(lang_code, parts[0], parts[1])
21
+
22
+
23
+ def match_lang_code(
24
+ req: str,
25
+ avail: Iterable[str],
26
+ ) -> str:
27
+ """Returns a proper available language code based on a list of available
28
+ language codes, and a request."""
29
+
30
+ if not isinstance(avail, set) or not isinstance(avail, frozenset):
31
+ avail = set(avail)
32
+
33
+ # return the only one choice if this is the case
34
+ if len(avail) == 1:
35
+ return next(iter(avail))
36
+
37
+ # try exact match
38
+ if req in avail:
39
+ return req
40
+
41
+ return _match_lang_code_slowpath(
42
+ lang_code_to_lang_region(req, True),
43
+ [lang_code_to_lang_region(x, False) for x in avail],
44
+ )
45
+
46
+
47
+ def _match_lang_code_slowpath(
48
+ req: LangAndRegion,
49
+ avail: list[LangAndRegion],
50
+ ) -> str:
51
+ # pick one with the requested region
52
+ if req.region is not None:
53
+ for x in avail:
54
+ if x.region == req.region:
55
+ return x.raw
56
+
57
+ # if no match, pick one with the requested language
58
+ for x in avail:
59
+ if x.lang == req.lang:
60
+ return x.raw
61
+
62
+ # neither matches, fallback to (en_US, en, en_*, zh_CN, zh, zh_*)
63
+ # in that order
64
+ fallback_en = {x.region: x.raw for x in avail if x.lang == "en"}
65
+ if fallback_en:
66
+ if "US" in fallback_en:
67
+ return fallback_en["US"]
68
+ if None in fallback_en:
69
+ return fallback_en[None]
70
+ return fallback_en[sorted(x for x in fallback_en.keys() if x is not None)[0]]
71
+
72
+ fallback_zh = {x.region: x.raw for x in avail if x.lang == "zh"}
73
+ if fallback_zh:
74
+ if "CN" in fallback_zh:
75
+ return fallback_zh["CN"]
76
+ if None in fallback_zh:
77
+ return fallback_zh[None]
78
+ return fallback_zh[sorted(x for x in fallback_zh.keys() if x is not None)[0]]
79
+
80
+ # neither en nor zh is available (which is highly unlikely at present)
81
+ # pick the first available one as a last resort
82
+ # sort the list before picking for determinism
83
+ return sorted(x.raw for x in avail)[0]
ruyi/utils/markdown.py ADDED
@@ -0,0 +1,73 @@
1
+ from rich.console import Console, ConsoleOptions, RenderResult
2
+ from rich.markdown import CodeBlock, Heading, Markdown, MarkdownContext
3
+ from rich.syntax import Syntax
4
+ from rich.text import Text
5
+
6
+
7
+ class SlimHeading(Heading):
8
+ def on_enter(self, context: MarkdownContext) -> None:
9
+ try:
10
+ # the heading level is indicated in the tag name in rich >= 13.2.0,
11
+ # e.g. self.tag == 'h1', but directly stored in earlier versions
12
+ # as self.level.
13
+ #
14
+ # see https://github.com/Textualize/rich/commit/a20c3d5468d02a55
15
+ heading_level = int(self.tag[1:]) # type: ignore[attr-defined,unused-ignore]
16
+ except AttributeError:
17
+ heading_level = self.level # type: ignore[attr-defined,unused-ignore]
18
+
19
+ context.enter_style(self.style_name)
20
+ self.text = Text("#" * heading_level + " ", context.current_style)
21
+
22
+ def __rich_console__(
23
+ self,
24
+ console: Console,
25
+ options: ConsoleOptions,
26
+ ) -> RenderResult:
27
+ yield self.text
28
+
29
+
30
+ # inspired by https://github.com/Textualize/rich/issues/3154
31
+ class NonWrappingCodeBlock(CodeBlock):
32
+ def __rich_console__(
33
+ self,
34
+ console: Console,
35
+ options: ConsoleOptions,
36
+ ) -> RenderResult:
37
+ # re-enable non-wrapping options locally for code blocks
38
+ render_options = options.update(no_wrap=True, overflow="ignore")
39
+
40
+ code = str(self.text).rstrip()
41
+ syntax = Syntax(
42
+ code,
43
+ self.lexer_name,
44
+ theme=self.theme,
45
+ word_wrap=False,
46
+ # not supported in rich <= 12.4.0 (Textualize/rich#2247) but fortunately
47
+ # zero padding is the default anyway
48
+ # padding=0,
49
+ )
50
+ return syntax.highlight(code).__rich_console__(console, render_options)
51
+
52
+
53
+ class RuyiStyledMarkdown(Markdown):
54
+ elements = Markdown.elements
55
+ elements["fence"] = NonWrappingCodeBlock
56
+ elements["heading_open"] = SlimHeading
57
+
58
+ # rich < 13.2.0
59
+ # see https://github.com/Textualize/rich/commit/745bd99e416c2806
60
+ # it doesn't hurt to just unconditionally add them like below
61
+ elements["code"] = NonWrappingCodeBlock
62
+ elements["code_block"] = NonWrappingCodeBlock
63
+ elements["heading"] = SlimHeading
64
+
65
+ def __rich_console__(
66
+ self,
67
+ console: Console,
68
+ options: ConsoleOptions,
69
+ ) -> RenderResult:
70
+ # we have to undo the ruyi-global console's non-wrapping setting
71
+ # for proper CLI rendering of long lines
72
+ render_options = options.update(no_wrap=False, overflow="fold")
73
+ return super().__rich_console__(console, render_options)
ruyi/utils/nuitka.py ADDED
@@ -0,0 +1,33 @@
1
+ import os
2
+ import sys
3
+
4
+
5
+ def get_nuitka_self_exe() -> str:
6
+ try:
7
+ # Assume we're a Nuitka onefile build, so our parent process is the onefile
8
+ # bootstrap process. The onefile bootstrapper puts "our path" in the
9
+ # undocumented environment variable $NUITKA_ONEFILE_BINARY, which works
10
+ # on both Linux and Windows.
11
+ return os.environ["NUITKA_ONEFILE_BINARY"]
12
+ except KeyError:
13
+ # It seems we are instead launched from the extracted onefile tempdir.
14
+ # Assume our name is "ruyi" in this case; directory is available in
15
+ # Nuitka metadata.
16
+ import ruyi
17
+
18
+ return os.path.join(ruyi.__compiled__.containing_dir, "ruyi")
19
+
20
+
21
+ def get_argv0() -> str:
22
+ import ruyi
23
+
24
+ try:
25
+ if ruyi.__compiled__.original_argv0 is not None:
26
+ return ruyi.__compiled__.original_argv0
27
+ except AttributeError:
28
+ # Either we're not packaged with Nuitka, or the Nuitka used is
29
+ # without our original_argv0 patch, in which case we cannot do any
30
+ # better than simply returning sys.argv[0].
31
+ pass
32
+
33
+ return sys.argv[0]
@@ -0,0 +1,51 @@
1
+ from contextlib import AbstractContextManager
2
+ import enum
3
+ import json
4
+ import sys
5
+ from types import TracebackType
6
+ from typing import BinaryIO, TypedDict, TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from typing_extensions import Self
10
+
11
+ if sys.version_info >= (3, 11):
12
+
13
+ class PorcelainEntityType(enum.StrEnum):
14
+ LogV1 = "log-v1"
15
+ NewsItemV1 = "newsitem-v1"
16
+ PkgListOutputV1 = "pkglistoutput-v1"
17
+ EntityListOutputV1 = "entitylistoutput-v1"
18
+
19
+ else:
20
+
21
+ class PorcelainEntityType(str, enum.Enum):
22
+ LogV1 = "log-v1"
23
+ NewsItemV1 = "newsitem-v1"
24
+ PkgListOutputV1 = "pkglistoutput-v1"
25
+ EntityListOutputV1 = "entitylistoutput-v1"
26
+
27
+
28
+ class PorcelainEntity(TypedDict):
29
+ ty: PorcelainEntityType
30
+
31
+
32
+ class PorcelainOutput(AbstractContextManager["PorcelainOutput"]):
33
+ def __init__(self, out: BinaryIO | None = None) -> None:
34
+ self.out = sys.stdout.buffer if out is None else out
35
+
36
+ def __enter__(self) -> "Self":
37
+ return self
38
+
39
+ def __exit__(
40
+ self,
41
+ exc_type: type[BaseException] | None,
42
+ exc_value: BaseException | None,
43
+ traceback: TracebackType | None,
44
+ ) -> bool | None:
45
+ self.out.flush()
46
+ return None
47
+
48
+ def emit(self, obj: PorcelainEntity) -> None:
49
+ s = json.dumps(obj, ensure_ascii=False, separators=(",", ":"))
50
+ self.out.write(s.encode("utf-8"))
51
+ self.out.write(b"\n")
ruyi/utils/prereqs.py ADDED
@@ -0,0 +1,77 @@
1
+ import shutil
2
+ import sys
3
+ from typing import Final, Iterable, NoReturn
4
+
5
+ from ..cli.user_input import pause_before_continuing
6
+ from ..log import RuyiLogger, humanize_list
7
+
8
+
9
+ def has_cmd_in_path(cmd: str) -> bool:
10
+ return shutil.which(cmd) is not None
11
+
12
+
13
+ _CMDS: Final = (
14
+ "bzip2",
15
+ "gunzip",
16
+ "lz4",
17
+ "tar",
18
+ "xz",
19
+ "zstd",
20
+ "unzip",
21
+ # commands used by the device provisioner
22
+ "sudo",
23
+ "dd",
24
+ "fastboot",
25
+ )
26
+
27
+ _CMD_PRESENCE_MAP: Final[dict[str, bool]] = {}
28
+
29
+
30
+ def init_cmd_presence_map() -> None:
31
+ _CMD_PRESENCE_MAP.clear()
32
+ for cmd in _CMDS:
33
+ _CMD_PRESENCE_MAP[cmd] = has_cmd_in_path(cmd)
34
+
35
+
36
+ def ensure_cmds(
37
+ logger: RuyiLogger,
38
+ cmds: Iterable[str],
39
+ interactive_retry: bool = True,
40
+ ) -> None | NoReturn:
41
+ # only allow interactive retry if stdin is a TTY
42
+ interactive_retry = interactive_retry and sys.stdin.isatty()
43
+
44
+ while True:
45
+ if not _CMD_PRESENCE_MAP or interactive_retry:
46
+ init_cmd_presence_map()
47
+
48
+ # in case any command's availability is not cached in advance
49
+ for cmd in cmds:
50
+ if cmd not in _CMD_PRESENCE_MAP:
51
+ _CMD_PRESENCE_MAP[cmd] = has_cmd_in_path(cmd)
52
+
53
+ absent_cmds = sorted(
54
+ cmd for cmd in cmds if not _CMD_PRESENCE_MAP.get(cmd, False)
55
+ )
56
+ if not absent_cmds:
57
+ return None
58
+
59
+ cmds_str = humanize_list(absent_cmds, item_color="yellow")
60
+ prompt = f"The command(s) {cmds_str} cannot be found in PATH, which [yellow]ruyi[/] requires"
61
+ if not interactive_retry:
62
+ logger.F(prompt)
63
+ logger.I("please install and retry")
64
+ sys.exit(1)
65
+
66
+ logger.W(prompt)
67
+ logger.I(
68
+ "please install them and press [green]Enter[/] to retry, or [green]Ctrl+C[/] to exit"
69
+ )
70
+ try:
71
+ pause_before_continuing(logger)
72
+ except EOFError:
73
+ logger.I("exiting due to EOF")
74
+ sys.exit(1)
75
+ except KeyboardInterrupt:
76
+ logger.I("exiting due to keyboard interrupt")
77
+ sys.exit(1)
@@ -0,0 +1,170 @@
1
+ import ctypes
2
+ import os
3
+ import ssl
4
+ import sys
5
+ from typing import Final, NamedTuple
6
+
7
+ import certifi
8
+
9
+ from ..log import RuyiConsoleLogger, RuyiLogger
10
+ from .global_mode import EnvGlobalModeProvider
11
+
12
+ _orig_get_default_verify_paths: Final = ssl.get_default_verify_paths
13
+ _cached_paths: ssl.DefaultVerifyPaths | None = None
14
+
15
+
16
+ def get_system_ssl_default_verify_paths() -> ssl.DefaultVerifyPaths:
17
+ global _cached_paths
18
+
19
+ if _cached_paths is None:
20
+ # no way to pass in the logger because of the function signature
21
+ # so we have to use a new logger
22
+ gm = EnvGlobalModeProvider(os.environ)
23
+ _cached_paths = _get_system_ssl_default_verify_paths(RuyiConsoleLogger(gm))
24
+ return _cached_paths
25
+
26
+
27
+ def _get_system_ssl_default_verify_paths(logger: RuyiLogger) -> ssl.DefaultVerifyPaths:
28
+ orig_paths = _orig_get_default_verify_paths()
29
+ if sys.platform != "linux":
30
+ return orig_paths
31
+
32
+ result: ssl.DefaultVerifyPaths | None = None
33
+
34
+ # imitate the stdlib flow but with overridden data source
35
+ try:
36
+ parts = _query_linux_system_ssl_default_cert_paths(logger)
37
+ if parts is None:
38
+ logger.W("failed to probe system libcrypto")
39
+ else:
40
+ result = to_ssl_paths(parts)
41
+ except Exception as e:
42
+ logger.D(f"cannot get system libcrypto default cert paths: {e}")
43
+
44
+ if result is None:
45
+ logger.D("falling back to probing hard-coded paths")
46
+ result = probe_fallback_verify_paths()
47
+
48
+ if result != orig_paths:
49
+ logger.D(
50
+ "get_default_verify_paths() values differ between bundled and system libssl"
51
+ )
52
+ logger.D(f"bundled: {orig_paths}")
53
+ logger.D(f" system: {result}")
54
+
55
+ return result
56
+
57
+
58
+ def to_ssl_paths(parts: tuple[str, str, str, str]) -> ssl.DefaultVerifyPaths | None:
59
+ cafile = os.environ.get(parts[0], parts[1])
60
+ capath = os.environ.get(parts[2], parts[3])
61
+
62
+ is_cafile_present = os.path.isfile(cafile)
63
+ is_capath_present = os.path.isdir(capath)
64
+ if not is_cafile_present and not is_capath_present:
65
+ return None
66
+
67
+ # must do "else None" like the stdlib, despite the type annotation being just "str"
68
+ return ssl.DefaultVerifyPaths(
69
+ cafile if is_cafile_present else None, # type: ignore[arg-type]
70
+ capath if is_capath_present else None, # type: ignore[arg-type]
71
+ *parts,
72
+ )
73
+
74
+
75
+ def _decode_fsdefault_or_none(val: int | None) -> str:
76
+ if val is None:
77
+ return ""
78
+
79
+ s = ctypes.c_char_p(val)
80
+ if s.value is None:
81
+ return ""
82
+
83
+ return s.value.decode(sys.getfilesystemencoding())
84
+
85
+
86
+ def _query_linux_system_ssl_default_cert_paths(
87
+ logger: RuyiLogger,
88
+ soname: str | None = None,
89
+ ) -> tuple[str, str, str, str] | None:
90
+ if soname is None:
91
+ # check libcrypto instead of libssl, because if the system libssl is
92
+ # newer than the bundled one, the system libssl will depend on the
93
+ # bundled libcrypto that may lack newer ELF symbol version(s). The
94
+ # functions actually reside in libcrypto, after all.
95
+ for soname in ("libcrypto.so", "libcrypto.so.3", "libcrypto.so.1.1"):
96
+ try:
97
+ return _query_linux_system_ssl_default_cert_paths(logger, soname)
98
+ except OSError as e:
99
+ logger.D(f"soname {soname} not working: {e}")
100
+ continue
101
+
102
+ return None
103
+
104
+ # dlopen-ing the bare soname will get us the system library
105
+ lib = ctypes.CDLL(soname)
106
+ lib.X509_get_default_cert_file_env.restype = ctypes.c_void_p
107
+ lib.X509_get_default_cert_file.restype = ctypes.c_void_p
108
+ lib.X509_get_default_cert_dir_env.restype = ctypes.c_void_p
109
+ lib.X509_get_default_cert_dir.restype = ctypes.c_void_p
110
+
111
+ result = (
112
+ _decode_fsdefault_or_none(lib.X509_get_default_cert_file_env()),
113
+ _decode_fsdefault_or_none(lib.X509_get_default_cert_file()),
114
+ _decode_fsdefault_or_none(lib.X509_get_default_cert_dir_env()),
115
+ _decode_fsdefault_or_none(lib.X509_get_default_cert_dir()),
116
+ )
117
+
118
+ logger.D(f"got defaults from system libcrypto {soname}")
119
+ logger.D(f"X509_get_default_cert_file_env() = {result[0]}")
120
+ logger.D(f"X509_get_default_cert_file() = {result[1]}")
121
+ logger.D(f"X509_get_default_cert_dir_env() = {result[2]}")
122
+ logger.D(f"X509_get_default_cert_dir() = {result[3]}")
123
+
124
+ return result
125
+
126
+
127
+ class WellKnownCALocation(NamedTuple):
128
+ cafile: str
129
+ capath: str
130
+
131
+
132
+ WELL_KNOWN_CA_LOCATIONS: Final[list[WellKnownCALocation]] = [
133
+ # Debian-based distros
134
+ WellKnownCALocation("/usr/lib/ssl/cert.pem", "/usr/lib/ssl/certs"),
135
+ # RPM-based distros
136
+ WellKnownCALocation("/etc/pki/tls/cert.pem", "/etc/pki/tls/certs"),
137
+ # Most others
138
+ WellKnownCALocation("/etc/ssl/cert.pem", "/etc/ssl/certs"),
139
+ ]
140
+
141
+
142
+ def probe_fallback_verify_paths() -> ssl.DefaultVerifyPaths:
143
+ for loc in WELL_KNOWN_CA_LOCATIONS:
144
+ is_file_present = os.path.isfile(loc.cafile)
145
+ is_dir_present = os.path.isdir(loc.capath)
146
+ if not is_file_present and not is_dir_present:
147
+ continue
148
+
149
+ return ssl.DefaultVerifyPaths(
150
+ loc.cafile if is_file_present else None, # type: ignore[arg-type]
151
+ loc.capath if is_dir_present else None, # type: ignore[arg-type]
152
+ "SSL_CERT_FILE",
153
+ loc.cafile,
154
+ "SSL_CERT_DIR",
155
+ loc.capath,
156
+ )
157
+
158
+ # fall back to certifi
159
+ cafile = certifi.where()
160
+ return ssl.DefaultVerifyPaths(
161
+ cafile,
162
+ None, # type: ignore[arg-type]
163
+ "SSL_CERT_FILE",
164
+ cafile,
165
+ "SSL_CERT_DIR",
166
+ "/etc/ssl/certs",
167
+ )
168
+
169
+
170
+ ssl.get_default_verify_paths = get_system_ssl_default_verify_paths
@@ -0,0 +1,34 @@
1
+ import shlex
2
+ from typing import Any, Final, Callable, Tuple
3
+
4
+ from jinja2 import BaseLoader, Environment, TemplateNotFound
5
+
6
+ from ..resource_bundle import get_template_str
7
+
8
+
9
+ class EmbeddedLoader(BaseLoader):
10
+ def __init__(self) -> None:
11
+ pass
12
+
13
+ def get_source(
14
+ self,
15
+ environment: Environment,
16
+ template: str,
17
+ ) -> Tuple[str, str | None, Callable[[], bool] | None]:
18
+ if payload := get_template_str(template):
19
+ return payload, None, None
20
+ raise TemplateNotFound(template)
21
+
22
+
23
+ _JINJA_ENV: Final = Environment(
24
+ loader=EmbeddedLoader(),
25
+ autoescape=False, # we're not producing HTML
26
+ auto_reload=False, # we're serving statically embedded assets
27
+ keep_trailing_newline=True, # to make shells happy
28
+ )
29
+ _JINJA_ENV.filters["sh"] = shlex.quote
30
+
31
+
32
+ def render_template_str(template_name: str, data: dict[str, Any]) -> str:
33
+ tmpl = _JINJA_ENV.get_template(template_name)
34
+ return tmpl.render(data)
ruyi/utils/toml.py ADDED
@@ -0,0 +1,115 @@
1
+ from contextlib import AbstractContextManager
2
+ from types import TracebackType
3
+ from typing import Iterable
4
+
5
+ import tomlkit
6
+ from tomlkit.container import Container
7
+ from tomlkit.items import Array, Comment, InlineTable, Item, Table, Trivia, Whitespace
8
+
9
+
10
+ def with_indent(item: Item, spaces: int = 2) -> Item:
11
+ item.indent(spaces)
12
+ return item
13
+
14
+
15
+ def inline_table_with_spaces() -> "InlineTableWithSpaces":
16
+ return InlineTableWithSpaces(Container(), Trivia(), new=True)
17
+
18
+
19
+ class InlineTableWithSpaces(InlineTable, AbstractContextManager[InlineTable]):
20
+ def __init__(
21
+ self,
22
+ value: Container,
23
+ trivia: Trivia,
24
+ new: bool = False,
25
+ ) -> None:
26
+ super().__init__(value, trivia, new)
27
+
28
+ def __enter__(self) -> InlineTable:
29
+ self.add(tomlkit.ws(" "))
30
+ return self
31
+
32
+ def __exit__(
33
+ self,
34
+ exc_type: type[BaseException] | None,
35
+ exc_value: BaseException | None,
36
+ traceback: TracebackType | None,
37
+ ) -> bool | None:
38
+ self.add(tomlkit.ws(" "))
39
+ return None
40
+
41
+
42
+ def _into_item(x: Item | str) -> Item:
43
+ if isinstance(x, Item):
44
+ return x
45
+ return tomlkit.string(x)
46
+
47
+
48
+ def str_array(
49
+ args: Iterable[Item | str],
50
+ *,
51
+ multiline: bool = False,
52
+ indent: int = 2,
53
+ ) -> Array:
54
+ items = [_into_item(i).indent(indent) for i in args]
55
+ return Array(items, Trivia(), multiline=multiline)
56
+
57
+
58
+ def sorted_table(x: dict[str, str]) -> Table:
59
+ y = tomlkit.table()
60
+ for k in sorted(x.keys()):
61
+ y.add(k, x[k])
62
+ return y
63
+
64
+
65
+ def extract_header_comments(
66
+ doc: Container,
67
+ ) -> list[str]:
68
+ comments: list[str] = []
69
+
70
+ # ignore leading whitespaces
71
+ is_skipping_leading_ws = True
72
+ for _key, item in doc.body:
73
+ if isinstance(item, Whitespace):
74
+ if is_skipping_leading_ws:
75
+ continue
76
+ # this is part of the header comments
77
+ comments.append(item.as_string())
78
+ elif isinstance(item, Comment):
79
+ is_skipping_leading_ws = False
80
+ comments.append(item.as_string())
81
+ else:
82
+ # we reached the first non-comment item
83
+ break
84
+ return comments
85
+
86
+
87
+ def extract_footer_comments(
88
+ doc: Container,
89
+ ) -> list[str]:
90
+ comments: list[str] = []
91
+
92
+ # ignore trailing whitespaces
93
+ is_skipping_trailing_ws = True
94
+ for _key, item in reversed(doc.body):
95
+ if isinstance(item, Whitespace):
96
+ if is_skipping_trailing_ws:
97
+ continue
98
+ # this is part of the footer comments
99
+ comments.append(item.as_string())
100
+ elif isinstance(item, Comment):
101
+ is_skipping_trailing_ws = False
102
+ comments.append(item.as_string())
103
+ else:
104
+ # we reached the first non-comment item
105
+ break
106
+
107
+ # if the footer comment was preceded by a table, then the comment would be
108
+ # nested inside the table and invisible in top-level doc.body, so we would
109
+ # have to check the last item as well
110
+ if not comments:
111
+ last_elem = doc.body[-1][1].value
112
+ if isinstance(last_elem, Container):
113
+ return extract_footer_comments(last_elem)
114
+
115
+ return list(reversed(comments))
ruyi/utils/url.py ADDED
@@ -0,0 +1,7 @@
1
+ from urllib import parse
2
+
3
+
4
+ def urljoin_for_sure(base: str, url: str) -> str:
5
+ if base.endswith("/"):
6
+ return parse.urljoin(base, url)
7
+ return parse.urljoin(base + "/", url)