proj-flow 0.9.3__py3-none-any.whl → 0.10.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 (66) hide show
  1. proj_flow/__init__.py +6 -1
  2. proj_flow/api/arg.py +47 -24
  3. proj_flow/api/ctx.py +43 -23
  4. proj_flow/api/env.py +7 -2
  5. proj_flow/api/makefile.py +1 -1
  6. proj_flow/api/step.py +3 -5
  7. proj_flow/base/name_list.py +19 -0
  8. proj_flow/base/plugins.py +1 -36
  9. proj_flow/base/registry.py +19 -4
  10. proj_flow/cli/__init__.py +2 -4
  11. proj_flow/cli/argument.py +3 -3
  12. proj_flow/{flow/dependency.py → dependency.py} +1 -1
  13. proj_flow/ext/cplusplus/__init__.py +10 -0
  14. proj_flow/ext/cplusplus/cmake/__init__.py +12 -0
  15. proj_flow/{plugins → ext/cplusplus}/cmake/__version__.py +5 -0
  16. proj_flow/{plugins → ext/cplusplus}/cmake/context.py +10 -8
  17. proj_flow/{plugins → ext/cplusplus}/cmake/parser.py +6 -28
  18. proj_flow/ext/cplusplus/cmake/steps.py +142 -0
  19. proj_flow/ext/cplusplus/cmake/version.py +35 -0
  20. proj_flow/{plugins → ext/cplusplus}/conan/__init__.py +7 -3
  21. proj_flow/{plugins → ext/cplusplus}/conan/_conan.py +8 -3
  22. proj_flow/ext/github/__init__.py +2 -2
  23. proj_flow/ext/github/cli.py +2 -11
  24. proj_flow/{plugins/github.py → ext/github/switches.py} +3 -3
  25. proj_flow/ext/{markdown_changelist.py → markdown_changelog.py} +2 -1
  26. proj_flow/ext/python/rtdocs.py +1 -1
  27. proj_flow/ext/python/version.py +1 -2
  28. proj_flow/ext/{re_structured_changelist.py → re_structured_changelog.py} +3 -1
  29. proj_flow/{plugins → ext}/sign/__init__.py +64 -44
  30. proj_flow/ext/sign/api.py +83 -0
  31. proj_flow/ext/sign/win32.py +152 -0
  32. proj_flow/{plugins/store/store_packages.py → ext/store.py} +51 -9
  33. proj_flow/flow/__init__.py +2 -2
  34. proj_flow/log/release.py +1 -1
  35. proj_flow/log/rich_text/markdown.py +1 -1
  36. proj_flow/log/rich_text/re_structured_text.py +1 -1
  37. proj_flow/minimal/__init__.py +2 -2
  38. proj_flow/{plugins → minimal}/base.py +3 -2
  39. proj_flow/{plugins/commands → minimal}/init.py +44 -11
  40. proj_flow/minimal/run.py +1 -2
  41. proj_flow/project/__init__.py +11 -0
  42. proj_flow/project/api.py +51 -0
  43. proj_flow/project/cplusplus.py +17 -0
  44. proj_flow/project/data.py +14 -0
  45. proj_flow/{flow → project}/interact.py +114 -13
  46. {proj_flow-0.9.3.dist-info → proj_flow-0.10.0.dist-info}/METADATA +3 -2
  47. {proj_flow-0.9.3.dist-info → proj_flow-0.10.0.dist-info}/RECORD +50 -55
  48. proj_flow/flow/init.py +0 -65
  49. proj_flow/plugins/__init__.py +0 -8
  50. proj_flow/plugins/cmake/__init__.py +0 -11
  51. proj_flow/plugins/cmake/build.py +0 -29
  52. proj_flow/plugins/cmake/config.py +0 -59
  53. proj_flow/plugins/cmake/pack.py +0 -37
  54. proj_flow/plugins/cmake/test.py +0 -29
  55. proj_flow/plugins/commands/__init__.py +0 -12
  56. proj_flow/plugins/commands/ci/__init__.py +0 -17
  57. proj_flow/plugins/commands/ci/changelog.py +0 -47
  58. proj_flow/plugins/commands/ci/matrix.py +0 -46
  59. proj_flow/plugins/commands/ci/release.py +0 -116
  60. proj_flow/plugins/sign/win32.py +0 -191
  61. proj_flow/plugins/store/__init__.py +0 -11
  62. proj_flow/plugins/store/store_both.py +0 -22
  63. proj_flow/plugins/store/store_tests.py +0 -21
  64. {proj_flow-0.9.3.dist-info → proj_flow-0.10.0.dist-info}/WHEEL +0 -0
  65. {proj_flow-0.9.3.dist-info → proj_flow-0.10.0.dist-info}/entry_points.txt +0 -0
  66. {proj_flow-0.9.3.dist-info → proj_flow-0.10.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,142 @@
1
+ # Copyright (c) 2025 Marcin Zdun
2
+ # This code is licensed under MIT license (see LICENSE for details)
3
+
4
+ """
5
+ The **proj_flow.ext.cplusplus.cmake.steps** defines steps for configuring,
6
+ building and archiving.
7
+ """
8
+
9
+ import os
10
+ from typing import Dict, List, cast
11
+
12
+ from proj_flow import api
13
+ from proj_flow.api import env, step
14
+
15
+ from .__version__ import CMAKE_VERSION
16
+
17
+
18
+ class CMakeBase(api.step.Step):
19
+ _name: str
20
+ _runs_after: List[str] = []
21
+ _runs_before: List[str] = []
22
+
23
+ @property
24
+ def name(self):
25
+ return self._name
26
+
27
+ @property
28
+ def runs_after(self):
29
+ return self._runs_after
30
+
31
+ @property
32
+ def runs_before(self):
33
+ return self._runs_before
34
+
35
+ def __init__(
36
+ self, name: str, runs_after: List[str] = [], runs_before: List[str] = []
37
+ ):
38
+ super().__init__()
39
+ self._name = name
40
+ self._runs_after = runs_after
41
+ self._runs_before = runs_before
42
+
43
+ def is_active(self, config: api.env.Config, rt: api.env.Runtime) -> int:
44
+ return os.path.isfile("CMakeLists.txt") and os.path.isfile("CMakePresets.json")
45
+
46
+ def platform_dependencies(self):
47
+ return [f"cmake>={CMAKE_VERSION}"]
48
+
49
+ def dep_with_tool(self, tool: str):
50
+ return [f"cmake>={CMAKE_VERSION}", f"{tool}>={CMAKE_VERSION}"]
51
+
52
+
53
+ @step.register
54
+ class CMakeConfig(CMakeBase):
55
+ """Configures the project using ``preset`` config."""
56
+
57
+ def __init__(self):
58
+ super().__init__(name="CMake")
59
+
60
+ def is_active(self, config: env.Config, rt: env.Runtime) -> int:
61
+ return os.path.isfile("CMakeLists.txt") and os.path.isfile("CMakePresets.json")
62
+
63
+ def directories_to_remove(self, config: env.Config) -> List[str]:
64
+ return [f"build/{config.build_type}"]
65
+
66
+ def run(self, config: env.Config, rt: env.Runtime) -> int:
67
+ cmake_vars = cast(Dict[str, str], rt._cfg.get("cmake", {}).get("vars", {}))
68
+ defines: List[str] = []
69
+ for var in cmake_vars:
70
+ value = cmake_vars[var]
71
+
72
+ is_flag = value.startswith("?")
73
+ if is_flag:
74
+ value = value[1:]
75
+
76
+ if value.startswith("config:"):
77
+ value = value[len("config:")]
78
+ value = config.get_path(value)
79
+ elif value.startswith("runtime:"):
80
+ value = value[len("runtime:")]
81
+ value = getattr(rt, value, None)
82
+
83
+ if is_flag:
84
+ value = "ON" if value else "OFF"
85
+
86
+ defines.append(f"-D{var}={value}")
87
+
88
+ return rt.cmd(
89
+ "cmake",
90
+ "--preset",
91
+ f"{config.preset}-{config.build_generator}",
92
+ *defines,
93
+ )
94
+
95
+
96
+ @step.register()
97
+ class CMakeBuild(CMakeBase):
98
+ """Builds the project using ``preset`` config."""
99
+
100
+ def __init__(self):
101
+ super().__init__(name="Build", runs_after=["CMake"])
102
+
103
+ def run(self, config: env.Config, rt: env.Runtime) -> int:
104
+ return rt.cmd("cmake", "--build", "--preset", config.preset, "--parallel")
105
+
106
+
107
+ @step.register
108
+ class CMakeTest(CMakeBase):
109
+ """Runs tests in the project using ``preset`` config."""
110
+
111
+ def __init__(self):
112
+ super().__init__(name="Test", runs_after=["Build"])
113
+
114
+ def platform_dependencies(self):
115
+ return self.dep_with_tool("ctest")
116
+
117
+ def run(self, config: env.Config, rt: env.Runtime) -> int:
118
+ return rt.cmd("ctest", "--preset", config.preset)
119
+
120
+
121
+ @step.register
122
+ class PackStep(CMakeBase):
123
+ """
124
+ Packs archives and installers from ``cpack_generator`` config, using
125
+ ``preset`` config.
126
+ """
127
+
128
+ def __init__(self):
129
+ super().__init__(name="Pack", runs_after=["Build"])
130
+
131
+ def platform_dependencies(self):
132
+ return self.dep_with_tool("cpack")
133
+
134
+ def is_active(self, config: env.Config, rt: env.Runtime) -> int:
135
+ return (
136
+ super().is_active(config, rt)
137
+ and len(config.items.get("cpack_generator", [])) > 0
138
+ )
139
+
140
+ def run(self, config: env.Config, rt: env.Runtime) -> int:
141
+ generators = ";".join(config.items.get("cpack_generator", []))
142
+ return rt.cmd("cpack", "--preset", config.preset, "-G", generators)
@@ -0,0 +1,35 @@
1
+ # Copyright (c) 2025 Marcin Zdun
2
+ # This code is licensed under MIT license (see LICENSE for details)
3
+
4
+ """
5
+ The **proj_flow.ext.cplusplus.cmake.version** provides project suite plugin.
6
+ """
7
+
8
+ import os
9
+ import re
10
+ from typing import NamedTuple, Optional
11
+
12
+ import toml
13
+
14
+ from proj_flow.api import env, release
15
+ from proj_flow.ext.cplusplus.cmake.parser import get_project
16
+
17
+
18
+ class QuickProjectInfo(NamedTuple):
19
+ name: Optional[str] = None
20
+ path: Optional[str] = None
21
+ pattern: Optional[str] = None
22
+
23
+
24
+ @release.project_suites.add
25
+ class ProjectSuite(release.ProjectSuite):
26
+ def get_project(self, rt: env.Runtime) -> Optional[release.Project]:
27
+ project = get_project(rt.root)
28
+ if project is None:
29
+ return None
30
+ return release.Project(
31
+ project.name.value, release.Version(project.version, project.stability)
32
+ )
33
+
34
+ def get_version_file_path(self, rt: env.Runtime) -> Optional[str]:
35
+ return "CMakeLists.txt"
@@ -2,14 +2,15 @@
2
2
  # This code is licensed under MIT license (see LICENSE for details)
3
3
 
4
4
  """
5
- The **proj_flow.plugins.conan** provides the ``"Conan"`` step.
5
+ The **proj_flow.ext.cplusplus.conan** provides the ``"Conan"`` step.
6
6
  """
7
7
 
8
8
  import os
9
9
  import textwrap
10
10
  from typing import List
11
11
 
12
- from proj_flow.api import ctx, env, step
12
+ from proj_flow.api import env, step
13
+ from proj_flow.project import cplusplus
13
14
 
14
15
  from ._conan import conan_api
15
16
 
@@ -23,6 +24,7 @@ class ConanConfig:
23
24
  """Configures the project for ``preset`` config using ``build_type`` config."""
24
25
 
25
26
  name = "Conan"
27
+ runs_before = ["CMake"]
26
28
 
27
29
  def platform_dependencies(self):
28
30
  return ["conan"]
@@ -63,4 +65,6 @@ class ConanConfig:
63
65
  return 0
64
66
 
65
67
 
66
- ctx.register_switch("with_conan", "Use Conan for dependency manager", True)
68
+ cplusplus.project.register_switch(
69
+ "with_conan", "Use Conan for dependency manager", True
70
+ )
@@ -2,16 +2,18 @@
2
2
  # This code is licensed under MIT license (see LICENSE for details)
3
3
 
4
4
  """
5
- The **proj_flow.plugins.conan._conan** adds support for both Conan v1 and v2.
5
+ The **proj_flow.ext.cplusplus.conan._conan** adds support for both Conan v1
6
+ and v2.
6
7
  """
7
8
 
8
9
  import shutil
10
+ import subprocess
9
11
  from abc import ABC, abstractmethod
10
12
  from typing import Callable, List, cast
11
13
 
12
14
  from proj_flow.api.env import Config, Runtime
13
15
  from proj_flow.base import cmd
14
- from proj_flow.flow.dependency import VER_REGEX
16
+ from proj_flow.dependency import VER_REGEX
15
17
 
16
18
 
17
19
  class conan(ABC):
@@ -119,7 +121,10 @@ def _conan_version():
119
121
  if found is None:
120
122
  return 1
121
123
 
122
- proc = cmd.run(found, "--version", capture_output=True)
124
+ proc = cast(
125
+ subprocess.CompletedProcess[str],
126
+ cmd.run(found, "--version", capture_output=True),
127
+ )
123
128
  if proc.returncode != 0:
124
129
  return 1
125
130
  m = VER_REGEX.search(proc.stdout)
@@ -6,6 +6,6 @@ The **proj_flow.ext.github** provides GitHub support through CLI and GitHub
6
6
  hosting plugin.
7
7
  """
8
8
 
9
- from . import cli, hosting
9
+ from . import cli, hosting, switches
10
10
 
11
- __all__ = ["cli", "hosting"]
11
+ __all__ = ["cli", "hosting", "switches"]
@@ -16,22 +16,13 @@ import typing
16
16
 
17
17
  from proj_flow import log
18
18
  from proj_flow.api import arg, env
19
+ from proj_flow.base.name_list import name_list
19
20
  from proj_flow.flow.configs import Configs
20
21
  from proj_flow.log import commit, hosting, rich_text
21
22
 
22
23
  FORCED_LEVEL_CHOICES = list(commit.FORCED_LEVEL.keys())
23
24
 
24
25
 
25
- def _name_list(names: typing.List[str]) -> str:
26
- if len(names) == 0:
27
- return ""
28
-
29
- prefix = ", ".join(names[:-1])
30
- if prefix:
31
- prefix += " and "
32
- return f"{prefix}{names[-1]}"
33
-
34
-
35
26
  @arg.command("github")
36
27
  def github():
37
28
  """Interact with GitHub workflows and releases"""
@@ -81,7 +72,7 @@ def release(
81
72
  typing.Optional[str],
82
73
  arg.Argument(
83
74
  help="Ignore the version change from changelog and instead use this value. "
84
- f"Allowed values are: {_name_list(FORCED_LEVEL_CHOICES)}",
75
+ f"Allowed values are: {name_list(FORCED_LEVEL_CHOICES)}",
85
76
  meta="level",
86
77
  choices=FORCED_LEVEL_CHOICES,
87
78
  ),
@@ -1,13 +1,13 @@
1
1
  # Copyright (c) 2025 Marcin Zdun
2
2
  # This code is licensed under MIT license (see LICENSE for details)
3
3
  """
4
- The **proj_flow.plugins.github** provides GitHub-related switches for new
4
+ The **proj_flow.ext.github.switches** provides GitHub-related switches for new
5
5
  projects.
6
6
  """
7
7
 
8
8
  from proj_flow.api import ctx
9
9
 
10
- ctx.register_switch("with_github_actions", "Use Github Actions", True)
11
- ctx.register_switch(
10
+ ctx.register_common_switch("with_github_actions", "Use Github Actions", True)
11
+ ctx.register_common_switch(
12
12
  "with_github_social", "Use Github ISSUE_TEMPLATE, CONTRIBUTING.md, etc.", True
13
13
  )
@@ -2,7 +2,8 @@
2
2
  # This code is licensed under MIT license (see LICENSE for details)
3
3
 
4
4
  """
5
- The **proj_flow.ext.markdown_changelist** .
5
+ The **proj_flow.ext.markdown_changelog** registers a
6
+ :class:`api.ChangelogGenerator` plugin implemented for markdown changelogs.
6
7
  """
7
8
 
8
9
  from proj_flow.log.rich_text.api import changelog_generators
@@ -2,7 +2,7 @@
2
2
  # This code is licensed under MIT license (see LICENSE for details)
3
3
 
4
4
  """
5
- The **proj_flow.ext.python.steps.rtdocs** defines RTDocs step, which uses
5
+ The **proj_flow.ext.python.rtdocs** defines RTDocs step (`"RTD"`), which uses
6
6
  .readthedocs.yaml to build the HTML documentation.
7
7
  """
8
8
 
@@ -2,8 +2,7 @@
2
2
  # This code is licensed under MIT license (see LICENSE for details)
3
3
 
4
4
  """
5
- The **proj_flow.ext.python.steps** defines steps for building, installing and
6
- documenting.
5
+ The **proj_flow.ext.python.version** provides project suite plugin.
7
6
  """
8
7
 
9
8
  import os
@@ -2,7 +2,9 @@
2
2
  # This code is licensed under MIT license (see LICENSE for details)
3
3
 
4
4
  """
5
- The **proj_flow.ext.re_structured_text** .
5
+ The **proj_flow.ext.re_structured_changelog** registers a
6
+ :class:`api.ChangelogGenerator` plugin implemented for reStructuredText
7
+ changelogs.
6
8
  """
7
9
 
8
10
  from proj_flow.log.rich_text.api import changelog_generators
@@ -2,7 +2,7 @@
2
2
  # This code is licensed under MIT license (see LICENSE for details)
3
3
 
4
4
  """
5
- The **proj_flow.plugins.sign** provides the ``"Sign"`` and ``"SignPackages"``
5
+ The **proj_flow.ext.sign** provides the ``"Sign"`` and ``"SignPackages"``
6
6
  steps.
7
7
  """
8
8
 
@@ -14,23 +14,7 @@ from typing import List, cast
14
14
 
15
15
  from proj_flow.api import env, init, step
16
16
 
17
- if sys.platform == "win32":
18
- from . import win32
19
-
20
- else:
21
-
22
- class win32:
23
- @staticmethod
24
- def is_active(*args):
25
- return False
26
-
27
- @staticmethod
28
- def sign(*args):
29
- return 0
30
-
31
- @staticmethod
32
- def is_pe_exec(arg):
33
- return False
17
+ from . import api, win32
34
18
 
35
19
 
36
20
  def should_exclude(filename: str, exclude: List[str], config_os: str):
@@ -44,34 +28,69 @@ def should_exclude(filename: str, exclude: List[str], config_os: str):
44
28
 
45
29
 
46
30
  class SignBase(step.Step):
31
+ _name: str
32
+ _runs_after: List[str] = []
33
+ _runs_before: List[str] = []
34
+
35
+ _active_tools: List[api.SigningTool] = []
36
+
37
+ @property
38
+ def name(self):
39
+ return self._name
40
+
41
+ @property
42
+ def runs_after(self):
43
+ return self._runs_after
44
+
45
+ @property
46
+ def runs_before(self):
47
+ return self._runs_before
48
+
49
+ def __init__(
50
+ self, name: str, runs_after: List[str] = [], runs_before: List[str] = []
51
+ ):
52
+ super().__init__()
53
+ self._name = name
54
+ self._runs_after = runs_after
55
+ self._runs_before = runs_before
56
+
47
57
  def is_active(self, config: env.Config, rt: env.Runtime) -> int:
48
- return win32.is_active(config.os, rt)
58
+ self._active_tools = [
59
+ tool for tool in api.signing_tool.get() if tool.is_active(config, rt)
60
+ ]
61
+ return len(self._active_tools) > 0
49
62
 
50
63
  @abstractmethod
51
- def get_files(self, config: env.Config, rt: env.Runtime) -> List[str]: ...
64
+ def get_files(
65
+ self, tool: api.SigningTool, config: env.Config, rt: env.Runtime
66
+ ) -> List[str]: ...
52
67
 
53
68
  def run(self, config: env.Config, rt: env.Runtime) -> int:
54
- files = [file.replace(os.sep, "/") for file in self.get_files(config, rt)]
55
- if len(files) == 0:
56
- return 0
69
+ for tool in self._active_tools:
70
+ files = [
71
+ file.replace(os.sep, "/") for file in self.get_files(tool, config, rt)
72
+ ]
57
73
 
58
- rt.print("signtool", *(os.path.basename(file) for file in files))
74
+ if len(files) == 0:
75
+ continue
59
76
 
60
- if rt.dry_run:
61
- return 0
77
+ result = tool.sign(config, rt, files)
78
+ if result:
79
+ return result
62
80
 
63
- return win32.sign(files, rt)
81
+ return 0
64
82
 
65
83
 
66
84
  @step.register
67
85
  class SignFiles(SignBase):
68
86
  """*(Windows)* Signs executable files in build directory"""
69
87
 
70
- name = "Sign"
71
- runs_after = ["Build"]
72
- runs_before = ["Pack"]
88
+ def __init__(self):
89
+ super().__init__(name="Sign", runs_after=["Build"], runs_before=["Pack"])
73
90
 
74
- def get_files(self, config: env.Config, rt: env.Runtime) -> List[str]:
91
+ def get_files(
92
+ self, tool: api.SigningTool, config: env.Config, rt: env.Runtime
93
+ ) -> List[str]:
75
94
  cfg = cast(dict, rt._cfg.get("sign", {}))
76
95
  roots = cfg.get("directories", ["bin", "lib", "libexec", "share"])
77
96
  exclude = cfg.get("exclude", ["*-test"])
@@ -85,10 +104,8 @@ class SignFiles(SignBase):
85
104
  continue
86
105
 
87
106
  full_path = os.path.join(curr_dir, filename)
88
- if not win32.is_pe_exec(full_path):
89
- continue
90
-
91
- result.append(full_path)
107
+ if tool.is_executable(full_path, as_package=False):
108
+ result.append(full_path)
92
109
  return result
93
110
 
94
111
 
@@ -96,26 +113,29 @@ class SignFiles(SignBase):
96
113
  class SignMsi(SignBase):
97
114
  """*(Windows)* Signs MSI installers in build directory"""
98
115
 
99
- name = "SignPackages"
100
- runs_after = ["Pack"]
101
- runs_before = ["StorePackages", "Store"]
116
+ def __init__(self):
117
+ super().__init__(
118
+ name="SignPackages",
119
+ runs_after=["Pack"],
120
+ runs_before=["StorePackages", "Store"],
121
+ )
102
122
 
103
123
  def is_active(self, config: env.Config, rt: env.Runtime) -> int:
104
124
  return super().is_active(config, rt) and "WIX" in config.items.get(
105
125
  "cpack_generator", []
106
126
  )
107
127
 
108
- def get_files(self, config: env.Config, rt: env.Runtime) -> List[str]:
128
+ def get_files(
129
+ self, tool: api.SigningTool, config: env.Config, rt: env.Runtime
130
+ ) -> List[str]:
109
131
  result: List[str] = []
110
132
  pkg_dir = os.path.join(config.build_dir, "packages")
111
133
  for curr_dir, dirnames, filenames in os.walk(pkg_dir):
112
134
  dirnames[:] = []
113
135
  for filename in filenames:
114
- _, ext = os.path.splitext(filename)
115
- if ext.lower() != ".msi":
116
- continue
117
-
118
- result.append(os.path.join(curr_dir, filename))
136
+ full_path = os.path.join(curr_dir, filename)
137
+ if tool.is_executable(full_path, as_package=False):
138
+ result.append(full_path)
119
139
 
120
140
  return result
121
141
 
@@ -0,0 +1,83 @@
1
+ # Copyright (c) 2025 Marcin Zdun
2
+ # This code is licensed under MIT license (see LICENSE for details)
3
+
4
+ """
5
+ The **proj_flow.ext.sign.api** defines an extension point for per-platform
6
+ sign tools.
7
+ """
8
+
9
+ import base64
10
+ import json
11
+ import os
12
+ from abc import ABC, abstractmethod
13
+ from typing import List, NamedTuple, Optional
14
+
15
+ from proj_flow import base
16
+ from proj_flow.api import env
17
+
18
+ ENV_KEY = "SIGN_TOKEN"
19
+
20
+
21
+ class Key(NamedTuple):
22
+ token: str
23
+ secret: bytes
24
+
25
+
26
+ def _get_key_from_contents(key: str, rt: env.Runtime):
27
+ try:
28
+ obj = json.loads(key)
29
+ except json.decoder.JSONDecodeError:
30
+ rt.message("sign: the signature is not a valid JSON document")
31
+ return None
32
+
33
+ if not isinstance(obj, dict):
34
+ rt.message("sign: the signature is missing required fields")
35
+ return None
36
+
37
+ token = obj.get("token")
38
+ secret = obj.get("secret")
39
+ if not isinstance(token, str) or not isinstance(secret, str):
40
+ rt.message("sign: the signature is missing required fields")
41
+ return None
42
+
43
+ return Key(
44
+ base64.b64decode(token).decode("UTF-8"),
45
+ base64.b64decode(secret),
46
+ )
47
+
48
+
49
+ def get_key(rt: env.Runtime) -> Optional[Key]:
50
+ rt.message(f"sign: trying ${ENV_KEY}")
51
+ env = os.environ.get(ENV_KEY)
52
+ if env:
53
+ key = _get_key_from_contents(env, rt)
54
+ if key is not None:
55
+ return key
56
+ local_signature = os.path.join(".", "signature.key")
57
+ home_signature = os.path.join(os.path.expanduser("~"), "signature.key")
58
+ for filename in [local_signature, home_signature]:
59
+ rt.message(f"sign: trying {filename}")
60
+ if os.path.isfile(filename):
61
+ with open(filename, encoding="UTF-8") as file:
62
+ result = file.read().strip()
63
+ key = _get_key_from_contents(result, rt)
64
+ if key is not None:
65
+ return key
66
+
67
+ rt.message("sign: no key set up")
68
+
69
+ return None
70
+
71
+
72
+ class SigningTool(ABC):
73
+ @abstractmethod
74
+ def is_active(self, config: env.Config, rt: env.Runtime): ...
75
+
76
+ @abstractmethod
77
+ def sign(self, config: env.Config, rt: env.Runtime, files: List[str]): ...
78
+
79
+ @abstractmethod
80
+ def is_executable(self, filename: str, as_package: bool): ...
81
+
82
+
83
+ signing_tool = base.registry.Registry[SigningTool]("SigningTool")