prefect-github 0.4.0__tar.gz → 0.4.2__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_github-0.4.0/prefect_github.egg-info → prefect_github-0.4.2}/PKG-INFO +2 -2
- {prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/_version.py +3 -3
- {prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/graphql.py +38 -8
- {prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/repository.py +58 -5
- {prefect_github-0.4.0 → prefect_github-0.4.2/prefect_github.egg-info}/PKG-INFO +2 -2
- prefect_github-0.4.2/prefect_github.egg-info/requires.txt +2 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/pyproject.toml +1 -1
- prefect_github-0.4.2/tests/test_graphql.py +117 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/tests/test_repository.py +71 -1
- prefect_github-0.4.0/prefect_github.egg-info/requires.txt +0 -2
- prefect_github-0.4.0/tests/test_graphql.py +0 -51
- {prefect_github-0.4.0 → prefect_github-0.4.2}/LICENSE +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/MANIFEST.in +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/README.md +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/__init__.py +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/configs/mutation/add_comment.json +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/configs/mutation/add_pull_request_review.json +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/configs/mutation/add_reaction.json +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/configs/mutation/add_star.json +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/configs/mutation/close_issue.json +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/configs/mutation/close_pull_request.json +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/configs/mutation/create_issue.json +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/configs/mutation/create_pull_request.json +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/configs/mutation/remove_reaction.json +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/configs/mutation/remove_star.json +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/configs/mutation/request_reviews.json +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/configs/query/organization.json +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/configs/query/repository.json +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/configs/query/repository_owner.json +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/configs/query/user.json +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/configs/query/viewer.json +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/credentials.py +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/exceptions.py +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/mutations.py +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/organization.py +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/repository_owner.py +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/schemas/__init__.py +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/schemas/graphql_schema.json +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/schemas/graphql_schema.py +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/user.py +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/utils.py +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/viewer.py +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github.egg-info/SOURCES.txt +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github.egg-info/dependency_links.txt +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github.egg-info/entry_points.txt +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github.egg-info/top_level.txt +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/setup.cfg +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/tests/conftest.py +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/tests/test_block_standards.py +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/tests/test_credentials.py +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/tests/test_utils.py +0 -0
- {prefect_github-0.4.0 → prefect_github-0.4.2}/tests/test_version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: prefect-github
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Prefect integrations interacting with GitHub
|
|
5
5
|
Author-email: "Prefect Technologies, Inc." <help@prefect.io>
|
|
6
6
|
License: Apache License 2.0
|
|
@@ -20,7 +20,7 @@ Requires-Python: >=3.10
|
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
License-File: LICENSE
|
|
22
22
|
Requires-Dist: sgqlc>=15.0
|
|
23
|
-
Requires-Dist: prefect>=3.
|
|
23
|
+
Requires-Dist: prefect>=3.6.17
|
|
24
24
|
Dynamic: license-file
|
|
25
25
|
|
|
26
26
|
# prefect-github
|
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.4.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 4,
|
|
31
|
+
__version__ = version = '0.4.2'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 4, 2)
|
|
33
33
|
|
|
34
|
-
__commit_id__ = commit_id = '
|
|
34
|
+
__commit_id__ = commit_id = 'gee531648b'
|
|
@@ -13,11 +13,28 @@ from anyio import to_thread
|
|
|
13
13
|
from sgqlc.operation import Operation, Selection
|
|
14
14
|
|
|
15
15
|
from prefect import task
|
|
16
|
-
from prefect.
|
|
16
|
+
from prefect._internal.compatibility.async_dispatch import async_dispatch
|
|
17
17
|
from prefect_github import GitHubCredentials
|
|
18
18
|
from prefect_github.utils import camel_to_snake_case
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
def _execute_graphql_op_sync(
|
|
22
|
+
op: Union[Operation, str],
|
|
23
|
+
github_credentials: GitHubCredentials,
|
|
24
|
+
error_key: str = "errors",
|
|
25
|
+
**vars,
|
|
26
|
+
) -> Dict[str, Any]:
|
|
27
|
+
"""
|
|
28
|
+
Sync helper function for executing GraphQL operations.
|
|
29
|
+
"""
|
|
30
|
+
endpoint = github_credentials.get_client()
|
|
31
|
+
result = endpoint(op, vars)
|
|
32
|
+
if error_key in result:
|
|
33
|
+
errors = pformat(result[error_key])
|
|
34
|
+
raise RuntimeError(f"Error encountered:\n{errors}")
|
|
35
|
+
return result["data"]
|
|
36
|
+
|
|
37
|
+
|
|
21
38
|
async def _execute_graphql_op(
|
|
22
39
|
op: Union[Operation, str],
|
|
23
40
|
github_credentials: GitHubCredentials,
|
|
@@ -25,7 +42,7 @@ async def _execute_graphql_op(
|
|
|
25
42
|
**vars,
|
|
26
43
|
) -> Dict[str, Any]:
|
|
27
44
|
"""
|
|
28
|
-
|
|
45
|
+
Async helper function for executing GraphQL operations.
|
|
29
46
|
"""
|
|
30
47
|
endpoint = github_credentials.get_client()
|
|
31
48
|
partial_endpoint = partial(endpoint, op, vars)
|
|
@@ -62,8 +79,24 @@ def _subset_return_fields(
|
|
|
62
79
|
|
|
63
80
|
|
|
64
81
|
@task
|
|
65
|
-
|
|
66
|
-
|
|
82
|
+
async def aexecute_graphql(
|
|
83
|
+
op: Union[Operation, str],
|
|
84
|
+
github_credentials: GitHubCredentials,
|
|
85
|
+
error_key: str = "errors",
|
|
86
|
+
**vars,
|
|
87
|
+
) -> Dict[str, Any]:
|
|
88
|
+
"""
|
|
89
|
+
Async version of execute_graphql. See execute_graphql for full documentation.
|
|
90
|
+
"""
|
|
91
|
+
result = await _execute_graphql_op(
|
|
92
|
+
op, github_credentials, error_key=error_key, **vars
|
|
93
|
+
)
|
|
94
|
+
return result
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@task
|
|
98
|
+
@async_dispatch(aexecute_graphql)
|
|
99
|
+
def execute_graphql(
|
|
67
100
|
op: Union[Operation, str],
|
|
68
101
|
github_credentials: GitHubCredentials,
|
|
69
102
|
error_key: str = "errors",
|
|
@@ -142,7 +175,4 @@ async def execute_graphql(
|
|
|
142
175
|
example_execute_graphql_flow()
|
|
143
176
|
```
|
|
144
177
|
"""
|
|
145
|
-
|
|
146
|
-
op, github_credentials, error_key=error_key, **vars
|
|
147
|
-
)
|
|
148
|
-
return result
|
|
178
|
+
return _execute_graphql_op_sync(op, github_credentials, error_key=error_key, **vars)
|
|
@@ -8,8 +8,10 @@ GitHub query_repository* tasks and the GitHub storage block.
|
|
|
8
8
|
# is outdated, rerun scripts/generate.py.
|
|
9
9
|
|
|
10
10
|
import io
|
|
11
|
+
import re
|
|
11
12
|
import shlex
|
|
12
13
|
import shutil
|
|
14
|
+
import subprocess
|
|
13
15
|
from datetime import datetime
|
|
14
16
|
from pathlib import Path
|
|
15
17
|
from tempfile import TemporaryDirectory
|
|
@@ -20,8 +22,9 @@ from pydantic import Field
|
|
|
20
22
|
from sgqlc.operation import Operation
|
|
21
23
|
|
|
22
24
|
from prefect import task
|
|
25
|
+
from prefect._internal.compatibility.async_dispatch import async_dispatch
|
|
26
|
+
from prefect._internal.urls import strip_auth_from_url
|
|
23
27
|
from prefect.filesystems import ReadableDeploymentStorage
|
|
24
|
-
from prefect.utilities.asyncutils import sync_compatible
|
|
25
28
|
from prefect.utilities.processutils import run_process
|
|
26
29
|
from prefect_github import GitHubCredentials
|
|
27
30
|
from prefect_github.graphql import _execute_graphql_op, _subset_return_fields
|
|
@@ -31,6 +34,15 @@ from prefect_github.utils import initialize_return_fields_defaults, strip_kwargs
|
|
|
31
34
|
config_path = Path(__file__).parent.resolve() / "configs" / "query" / "repository.json"
|
|
32
35
|
return_fields_defaults = initialize_return_fields_defaults(config_path)
|
|
33
36
|
|
|
37
|
+
_URL_PATTERN = re.compile(r"https?://[^\s'\"<>]+")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _sanitize_git_error(message: str) -> str:
|
|
41
|
+
def _replace(match: re.Match[str]) -> str:
|
|
42
|
+
return strip_auth_from_url(match.group(0))
|
|
43
|
+
|
|
44
|
+
return _URL_PATTERN.sub(_replace, message)
|
|
45
|
+
|
|
34
46
|
|
|
35
47
|
class GitHubRepository(ReadableDeploymentStorage):
|
|
36
48
|
"""
|
|
@@ -96,14 +108,13 @@ class GitHubRepository(ReadableDeploymentStorage):
|
|
|
96
108
|
|
|
97
109
|
return str(content_source), str(content_destination)
|
|
98
110
|
|
|
99
|
-
|
|
100
|
-
async def get_directory(
|
|
111
|
+
async def aget_directory(
|
|
101
112
|
self, from_path: Optional[str] = None, local_path: Optional[str] = None
|
|
102
113
|
) -> None:
|
|
103
114
|
"""
|
|
104
115
|
Clones a GitHub project specified in `from_path` to the provided `local_path`;
|
|
105
116
|
defaults to cloning the repository reference configured on the Block to the
|
|
106
|
-
present working directory.
|
|
117
|
+
present working directory. Async version.
|
|
107
118
|
|
|
108
119
|
Args:
|
|
109
120
|
from_path: If provided, interpreted as a subdirectory of the underlying
|
|
@@ -130,7 +141,49 @@ class GitHubRepository(ReadableDeploymentStorage):
|
|
|
130
141
|
process = await run_process(cmd, stream_output=(out_stream, err_stream))
|
|
131
142
|
if process.returncode != 0:
|
|
132
143
|
err_stream.seek(0)
|
|
133
|
-
|
|
144
|
+
sanitized_error = _sanitize_git_error(err_stream.read())
|
|
145
|
+
raise RuntimeError(f"Failed to pull from remote:\n {sanitized_error}")
|
|
146
|
+
|
|
147
|
+
content_source, content_destination = self._get_paths(
|
|
148
|
+
dst_dir=local_path, src_dir=tmp_path_str, sub_directory=from_path
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
shutil.copytree(
|
|
152
|
+
src=content_source,
|
|
153
|
+
dst=content_destination,
|
|
154
|
+
dirs_exist_ok=True,
|
|
155
|
+
symlinks=True,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
@async_dispatch(aget_directory)
|
|
159
|
+
def get_directory(
|
|
160
|
+
self, from_path: Optional[str] = None, local_path: Optional[str] = None
|
|
161
|
+
) -> None:
|
|
162
|
+
"""
|
|
163
|
+
Clones a GitHub project specified in `from_path` to the provided `local_path`;
|
|
164
|
+
defaults to cloning the repository reference configured on the Block to the
|
|
165
|
+
present working directory.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
from_path: If provided, interpreted as a subdirectory of the underlying
|
|
169
|
+
repository that will be copied to the provided local path.
|
|
170
|
+
local_path: A local path to clone to; defaults to present working directory.
|
|
171
|
+
"""
|
|
172
|
+
cmd = f"git clone {self._create_repo_url()}"
|
|
173
|
+
if self.reference:
|
|
174
|
+
cmd += f" -b {self.reference}"
|
|
175
|
+
|
|
176
|
+
cmd += " --depth 1"
|
|
177
|
+
|
|
178
|
+
with TemporaryDirectory(suffix="prefect") as tmp_dir:
|
|
179
|
+
tmp_path_str = tmp_dir
|
|
180
|
+
cmd += f' "{tmp_path_str}"'
|
|
181
|
+
cmd = shlex.split(cmd)
|
|
182
|
+
|
|
183
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
184
|
+
if result.returncode != 0:
|
|
185
|
+
sanitized_error = _sanitize_git_error(result.stderr)
|
|
186
|
+
raise RuntimeError(f"Failed to pull from remote:\n {sanitized_error}")
|
|
134
187
|
|
|
135
188
|
content_source, content_destination = self._get_paths(
|
|
136
189
|
dst_dir=local_path, src_dir=tmp_path_str, sub_directory=from_path
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: prefect-github
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Prefect integrations interacting with GitHub
|
|
5
5
|
Author-email: "Prefect Technologies, Inc." <help@prefect.io>
|
|
6
6
|
License: Apache License 2.0
|
|
@@ -20,7 +20,7 @@ Requires-Python: >=3.10
|
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
License-File: LICENSE
|
|
22
22
|
Requires-Dist: sgqlc>=15.0
|
|
23
|
-
Requires-Dist: prefect>=3.
|
|
23
|
+
Requires-Dist: prefect>=3.6.17
|
|
24
24
|
Dynamic: license-file
|
|
25
25
|
|
|
26
26
|
# prefect-github
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "prefect-github"
|
|
7
|
-
dependencies = ["sgqlc>=15.0", "prefect>=3.
|
|
7
|
+
dependencies = ["sgqlc>=15.0", "prefect>=3.6.17"]
|
|
8
8
|
dynamic = ["version"]
|
|
9
9
|
description = "Prefect integrations interacting with GitHub"
|
|
10
10
|
readme = "README.md"
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from typing import Coroutine
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from prefect_github.graphql import (
|
|
5
|
+
_subset_return_fields,
|
|
6
|
+
aexecute_graphql,
|
|
7
|
+
execute_graphql,
|
|
8
|
+
)
|
|
9
|
+
from prefect_github.schemas import graphql_schema
|
|
10
|
+
from sgqlc.operation import Operation, Selector
|
|
11
|
+
|
|
12
|
+
from prefect import flow
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.mark.parametrize(
|
|
16
|
+
"return_fields_key", ["return_fields", "return_fields_defaults"]
|
|
17
|
+
)
|
|
18
|
+
def test_subset_return_fields(return_fields_key):
|
|
19
|
+
op = Operation(graphql_schema.Query)
|
|
20
|
+
op_stack = graphql_schema.Query.__field_names__[:1]
|
|
21
|
+
op_selection = getattr(op, op_stack[0])
|
|
22
|
+
subset_kwargs = {"return_fields": [], "return_fields_defaults": {}}
|
|
23
|
+
return_fields = ["id"]
|
|
24
|
+
if return_fields_key == "return_fields":
|
|
25
|
+
subset_kwargs[return_fields_key] = return_fields
|
|
26
|
+
else:
|
|
27
|
+
subset_kwargs[return_fields_key] = {op_stack: return_fields}
|
|
28
|
+
op_selection = _subset_return_fields(op_selection, op_stack, **subset_kwargs)
|
|
29
|
+
assert isinstance(op_selection, Selector)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MockCredentials:
|
|
33
|
+
def __init__(self, error_key=None):
|
|
34
|
+
self.result = (
|
|
35
|
+
{error_key: "Errors encountered:"} if error_key else {"data": "success"}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def get_endpoint(self):
|
|
39
|
+
return lambda op, vars: self.result
|
|
40
|
+
|
|
41
|
+
def get_client(self):
|
|
42
|
+
return lambda op, vars: self.result
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@pytest.mark.parametrize("error_key", ["errors", False])
|
|
46
|
+
def test_execute_graphql(error_key):
|
|
47
|
+
mock_credentials = MockCredentials(error_key=error_key)
|
|
48
|
+
|
|
49
|
+
@flow
|
|
50
|
+
def test_flow():
|
|
51
|
+
return execute_graphql("op", mock_credentials)
|
|
52
|
+
|
|
53
|
+
if error_key:
|
|
54
|
+
with pytest.raises(RuntimeError, match="Errors encountered:"):
|
|
55
|
+
test_flow()
|
|
56
|
+
else:
|
|
57
|
+
assert test_flow() == "success"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class TestExecuteGraphqlAsyncDispatch:
|
|
61
|
+
"""Tests for execute_graphql migrated from @sync_compatible to @async_dispatch.
|
|
62
|
+
|
|
63
|
+
These tests verify the critical behavior from issue #15008 where
|
|
64
|
+
@sync_compatible would incorrectly return coroutines in sync context.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def test_execute_graphql_sync_context_returns_value_not_coroutine(self):
|
|
68
|
+
"""execute_graphql must return value (not coroutine) in sync context.
|
|
69
|
+
|
|
70
|
+
This is the critical regression test for issues #14712 and #14625.
|
|
71
|
+
"""
|
|
72
|
+
mock_credentials = MockCredentials(error_key=False)
|
|
73
|
+
|
|
74
|
+
@flow
|
|
75
|
+
def test_flow():
|
|
76
|
+
result = execute_graphql("op", mock_credentials)
|
|
77
|
+
# the result inside the flow should be the actual value, not a coroutine
|
|
78
|
+
assert not isinstance(result, Coroutine), "sync context returned coroutine"
|
|
79
|
+
return result
|
|
80
|
+
|
|
81
|
+
assert test_flow() == "success"
|
|
82
|
+
|
|
83
|
+
async def test_execute_graphql_async_context_works(self):
|
|
84
|
+
"""execute_graphql should work correctly in async context."""
|
|
85
|
+
mock_credentials = MockCredentials(error_key=False)
|
|
86
|
+
|
|
87
|
+
@flow
|
|
88
|
+
async def test_flow():
|
|
89
|
+
# when decorated with @task, the task machinery handles execution
|
|
90
|
+
# so we get a result directly, not a coroutine
|
|
91
|
+
result = execute_graphql("op", mock_credentials)
|
|
92
|
+
return result
|
|
93
|
+
|
|
94
|
+
assert await test_flow() == "success"
|
|
95
|
+
|
|
96
|
+
def test_aexecute_graphql_is_exported(self):
|
|
97
|
+
"""aexecute_graphql should be available for direct async usage."""
|
|
98
|
+
# just verify it's importable and is a task
|
|
99
|
+
assert callable(aexecute_graphql)
|
|
100
|
+
|
|
101
|
+
def test_task_identity_preserved(self):
|
|
102
|
+
"""Both execute_graphql and aexecute_graphql should have Task identity.
|
|
103
|
+
|
|
104
|
+
This verifies decorator order is correct (@task must be outermost).
|
|
105
|
+
See PR #20300 for the dbt decorator ordering fix.
|
|
106
|
+
"""
|
|
107
|
+
# .with_options() is only available on proper Task objects
|
|
108
|
+
assert hasattr(execute_graphql, "with_options"), (
|
|
109
|
+
"execute_graphql missing .with_options() - check decorator order"
|
|
110
|
+
)
|
|
111
|
+
assert hasattr(aexecute_graphql, "with_options"), (
|
|
112
|
+
"aexecute_graphql missing .with_options() - check decorator order"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# verify we can actually call with_options
|
|
116
|
+
configured = execute_graphql.with_options(retries=3)
|
|
117
|
+
assert configured is not None
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
from tempfile import TemporaryDirectory
|
|
4
|
-
from typing import Tuple
|
|
4
|
+
from typing import Coroutine, Tuple
|
|
5
5
|
from unittest.mock import AsyncMock
|
|
6
6
|
|
|
7
7
|
import prefect_github
|
|
@@ -72,6 +72,34 @@ class TestGitHubRepository:
|
|
|
72
72
|
in " ".join(mock.await_args[0][0])
|
|
73
73
|
)
|
|
74
74
|
|
|
75
|
+
async def test_get_directory_redacts_token_in_error(self, monkeypatch):
|
|
76
|
+
"""Ensure GitHub access tokens are not surfaced in git error messages."""
|
|
77
|
+
|
|
78
|
+
class p:
|
|
79
|
+
returncode = 1
|
|
80
|
+
|
|
81
|
+
async def mock(cmd, stream_output=None, **kwargs):
|
|
82
|
+
if stream_output:
|
|
83
|
+
_, err_stream = stream_output
|
|
84
|
+
err_stream.write(
|
|
85
|
+
"fatal: Authentication failed for "
|
|
86
|
+
"'https://XYZ@github.com/PrefectHQ/prefect.git/'"
|
|
87
|
+
)
|
|
88
|
+
return p()
|
|
89
|
+
|
|
90
|
+
monkeypatch.setattr(prefect_github.repository, "run_process", mock)
|
|
91
|
+
credential = GitHubCredentials(token="XYZ")
|
|
92
|
+
g = GitHubRepository(
|
|
93
|
+
repository_url="https://github.com/PrefectHQ/prefect.git",
|
|
94
|
+
credentials=credential,
|
|
95
|
+
)
|
|
96
|
+
with pytest.raises(RuntimeError) as excinfo:
|
|
97
|
+
await g.get_directory()
|
|
98
|
+
|
|
99
|
+
message = str(excinfo.value)
|
|
100
|
+
assert "XYZ" not in message
|
|
101
|
+
assert "https://github.com/PrefectHQ/prefect.git" in message
|
|
102
|
+
|
|
75
103
|
def setup_test_directory(
|
|
76
104
|
self, tmp_src: str, sub_dir: str = "puppy"
|
|
77
105
|
) -> Tuple[str, str]:
|
|
@@ -235,3 +263,45 @@ class TestGitHubRepository:
|
|
|
235
263
|
copied_real = Path(tmp_dst) / "real_file.txt"
|
|
236
264
|
assert copied_real.exists()
|
|
237
265
|
assert copied_real.read_text() == "real content"
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class TestGitHubRepositoryAsyncDispatch:
|
|
269
|
+
"""Tests for GitHubRepository.get_directory migrated from @sync_compatible to @async_dispatch.
|
|
270
|
+
|
|
271
|
+
These tests verify the critical behavior from issue #15008 where
|
|
272
|
+
@sync_compatible would incorrectly return coroutines in sync context.
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
def test_get_directory_sync_context_returns_none_not_coroutine(self, monkeypatch):
|
|
276
|
+
"""get_directory must return None (not coroutine) in sync context.
|
|
277
|
+
|
|
278
|
+
This is the critical regression test for issues #14712 and #14625.
|
|
279
|
+
"""
|
|
280
|
+
import subprocess
|
|
281
|
+
from unittest.mock import MagicMock
|
|
282
|
+
|
|
283
|
+
mock_result = MagicMock()
|
|
284
|
+
mock_result.returncode = 0
|
|
285
|
+
mock_result.stderr = ""
|
|
286
|
+
monkeypatch.setattr(subprocess, "run", MagicMock(return_value=mock_result))
|
|
287
|
+
|
|
288
|
+
g = GitHubRepository(repository_url="prefect")
|
|
289
|
+
result = g.get_directory()
|
|
290
|
+
|
|
291
|
+
assert not isinstance(result, Coroutine), "sync context returned coroutine"
|
|
292
|
+
assert result is None
|
|
293
|
+
|
|
294
|
+
async def test_get_directory_async_context_returns_coroutine(self, monkeypatch):
|
|
295
|
+
"""get_directory should dispatch to async and return coroutine in async context."""
|
|
296
|
+
|
|
297
|
+
class p:
|
|
298
|
+
returncode = 0
|
|
299
|
+
|
|
300
|
+
mock = AsyncMock(return_value=p())
|
|
301
|
+
monkeypatch.setattr(prefect_github.repository, "run_process", mock)
|
|
302
|
+
|
|
303
|
+
g = GitHubRepository(repository_url="prefect")
|
|
304
|
+
result = g.get_directory()
|
|
305
|
+
|
|
306
|
+
assert isinstance(result, Coroutine)
|
|
307
|
+
await result # should complete without error
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
from prefect_github.graphql import _subset_return_fields, execute_graphql
|
|
3
|
-
from prefect_github.schemas import graphql_schema
|
|
4
|
-
from sgqlc.operation import Operation, Selector
|
|
5
|
-
|
|
6
|
-
from prefect import flow
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@pytest.mark.parametrize(
|
|
10
|
-
"return_fields_key", ["return_fields", "return_fields_defaults"]
|
|
11
|
-
)
|
|
12
|
-
def test_subset_return_fields(return_fields_key):
|
|
13
|
-
op = Operation(graphql_schema.Query)
|
|
14
|
-
op_stack = graphql_schema.Query.__field_names__[:1]
|
|
15
|
-
op_selection = getattr(op, op_stack[0])
|
|
16
|
-
subset_kwargs = {"return_fields": [], "return_fields_defaults": {}}
|
|
17
|
-
return_fields = ["id"]
|
|
18
|
-
if return_fields_key == "return_fields":
|
|
19
|
-
subset_kwargs[return_fields_key] = return_fields
|
|
20
|
-
else:
|
|
21
|
-
subset_kwargs[return_fields_key] = {op_stack: return_fields}
|
|
22
|
-
op_selection = _subset_return_fields(op_selection, op_stack, **subset_kwargs)
|
|
23
|
-
assert isinstance(op_selection, Selector)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class MockCredentials:
|
|
27
|
-
def __init__(self, error_key=None):
|
|
28
|
-
self.result = (
|
|
29
|
-
{error_key: "Errors encountered:"} if error_key else {"data": "success"}
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
def get_endpoint(self):
|
|
33
|
-
return lambda op, vars: self.result
|
|
34
|
-
|
|
35
|
-
def get_client(self):
|
|
36
|
-
return lambda op, vars: self.result
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
@pytest.mark.parametrize("error_key", ["errors", False])
|
|
40
|
-
def test_execute_graphql(error_key):
|
|
41
|
-
mock_credentials = MockCredentials(error_key=error_key)
|
|
42
|
-
|
|
43
|
-
@flow
|
|
44
|
-
def test_flow():
|
|
45
|
-
return execute_graphql("op", mock_credentials)
|
|
46
|
-
|
|
47
|
-
if error_key:
|
|
48
|
-
with pytest.raises(RuntimeError, match="Errors encountered:"):
|
|
49
|
-
test_flow()
|
|
50
|
-
else:
|
|
51
|
-
assert test_flow() == "success"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/configs/mutation/add_comment.json
RENAMED
|
File without changes
|
|
File without changes
|
{prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/configs/mutation/add_reaction.json
RENAMED
|
File without changes
|
|
File without changes
|
{prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/configs/mutation/close_issue.json
RENAMED
|
File without changes
|
|
File without changes
|
{prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/configs/mutation/create_issue.json
RENAMED
|
File without changes
|
|
File without changes
|
{prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/configs/mutation/remove_reaction.json
RENAMED
|
File without changes
|
{prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/configs/mutation/remove_star.json
RENAMED
|
File without changes
|
{prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/configs/mutation/request_reviews.json
RENAMED
|
File without changes
|
{prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/configs/query/organization.json
RENAMED
|
File without changes
|
|
File without changes
|
{prefect_github-0.4.0 → prefect_github-0.4.2}/prefect_github/configs/query/repository_owner.json
RENAMED
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|