saltext.github 1.1.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 (73) hide show
  1. saltext/github/__init__.py +31 -0
  2. saltext/github/modules/__init__.py +0 -0
  3. saltext/github/modules/github.py +1873 -0
  4. saltext/github/modules/github_org.py +667 -0
  5. saltext/github/modules/github_repo.py +1658 -0
  6. saltext/github/modules/github_user.py +75 -0
  7. saltext/github/resources/__init__.py +0 -0
  8. saltext/github/resources/github/__init__.py +251 -0
  9. saltext/github/resources/github/modules/__init__.py +0 -0
  10. saltext/github/resources/github/modules/actions.py +175 -0
  11. saltext/github/resources/github/modules/config.py +69 -0
  12. saltext/github/resources/github/modules/deploy_key.py +122 -0
  13. saltext/github/resources/github/modules/environment.py +241 -0
  14. saltext/github/resources/github/modules/grains.py +18 -0
  15. saltext/github/resources/github/modules/issue.py +157 -0
  16. saltext/github/resources/github/modules/label.py +161 -0
  17. saltext/github/resources/github/modules/milestone.py +103 -0
  18. saltext/github/resources/github/modules/pages.py +149 -0
  19. saltext/github/resources/github/modules/pull.py +90 -0
  20. saltext/github/resources/github/modules/repo.py +155 -0
  21. saltext/github/resources/github/modules/ruleset.py +144 -0
  22. saltext/github/resources/github/modules/saltutil.py +24 -0
  23. saltext/github/resources/github/modules/state.py +116 -0
  24. saltext/github/resources/github/modules/test.py +29 -0
  25. saltext/github/resources/github/states/__init__.py +0 -0
  26. saltext/github/resources/github/states/actions.py +117 -0
  27. saltext/github/resources/github/states/deploy_key.py +130 -0
  28. saltext/github/resources/github/states/environment.py +99 -0
  29. saltext/github/resources/github/states/label.py +71 -0
  30. saltext/github/resources/github/states/pages.py +92 -0
  31. saltext/github/resources/github/states/repo.py +108 -0
  32. saltext/github/resources/github/states/ruleset.py +81 -0
  33. saltext/github/states/__init__.py +0 -0
  34. saltext/github/states/github.py +966 -0
  35. saltext/github/states/github_org.py +369 -0
  36. saltext/github/states/github_repo.py +722 -0
  37. saltext/github/utils/__init__.py +72 -0
  38. saltext/github/utils/gh/__init__.py +14 -0
  39. saltext/github/utils/gh/actions.py +195 -0
  40. saltext/github/utils/gh/cache.py +126 -0
  41. saltext/github/utils/gh/client.py +534 -0
  42. saltext/github/utils/gh/deploy_keys.py +121 -0
  43. saltext/github/utils/gh/environments.py +271 -0
  44. saltext/github/utils/gh/exceptions.py +28 -0
  45. saltext/github/utils/gh/grains_files.py +366 -0
  46. saltext/github/utils/gh/issues.py +193 -0
  47. saltext/github/utils/gh/labels.py +142 -0
  48. saltext/github/utils/gh/members.py +157 -0
  49. saltext/github/utils/gh/milestones.py +122 -0
  50. saltext/github/utils/gh/pages.py +128 -0
  51. saltext/github/utils/gh/profile.py +458 -0
  52. saltext/github/utils/gh/pulls.py +99 -0
  53. saltext/github/utils/gh/repos.py +465 -0
  54. saltext/github/utils/gh/resource.py +35 -0
  55. saltext/github/utils/gh/rulesets.py +191 -0
  56. saltext/github/utils/gh/statecores/__init__.py +70 -0
  57. saltext/github/utils/gh/statecores/actions.py +180 -0
  58. saltext/github/utils/gh/statecores/deploy_keys.py +241 -0
  59. saltext/github/utils/gh/statecores/environments.py +212 -0
  60. saltext/github/utils/gh/statecores/labels.py +116 -0
  61. saltext/github/utils/gh/statecores/pages.py +177 -0
  62. saltext/github/utils/gh/statecores/repo.py +249 -0
  63. saltext/github/utils/gh/statecores/rulesets.py +115 -0
  64. saltext/github/utils/gh/teams.py +375 -0
  65. saltext/github/utils/gh/users.py +87 -0
  66. saltext/github/version.py +1 -0
  67. saltext_github-1.1.0.dist-info/METADATA +151 -0
  68. saltext_github-1.1.0.dist-info/RECORD +73 -0
  69. saltext_github-1.1.0.dist-info/WHEEL +5 -0
  70. saltext_github-1.1.0.dist-info/entry_points.txt +2 -0
  71. saltext_github-1.1.0.dist-info/licenses/LICENSE +201 -0
  72. saltext_github-1.1.0.dist-info/licenses/NOTICE +10 -0
  73. saltext_github-1.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,72 @@
