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.
- ruyi/__init__.py +21 -0
- ruyi/__main__.py +98 -0
- ruyi/cli/__init__.py +5 -0
- ruyi/cli/builtin_commands.py +14 -0
- ruyi/cli/cmd.py +224 -0
- ruyi/cli/completer.py +50 -0
- ruyi/cli/completion.py +26 -0
- ruyi/cli/config_cli.py +153 -0
- ruyi/cli/main.py +111 -0
- ruyi/cli/self_cli.py +295 -0
- ruyi/cli/user_input.py +127 -0
- ruyi/cli/version_cli.py +45 -0
- ruyi/config/__init__.py +401 -0
- ruyi/config/editor.py +92 -0
- ruyi/config/errors.py +76 -0
- ruyi/config/news.py +39 -0
- ruyi/config/schema.py +197 -0
- ruyi/device/__init__.py +0 -0
- ruyi/device/provision.py +591 -0
- ruyi/device/provision_cli.py +40 -0
- ruyi/log/__init__.py +272 -0
- ruyi/mux/.gitignore +1 -0
- ruyi/mux/__init__.py +0 -0
- ruyi/mux/runtime.py +213 -0
- ruyi/mux/venv/__init__.py +12 -0
- ruyi/mux/venv/emulator_cfg.py +41 -0
- ruyi/mux/venv/maker.py +782 -0
- ruyi/mux/venv/venv_cli.py +92 -0
- ruyi/mux/venv_cfg.py +214 -0
- ruyi/pluginhost/__init__.py +0 -0
- ruyi/pluginhost/api.py +206 -0
- ruyi/pluginhost/ctx.py +222 -0
- ruyi/pluginhost/paths.py +135 -0
- ruyi/pluginhost/plugin_cli.py +37 -0
- ruyi/pluginhost/unsandboxed.py +246 -0
- ruyi/py.typed +0 -0
- ruyi/resource_bundle/__init__.py +20 -0
- ruyi/resource_bundle/__main__.py +55 -0
- ruyi/resource_bundle/data.py +26 -0
- ruyi/ruyipkg/__init__.py +0 -0
- ruyi/ruyipkg/admin_checksum.py +88 -0
- ruyi/ruyipkg/admin_cli.py +83 -0
- ruyi/ruyipkg/atom.py +184 -0
- ruyi/ruyipkg/augmented_pkg.py +212 -0
- ruyi/ruyipkg/canonical_dump.py +320 -0
- ruyi/ruyipkg/checksum.py +39 -0
- ruyi/ruyipkg/cli_completion.py +42 -0
- ruyi/ruyipkg/distfile.py +208 -0
- ruyi/ruyipkg/entity.py +387 -0
- ruyi/ruyipkg/entity_cli.py +123 -0
- ruyi/ruyipkg/entity_provider.py +273 -0
- ruyi/ruyipkg/fetch.py +271 -0
- ruyi/ruyipkg/host.py +55 -0
- ruyi/ruyipkg/install.py +554 -0
- ruyi/ruyipkg/install_cli.py +150 -0
- ruyi/ruyipkg/list.py +126 -0
- ruyi/ruyipkg/list_cli.py +79 -0
- ruyi/ruyipkg/list_filter.py +173 -0
- ruyi/ruyipkg/msg.py +99 -0
- ruyi/ruyipkg/news.py +123 -0
- ruyi/ruyipkg/news_cli.py +78 -0
- ruyi/ruyipkg/news_store.py +183 -0
- ruyi/ruyipkg/pkg_manifest.py +657 -0
- ruyi/ruyipkg/profile.py +208 -0
- ruyi/ruyipkg/profile_cli.py +33 -0
- ruyi/ruyipkg/protocols.py +55 -0
- ruyi/ruyipkg/repo.py +763 -0
- ruyi/ruyipkg/state.py +345 -0
- ruyi/ruyipkg/unpack.py +369 -0
- ruyi/ruyipkg/unpack_method.py +91 -0
- ruyi/ruyipkg/update_cli.py +54 -0
- ruyi/telemetry/__init__.py +0 -0
- ruyi/telemetry/aggregate.py +72 -0
- ruyi/telemetry/event.py +41 -0
- ruyi/telemetry/node_info.py +192 -0
- ruyi/telemetry/provider.py +411 -0
- ruyi/telemetry/scope.py +43 -0
- ruyi/telemetry/store.py +238 -0
- ruyi/telemetry/telemetry_cli.py +127 -0
- ruyi/utils/__init__.py +0 -0
- ruyi/utils/ar.py +74 -0
- ruyi/utils/ci.py +63 -0
- ruyi/utils/frontmatter.py +38 -0
- ruyi/utils/git.py +169 -0
- ruyi/utils/global_mode.py +204 -0
- ruyi/utils/l10n.py +83 -0
- ruyi/utils/markdown.py +73 -0
- ruyi/utils/nuitka.py +33 -0
- ruyi/utils/porcelain.py +51 -0
- ruyi/utils/prereqs.py +77 -0
- ruyi/utils/ssl_patch.py +170 -0
- ruyi/utils/templating.py +34 -0
- ruyi/utils/toml.py +115 -0
- ruyi/utils/url.py +7 -0
- ruyi/utils/xdg_basedir.py +80 -0
- ruyi/version.py +67 -0
- ruyi-0.39.0.dist-info/LICENSE-Apache.txt +201 -0
- ruyi-0.39.0.dist-info/METADATA +403 -0
- ruyi-0.39.0.dist-info/RECORD +101 -0
- ruyi-0.39.0.dist-info/WHEEL +4 -0
- ruyi-0.39.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Regenerates data.py from fresh contents.
|
|
3
|
+
|
|
4
|
+
import base64
|
|
5
|
+
import pathlib
|
|
6
|
+
from typing import Any
|
|
7
|
+
import zlib
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def make_payload_from_file(path: pathlib.Path) -> str:
|
|
11
|
+
with open(path, "rb") as fp:
|
|
12
|
+
content = fp.read()
|
|
13
|
+
|
|
14
|
+
return base64.b64encode(zlib.compress(content, 9)).decode("ascii")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main() -> None:
|
|
18
|
+
self_path = pathlib.Path(__file__).parent.resolve()
|
|
19
|
+
bundled_resource_root = self_path / ".." / ".." / "resources" / "bundled"
|
|
20
|
+
|
|
21
|
+
resources: dict[str, str] = {}
|
|
22
|
+
template_names: dict[str, str] = {}
|
|
23
|
+
for f in bundled_resource_root.iterdir():
|
|
24
|
+
if not f.is_file():
|
|
25
|
+
continue
|
|
26
|
+
|
|
27
|
+
resources[f.name] = make_payload_from_file(f)
|
|
28
|
+
|
|
29
|
+
if f.suffix.lower() == ".jinja":
|
|
30
|
+
# strip the .jinja suffix for the template name
|
|
31
|
+
template_names[f.stem] = f.name
|
|
32
|
+
|
|
33
|
+
with open(self_path / "data.py", "w", encoding="utf-8") as fp:
|
|
34
|
+
|
|
35
|
+
def p(*args: Any) -> None:
|
|
36
|
+
return print(*args, file=fp)
|
|
37
|
+
|
|
38
|
+
p("# NOTE: This file is auto-generated. DO NOT EDIT!")
|
|
39
|
+
p("# Update by running the __main__.py alongside this file\n")
|
|
40
|
+
|
|
41
|
+
p("from typing import Final\n\n")
|
|
42
|
+
|
|
43
|
+
p("RESOURCES: Final = {")
|
|
44
|
+
for filename, payload in sorted(resources.items()):
|
|
45
|
+
p(f' "{filename}": b"{payload}", # fmt: skip')
|
|
46
|
+
p("}\n")
|
|
47
|
+
|
|
48
|
+
p("TEMPLATES: Final = {")
|
|
49
|
+
for stem, full_filename in sorted(template_names.items()):
|
|
50
|
+
p(f' "{stem}": RESOURCES["{full_filename}"],')
|
|
51
|
+
p("}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
main()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# NOTE: This file is auto-generated. DO NOT EDIT!
|
|
2
|
+
# Update by running the __main__.py alongside this file
|
|
3
|
+
|
|
4
|
+
from typing import Final
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
RESOURCES: Final = {
|
|
8
|
+
"_ruyi_completion": b"eNqlVntP40YQ/z+fYmosCC0h5KpKd7Tmjoc5ogYSxUlPd4FaTryJV9i7kXdNLofoZ++M7TgmmOiutVDYncdvZuexOzsTGc19NoU4WfJabQc6/J6FS/BAxzyKmA++XAh4YLHiUoCcQqD1XB03mzOug2R8iPrNe67CZXLf9OIZwYVMs+Y4lONm5HHxnOqpwM13iHfoN935UgdSNEpS6MUVC+doEgLvgYHPleZiomEesyn/CtNYRqADBjLmMy68ENQk5nMNWoL3ILkPwosYTGQYcvJa0bn6iQAlI6YDLmYHECUIOQOZ6HmiEQhi5vOYTVIqT6HIgs/GyQyUjpkXIYjP5kz4qaJI+Q9emDCKinvaP3cv7LPhx0OUa0/htP/xvHvd69gD2x06tjuwr3uX7Y7tAFegmD6ARDHQLJpPecgUTNGHdu/8sObmEXFLEXEpO/gj6vvwWAP8+BRGI2h8A8N8fNVU48mAu7vfyVORatG3Fd/lQrAYQT8YhULMdBJn+lOe/gvlBIOuM9ctw6xH93SQ/UzHLfvjDC66w4FL7tycXtsonKsZP+ZIZnIifWaZ71PKxNOwRktJcfSCkvoOJinWnmq177BZGeJ1eitjSm7C25PdFrw72X0DrZOmzx6aIglDeEPUP4p9qsJCxV7X3X1XqYSxpwO84n/hdRao9qVjmXu3R61f90rUrE0swyifT9D5vjhX7l9232l3byoPuAr/qnFVwSnRrLpZJ8OGib8G3BYy9FFFuJ32DZXA2fDy0u5XSvS67ZsBipwP+073hUi5tKzWFqbrXNmdjmV8U4GxVWzY6/Vtx3Gd3ul5BeS2cgHzMQvoccN8XMjYV6PW3dPT/v5G1IRUMtZWvYI+9yaszMCUcNXwdCNkntLw2+HbjTzQt8JryHy51s/7c51dTG0pQZRa6x+oj/6+vb3bP4bdXRKIPD0JyPWUObKax3cvCyA3nDnccGBvr9Kq6zOKyZgRcBETo1wlxMn8Hn0gVrYl4HT/skGyaG1k6qh8UEKXeP//Ilc+YgdB0T4VR9nMe9VZqEj6dq/z+bvrulhvL+1iUyk2+NwrsGi9tX5TqU/d/sVZ3z7901nprSn/u4Poxf6RFjKf7/9rQ7XKfZTVsvkefrLgqLo2E4Fv6jplBY+FufKGW5YFLUSiFsjapFBtlPrArDa2qrdGUW+b9ZNe1uUHZNsFuwpECY+WOJV5SZhaoSQU20t4LYjZEFc0zw58QhP4UIYhznHjZTqxfFFBqRtBLRU+3QfIwrFkwfHBwgEHFzrIIYxQer6XaDlNxMSABSFywTVHVBwTkUECiO+hnqGCTCz0NL7eUhyARKPxgit2kAOSE/m8tvDwNmA4QxkNBFAyHYmMfCI1aAaL2QyHP4TCkYycIu0c58UhUvrKofI9WnrtTHwQXLKIQ6jQ7KumQviZfK4YlF6LcjGUPLumVoP09uTkxYF//wJg/FlP", # fmt: skip
|
|
9
|
+
"binfmt.conf.jinja": b"eNpNUrlu20AQ7f0VAwgCbECk+wQp06Zwl0oakUNyoD2UvQRC1r/7LUkhrkjuzjuHOzqrG2w6Wo0ddd4NOlLMmvhshAYfKE0a6SPPSkVDymxIHN68s+LS4WVH6ijOMYntm5XrSQO05fSTogidLLtNqu1PC3EvidXEFhR/faY4+Wx6CjIqyAJ5J+QHyFcbxvibuhGYznDgpN69xrdD1Wa68QwOvl6DvwbltPqefYYGuIKecwXQO5yEop0QzPAIjXeS1B0I0+qKvwhoqp7YbDjhFIY5zAtVFDOgAV4GTgF9NP8wd6JbgLKE9uW+X3ThYqy+gkRvivTHehBp/wD57yfx/b7MtfB3NTwfHVuhxwMcDemw3qHmBQXcH49QaeJUrRCb6GnigiNfR4v29VaifF8NFUYZ2GIk4EJ2Sa1s+/KhR3qguZtUQNT5EKRLW/JaVhSUlLSLP+Bgi3Y5UKnwp71WsfX4+gabO2pqqAtC/MKz0CcWuiaCqb6i92s8fCFhU7+2Er79gUes69nDf9gX9HfteA==", # fmt: skip
|
|
10
|
+
"meson-cross.ini.jinja": b"eNptkMFqwzAQRO/6ioUQ0tJazaU9FPoLvfUUglDkNV4iS0IrB4eQf69k2ZRCbmI083Z3NvDDCJbO+Ck2YgMDsnfAmMYATWOiZ246sgi3G1zQXVT0PsH9/jYbq0GSI5BSzoBvxJYXzF6+f8j9ixTicCKnIyEfhYEv2GWaMRmzEyaEVZimWbltG+h8hDNeXyHo1AO5ClS6bSmRd9qqFSgp4cBPz7C9iwzJoQxZiHN4RaJrC7XYtkAd8JXnW7JwOI1kU5PH+FDwecsQsaNp4azWiiqknC+5EH3AmOpdgz6jSt5b02tyam5tuezR17rX/1Xys1b8aHLzN1oces9JDdr05PBYcrmHkrLkxqn0OqpOD2SvaxnRG2TOFdTax4d6GaBd5aRkcSd+ATx1t64=", # fmt: skip
|
|
11
|
+
"prompt.venv-created.txt.jinja": b"eNp1kTFvwkAMhff7FV7YStjbqWLqwMZSISSOO4dYJOfIvoRGiP9eXwR0KTedbOv5e8/bBmEkyYNvAZP9OHWYMpBC4gsEQZ8xVs598wCdn8CHTKPVgDIcJ1AeJFA6QTYh3/fCvVBp3+eIE2gQ6k0ylSG3Owli2h8p7VZ7iCQYMsv0Bj5FiPjUN3FKI5+L+EGGiZZ/zUPlroslUA06qTBnWNyc+4RaUJtHbdUL1vRTrPhWGYxtJDUgjHeW/5xX7mt270dPrT+25iTPszW3LV8KTe9z8+4c2LubuV6fILeb2ZrpMEUDLGDbFyHPWIE7VLhQbsDCjtOy8xFhvfFnhMzchsYbbU0tupLQBtUiDcKqc7GCdYPh/MoOzFA1iw2w4oezdaWujwiKhhpEymXNoP5kp02aZQjleFq5Xz21xdY=", # fmt: skip
|
|
12
|
+
"ruyi-activate.bash.jinja": b"eNp9lN1u2kAQhe/3KSYGqiSVRUnuqIhEGqRESgLCBClKI2uxx/JKZh2t126o6yfqI/QuT9bxH2CHhBvEzpn1d2YO7sDCFxF4IkBYx5GGFUIcoQu/hPbBiMJYOQgrIfsq3giTO1okXKMBp54K17DikX/KOrAJY3C4lKEGFUsQGlyh0NHBhjEmPHgCo5tejq1r25o+zH9MzMyAEZ19M+D5O2gfJQP6oOOHYDzSZQVL9XSdE0aOEi96CD+79XHefPHlrGx8pUeenzNPMFaAulijwvEJpIWoAwoj1BAGLqBMhArlGqWGhCvBVwFGleqIcM3f0E2X4/lXO4Nn6tOxkhFoFSOQHSoAMbnoBFzRsLgGHgRFO1WrfnJszx8eb+zp7ZU9Gy+uh3QZ+YU9w/knL42MblNrbMv4+hIqXai2Z7HMfTQ7imLuvzQhZMID4eYDoKcV7bQhx8ei7tPewFRwdtF3MenLmOg/xbcGH8Bbgya7NXiPbg0+Jq9qNXhZL8rLyf3y0Jk9m0/vZosa94mAiXVQRUqG0sWIFpWvH1vEHbAw8KAWHLWwTA9a4anZMkYhL0VCKQww4Y3gtDPXpGBsCz9K050T+AO0hixj1aB2tllztzTiMhTVj62wT//MYVXbDwr7LAAHl5/28mHuhnw/vpuA2ctYe+4NB6WstpH2TMAgwsN9RveYXhco+Rphz4JxYpSN0iWA3vth1Psu3yNlMnfFqxtrfHk7qURmO6ONpBFCSl8kYtvsvv2d0+5g/8ryquztHzTke2mmRPwHax2J0Q==", # fmt: skip
|
|
13
|
+
"ruyi-cache.toml.jinja": b"eNptUs1uhCAYvPsUX2nMtknjoccmve0e9tK9eDOGsIJK1r8CmhjjuxcUF6uexG+GmWHgFX5u4eULwpxLSHnBQH9LUpGMUbj3INqeB3C+GRpcztfwxfOihCQ5o7j7jL3BB57CLytbfOcV+ONz+Q1oGBwyjkhzWUU13R/tvkbUxhMbEqs6AzjxYINOZmkt4PEBHWjJDR5wxUr59q5VtPFDO9oMnTH3ZnezX+P/k6xFFREZU3Iltkq0gFY/9lRdF0lOeGXOSLlYHKPTFjnFUwo3TguSySP+BFj6XFMXIQfLXoq6VijW0XbTIz0LzYr7O9DiWZJgXklFigLrqJP0ZuaEN8Ch7LPobbtJSXHJFKFEEVyS5rjmHQvZwlHsUSZXpzR/S7PT3WDVNvoRuxpWU9epy/cHatoHPQ==", # fmt: skip
|
|
14
|
+
"ruyi-venv.toml.jinja": b"eNqLTs7PS8tMj+UqKMpPy8xJVbBVUKquVoDxamuVuKpVFTLTFIori4vy80sUdFVruWBsiFoYD6JWVyE1LwWoHqgMAMZMHuE=", # fmt: skip
|
|
15
|
+
"toolchain.cmake.jinja": b"eNqFkVFrwjAUhd/7Ky6IMGF274M9dG2cZWlT0joUhJDVaIuajKQTR+l/X6zOWccwb+F+5+Scmx5MjIBNuRaPTs/pQb7lawGu68Lc3g5nGPiR94pYRgj2x14Ys1GI0VNdw07IHdNKVdA0D5VSm7zgpXSPFnOn7g+hXIL5Mi3Tb64MwzjNPIxZQtEonB4Mf9CmOcmFXFiHs9LGchwjqrujQTpLMxSx2IsQ4FJ+7gd/hwklPkpTQsH6f2iVC2OUti9csj7zSZTYVi2V59fj6bQL7PctUfcv+w1tzF/NKIwDRgnJWOJlY+iUa5UX1YzgOi9gaXMVgi+ENsDlwn7Ku+a6FAZKCVUhoOJ6JSqr3JVaya2Q1X1XbfutNN+eBYUyHfzffCwiATrs6oV6EcToDdHBDRiHz9SjMyAxnt1iw9jHkwCd2G+4PsQk", # fmt: skip
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
TEMPLATES: Final = {
|
|
19
|
+
"binfmt.conf": RESOURCES["binfmt.conf.jinja"],
|
|
20
|
+
"meson-cross.ini": RESOURCES["meson-cross.ini.jinja"],
|
|
21
|
+
"prompt.venv-created.txt": RESOURCES["prompt.venv-created.txt.jinja"],
|
|
22
|
+
"ruyi-activate.bash": RESOURCES["ruyi-activate.bash.jinja"],
|
|
23
|
+
"ruyi-cache.toml": RESOURCES["ruyi-cache.toml.jinja"],
|
|
24
|
+
"ruyi-venv.toml": RESOURCES["ruyi-venv.toml.jinja"],
|
|
25
|
+
"toolchain.cmake": RESOURCES["toolchain.cmake.jinja"],
|
|
26
|
+
}
|
ruyi/ruyipkg/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
from typing import Any, TypeGuard
|
|
4
|
+
|
|
5
|
+
from tomlkit import document, table
|
|
6
|
+
from tomlkit.items import AoT, Table
|
|
7
|
+
from tomlkit.toml_document import TOMLDocument
|
|
8
|
+
|
|
9
|
+
from ..log import RuyiLogger
|
|
10
|
+
from . import checksum
|
|
11
|
+
from .pkg_manifest import DistfileDeclType, RestrictKind
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def do_admin_checksum(
|
|
15
|
+
logger: RuyiLogger,
|
|
16
|
+
files: list[os.PathLike[Any]],
|
|
17
|
+
format: str,
|
|
18
|
+
restrict: list[str],
|
|
19
|
+
) -> int:
|
|
20
|
+
if not validate_restrict_kinds(restrict):
|
|
21
|
+
logger.F(f"invalid restrict kinds given: {restrict}")
|
|
22
|
+
return 1
|
|
23
|
+
|
|
24
|
+
entries = [gen_distfile_entry(logger, f, restrict) for f in files]
|
|
25
|
+
if format == "toml":
|
|
26
|
+
doc = emit_toml_distfiles_section(entries)
|
|
27
|
+
logger.D(f"{doc}")
|
|
28
|
+
sys.stdout.write(doc.as_string())
|
|
29
|
+
return 0
|
|
30
|
+
|
|
31
|
+
raise RuntimeError("unrecognized output format; should never happen")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def validate_restrict_kinds(input: list[str]) -> TypeGuard[list[RestrictKind]]:
|
|
35
|
+
for x in input:
|
|
36
|
+
match x:
|
|
37
|
+
case "fetch" | "mirror":
|
|
38
|
+
pass
|
|
39
|
+
case _:
|
|
40
|
+
return False
|
|
41
|
+
return True
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def gen_distfile_entry(
|
|
45
|
+
logger: RuyiLogger,
|
|
46
|
+
path: os.PathLike[Any],
|
|
47
|
+
restrict: list[RestrictKind],
|
|
48
|
+
) -> DistfileDeclType:
|
|
49
|
+
logger.D(f"generating distfile entry for {path}")
|
|
50
|
+
with open(path, "rb") as fp:
|
|
51
|
+
filesize = os.stat(fp.fileno()).st_size
|
|
52
|
+
c = checksum.Checksummer(fp, {})
|
|
53
|
+
checksums = c.compute(kinds=checksum.SUPPORTED_CHECKSUM_KINDS)
|
|
54
|
+
|
|
55
|
+
obj: DistfileDeclType = {
|
|
56
|
+
"name": os.path.basename(path),
|
|
57
|
+
"size": filesize,
|
|
58
|
+
"checksums": checksums,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if restrict:
|
|
62
|
+
obj["restrict"] = restrict
|
|
63
|
+
|
|
64
|
+
return obj
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def emit_toml_distfiles_section(x: list[DistfileDeclType]) -> TOMLDocument:
|
|
68
|
+
doc = document()
|
|
69
|
+
|
|
70
|
+
arr: list[Table] = []
|
|
71
|
+
for dd in x:
|
|
72
|
+
t = table()
|
|
73
|
+
t.add("name", dd["name"])
|
|
74
|
+
t.add("size", dd["size"])
|
|
75
|
+
if r := dd.get("restrict"):
|
|
76
|
+
t.add("restrict", r)
|
|
77
|
+
t.add("checksums", emit_toml_checksums(dd["checksums"]))
|
|
78
|
+
arr.append(t)
|
|
79
|
+
|
|
80
|
+
doc.add("distfiles", AoT(arr))
|
|
81
|
+
return doc
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def emit_toml_checksums(x: dict[str, str]) -> Table:
|
|
85
|
+
t = table()
|
|
86
|
+
for k in sorted(x.keys()):
|
|
87
|
+
t.add(k, x[k])
|
|
88
|
+
return t
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import pathlib
|
|
3
|
+
from typing import TYPE_CHECKING, cast
|
|
4
|
+
|
|
5
|
+
from ..cli.cmd import AdminCommand
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from ..cli.completion import ArgumentParser
|
|
9
|
+
from ..config import GlobalConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AdminChecksumCommand(
|
|
13
|
+
AdminCommand,
|
|
14
|
+
cmd="checksum",
|
|
15
|
+
help="Generate a checksum section for a manifest file for the distfiles given",
|
|
16
|
+
):
|
|
17
|
+
@classmethod
|
|
18
|
+
def configure_args(cls, gc: "GlobalConfig", p: "ArgumentParser") -> None:
|
|
19
|
+
p.add_argument(
|
|
20
|
+
"--format",
|
|
21
|
+
"-f",
|
|
22
|
+
type=str,
|
|
23
|
+
choices=["toml"],
|
|
24
|
+
default="toml",
|
|
25
|
+
help="Format of checksum section to generate in",
|
|
26
|
+
)
|
|
27
|
+
p.add_argument(
|
|
28
|
+
"--restrict",
|
|
29
|
+
type=str,
|
|
30
|
+
default="",
|
|
31
|
+
help="the 'restrict' field to use for all mentioned distfiles, separated with comma",
|
|
32
|
+
)
|
|
33
|
+
p.add_argument(
|
|
34
|
+
"file",
|
|
35
|
+
type=str,
|
|
36
|
+
nargs="+",
|
|
37
|
+
help="Path to the distfile(s) to checksum",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def main(cls, cfg: "GlobalConfig", args: argparse.Namespace) -> int:
|
|
42
|
+
from .admin_checksum import do_admin_checksum
|
|
43
|
+
|
|
44
|
+
logger = cfg.logger
|
|
45
|
+
files = args.file
|
|
46
|
+
format = args.format
|
|
47
|
+
restrict_str = cast(str, args.restrict)
|
|
48
|
+
restrict = restrict_str.split(",") if restrict_str else []
|
|
49
|
+
|
|
50
|
+
return do_admin_checksum(logger, files, format, restrict)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class AdminFormatManifestCommand(
|
|
54
|
+
AdminCommand,
|
|
55
|
+
cmd="format-manifest",
|
|
56
|
+
help="Format the given package manifests into canonical TOML representation",
|
|
57
|
+
):
|
|
58
|
+
@classmethod
|
|
59
|
+
def configure_args(cls, gc: "GlobalConfig", p: "ArgumentParser") -> None:
|
|
60
|
+
p.add_argument(
|
|
61
|
+
"file",
|
|
62
|
+
type=str,
|
|
63
|
+
nargs="+",
|
|
64
|
+
help="Path to the distfile(s) to generate manifest for",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def main(cls, cfg: "GlobalConfig", args: argparse.Namespace) -> int:
|
|
69
|
+
from .canonical_dump import dumps_canonical_package_manifest_toml
|
|
70
|
+
from .pkg_manifest import PackageManifest
|
|
71
|
+
|
|
72
|
+
files = args.file
|
|
73
|
+
|
|
74
|
+
for f in files:
|
|
75
|
+
p = pathlib.Path(f)
|
|
76
|
+
pm = PackageManifest.load_from_path(p)
|
|
77
|
+
d = dumps_canonical_package_manifest_toml(pm)
|
|
78
|
+
|
|
79
|
+
dest_path = p.with_suffix(".toml")
|
|
80
|
+
with open(dest_path, "w", encoding="utf-8") as fp:
|
|
81
|
+
fp.write(d)
|
|
82
|
+
|
|
83
|
+
return 0
|
ruyi/ruyipkg/atom.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import re
|
|
3
|
+
from typing import Final, Iterator, Literal, Tuple
|
|
4
|
+
|
|
5
|
+
import semver
|
|
6
|
+
|
|
7
|
+
from .pkg_manifest import BoundPackageManifest, is_prerelease
|
|
8
|
+
from .protocols import ProvidesPackageManifests
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
AtomKind = Literal["name"] | Literal["expr"] | Literal["slug"]
|
|
12
|
+
|
|
13
|
+
RE_ATOM_EXPR: Final = re.compile(r"^([^:(]+)\((.+)\)$")
|
|
14
|
+
RE_ATOM_NAME: Final = re.compile(r"^[^:()]+$")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Atom(abc.ABC):
|
|
18
|
+
def __init__(self, s: str, kind: AtomKind) -> None:
|
|
19
|
+
self._s = s
|
|
20
|
+
self._kind: AtomKind = kind
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def input_str(self) -> str:
|
|
24
|
+
return self._s
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def kind(self) -> AtomKind:
|
|
28
|
+
return self._kind
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def parse(cls, s: str) -> "SlugAtom | NameAtom | ExprAtom":
|
|
32
|
+
if s.startswith("slug:"):
|
|
33
|
+
return SlugAtom(s)
|
|
34
|
+
|
|
35
|
+
if s.startswith("name:"):
|
|
36
|
+
return NameAtom(s, s[5:]) # strip the "name:" prefix
|
|
37
|
+
|
|
38
|
+
if match := RE_ATOM_EXPR.match(s):
|
|
39
|
+
return ExprAtom(s, match[1], match[2])
|
|
40
|
+
|
|
41
|
+
# fallback
|
|
42
|
+
if match := RE_ATOM_NAME.match(s):
|
|
43
|
+
return NameAtom(s, s)
|
|
44
|
+
|
|
45
|
+
raise ValueError(f"invalid atom: '{s}'")
|
|
46
|
+
|
|
47
|
+
@abc.abstractmethod
|
|
48
|
+
def match_in_repo(
|
|
49
|
+
self,
|
|
50
|
+
repo: ProvidesPackageManifests,
|
|
51
|
+
include_prerelease_vers: bool,
|
|
52
|
+
) -> BoundPackageManifest | None:
|
|
53
|
+
raise NotImplementedError
|
|
54
|
+
|
|
55
|
+
@abc.abstractmethod
|
|
56
|
+
def iter_in_repo(
|
|
57
|
+
self,
|
|
58
|
+
repo: ProvidesPackageManifests,
|
|
59
|
+
include_prerelease_vers: bool,
|
|
60
|
+
) -> Iterator[BoundPackageManifest]:
|
|
61
|
+
raise NotImplementedError
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def split_category(name: str) -> Tuple[str | None, str]:
|
|
65
|
+
fragments = name.split("/", 1)
|
|
66
|
+
if len(fragments) == 2:
|
|
67
|
+
return (fragments[0], fragments[1])
|
|
68
|
+
return (None, name)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class NameAtom(Atom):
|
|
72
|
+
def __init__(self, s: str, name: str) -> None:
|
|
73
|
+
super().__init__(s, "name")
|
|
74
|
+
|
|
75
|
+
self.category, self.name = split_category(name)
|
|
76
|
+
|
|
77
|
+
def match_in_repo(
|
|
78
|
+
self,
|
|
79
|
+
repo: ProvidesPackageManifests,
|
|
80
|
+
include_prerelease_vers: bool,
|
|
81
|
+
) -> BoundPackageManifest | None:
|
|
82
|
+
# return the latest version of the package named self.name in the given repo
|
|
83
|
+
try:
|
|
84
|
+
return repo.get_pkg_latest_ver(
|
|
85
|
+
self.name,
|
|
86
|
+
self.category,
|
|
87
|
+
include_prerelease_vers,
|
|
88
|
+
)
|
|
89
|
+
except KeyError:
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
def iter_in_repo(
|
|
93
|
+
self,
|
|
94
|
+
repo: ProvidesPackageManifests,
|
|
95
|
+
include_prerelease_vers: bool,
|
|
96
|
+
) -> Iterator[BoundPackageManifest]:
|
|
97
|
+
# return all versions of the package named self.name in the given repo
|
|
98
|
+
for pm in repo.iter_pkg_vers(self.name, self.category):
|
|
99
|
+
if not is_prerelease(pm.semver) or include_prerelease_vers:
|
|
100
|
+
yield pm
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def fix_version_matcher_for_semver2(match_expr: str) -> str:
|
|
104
|
+
# equivalent of https://github.com/python-semver/python-semver/pull/362
|
|
105
|
+
# for semver 2.x
|
|
106
|
+
if match_expr and match_expr[0] in "0123456789":
|
|
107
|
+
return f"=={match_expr}"
|
|
108
|
+
return match_expr
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class ExprAtom(Atom):
|
|
112
|
+
def __init__(self, s: str, name: str, expr: str) -> None:
|
|
113
|
+
super().__init__(s, "expr")
|
|
114
|
+
self.exprs = expr.split(",")
|
|
115
|
+
|
|
116
|
+
if semver.__version__ < "3":
|
|
117
|
+
self.exprs = list(map(fix_version_matcher_for_semver2, self.exprs))
|
|
118
|
+
|
|
119
|
+
self.category, self.name = split_category(name)
|
|
120
|
+
|
|
121
|
+
def _is_pm_matching_my_exprs(self, pm: BoundPackageManifest) -> bool:
|
|
122
|
+
for e in self.exprs:
|
|
123
|
+
if not pm.semver.match(e):
|
|
124
|
+
return False
|
|
125
|
+
return True
|
|
126
|
+
|
|
127
|
+
def match_in_repo(
|
|
128
|
+
self,
|
|
129
|
+
repo: ProvidesPackageManifests,
|
|
130
|
+
include_prerelease_vers: bool,
|
|
131
|
+
) -> BoundPackageManifest | None:
|
|
132
|
+
matching_pms = {
|
|
133
|
+
pm.ver: pm
|
|
134
|
+
for pm in repo.iter_pkg_vers(self.name, self.category)
|
|
135
|
+
if self._is_pm_matching_my_exprs(pm)
|
|
136
|
+
}
|
|
137
|
+
if not matching_pms:
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
semvers = [pm.semver for pm in matching_pms.values()]
|
|
141
|
+
if not include_prerelease_vers:
|
|
142
|
+
semvers = [sv for sv in semvers if not is_prerelease(sv)]
|
|
143
|
+
if not semvers:
|
|
144
|
+
return None
|
|
145
|
+
latest_ver = max(semvers)
|
|
146
|
+
return matching_pms[str(latest_ver)]
|
|
147
|
+
|
|
148
|
+
def iter_in_repo(
|
|
149
|
+
self,
|
|
150
|
+
repo: ProvidesPackageManifests,
|
|
151
|
+
include_prerelease_vers: bool,
|
|
152
|
+
) -> Iterator[BoundPackageManifest]:
|
|
153
|
+
for pm in repo.iter_pkg_vers(self.name, self.category):
|
|
154
|
+
if self._is_pm_matching_my_exprs(pm):
|
|
155
|
+
if not is_prerelease(pm.semver) or include_prerelease_vers:
|
|
156
|
+
yield pm
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class SlugAtom(Atom):
|
|
160
|
+
def __init__(self, s: str) -> None:
|
|
161
|
+
super().__init__(s, "slug")
|
|
162
|
+
self.slug = s[5:] # strip the "slug:" prefix
|
|
163
|
+
|
|
164
|
+
def match_in_repo(
|
|
165
|
+
self,
|
|
166
|
+
repo: ProvidesPackageManifests,
|
|
167
|
+
include_prerelease_vers: bool,
|
|
168
|
+
) -> BoundPackageManifest | None:
|
|
169
|
+
pm = repo.get_pkg_by_slug(self.slug)
|
|
170
|
+
if pm and is_prerelease(pm.semver):
|
|
171
|
+
return pm if include_prerelease_vers else None
|
|
172
|
+
return pm
|
|
173
|
+
|
|
174
|
+
def iter_in_repo(
|
|
175
|
+
self,
|
|
176
|
+
repo: ProvidesPackageManifests,
|
|
177
|
+
include_prerelease_vers: bool,
|
|
178
|
+
) -> Iterator[BoundPackageManifest]:
|
|
179
|
+
pm = repo.get_pkg_by_slug(self.slug)
|
|
180
|
+
if pm is None:
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
if is_prerelease(pm.semver) and include_prerelease_vers:
|
|
184
|
+
yield pm
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
import sys
|
|
3
|
+
from typing import Iterable, TypedDict, TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from typing_extensions import Self
|
|
7
|
+
|
|
8
|
+
from ..config import GlobalConfig
|
|
9
|
+
from ..utils.porcelain import PorcelainEntity, PorcelainEntityType
|
|
10
|
+
from .distfile import Distfile
|
|
11
|
+
from .host import get_native_host
|
|
12
|
+
from .list_filter import ListFilter
|
|
13
|
+
from .pkg_manifest import BoundPackageManifest, PackageManifestType
|
|
14
|
+
from .repo import MetadataRepo
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
if sys.version_info >= (3, 11):
|
|
18
|
+
|
|
19
|
+
class PkgRemark(enum.StrEnum):
|
|
20
|
+
Latest = "latest"
|
|
21
|
+
LatestPreRelease = "latest-prerelease"
|
|
22
|
+
NoBinaryForCurrentHost = "no-binary-for-current-host"
|
|
23
|
+
PreRelease = "prerelease"
|
|
24
|
+
HasKnownIssue = "known-issue"
|
|
25
|
+
Downloaded = "downloaded"
|
|
26
|
+
Installed = "installed"
|
|
27
|
+
|
|
28
|
+
def as_rich_markup(self) -> str:
|
|
29
|
+
match self:
|
|
30
|
+
case self.Latest:
|
|
31
|
+
return "latest"
|
|
32
|
+
case self.LatestPreRelease:
|
|
33
|
+
return "latest-prerelease"
|
|
34
|
+
case self.NoBinaryForCurrentHost:
|
|
35
|
+
return "[red]no binary for current host[/]"
|
|
36
|
+
case self.PreRelease:
|
|
37
|
+
return "prerelease"
|
|
38
|
+
case self.HasKnownIssue:
|
|
39
|
+
return "[yellow]has known issue[/]"
|
|
40
|
+
case self.Downloaded:
|
|
41
|
+
return "[green]downloaded[/]"
|
|
42
|
+
case self.Installed:
|
|
43
|
+
return "[green]installed[/]"
|
|
44
|
+
return ""
|
|
45
|
+
|
|
46
|
+
else:
|
|
47
|
+
|
|
48
|
+
class PkgRemark(str, enum.Enum):
|
|
49
|
+
Latest = "latest"
|
|
50
|
+
LatestPreRelease = "latest-prerelease"
|
|
51
|
+
NoBinaryForCurrentHost = "no-binary-for-current-host"
|
|
52
|
+
PreRelease = "prerelease"
|
|
53
|
+
HasKnownIssue = "known-issue"
|
|
54
|
+
Downloaded = "downloaded"
|
|
55
|
+
Installed = "installed"
|
|
56
|
+
|
|
57
|
+
def as_rich_markup(self) -> str:
|
|
58
|
+
match self:
|
|
59
|
+
case self.Latest:
|
|
60
|
+
return "latest"
|
|
61
|
+
case self.LatestPreRelease:
|
|
62
|
+
return "latest-prerelease"
|
|
63
|
+
case self.NoBinaryForCurrentHost:
|
|
64
|
+
return "[red]no binary for current host[/]"
|
|
65
|
+
case self.PreRelease:
|
|
66
|
+
return "prerelease"
|
|
67
|
+
case self.HasKnownIssue:
|
|
68
|
+
return "[yellow]has known issue[/]"
|
|
69
|
+
case self.Downloaded:
|
|
70
|
+
return "[green]downloaded[/]"
|
|
71
|
+
case self.Installed:
|
|
72
|
+
return "[green]installed[/]"
|
|
73
|
+
return ""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class AugmentedPkgManifest:
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
pm: BoundPackageManifest,
|
|
80
|
+
remarks: list[PkgRemark],
|
|
81
|
+
) -> None:
|
|
82
|
+
self.pm = pm
|
|
83
|
+
self.remarks = remarks
|
|
84
|
+
self._is_downloaded = PkgRemark.Downloaded in remarks
|
|
85
|
+
self._is_installed = PkgRemark.Installed in remarks
|
|
86
|
+
|
|
87
|
+
def to_porcelain(self) -> "PorcelainPkgVersionV1":
|
|
88
|
+
return {
|
|
89
|
+
"semver": str(self.pm.semver),
|
|
90
|
+
"pm": self.pm.to_raw(),
|
|
91
|
+
"remarks": self.remarks,
|
|
92
|
+
"is_downloaded": self._is_downloaded,
|
|
93
|
+
"is_installed": self._is_installed,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class AugmentedPkg:
|
|
98
|
+
def __init__(self) -> None:
|
|
99
|
+
self.versions: list[AugmentedPkgManifest] = []
|
|
100
|
+
|
|
101
|
+
def add_version(self, v: AugmentedPkgManifest) -> None:
|
|
102
|
+
if self.versions:
|
|
103
|
+
if v.pm.category != self.category or v.pm.name != self.name:
|
|
104
|
+
raise ValueError("cannot add a version of a different pkg")
|
|
105
|
+
self.versions.append(v)
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def category(self) -> str | None:
|
|
109
|
+
return self.versions[0].pm.category if self.versions else None
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def name(self) -> str | None:
|
|
113
|
+
return self.versions[0].pm.name if self.versions else None
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
def yield_from_repo(
|
|
117
|
+
cls,
|
|
118
|
+
cfg: GlobalConfig,
|
|
119
|
+
mr: MetadataRepo,
|
|
120
|
+
filters: ListFilter,
|
|
121
|
+
*,
|
|
122
|
+
ensure_repo: bool = True,
|
|
123
|
+
) -> "Iterable[Self]":
|
|
124
|
+
rgs = cfg.ruyipkg_global_state
|
|
125
|
+
native_host = str(get_native_host())
|
|
126
|
+
|
|
127
|
+
for category, pkg_name, pkg_vers in mr.iter_pkgs(ensure_repo=ensure_repo):
|
|
128
|
+
if not filters.check_pkg_name(cfg, mr, category, pkg_name):
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
pkg = cls()
|
|
132
|
+
|
|
133
|
+
semvers = [pm.semver for pm in pkg_vers.values()]
|
|
134
|
+
semvers.sort(reverse=True)
|
|
135
|
+
found_latest = False
|
|
136
|
+
for i, sv in enumerate(semvers):
|
|
137
|
+
# TODO: support filter ops against individual versions
|
|
138
|
+
|
|
139
|
+
pm = pkg_vers[str(sv)]
|
|
140
|
+
|
|
141
|
+
latest = False
|
|
142
|
+
latest_prerelease = i == 0
|
|
143
|
+
prerelease = pm.is_prerelease
|
|
144
|
+
if not found_latest and not prerelease:
|
|
145
|
+
latest = True
|
|
146
|
+
found_latest = True
|
|
147
|
+
|
|
148
|
+
remarks: list[PkgRemark] = []
|
|
149
|
+
if latest or latest_prerelease or prerelease:
|
|
150
|
+
if prerelease:
|
|
151
|
+
remarks.append(PkgRemark.PreRelease)
|
|
152
|
+
if latest:
|
|
153
|
+
remarks.append(PkgRemark.Latest)
|
|
154
|
+
if latest_prerelease and not latest:
|
|
155
|
+
remarks.append(PkgRemark.LatestPreRelease)
|
|
156
|
+
if pm.service_level.has_known_issues:
|
|
157
|
+
remarks.append(PkgRemark.HasKnownIssue)
|
|
158
|
+
if bm := pm.binary_metadata:
|
|
159
|
+
if not bm.is_available_for_current_host:
|
|
160
|
+
remarks.append(PkgRemark.NoBinaryForCurrentHost)
|
|
161
|
+
if _is_pkg_fully_downloaded(pm):
|
|
162
|
+
remarks.append(PkgRemark.Downloaded)
|
|
163
|
+
|
|
164
|
+
host = native_host if bm is not None else ""
|
|
165
|
+
is_installed = rgs.is_package_installed(
|
|
166
|
+
pm.repo_id,
|
|
167
|
+
pm.category,
|
|
168
|
+
pm.name,
|
|
169
|
+
str(sv),
|
|
170
|
+
host,
|
|
171
|
+
)
|
|
172
|
+
if is_installed:
|
|
173
|
+
remarks.append(PkgRemark.Installed)
|
|
174
|
+
|
|
175
|
+
pkg.add_version(AugmentedPkgManifest(pm, remarks))
|
|
176
|
+
|
|
177
|
+
yield pkg
|
|
178
|
+
|
|
179
|
+
def to_porcelain(self) -> "PorcelainPkgListOutputV1":
|
|
180
|
+
return {
|
|
181
|
+
"ty": PorcelainEntityType.PkgListOutputV1,
|
|
182
|
+
"category": self.category or "",
|
|
183
|
+
"name": self.name or "",
|
|
184
|
+
"vers": [x.to_porcelain() for x in self.versions],
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class PorcelainPkgVersionV1(TypedDict):
|
|
189
|
+
semver: str
|
|
190
|
+
pm: PackageManifestType
|
|
191
|
+
remarks: list[PkgRemark]
|
|
192
|
+
is_downloaded: bool
|
|
193
|
+
is_installed: bool
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class PorcelainPkgListOutputV1(PorcelainEntity):
|
|
197
|
+
category: str
|
|
198
|
+
name: str
|
|
199
|
+
vers: list[PorcelainPkgVersionV1]
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _is_pkg_fully_downloaded(pm: BoundPackageManifest) -> bool:
|
|
203
|
+
dfs = pm.distfiles
|
|
204
|
+
if not dfs:
|
|
205
|
+
return True
|
|
206
|
+
|
|
207
|
+
for df_decl in dfs.values():
|
|
208
|
+
df = Distfile(df_decl, pm.repo)
|
|
209
|
+
if not df.is_downloaded():
|
|
210
|
+
return False
|
|
211
|
+
|
|
212
|
+
return True
|