cmem-plugin-git 1.0.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.
- cmem_plugin_git/__init__.py +1 -0
- cmem_plugin_git/autocompletion.py +86 -0
- cmem_plugin_git/download.py +272 -0
- cmem_plugin_git/git-download.svg +2 -0
- cmem_plugin_git/git-list.svg +2 -0
- cmem_plugin_git/git-upload.svg +2 -0
- cmem_plugin_git/list.py +161 -0
- cmem_plugin_git/parameters.py +180 -0
- cmem_plugin_git/remote.py +774 -0
- cmem_plugin_git/upload.py +261 -0
- cmem_plugin_git-1.0.0.dist-info/METADATA +82 -0
- cmem_plugin_git-1.0.0.dist-info/RECORD +14 -0
- cmem_plugin_git-1.0.0.dist-info/WHEEL +4 -0
- cmem_plugin_git-1.0.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""What the three tasks have in common: parameters, schema and small helpers"""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import binascii
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from cmem_plugin_base.dataintegration.context import ExecutionContext
|
|
9
|
+
from cmem_plugin_base.dataintegration.description import PluginParameter
|
|
10
|
+
from cmem_plugin_base.dataintegration.entity import EntityPath, EntitySchema
|
|
11
|
+
from cmem_plugin_base.dataintegration.parameter.password import Password, PasswordParameterType
|
|
12
|
+
|
|
13
|
+
from cmem_plugin_git.autocompletion import FolderParameterType, RefParameterType
|
|
14
|
+
from cmem_plugin_git.remote import (
|
|
15
|
+
DEFAULT_AUTHOR,
|
|
16
|
+
DEFAULT_USERNAME,
|
|
17
|
+
GitRemote,
|
|
18
|
+
clean_identity_part,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
LIST_TYPE_URI = "https://vocab.eccenca.com/plugin/git/File"
|
|
22
|
+
LIST_PATHS = ["path", "name", "size", "mode", "blob_id", "commit_id", "url"]
|
|
23
|
+
|
|
24
|
+
URL_DESCRIPTION = (
|
|
25
|
+
"The HTTP(S) address of the repository. The address of its web page "
|
|
26
|
+
"works as well as its clone URL, with or without a trailing '.git'."
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def list_schema() -> EntitySchema:
|
|
31
|
+
"""Describe what the list task hands to the next task."""
|
|
32
|
+
return EntitySchema(
|
|
33
|
+
type_uri=LIST_TYPE_URI,
|
|
34
|
+
paths=[EntityPath(path=name) for name in LIST_PATHS],
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def secret(value: str | Password) -> str:
|
|
39
|
+
"""Read a parameter that may arrive as a password or as plain text."""
|
|
40
|
+
return value if isinstance(value, str) else value.decrypt()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def connection_parameters(
|
|
44
|
+
ref_label: str,
|
|
45
|
+
ref_description: str,
|
|
46
|
+
access_description: str,
|
|
47
|
+
url_description: str = URL_DESCRIPTION,
|
|
48
|
+
url_optional: bool = False,
|
|
49
|
+
) -> list[PluginParameter]:
|
|
50
|
+
"""Build the parameters that say which repository and which revision to use.
|
|
51
|
+
|
|
52
|
+
The repository URL is required everywhere except where a caller passes
|
|
53
|
+
``url_optional=True``, which also gives it an empty default so it can be
|
|
54
|
+
left blank.
|
|
55
|
+
"""
|
|
56
|
+
return [
|
|
57
|
+
PluginParameter(
|
|
58
|
+
name="url",
|
|
59
|
+
label="Repository URL",
|
|
60
|
+
description=url_description,
|
|
61
|
+
default_value="" if url_optional else None,
|
|
62
|
+
),
|
|
63
|
+
PluginParameter(
|
|
64
|
+
name="token",
|
|
65
|
+
label="Access token",
|
|
66
|
+
description=access_description,
|
|
67
|
+
param_type=PasswordParameterType(),
|
|
68
|
+
default_value="",
|
|
69
|
+
),
|
|
70
|
+
PluginParameter(
|
|
71
|
+
name="ref",
|
|
72
|
+
label=ref_label,
|
|
73
|
+
description=ref_description,
|
|
74
|
+
param_type=RefParameterType(),
|
|
75
|
+
default_value="",
|
|
76
|
+
),
|
|
77
|
+
PluginParameter(
|
|
78
|
+
name="username",
|
|
79
|
+
label="User name",
|
|
80
|
+
description="The user name sent with the access token. GitHub ignores it, GitLab "
|
|
81
|
+
"checks it, and the default is the value GitLab accepts for a repository token.",
|
|
82
|
+
default_value=DEFAULT_USERNAME,
|
|
83
|
+
advanced=True,
|
|
84
|
+
),
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def selection_parameters(note: str = "") -> list[PluginParameter]:
|
|
89
|
+
"""Build the parameters that say which files of the revision to work with.
|
|
90
|
+
|
|
91
|
+
``note`` is appended to every description, for a caller where these
|
|
92
|
+
parameters do not always apply - such as Download Git files, where they
|
|
93
|
+
are ignored whenever the input drives the selection instead.
|
|
94
|
+
"""
|
|
95
|
+
suffix = f" {note}" if note else ""
|
|
96
|
+
return [
|
|
97
|
+
PluginParameter(
|
|
98
|
+
name="path",
|
|
99
|
+
label="Folder",
|
|
100
|
+
description="The folder inside the repository to read from. "
|
|
101
|
+
"Leave it empty to start at the top of the repository." + suffix,
|
|
102
|
+
param_type=FolderParameterType(),
|
|
103
|
+
default_value="",
|
|
104
|
+
),
|
|
105
|
+
PluginParameter(
|
|
106
|
+
name="regex",
|
|
107
|
+
label="Regular expression",
|
|
108
|
+
description="A regular expression the file name has to match. "
|
|
109
|
+
"An empty expression matches every file." + suffix,
|
|
110
|
+
default_value="",
|
|
111
|
+
),
|
|
112
|
+
PluginParameter(
|
|
113
|
+
name="no_subfolder",
|
|
114
|
+
label="No subfolder",
|
|
115
|
+
description="If enabled, only files directly in the selected folder are used, "
|
|
116
|
+
"and its subfolders are left alone." + suffix,
|
|
117
|
+
default_value=False,
|
|
118
|
+
),
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def preview_of(paths: list[str], warnings: list[str], limit: int = 10) -> str:
|
|
123
|
+
"""Render what a preview action found, as Markdown."""
|
|
124
|
+
if not paths and not warnings:
|
|
125
|
+
return "No file matches this configuration."
|
|
126
|
+
lines = [f"Found {len(paths)} file(s)."]
|
|
127
|
+
if paths:
|
|
128
|
+
lines += ["", "| File |", "| ---- |"]
|
|
129
|
+
lines += [f"| `{path}` |" for path in paths[:limit]]
|
|
130
|
+
if len(paths) > limit:
|
|
131
|
+
lines.append(f"| ... and {len(paths) - limit} more |")
|
|
132
|
+
if warnings:
|
|
133
|
+
lines += ["", "Skipped:", ""]
|
|
134
|
+
lines += [f"- {warning}" for warning in warnings[:limit]]
|
|
135
|
+
return "\n".join(lines)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def remote_of(url: str, token: str | Password, username: str) -> GitRemote:
|
|
139
|
+
"""Build the remote a task talks to."""
|
|
140
|
+
return GitRemote(url=url, token=secret(token), username=username)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _claims(token: str) -> dict[str, Any]:
|
|
144
|
+
"""Read the payload of an access token
|
|
145
|
+
|
|
146
|
+
The token is the plugin's own credential rather than something it
|
|
147
|
+
authenticates, so the payload is decoded and its signature is not checked.
|
|
148
|
+
"""
|
|
149
|
+
parts = token.split(".")
|
|
150
|
+
expected_parts = 3
|
|
151
|
+
if len(parts) != expected_parts:
|
|
152
|
+
return {}
|
|
153
|
+
payload = parts[1]
|
|
154
|
+
payload += "=" * (-len(payload) % 4)
|
|
155
|
+
try:
|
|
156
|
+
decoded = json.loads(base64.urlsafe_b64decode(payload))
|
|
157
|
+
except (binascii.Error, ValueError, UnicodeDecodeError):
|
|
158
|
+
return {}
|
|
159
|
+
return decoded if isinstance(decoded, dict) else {}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def identity_of(context: ExecutionContext, name: str = "", mail: str = "") -> str:
|
|
163
|
+
"""Decide who authors a commit
|
|
164
|
+
|
|
165
|
+
Configured values win. Otherwise the executing user's name and mail address
|
|
166
|
+
are read from their access token, which a deployment may or may not put
|
|
167
|
+
there, and the package identity is used when they are not available.
|
|
168
|
+
"""
|
|
169
|
+
chosen_name = clean_identity_part(name)
|
|
170
|
+
chosen_mail = clean_identity_part(mail)
|
|
171
|
+
if not (chosen_name and chosen_mail):
|
|
172
|
+
user = getattr(context, "user", None)
|
|
173
|
+
claims = _claims(user.token()) if user is not None else {}
|
|
174
|
+
chosen_name = chosen_name or clean_identity_part(
|
|
175
|
+
str(claims.get("name") or claims.get("preferred_username") or "")
|
|
176
|
+
)
|
|
177
|
+
chosen_mail = chosen_mail or clean_identity_part(str(claims.get("email") or ""))
|
|
178
|
+
if chosen_name and chosen_mail:
|
|
179
|
+
return f"{chosen_name} <{chosen_mail}>"
|
|
180
|
+
return DEFAULT_AUTHOR
|