1
+ import typing
2
+ from collections.abc import Mapping
3
+ from types import EllipsisType
4
+
5
+ from salt.exceptions import CommandExecutionError
6
+
7
+ K = typing.TypeVar("K")
8
+ V = typing.TypeVar("V")
9
+
10
+
11
+ @typing.overload
12
+ def filter_unset(
13
+ data: Mapping[K, V | None],
14
+ unset: None = None,
15
+ ) -> dict[K, V]: ...
16
+ @typing.overload
17
+ def filter_unset(
18
+ data: Mapping[K, V | EllipsisType],
19
+ unset: EllipsisType,
20
+ ) -> dict[K, V]: ...
21
+ @typing.overload
22
+ def filter_unset(
23
+ data: Mapping[K, V],
24
+ unset: object,
25
+ ) -> dict[K, V]: ...
26
+ def filter_unset(
27
+ data: Mapping[K, V],
28
+ unset: object = None,
29
+ ) -> dict[K, object]:
30
+ """
31
+ Remove all unset values in a dictionary and return the filtered result.
32
+
33
+ data
34
+ Dictionary to filter.
35
+
36
+ unset
37
+ Identity of what counts as unset; evaluated via ``is not``.
38
+ Defaults to ``None``. Other working examples: ``...`` (ellipsis),
39
+ a specific object such as ``salt.defaults.NOT_SET`` or
40
+ a string literal (not recommended though).
41
+ """
42
+ return {k: v for k, v in data.items() if v is not unset}
43
+
44
+
45
+ def filter_fields(data, fields):
46
+ """
47
+ Reduce the entities in a collection to a subset of their fields.
48
+
49
+ data
50
+ List of mappings or mapping of key to mapping.
51
+
52
+ fields
53
+ Single field name or list of field names. A list reduces each
54
+ entity to a mapping of the listed fields, a single name to the
55
+ field's value. ``None`` (the default) returns ``data`` unchanged.
56
+ """
57
+ if fields is None:
58
+ return data
59
+
60
+ def _reduce(entity):
61
+ try:
62
+ if isinstance(fields, str):
63
+ return entity[fields]
64
+ return {field: entity[field] for field in fields}
65
+ except KeyError as err:
66
+ raise CommandExecutionError(
67
+ f"Unknown field {err}. Available fields: {', '.join(entity)}"
68
+ ) from None
69
+
70
+ if isinstance(data, Mapping):
71
+ return {key: _reduce(val) for key, val in data.items()}
72
+ return [_reduce(entity) for entity in data]
@@ -0,0 +1,14 @@
1
+ """
2
+ Connection library for GitHub.
3
+
4
+ The submodules of this package provide reusable primitives for talking
5
+ to the GitHub API on behalf of an organization, built around
6
+ :class:`~saltext.github.utils.gh.client.GitHubConnection`.
7
+ They raise :class:`~saltext.github.utils.gh.exceptions.GitHubApiError`
8
+ (or its more specific subclass
9
+ :class:`~saltext.github.utils.gh.exceptions.GitHubNotFoundError`)
10
+ on API errors.
11
+ """
12
+
13
+ from saltext.github.utils.gh.exceptions import GitHubApiError # noqa: F401
14
+ from saltext.github.utils.gh.exceptions import GitHubNotFoundError # noqa: F401
@@ -0,0 +1,195 @@
1
+ """
2
+ Operations on GitHub Actions repository secrets and settings.
3
+ """
4
+
5
+ import base64
6
+ import logging
7
+
8
+ from nacl import encoding
9
+ from nacl import public
10
+
11
+ from saltext.github.utils import filter_unset
12
+
13
+ log = logging.getLogger(__name__)
14
+
15
+
16
+ def list_secrets(conn, repo_name, *, owner=None):
17
+ """
18
+ Return all Actions secrets of a repository. Secret values are write-only,
19
+ so this only carries metadata (``name``, ``created_at``, ``updated_at``).
20
+
21
+ conn
22
+ :class:`~saltext.github.utils.gh.client.GitHubConnection` to use.
23
+
24
+ repo_name
25
+ Repository name.
26
+
27
+ owner
28
+ Owner of the repository. Defaults to the connection's owner.
29
+ """
30
+ return conn.repo_query(owner, repo_name, "actions", "secrets", per_page=100)["secrets"]
31
+
32
+
33
+ def get_secret(conn, repo_name, name, *, owner=None):
34
+ """
35
+ Return the metadata of a single Actions secret.
36
+
37
+ conn
38
+ :class:`~saltext.github.utils.gh.client.GitHubConnection` to use.
39
+
40
+ repo_name
41
+ Repository name.
42
+
43
+ name
44
+ Secret name.
45
+
46
+ owner
47
+ Owner of the repository. Defaults to the connection's owner.
48
+
49
+ Raises :class:`~saltext.github.utils.gh.exceptions.GitHubNotFoundError`
50
+ if the secret does not exist.
51
+ """
52
+ return conn.repo_query(owner, repo_name, "actions", "secrets", name)
53
+
54
+
55
+ def get_public_key(conn, repo_name, *, owner=None):
56
+ """
57
+ Return the public key used for encrypting Actions secrets of a repository,
58
+ a mapping with ``key_id`` and (base64-encoded) ``key``.
59
+
60
+ conn
61
+ :class:`~saltext.github.utils.gh.client.GitHubConnection` to use.
62
+
63
+ repo_name
64
+ Repository name.
65
+
66
+ owner
67
+ Owner of the repository. Defaults to the connection's owner.
68
+ """
69
+ return conn.repo_query(owner, repo_name, "actions", "secrets", "public-key")
70
+
71
+
72
+ def encrypt_secret(public_key, value):
73
+ """
74
+ Encrypt a secret value for upload using a repository public key
75
+ (libsodium sealed box). Returns the base64-encoded ciphertext.
76
+
77
+ public_key
78
+ Base64-encoded public key of the repository, as returned
79
+ by :func:`get_public_key` in ``key``.
80
+
81
+ value
82
+ Secret value to encrypt.
83
+ """
84
+ sealed_box = public.SealedBox(public.PublicKey(public_key.encode(), encoding.Base64Encoder))
85
+ return base64.b64encode(sealed_box.encrypt(value.encode())).decode()
86
+
87
+
88
+ def set_secret(conn, repo_name, name, value, *, owner=None):
89
+ """
90
+ Create or update an Actions secret. The value is encrypted to the
91
+ repository public key before leaving this host.
92
+
93
+ conn
94
+ :class:`~saltext.github.utils.gh.client.GitHubConnection` to use.
95
+
96
+ repo_name
97
+ Repository name.
98
+
99
+ name
100
+ Secret name.
101
+
102
+ value
103
+ Plaintext secret value.
104
+
105
+ owner
106
+ Owner of the repository. Defaults to the connection's owner.
107
+ """
108
+ pubkey = get_public_key(conn, repo_name, owner=owner)
109
+ conn.repo_query(
110
+ owner,
111
+ *(repo_name, "actions", "secrets", name),
112
+ method="PUT",
113
+ data={
114
+ "encrypted_value": encrypt_secret(pubkey["key"], value),
115
+ "key_id": pubkey["key_id"],
116
+ },
117
+ )
118
+
119
+
120
+ def delete_secret(conn, repo_name, name, *, owner=None):
121
+ """
122
+ Remove an Actions secret from a repository.
123
+
124
+ conn
125
+ :class:`~saltext.github.utils.gh.client.GitHubConnection` to use.
126
+
127
+ repo_name
128
+ Repository name.
129
+
130
+ name
131
+ Secret name.
132
+
133
+ owner
134
+ Owner of the repository. Defaults to the connection's owner.
135
+ """
136
+ conn.repo_query(owner, repo_name, "actions", "secrets", name, method="DELETE")
137
+
138
+
139
+ def get_workflow_permissions(conn, repo_name, *, owner=None):
140
+ """
141
+ Return the default workflow permissions granted to the ``GITHUB_TOKEN``
142
+ in a repository, a mapping with ``default_workflow_permissions``
143
+ (``read``/``write``) and ``can_approve_pull_request_reviews``.
144
+
145
+ conn
146
+ :class:`~saltext.github.utils.gh.client.GitHubConnection` to use.
147
+
148
+ repo_name
149
+ Repository name.
150
+
151
+ owner
152
+ Owner of the repository. Defaults to the connection's owner.
153
+ """
154
+ return conn.repo_query(owner, repo_name, "actions", "permissions", "workflow")
155
+
156
+
157
+ def set_workflow_permissions(
158
+ conn,
159
+ repo_name,
160
+ *,
161
+ default_workflow_permissions=None,
162
+ can_approve_pull_request_reviews=None,
163
+ owner=None,
164
+ ):
165
+ """
166
+ Update the default workflow permissions granted to the ``GITHUB_TOKEN``
167
+ in a repository.
168
+
169
+ conn
170
+ :class:`~saltext.github.utils.gh.client.GitHubConnection` to use.
171
+
172
+ repo_name
173
+ Repository name.
174
+
175
+ default_workflow_permissions
176
+ Default permissions for the ``GITHUB_TOKEN``, either ``read``
177
+ or ``write``.
178
+
179
+ can_approve_pull_request_reviews
180
+ Allow GitHub Actions to approve pull requests.
181
+
182
+ owner
183
+ Owner of the repository. Defaults to the connection's owner.
184
+ """
185
+ conn.repo_query(
186
+ owner,
187
+ *(repo_name, "actions", "permissions", "workflow"),
188
+ method="PUT",
189
+ data=filter_unset(
190
+ {
191
+ "default_workflow_permissions": default_workflow_permissions,
192
+ "can_approve_pull_request_reviews": can_approve_pull_request_reviews,
193
+ }
194
+ ),
195
+ )
@@ -0,0 +1,126 @@
1
+ """
2
+ Optional persistent cross-process cache for slow-changing GitHub API
3
+ data, backed by the minion's :py:mod:`salt.cache` (``cachedir``).
4
+
5
+ Attached to every profile-resolved
6
+ :class:`~saltext.github.utils.gh.client.GitHubConnection` as its
7
+ ``store`` unless the profile opts out via :ghconf:`cache`. Particularly
8
+ useful for ``salt-call --local`` setups, where every invocation starts
9
+ a fresh process and the in-memory caches never survive.
10
+
11
+ Two kinds of data are stored, namespaced by a token/API URL-derived
12
+ identity so rotated credentials or different endpoints never share
13
+ entries:
14
+
15
+ * Repository file contents (fetched for
16
+ :ghconf:`resource:grains_files`), validated against the repository's
17
+ ``pushed_at`` timestamp – entries are served until the repository
18
+ receives a new push. Negative lookups (missing files) are cached the
19
+ same way.
20
+ * Metadata that is stable for a given token/owner: account types
21
+ (organization vs user) and the token's user login. These are stored
22
+ indefinitely.
23
+
24
+ All storage errors degrade gracefully to cache misses.
25
+ """
26
+
27
+ import logging
28
+
29
+ log = logging.getLogger(__name__)
30
+
31
+ CACHE_ROOT = "saltext_github"
32
+ #: Sentinel for missing cache entries (``None`` is a valid cached value).
33
+ MISS = object()
34
+
35
+
36
+ class PersistentCache:
37
+ """
38
+ ``salt.cache``-backed store, see the module docstring.
39
+
40
+ opts
41
+ The minion configuration (``__opts__``), selecting the cache
42
+ backend and ``cachedir``.
43
+
44
+ ident
45
+ Identity string namespacing all entries, derived from the
46
+ connection's token and API URL
47
+ (:attr:`~saltext.github.utils.gh.client.GitHubConnection.ident`).
48
+ """
49
+
50
+ def __init__(self, opts, ident):
51
+ # Local import: salt.cache pulls in the loader machinery, which
52
+ # not all importers of this package need.
53
+ import salt.cache # pylint: disable=import-outside-toplevel
54
+
55
+ self._cache = salt.cache.factory(opts)
56
+ self._base = f"{CACHE_ROOT}/{ident}"
57
+
58
+ def _fetch(self, bank, key):
59
+ try:
60
+ return self._cache.fetch(f"{self._base}/{bank}", key)
61
+ except Exception as err: # pylint: disable=broad-except
62
+ log.warning("Failed reading from the persistent GitHub cache: %s", err)
63
+ return None
64
+
65
+ def _store(self, bank, key, value):
66
+ try:
67
+ self._cache.store(f"{self._base}/{bank}", key, value)
68
+ except Exception as err: # pylint: disable=broad-except
69
+ log.warning("Failed writing to the persistent GitHub cache: %s", err)
70
+
71
+ def get_meta(self, key):
72
+ """
73
+ Return a stored metadata value, or :data:`MISS`.
74
+ """
75
+ data = self._fetch("meta", key)
76
+ if isinstance(data, dict) and "value" in data:
77
+ return data["value"]
78
+ return MISS
79
+
80
+ def set_meta(self, key, value):
81
+ """
82
+ Store a metadata value.
83
+ """
84
+ self._store("meta", key, {"value": value})
85
+
86
+ def get_file(self, owner, repo, path, pushed_at):
87
+ """
88
+ Return a repository file's stored content (which is ``None`` for
89
+ cached negative lookups), or :data:`MISS` when there is no entry
90
+ or the stored entry does not match ``pushed_at``.
91
+ """
92
+ if not pushed_at:
93
+ return MISS
94
+ entry = self._fetch(f"files/{owner.lower()}", repo.lower())
95
+ if (
96
+ isinstance(entry, dict)
97
+ and entry.get("pushed_at") == pushed_at
98
+ and path in entry.get("files", {})
99
+ ):
100
+ return entry["files"][path]
101
+ return MISS
102
+
103
+ def set_file(self, owner, repo, path, content, pushed_at):
104
+ """
105
+ Store a repository file's content (``None`` for a negative
106
+ lookup) under the repository's current ``pushed_at``. Entries
107
+ stored for a different ``pushed_at`` are discarded.
108
+ """
109
+ if not pushed_at:
110
+ return
111
+ bank = f"files/{owner.lower()}"
112
+ key = repo.lower()
113
+ entry = self._fetch(bank, key)
114
+ if not isinstance(entry, dict) or entry.get("pushed_at") != pushed_at:
115
+ entry = {"pushed_at": pushed_at, "files": {}}
116
+ entry["files"][path] = content
117
+ self._store(bank, key, entry)
118
+
119
+ def flush_repo(self, owner, repo):
120
+ """
121
+ Drop all stored file contents of one repository.
122
+ """
123
+ try:
124
+ self._cache.flush(f"{self._base}/files/{owner.lower()}", repo.lower())
125
+ except Exception as err: # pylint: disable=broad-except
126
+ log.warning("Failed flushing the persistent GitHub cache: %s", err)