prefect-gitlab 0.3.1__tar.gz → 0.3.3__tar.gz
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.
- {prefect_gitlab-0.3.1/prefect_gitlab.egg-info → prefect_gitlab-0.3.3}/PKG-INFO +5 -19
- prefect_gitlab-0.3.3/prefect_gitlab/_version.py +34 -0
- prefect_gitlab-0.3.3/prefect_gitlab/credentials.py +91 -0
- {prefect_gitlab-0.3.1 → prefect_gitlab-0.3.3}/prefect_gitlab/repositories.py +50 -4
- {prefect_gitlab-0.3.1 → prefect_gitlab-0.3.3/prefect_gitlab.egg-info}/PKG-INFO +5 -19
- prefect_gitlab-0.3.3/prefect_gitlab.egg-info/requires.txt +3 -0
- {prefect_gitlab-0.3.1 → prefect_gitlab-0.3.3}/pyproject.toml +6 -3
- prefect_gitlab-0.3.3/tests/test_credentials.py +46 -0
- {prefect_gitlab-0.3.1 → prefect_gitlab-0.3.3}/tests/test_repositories.py +44 -3
- prefect_gitlab-0.3.1/prefect_gitlab/_version.py +0 -16
- prefect_gitlab-0.3.1/prefect_gitlab/credentials.py +0 -50
- prefect_gitlab-0.3.1/prefect_gitlab.egg-info/requires.txt +0 -19
- prefect_gitlab-0.3.1/tests/test_credentials.py +0 -17
- {prefect_gitlab-0.3.1 → prefect_gitlab-0.3.3}/LICENSE +0 -0
- {prefect_gitlab-0.3.1 → prefect_gitlab-0.3.3}/MANIFEST.in +0 -0
- {prefect_gitlab-0.3.1 → prefect_gitlab-0.3.3}/README.md +0 -0
- {prefect_gitlab-0.3.1 → prefect_gitlab-0.3.3}/prefect_gitlab/__init__.py +0 -0
- {prefect_gitlab-0.3.1 → prefect_gitlab-0.3.3}/prefect_gitlab.egg-info/SOURCES.txt +0 -0
- {prefect_gitlab-0.3.1 → prefect_gitlab-0.3.3}/prefect_gitlab.egg-info/dependency_links.txt +0 -0
- {prefect_gitlab-0.3.1 → prefect_gitlab-0.3.3}/prefect_gitlab.egg-info/entry_points.txt +0 -0
- {prefect_gitlab-0.3.1 → prefect_gitlab-0.3.3}/prefect_gitlab.egg-info/top_level.txt +0 -0
- {prefect_gitlab-0.3.1 → prefect_gitlab-0.3.3}/setup.cfg +0 -0
- {prefect_gitlab-0.3.1 → prefect_gitlab-0.3.3}/tests/conftest.py +0 -0
- {prefect_gitlab-0.3.1 → prefect_gitlab-0.3.3}/tests/test_version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: prefect-gitlab
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: A Prefect collection for working with GitLab repositories.
|
|
5
5
|
Author-email: "Prefect Technologies, Inc." <help@prefect.io>
|
|
6
6
|
License: Apache License 2.0
|
|
@@ -11,32 +11,18 @@ Classifier: Intended Audience :: Developers
|
|
|
11
11
|
Classifier: Intended Audience :: System Administrators
|
|
12
12
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
13
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.10
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
18
|
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
-
Requires-Python: >=3.
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
License-File: LICENSE
|
|
22
22
|
Requires-Dist: prefect>=3.0.0
|
|
23
23
|
Requires-Dist: python-gitlab>=3.12.0
|
|
24
24
|
Requires-Dist: tenacity>=8.2.3
|
|
25
|
-
|
|
26
|
-
Requires-Dist: aiohttp; extra == "dev"
|
|
27
|
-
Requires-Dist: coverage; extra == "dev"
|
|
28
|
-
Requires-Dist: interrogate; extra == "dev"
|
|
29
|
-
Requires-Dist: mkdocs-gen-files; extra == "dev"
|
|
30
|
-
Requires-Dist: mkdocs-material; extra == "dev"
|
|
31
|
-
Requires-Dist: mkdocs; extra == "dev"
|
|
32
|
-
Requires-Dist: mkdocstrings[python]; extra == "dev"
|
|
33
|
-
Requires-Dist: mypy; extra == "dev"
|
|
34
|
-
Requires-Dist: pillow; extra == "dev"
|
|
35
|
-
Requires-Dist: pre-commit; extra == "dev"
|
|
36
|
-
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
37
|
-
Requires-Dist: pytest>=8.3; extra == "dev"
|
|
38
|
-
Requires-Dist: pytest-env; extra == "dev"
|
|
39
|
-
Requires-Dist: pytest-xdist; extra == "dev"
|
|
25
|
+
Dynamic: license-file
|
|
40
26
|
|
|
41
27
|
# prefect-gitlab
|
|
42
28
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '0.3.3'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 3, 3)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = 'gd77914141'
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Module used to enable authenticated interactions with GitLab"""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from urllib.parse import urlparse, urlunparse
|
|
5
|
+
|
|
6
|
+
from gitlab import Gitlab
|
|
7
|
+
from pydantic import Field, SecretStr
|
|
8
|
+
|
|
9
|
+
from prefect.blocks.core import Block
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GitLabCredentials(Block):
|
|
13
|
+
"""
|
|
14
|
+
Store a GitLab personal access token to interact with private GitLab
|
|
15
|
+
repositories.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
token: The personal access token to authenticate with GitLab.
|
|
19
|
+
url: URL to self-hosted GitLab instances.
|
|
20
|
+
|
|
21
|
+
Examples:
|
|
22
|
+
Load stored GitLab credentials:
|
|
23
|
+
```python
|
|
24
|
+
from prefect_gitlab import GitLabCredentials
|
|
25
|
+
gitlab_credentials_block = GitLabCredentials.load("BLOCK_NAME")
|
|
26
|
+
```
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
_block_type_name = "GitLab Credentials"
|
|
30
|
+
_logo_url = "https://images.ctfassets.net/gm98wzqotmnx/55edIimT4g9gbjhkh5a3Sp/dfdb9391d8f45c2e93e72e3a4d350771/gitlab-logo-500.png?h=250"
|
|
31
|
+
|
|
32
|
+
token: Optional[SecretStr] = Field(
|
|
33
|
+
title="Personal Access Token",
|
|
34
|
+
default=None,
|
|
35
|
+
description="A GitLab Personal Access Token with read_repository scope.",
|
|
36
|
+
)
|
|
37
|
+
url: Optional[str] = Field(
|
|
38
|
+
default=None, title="URL", description="URL to self-hosted GitLab instances."
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def format_git_credentials(self, url: str) -> str:
|
|
42
|
+
"""
|
|
43
|
+
Format and return the full git URL with GitLab credentials embedded.
|
|
44
|
+
|
|
45
|
+
Handles both personal access tokens and deploy tokens correctly:
|
|
46
|
+
- Personal access tokens: prefixed with "oauth2:"
|
|
47
|
+
- Deploy tokens (username:token format): used as-is
|
|
48
|
+
- Already prefixed tokens: not double-prefixed
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
url: Repository URL (e.g., "https://gitlab.com/org/repo.git")
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Complete URL with credentials embedded
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
ValueError: If token is not configured
|
|
58
|
+
"""
|
|
59
|
+
if not self.token:
|
|
60
|
+
raise ValueError("Token is required for GitLab authentication")
|
|
61
|
+
|
|
62
|
+
token_value = self.token.get_secret_value()
|
|
63
|
+
|
|
64
|
+
# Deploy token detection: contains ":" but not "oauth2:" prefix
|
|
65
|
+
# Deploy tokens should not have oauth2: prefix (GitLab 16.3.4+ rejects them)
|
|
66
|
+
# See: https://github.com/PrefectHQ/prefect/issues/10832
|
|
67
|
+
if ":" in token_value and not token_value.startswith("oauth2:"):
|
|
68
|
+
credentials = token_value
|
|
69
|
+
# Personal access token: add oauth2: prefix
|
|
70
|
+
# See: https://github.com/PrefectHQ/prefect/issues/16836
|
|
71
|
+
elif not token_value.startswith("oauth2:"):
|
|
72
|
+
credentials = f"oauth2:{token_value}"
|
|
73
|
+
else:
|
|
74
|
+
# Already prefixed
|
|
75
|
+
credentials = token_value
|
|
76
|
+
|
|
77
|
+
# Insert credentials into URL
|
|
78
|
+
parsed = urlparse(url)
|
|
79
|
+
return urlunparse(parsed._replace(netloc=f"{credentials}@{parsed.netloc}"))
|
|
80
|
+
|
|
81
|
+
def get_client(self) -> Gitlab:
|
|
82
|
+
"""
|
|
83
|
+
Gets an authenticated GitLab client.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
An authenticated GitLab client.
|
|
87
|
+
"""
|
|
88
|
+
# ref: https://python-gitlab.readthedocs.io/en/stable/
|
|
89
|
+
gitlab = Gitlab(url=self.url, oauth_token=self.token.get_secret_value())
|
|
90
|
+
gitlab.auth()
|
|
91
|
+
return gitlab
|
|
@@ -43,6 +43,7 @@ Examples:
|
|
|
43
43
|
|
|
44
44
|
import io
|
|
45
45
|
import shutil
|
|
46
|
+
import subprocess
|
|
46
47
|
import urllib.parse
|
|
47
48
|
from pathlib import Path
|
|
48
49
|
from tempfile import TemporaryDirectory
|
|
@@ -51,8 +52,8 @@ from typing import Optional, Tuple, Union
|
|
|
51
52
|
from pydantic import Field
|
|
52
53
|
from tenacity import retry, stop_after_attempt, wait_fixed, wait_random
|
|
53
54
|
|
|
55
|
+
from prefect._internal.compatibility.async_dispatch import async_dispatch
|
|
54
56
|
from prefect.filesystems import ReadableDeploymentStorage
|
|
55
|
-
from prefect.utilities.asyncutils import sync_compatible
|
|
56
57
|
from prefect.utilities.processutils import run_process
|
|
57
58
|
from prefect_gitlab.credentials import GitLabCredentials
|
|
58
59
|
|
|
@@ -135,7 +136,6 @@ class GitLabRepository(ReadableDeploymentStorage):
|
|
|
135
136
|
|
|
136
137
|
return str(content_source), str(content_destination)
|
|
137
138
|
|
|
138
|
-
@sync_compatible
|
|
139
139
|
@retry(
|
|
140
140
|
stop=stop_after_attempt(MAX_CLONE_ATTEMPTS),
|
|
141
141
|
wait=wait_fixed(CLONE_RETRY_MIN_DELAY_SECONDS)
|
|
@@ -145,13 +145,14 @@ class GitLabRepository(ReadableDeploymentStorage):
|
|
|
145
145
|
),
|
|
146
146
|
reraise=True,
|
|
147
147
|
)
|
|
148
|
-
async def
|
|
148
|
+
async def aget_directory(
|
|
149
149
|
self, from_path: Optional[str] = None, local_path: Optional[str] = None
|
|
150
150
|
) -> None:
|
|
151
151
|
"""
|
|
152
152
|
Clones a GitLab project specified in `from_path` to the provided `local_path`;
|
|
153
153
|
defaults to cloning the repository reference configured on the Block to the
|
|
154
|
-
present working directory.
|
|
154
|
+
present working directory. Async version.
|
|
155
|
+
|
|
155
156
|
Args:
|
|
156
157
|
from_path: If provided, interpreted as a subdirectory of the underlying
|
|
157
158
|
repository that will be copied to the provided local path.
|
|
@@ -184,3 +185,48 @@ class GitLabRepository(ReadableDeploymentStorage):
|
|
|
184
185
|
shutil.copytree(
|
|
185
186
|
src=content_source, dst=content_destination, dirs_exist_ok=True
|
|
186
187
|
)
|
|
188
|
+
|
|
189
|
+
@retry(
|
|
190
|
+
stop=stop_after_attempt(MAX_CLONE_ATTEMPTS),
|
|
191
|
+
wait=wait_fixed(CLONE_RETRY_MIN_DELAY_SECONDS)
|
|
192
|
+
+ wait_random(
|
|
193
|
+
CLONE_RETRY_MIN_DELAY_JITTER_SECONDS,
|
|
194
|
+
CLONE_RETRY_MAX_DELAY_JITTER_SECONDS,
|
|
195
|
+
),
|
|
196
|
+
reraise=True,
|
|
197
|
+
)
|
|
198
|
+
@async_dispatch(aget_directory)
|
|
199
|
+
def get_directory(
|
|
200
|
+
self, from_path: Optional[str] = None, local_path: Optional[str] = None
|
|
201
|
+
) -> None:
|
|
202
|
+
"""
|
|
203
|
+
Clones a GitLab project specified in `from_path` to the provided `local_path`;
|
|
204
|
+
defaults to cloning the repository reference configured on the Block to the
|
|
205
|
+
present working directory.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
from_path: If provided, interpreted as a subdirectory of the underlying
|
|
209
|
+
repository that will be copied to the provided local path.
|
|
210
|
+
local_path: A local path to clone to; defaults to present working directory.
|
|
211
|
+
"""
|
|
212
|
+
cmd = ["git", "clone", self._create_repo_url()]
|
|
213
|
+
if self.reference:
|
|
214
|
+
cmd += ["-b", self.reference]
|
|
215
|
+
|
|
216
|
+
if self.git_depth is not None:
|
|
217
|
+
cmd += ["--depth", str(self.git_depth)]
|
|
218
|
+
|
|
219
|
+
with TemporaryDirectory(suffix="prefect") as tmp_dir:
|
|
220
|
+
cmd.append(tmp_dir)
|
|
221
|
+
|
|
222
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
223
|
+
if result.returncode != 0:
|
|
224
|
+
raise OSError(f"Failed to pull from remote:\n {result.stderr}")
|
|
225
|
+
|
|
226
|
+
content_source, content_destination = self._get_paths(
|
|
227
|
+
dst_dir=local_path, src_dir=tmp_dir, sub_directory=from_path
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
shutil.copytree(
|
|
231
|
+
src=content_source, dst=content_destination, dirs_exist_ok=True
|
|
232
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: prefect-gitlab
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: A Prefect collection for working with GitLab repositories.
|
|
5
5
|
Author-email: "Prefect Technologies, Inc." <help@prefect.io>
|
|
6
6
|
License: Apache License 2.0
|
|
@@ -11,32 +11,18 @@ Classifier: Intended Audience :: Developers
|
|
|
11
11
|
Classifier: Intended Audience :: System Administrators
|
|
12
12
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
13
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.10
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
18
|
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
-
Requires-Python: >=3.
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
License-File: LICENSE
|
|
22
22
|
Requires-Dist: prefect>=3.0.0
|
|
23
23
|
Requires-Dist: python-gitlab>=3.12.0
|
|
24
24
|
Requires-Dist: tenacity>=8.2.3
|
|
25
|
-
|
|
26
|
-
Requires-Dist: aiohttp; extra == "dev"
|
|
27
|
-
Requires-Dist: coverage; extra == "dev"
|
|
28
|
-
Requires-Dist: interrogate; extra == "dev"
|
|
29
|
-
Requires-Dist: mkdocs-gen-files; extra == "dev"
|
|
30
|
-
Requires-Dist: mkdocs-material; extra == "dev"
|
|
31
|
-
Requires-Dist: mkdocs; extra == "dev"
|
|
32
|
-
Requires-Dist: mkdocstrings[python]; extra == "dev"
|
|
33
|
-
Requires-Dist: mypy; extra == "dev"
|
|
34
|
-
Requires-Dist: pillow; extra == "dev"
|
|
35
|
-
Requires-Dist: pre-commit; extra == "dev"
|
|
36
|
-
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
37
|
-
Requires-Dist: pytest>=8.3; extra == "dev"
|
|
38
|
-
Requires-Dist: pytest-env; extra == "dev"
|
|
39
|
-
Requires-Dist: pytest-xdist; extra == "dev"
|
|
25
|
+
Dynamic: license-file
|
|
40
26
|
|
|
41
27
|
# prefect-gitlab
|
|
42
28
|
|
|
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
|
|
|
6
6
|
name = "prefect-gitlab"
|
|
7
7
|
description = "A Prefect collection for working with GitLab repositories."
|
|
8
8
|
readme = "README.md"
|
|
9
|
-
requires-python = ">=3.
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
10
|
license = { text = "Apache License 2.0" }
|
|
11
11
|
keywords = ["prefect"]
|
|
12
12
|
authors = [{ name = "Prefect Technologies, Inc.", email = "help@prefect.io" }]
|
|
@@ -16,16 +16,16 @@ classifiers = [
|
|
|
16
16
|
"Intended Audience :: System Administrators",
|
|
17
17
|
"License :: OSI Approved :: Apache Software License",
|
|
18
18
|
"Programming Language :: Python :: 3 :: Only",
|
|
19
|
-
"Programming Language :: Python :: 3.9",
|
|
20
19
|
"Programming Language :: Python :: 3.10",
|
|
21
20
|
"Programming Language :: Python :: 3.11",
|
|
22
21
|
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
23
|
"Topic :: Software Development :: Libraries",
|
|
24
24
|
]
|
|
25
25
|
dependencies = ["prefect>=3.0.0", "python-gitlab>=3.12.0", "tenacity>=8.2.3"]
|
|
26
26
|
dynamic = ["version"]
|
|
27
27
|
|
|
28
|
-
[
|
|
28
|
+
[dependency-groups]
|
|
29
29
|
dev = [
|
|
30
30
|
"aiohttp",
|
|
31
31
|
"coverage",
|
|
@@ -74,3 +74,6 @@ show_missing = true
|
|
|
74
74
|
asyncio_default_fixture_loop_scope = "session"
|
|
75
75
|
asyncio_mode = "auto"
|
|
76
76
|
env = ["PREFECT_TEST_MODE=1"]
|
|
77
|
+
|
|
78
|
+
[tool.uv.sources]
|
|
79
|
+
prefect = { path = "../../../" }
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from unittest.mock import MagicMock
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from prefect_gitlab.credentials import GitLabCredentials
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_gitlab_credentials_get_client(monkeypatch):
|
|
8
|
+
mock_gitlab = MagicMock()
|
|
9
|
+
monkeypatch.setattr("prefect_gitlab.credentials.Gitlab", mock_gitlab)
|
|
10
|
+
gitlab_credentials = GitLabCredentials(
|
|
11
|
+
url="https://gitlab.example.com", token="my-token"
|
|
12
|
+
)
|
|
13
|
+
gitlab_credentials.get_client()
|
|
14
|
+
mock_gitlab.assert_called_once_with(
|
|
15
|
+
url=gitlab_credentials.url,
|
|
16
|
+
oauth_token=gitlab_credentials.token.get_secret_value(),
|
|
17
|
+
)
|
|
18
|
+
mock_gitlab.assert_called_once()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_format_git_credentials_personal_access_token():
|
|
22
|
+
"""Test that personal access tokens get oauth2: prefix and are embedded in URL."""
|
|
23
|
+
credentials = GitLabCredentials(token="my-personal-token")
|
|
24
|
+
result = credentials.format_git_credentials("https://gitlab.com/org/repo.git")
|
|
25
|
+
assert result == "https://oauth2:my-personal-token@gitlab.com/org/repo.git"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_format_git_credentials_deploy_token():
|
|
29
|
+
"""Test that deploy tokens (username:token format) are used as-is in URL."""
|
|
30
|
+
credentials = GitLabCredentials(token="deploy-user:deploy-token-value")
|
|
31
|
+
result = credentials.format_git_credentials("https://gitlab.com/org/repo.git")
|
|
32
|
+
assert result == "https://deploy-user:deploy-token-value@gitlab.com/org/repo.git"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_format_git_credentials_already_prefixed():
|
|
36
|
+
"""Test that already-prefixed tokens don't get double-prefixed in URL."""
|
|
37
|
+
credentials = GitLabCredentials(token="oauth2:my-token")
|
|
38
|
+
result = credentials.format_git_credentials("https://gitlab.com/org/repo.git")
|
|
39
|
+
assert result == "https://oauth2:my-token@gitlab.com/org/repo.git"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_format_git_credentials_no_token_raises():
|
|
43
|
+
"""Test that missing token raises ValueError."""
|
|
44
|
+
credentials = GitLabCredentials()
|
|
45
|
+
with pytest.raises(ValueError, match="Token is required for GitLab authentication"):
|
|
46
|
+
credentials.format_git_credentials("https://gitlab.com/org/repo.git")
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
from tempfile import TemporaryDirectory
|
|
4
|
-
from typing import Set, Tuple
|
|
4
|
+
from typing import Coroutine, Set, Tuple
|
|
5
|
+
from unittest.mock import AsyncMock
|
|
5
6
|
|
|
6
7
|
import prefect_gitlab
|
|
7
8
|
import pytest
|
|
@@ -9,8 +10,6 @@ from prefect_gitlab.credentials import GitLabCredentials
|
|
|
9
10
|
from prefect_gitlab.repositories import GitLabRepository # noqa: E402
|
|
10
11
|
from pydantic import SecretStr
|
|
11
12
|
|
|
12
|
-
from prefect.testing.utilities import AsyncMock
|
|
13
|
-
|
|
14
13
|
|
|
15
14
|
class TestGitLab:
|
|
16
15
|
def setup_test_directory(
|
|
@@ -253,3 +252,45 @@ class TestGitLab:
|
|
|
253
252
|
print(mock.call_count)
|
|
254
253
|
# Verify that the function retried the expected number of times
|
|
255
254
|
assert mock.call_count == MAX_CLONE_ATTEMPTS
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class TestGitLabRepositoryAsyncDispatch:
|
|
258
|
+
"""Tests for GitLabRepository.get_directory migrated from @sync_compatible to @async_dispatch.
|
|
259
|
+
|
|
260
|
+
These tests verify the critical behavior from issue #15008 where
|
|
261
|
+
@sync_compatible would incorrectly return coroutines in sync context.
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
def test_get_directory_sync_context_returns_none_not_coroutine(self, monkeypatch):
|
|
265
|
+
"""get_directory must return None (not coroutine) in sync context.
|
|
266
|
+
|
|
267
|
+
This is the critical regression test for issues #14712 and #14625.
|
|
268
|
+
"""
|
|
269
|
+
import subprocess
|
|
270
|
+
from unittest.mock import MagicMock
|
|
271
|
+
|
|
272
|
+
mock_result = MagicMock()
|
|
273
|
+
mock_result.returncode = 0
|
|
274
|
+
mock_result.stderr = ""
|
|
275
|
+
monkeypatch.setattr(subprocess, "run", MagicMock(return_value=mock_result))
|
|
276
|
+
|
|
277
|
+
g = GitLabRepository(repository="prefect")
|
|
278
|
+
result = g.get_directory()
|
|
279
|
+
|
|
280
|
+
assert not isinstance(result, Coroutine), "sync context returned coroutine"
|
|
281
|
+
assert result is None
|
|
282
|
+
|
|
283
|
+
async def test_get_directory_async_context_returns_coroutine(self, monkeypatch):
|
|
284
|
+
"""get_directory should dispatch to async and return coroutine in async context."""
|
|
285
|
+
|
|
286
|
+
class p:
|
|
287
|
+
returncode = 0
|
|
288
|
+
|
|
289
|
+
mock = AsyncMock(return_value=p())
|
|
290
|
+
monkeypatch.setattr(prefect_gitlab.repositories, "run_process", mock)
|
|
291
|
+
|
|
292
|
+
g = GitLabRepository(repository="prefect")
|
|
293
|
+
result = g.get_directory()
|
|
294
|
+
|
|
295
|
+
assert isinstance(result, Coroutine)
|
|
296
|
+
await result # should complete without error
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
# file generated by setuptools_scm
|
|
2
|
-
# don't change, don't track in version control
|
|
3
|
-
TYPE_CHECKING = False
|
|
4
|
-
if TYPE_CHECKING:
|
|
5
|
-
from typing import Tuple, Union
|
|
6
|
-
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
7
|
-
else:
|
|
8
|
-
VERSION_TUPLE = object
|
|
9
|
-
|
|
10
|
-
version: str
|
|
11
|
-
__version__: str
|
|
12
|
-
__version_tuple__: VERSION_TUPLE
|
|
13
|
-
version_tuple: VERSION_TUPLE
|
|
14
|
-
|
|
15
|
-
__version__ = version = '0.3.1'
|
|
16
|
-
__version_tuple__ = version_tuple = (0, 3, 1)
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
"""Module used to enable authenticated interactions with GitLab"""
|
|
2
|
-
|
|
3
|
-
from typing import Optional
|
|
4
|
-
|
|
5
|
-
from gitlab import Gitlab
|
|
6
|
-
from pydantic import Field, SecretStr
|
|
7
|
-
|
|
8
|
-
from prefect.blocks.core import Block
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class GitLabCredentials(Block):
|
|
12
|
-
"""
|
|
13
|
-
Store a GitLab personal access token to interact with private GitLab
|
|
14
|
-
repositories.
|
|
15
|
-
|
|
16
|
-
Attributes:
|
|
17
|
-
token: The personal access token to authenticate with GitLab.
|
|
18
|
-
url: URL to self-hosted GitLab instances.
|
|
19
|
-
|
|
20
|
-
Examples:
|
|
21
|
-
Load stored GitLab credentials:
|
|
22
|
-
```python
|
|
23
|
-
from prefect_gitlab import GitLabCredentials
|
|
24
|
-
gitlab_credentials_block = GitLabCredentials.load("BLOCK_NAME")
|
|
25
|
-
```
|
|
26
|
-
"""
|
|
27
|
-
|
|
28
|
-
_block_type_name = "GitLab Credentials"
|
|
29
|
-
_logo_url = "https://images.ctfassets.net/gm98wzqotmnx/55edIimT4g9gbjhkh5a3Sp/dfdb9391d8f45c2e93e72e3a4d350771/gitlab-logo-500.png?h=250"
|
|
30
|
-
|
|
31
|
-
token: Optional[SecretStr] = Field(
|
|
32
|
-
title="Personal Access Token",
|
|
33
|
-
default=None,
|
|
34
|
-
description="A GitLab Personal Access Token with read_repository scope.",
|
|
35
|
-
)
|
|
36
|
-
url: Optional[str] = Field(
|
|
37
|
-
default=None, title="URL", description="URL to self-hosted GitLab instances."
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
def get_client(self) -> Gitlab:
|
|
41
|
-
"""
|
|
42
|
-
Gets an authenticated GitLab client.
|
|
43
|
-
|
|
44
|
-
Returns:
|
|
45
|
-
An authenticated GitLab client.
|
|
46
|
-
"""
|
|
47
|
-
# ref: https://python-gitlab.readthedocs.io/en/stable/
|
|
48
|
-
gitlab = Gitlab(url=self.url, oauth_token=self.token.get_secret_value())
|
|
49
|
-
gitlab.auth()
|
|
50
|
-
return gitlab
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
prefect>=3.0.0
|
|
2
|
-
python-gitlab>=3.12.0
|
|
3
|
-
tenacity>=8.2.3
|
|
4
|
-
|
|
5
|
-
[dev]
|
|
6
|
-
aiohttp
|
|
7
|
-
coverage
|
|
8
|
-
interrogate
|
|
9
|
-
mkdocs-gen-files
|
|
10
|
-
mkdocs-material
|
|
11
|
-
mkdocs
|
|
12
|
-
mkdocstrings[python]
|
|
13
|
-
mypy
|
|
14
|
-
pillow
|
|
15
|
-
pre-commit
|
|
16
|
-
pytest-asyncio
|
|
17
|
-
pytest>=8.3
|
|
18
|
-
pytest-env
|
|
19
|
-
pytest-xdist
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
from unittest.mock import MagicMock
|
|
2
|
-
|
|
3
|
-
from prefect_gitlab.credentials import GitLabCredentials
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def test_gitlab_credentials_get_client(monkeypatch):
|
|
7
|
-
mock_gitlab = MagicMock()
|
|
8
|
-
monkeypatch.setattr("prefect_gitlab.credentials.Gitlab", mock_gitlab)
|
|
9
|
-
gitlab_credentials = GitLabCredentials(
|
|
10
|
-
url="https://gitlab.example.com", token="my-token"
|
|
11
|
-
)
|
|
12
|
-
gitlab_credentials.get_client()
|
|
13
|
-
mock_gitlab.assert_called_once_with(
|
|
14
|
-
url=gitlab_credentials.url,
|
|
15
|
-
oauth_token=gitlab_credentials.token.get_secret_value(),
|
|
16
|
-
)
|
|
17
|
-
mock_gitlab.assert_called_once()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|