dagster-cloud-cli 1.12.16__py2.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.
- dagster_cloud_cli/__init__.py +5 -0
- dagster_cloud_cli/commands/__init__.py +0 -0
- dagster_cloud_cli/commands/branch_deployment/__init__.py +154 -0
- dagster_cloud_cli/commands/ci/__init__.py +1207 -0
- dagster_cloud_cli/commands/ci/checks.py +135 -0
- dagster_cloud_cli/commands/ci/report.py +49 -0
- dagster_cloud_cli/commands/ci/state.py +129 -0
- dagster_cloud_cli/commands/ci/utils.py +22 -0
- dagster_cloud_cli/commands/config.py +250 -0
- dagster_cloud_cli/commands/deployment/__init__.py +108 -0
- dagster_cloud_cli/commands/deployment/alert_policies/__init__.py +0 -0
- dagster_cloud_cli/commands/deployment/alert_policies/commands.py +52 -0
- dagster_cloud_cli/commands/integration/__init__.py +6 -0
- dagster_cloud_cli/commands/integration/atlan/__init__.py +94 -0
- dagster_cloud_cli/commands/job/__init__.py +117 -0
- dagster_cloud_cli/commands/metrics.py +114 -0
- dagster_cloud_cli/commands/organization/__init__.py +134 -0
- dagster_cloud_cli/commands/organization/saml/__init__.py +0 -0
- dagster_cloud_cli/commands/organization/saml/commands.py +85 -0
- dagster_cloud_cli/commands/run/__init__.py +27 -0
- dagster_cloud_cli/commands/serverless/Dockerfile +38 -0
- dagster_cloud_cli/commands/serverless/Dockerfile.dagster-cloud-cli +20 -0
- dagster_cloud_cli/commands/serverless/__init__.py +560 -0
- dagster_cloud_cli/commands/workspace/__init__.py +358 -0
- dagster_cloud_cli/config/__init__.py +6 -0
- dagster_cloud_cli/config/jinja_template_loader.py +45 -0
- dagster_cloud_cli/config/list_merge_strategies.py +77 -0
- dagster_cloud_cli/config/loader.py +23 -0
- dagster_cloud_cli/config/merger.py +92 -0
- dagster_cloud_cli/config/models.py +76 -0
- dagster_cloud_cli/config_utils.py +506 -0
- dagster_cloud_cli/core/__init__.py +0 -0
- dagster_cloud_cli/core/agent_queue.py +3 -0
- dagster_cloud_cli/core/alert_types.py +26 -0
- dagster_cloud_cli/core/artifacts.py +192 -0
- dagster_cloud_cli/core/docker_runner.py +57 -0
- dagster_cloud_cli/core/errors.py +32 -0
- dagster_cloud_cli/core/graphql_client.py +420 -0
- dagster_cloud_cli/core/headers/__init__.py +0 -0
- dagster_cloud_cli/core/headers/auth.py +16 -0
- dagster_cloud_cli/core/headers/impl.py +31 -0
- dagster_cloud_cli/core/headers/versioning/__init__.py +0 -0
- dagster_cloud_cli/core/headers/versioning/constants.py +2 -0
- dagster_cloud_cli/core/pex_builder/__init__.py +8 -0
- dagster_cloud_cli/core/pex_builder/code_location.py +126 -0
- dagster_cloud_cli/core/pex_builder/deploy.py +255 -0
- dagster_cloud_cli/core/pex_builder/deps.py +495 -0
- dagster_cloud_cli/core/pex_builder/git_context.py +29 -0
- dagster_cloud_cli/core/pex_builder/github_context.py +178 -0
- dagster_cloud_cli/core/pex_builder/gitlab_context.py +30 -0
- dagster_cloud_cli/core/pex_builder/parse_workspace.py +39 -0
- dagster_cloud_cli/core/pex_builder/pex_registry.py +148 -0
- dagster_cloud_cli/core/pex_builder/platforms.py +6105 -0
- dagster_cloud_cli/core/pex_builder/selftest.py +14 -0
- dagster_cloud_cli/core/pex_builder/source.py +166 -0
- dagster_cloud_cli/core/pex_builder/util.py +165 -0
- dagster_cloud_cli/core/workspace.py +145 -0
- dagster_cloud_cli/docker_utils.py +98 -0
- dagster_cloud_cli/entrypoint.py +209 -0
- dagster_cloud_cli/gql.py +1025 -0
- dagster_cloud_cli/pex_utils.py +180 -0
- dagster_cloud_cli/py.typed +0 -0
- dagster_cloud_cli/types.py +78 -0
- dagster_cloud_cli/ui.py +101 -0
- dagster_cloud_cli/utils.py +107 -0
- dagster_cloud_cli/version.py +1 -0
- dagster_cloud_cli-1.12.16.dist-info/METADATA +31 -0
- dagster_cloud_cli-1.12.16.dist-info/RECORD +71 -0
- dagster_cloud_cli-1.12.16.dist-info/WHEEL +5 -0
- dagster_cloud_cli-1.12.16.dist-info/entry_points.txt +3 -0
- dagster_cloud_cli-1.12.16.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
from dagster_cloud_cli.config_utils import (
|
|
7
|
+
URL_ENV_VAR_NAME,
|
|
8
|
+
get_deployment,
|
|
9
|
+
get_organization,
|
|
10
|
+
get_user_token,
|
|
11
|
+
)
|
|
12
|
+
from dagster_cloud_cli.core.headers.auth import DagsterCloudInstanceScope
|
|
13
|
+
from dagster_cloud_cli.core.headers.impl import get_dagster_cloud_api_headers
|
|
14
|
+
from dagster_cloud_cli.gql import url_from_config
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _dagster_cloud_http_client():
|
|
18
|
+
# indirection for mocking, could share a session
|
|
19
|
+
return requests
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def download_artifact(
|
|
23
|
+
*,
|
|
24
|
+
url: str,
|
|
25
|
+
api_token: str,
|
|
26
|
+
scope: DagsterCloudInstanceScope,
|
|
27
|
+
key: str,
|
|
28
|
+
path: Path | str,
|
|
29
|
+
deployment: str | None = None,
|
|
30
|
+
):
|
|
31
|
+
response = _dagster_cloud_http_client().post(
|
|
32
|
+
url=f"{url}/gen_artifact_get",
|
|
33
|
+
headers=get_dagster_cloud_api_headers(
|
|
34
|
+
api_token,
|
|
35
|
+
scope,
|
|
36
|
+
deployment_name=deployment,
|
|
37
|
+
),
|
|
38
|
+
json={"key": key},
|
|
39
|
+
)
|
|
40
|
+
response.raise_for_status()
|
|
41
|
+
|
|
42
|
+
payload = response.json()
|
|
43
|
+
|
|
44
|
+
response = requests.get(
|
|
45
|
+
payload["url"],
|
|
46
|
+
)
|
|
47
|
+
response.raise_for_status()
|
|
48
|
+
Path(path).write_bytes(response.content)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def upload_artifact(
|
|
52
|
+
*,
|
|
53
|
+
url: str,
|
|
54
|
+
api_token: str,
|
|
55
|
+
scope: DagsterCloudInstanceScope,
|
|
56
|
+
key: str,
|
|
57
|
+
path: Path | str,
|
|
58
|
+
deployment: str | None = None,
|
|
59
|
+
):
|
|
60
|
+
upload_file = Path(path).resolve(strict=True)
|
|
61
|
+
|
|
62
|
+
response = _dagster_cloud_http_client().post(
|
|
63
|
+
url=f"{url}/gen_artifact_post",
|
|
64
|
+
headers=get_dagster_cloud_api_headers(
|
|
65
|
+
api_token,
|
|
66
|
+
scope,
|
|
67
|
+
deployment_name=deployment,
|
|
68
|
+
),
|
|
69
|
+
json={"key": key},
|
|
70
|
+
)
|
|
71
|
+
response.raise_for_status()
|
|
72
|
+
payload = response.json()
|
|
73
|
+
|
|
74
|
+
response = requests.post(
|
|
75
|
+
payload["url"],
|
|
76
|
+
data=payload["fields"],
|
|
77
|
+
files={
|
|
78
|
+
"file": upload_file.open(),
|
|
79
|
+
},
|
|
80
|
+
)
|
|
81
|
+
response.raise_for_status()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _resolve_org(passed_org: str | None) -> str:
|
|
85
|
+
org = passed_org or get_organization()
|
|
86
|
+
if org is None:
|
|
87
|
+
raise Exception(
|
|
88
|
+
"Unable to resolve organization, pass organization or set the "
|
|
89
|
+
"DAGSTER_CLOUD_ORGANIZATION environment variable."
|
|
90
|
+
)
|
|
91
|
+
return org
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _resolve_token(passed_token: str | None) -> str:
|
|
95
|
+
api_token = get_user_token()
|
|
96
|
+
if api_token is None:
|
|
97
|
+
raise Exception(
|
|
98
|
+
"Unable to resolve api_token, pass api_token or set the "
|
|
99
|
+
"DAGSTER_CLOUD_API_TOKEN environment variable"
|
|
100
|
+
)
|
|
101
|
+
return api_token
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _resolve_deploy(passed_deploy: str | None) -> str:
|
|
105
|
+
deploy = passed_deploy or get_deployment()
|
|
106
|
+
if deploy is None:
|
|
107
|
+
raise Exception(
|
|
108
|
+
"Unable to resolve deployment, pass deployment or set the "
|
|
109
|
+
"DAGSTER_CLOUD_DEPLOYMENT environment variable."
|
|
110
|
+
)
|
|
111
|
+
return deploy
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _resolve_url(organization: str, deployment: str | None = None) -> str:
|
|
115
|
+
env_url = os.getenv(URL_ENV_VAR_NAME)
|
|
116
|
+
if env_url:
|
|
117
|
+
return env_url
|
|
118
|
+
|
|
119
|
+
return url_from_config(organization, deployment)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def upload_organization_artifact(
|
|
123
|
+
key: str,
|
|
124
|
+
path: str | Path,
|
|
125
|
+
organization: str | None = None,
|
|
126
|
+
api_token: str | None = None,
|
|
127
|
+
):
|
|
128
|
+
upload_artifact(
|
|
129
|
+
url=_resolve_url(
|
|
130
|
+
organization=_resolve_org(organization),
|
|
131
|
+
),
|
|
132
|
+
api_token=_resolve_token(api_token),
|
|
133
|
+
scope=DagsterCloudInstanceScope.ORGANIZATION,
|
|
134
|
+
key=key,
|
|
135
|
+
path=path,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def upload_deployment_artifact(
|
|
140
|
+
key: str,
|
|
141
|
+
path: str | Path,
|
|
142
|
+
organization: str | None = None,
|
|
143
|
+
deployment: str | None = None,
|
|
144
|
+
api_token: str | None = None,
|
|
145
|
+
):
|
|
146
|
+
upload_artifact(
|
|
147
|
+
url=_resolve_url(
|
|
148
|
+
organization=_resolve_org(organization),
|
|
149
|
+
deployment=_resolve_deploy(deployment),
|
|
150
|
+
),
|
|
151
|
+
api_token=_resolve_token(api_token),
|
|
152
|
+
scope=DagsterCloudInstanceScope.DEPLOYMENT,
|
|
153
|
+
key=key,
|
|
154
|
+
path=path,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def download_organization_artifact(
|
|
159
|
+
key: str,
|
|
160
|
+
path: str | Path,
|
|
161
|
+
organization: str | None = None,
|
|
162
|
+
api_token: str | None = None,
|
|
163
|
+
):
|
|
164
|
+
download_artifact(
|
|
165
|
+
url=_resolve_url(
|
|
166
|
+
organization=_resolve_org(organization),
|
|
167
|
+
),
|
|
168
|
+
api_token=_resolve_token(api_token),
|
|
169
|
+
scope=DagsterCloudInstanceScope.ORGANIZATION,
|
|
170
|
+
key=key,
|
|
171
|
+
path=path,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def download_deployment_artifact(
|
|
176
|
+
key: str,
|
|
177
|
+
path: str | Path,
|
|
178
|
+
organization: str | None = None,
|
|
179
|
+
deployment: str | None = None,
|
|
180
|
+
api_token: str | None = None,
|
|
181
|
+
):
|
|
182
|
+
deployment = _resolve_deploy(deployment)
|
|
183
|
+
download_artifact(
|
|
184
|
+
url=_resolve_url(
|
|
185
|
+
organization=_resolve_org(organization),
|
|
186
|
+
deployment=deployment,
|
|
187
|
+
),
|
|
188
|
+
api_token=_resolve_token(api_token),
|
|
189
|
+
scope=DagsterCloudInstanceScope.DEPLOYMENT,
|
|
190
|
+
key=key,
|
|
191
|
+
path=path,
|
|
192
|
+
)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
|
|
4
|
+
from dagster_cloud_cli import ui
|
|
5
|
+
|
|
6
|
+
DAGSTER_CLOUD_BUILDER_IMAGE = "ghcr.io/dagster-io/dagster-manylinux-builder:manylinux_2_28_x86_64"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def run_dagster_cloud(
|
|
10
|
+
map_folders: dict[str, str], run_args: list[str], env: dict[str, str | None]
|
|
11
|
+
) -> subprocess.CompletedProcess:
|
|
12
|
+
proc = run_docker_image(
|
|
13
|
+
image_name=os.getenv("DAGSTER_CLOUD_BUILDER_IMAGE", DAGSTER_CLOUD_BUILDER_IMAGE),
|
|
14
|
+
map_folders=map_folders,
|
|
15
|
+
env=env,
|
|
16
|
+
entrypoint="dagster-cloud",
|
|
17
|
+
run_args=run_args,
|
|
18
|
+
)
|
|
19
|
+
return proc
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def run_docker_image(
|
|
23
|
+
image_name,
|
|
24
|
+
map_folders: dict[str, str],
|
|
25
|
+
env: dict[str, str | None],
|
|
26
|
+
entrypoint: str,
|
|
27
|
+
run_args: list[str],
|
|
28
|
+
) -> subprocess.CompletedProcess:
|
|
29
|
+
mount_args = []
|
|
30
|
+
for target_folder, source_folder in map_folders.items():
|
|
31
|
+
mount_args.extend(["--mount", f"type=bind,source={source_folder},target={target_folder}"])
|
|
32
|
+
|
|
33
|
+
env_args = []
|
|
34
|
+
for env_name, env_value in env.items():
|
|
35
|
+
if env_value is None:
|
|
36
|
+
# specifying just the name copies the env from the calling environment
|
|
37
|
+
env_args.extend(["--env", env_name])
|
|
38
|
+
else:
|
|
39
|
+
env_args.extend(["--env", f"{env_name}={env_value}"])
|
|
40
|
+
|
|
41
|
+
ui.print(f"Running docker {image_name}")
|
|
42
|
+
ui.print("Mapped folders:")
|
|
43
|
+
for target_dir, source_dir in map_folders.items():
|
|
44
|
+
ui.print(f" - {source_dir} -> {target_dir}")
|
|
45
|
+
|
|
46
|
+
cmd = [
|
|
47
|
+
"docker",
|
|
48
|
+
"run",
|
|
49
|
+
"--platform=linux/amd64",
|
|
50
|
+
*env_args,
|
|
51
|
+
*mount_args,
|
|
52
|
+
"--entrypoint",
|
|
53
|
+
entrypoint,
|
|
54
|
+
image_name,
|
|
55
|
+
*run_args,
|
|
56
|
+
]
|
|
57
|
+
return subprocess.run(cmd, capture_output=True, check=False)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from requests import HTTPError
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class DagsterCloudAgentServerError(Exception):
|
|
5
|
+
"""Raise this when there's an error in the GraphQL layer."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
DEFAULT_MAINTENANCE_TIMEOUT = 3600 * 4 * 4
|
|
9
|
+
DEFAULT_MAINTENANCE_RETRY_INTERVAL = 30
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DagsterCloudMaintenanceException(Exception):
|
|
13
|
+
def __init__(self, message, timeout, retry_interval):
|
|
14
|
+
self.timeout = timeout or DEFAULT_MAINTENANCE_TIMEOUT
|
|
15
|
+
self.retry_interval = retry_interval or DEFAULT_MAINTENANCE_RETRY_INTERVAL
|
|
16
|
+
super().__init__(message)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DagsterCloudHTTPError(Exception):
|
|
20
|
+
"""Clearer error message for exceptions hitting Dagster Cloud servers."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, http_error: HTTPError):
|
|
23
|
+
self.response = http_error.response
|
|
24
|
+
error_content = http_error.response.content.decode("utf-8", errors="ignore")
|
|
25
|
+
super().__init__(http_error.__str__() + ": " + str(error_content))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def raise_http_error(response):
|
|
29
|
+
try:
|
|
30
|
+
response.raise_for_status()
|
|
31
|
+
except HTTPError as e:
|
|
32
|
+
raise DagsterCloudHTTPError(e) from e
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import re
|
|
3
|
+
import time
|
|
4
|
+
from collections.abc import Callable, Mapping
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from email.utils import mktime_tz, parsedate_tz
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
from dagster_shared import check
|
|
11
|
+
from requests.adapters import HTTPAdapter
|
|
12
|
+
from requests.exceptions import (
|
|
13
|
+
ChunkedEncodingError,
|
|
14
|
+
ConnectionError as RequestsConnectionError,
|
|
15
|
+
HTTPError,
|
|
16
|
+
ReadTimeout as RequestsReadTimeout,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from dagster_cloud_cli.core.errors import (
|
|
20
|
+
DagsterCloudAgentServerError,
|
|
21
|
+
DagsterCloudHTTPError,
|
|
22
|
+
DagsterCloudMaintenanceException,
|
|
23
|
+
)
|
|
24
|
+
from dagster_cloud_cli.core.headers.auth import DagsterCloudInstanceScope
|
|
25
|
+
from dagster_cloud_cli.core.headers.impl import get_dagster_cloud_api_headers
|
|
26
|
+
|
|
27
|
+
DEFAULT_RETRIES = 6
|
|
28
|
+
DEFAULT_BACKOFF_FACTOR = 0.5
|
|
29
|
+
DEFAULT_TIMEOUT = 60
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger("dagster_cloud")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
RETRY_STATUS_CODES = [
|
|
35
|
+
502,
|
|
36
|
+
503,
|
|
37
|
+
504,
|
|
38
|
+
429,
|
|
39
|
+
409,
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class DagsterCloudAgentHttpClient:
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
session: requests.Session,
|
|
47
|
+
headers: dict[str, Any] | None = None,
|
|
48
|
+
verify: bool = True,
|
|
49
|
+
timeout: int = DEFAULT_TIMEOUT,
|
|
50
|
+
cookies: dict[str, Any] | None = None,
|
|
51
|
+
proxies: dict[str, Any] | None = None,
|
|
52
|
+
max_retries: int = 0,
|
|
53
|
+
backoff_factor: float = DEFAULT_BACKOFF_FACTOR,
|
|
54
|
+
):
|
|
55
|
+
self.headers = headers or {}
|
|
56
|
+
self.verify = verify
|
|
57
|
+
self.timeout = timeout
|
|
58
|
+
self.cookies = cookies
|
|
59
|
+
self._session = session
|
|
60
|
+
self._proxies = proxies
|
|
61
|
+
self._max_retries = max_retries
|
|
62
|
+
self._backoff_factor = backoff_factor
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def session(self) -> requests.Session:
|
|
66
|
+
return self._session
|
|
67
|
+
|
|
68
|
+
def post(self, *args, **kwargs):
|
|
69
|
+
return self.execute("POST", *args, **kwargs)
|
|
70
|
+
|
|
71
|
+
def get(self, *args, **kwargs):
|
|
72
|
+
return self.execute("GET", *args, **kwargs)
|
|
73
|
+
|
|
74
|
+
def put(self, *args, **kwargs):
|
|
75
|
+
return self.execute("PUT", *args, **kwargs)
|
|
76
|
+
|
|
77
|
+
def execute(
|
|
78
|
+
self,
|
|
79
|
+
method: str,
|
|
80
|
+
url: str,
|
|
81
|
+
headers: Mapping[str, str] | None = None,
|
|
82
|
+
idempotent: bool = False,
|
|
83
|
+
**kwargs,
|
|
84
|
+
):
|
|
85
|
+
retry_on_read_timeout = idempotent or bool(
|
|
86
|
+
headers.get("Idempotency-Key") if headers else False
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
return _retry_loop(
|
|
90
|
+
lambda: self._execute_retry(method, url, headers, **kwargs),
|
|
91
|
+
max_retries=self._max_retries,
|
|
92
|
+
backoff_factor=self._backoff_factor,
|
|
93
|
+
retry_on_read_timeout=retry_on_read_timeout,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def _execute_retry(
|
|
97
|
+
self,
|
|
98
|
+
method: str,
|
|
99
|
+
url: str,
|
|
100
|
+
headers: Mapping[str, Any] | None,
|
|
101
|
+
**kwargs,
|
|
102
|
+
):
|
|
103
|
+
response = self._session.request(
|
|
104
|
+
method,
|
|
105
|
+
url,
|
|
106
|
+
headers={
|
|
107
|
+
**(self.headers if self.headers is not None else {}),
|
|
108
|
+
**(headers if headers is not None else {}),
|
|
109
|
+
},
|
|
110
|
+
cookies=self.cookies,
|
|
111
|
+
timeout=self.timeout,
|
|
112
|
+
verify=self.verify,
|
|
113
|
+
proxies=self._proxies,
|
|
114
|
+
**kwargs,
|
|
115
|
+
)
|
|
116
|
+
try:
|
|
117
|
+
result = response.json()
|
|
118
|
+
if not isinstance(result, dict):
|
|
119
|
+
result = {}
|
|
120
|
+
except ValueError:
|
|
121
|
+
result = {}
|
|
122
|
+
|
|
123
|
+
if "maintenance" in result:
|
|
124
|
+
maintenance_info = result["maintenance"]
|
|
125
|
+
raise DagsterCloudMaintenanceException(
|
|
126
|
+
message=maintenance_info.get("message"),
|
|
127
|
+
timeout=maintenance_info.get("timeout"),
|
|
128
|
+
retry_interval=maintenance_info.get("retry_interval"),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if "errors" in result:
|
|
132
|
+
raise DagsterCloudAgentServerError(f"Error in GraphQL response: {result['errors']}")
|
|
133
|
+
|
|
134
|
+
response.raise_for_status()
|
|
135
|
+
|
|
136
|
+
return result
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _retry_loop(
|
|
140
|
+
execute_retry: Callable,
|
|
141
|
+
max_retries: int,
|
|
142
|
+
backoff_factor: float,
|
|
143
|
+
retry_on_read_timeout: bool,
|
|
144
|
+
):
|
|
145
|
+
start_time = time.time()
|
|
146
|
+
retry_number = 0
|
|
147
|
+
error_msg_set = set()
|
|
148
|
+
requested_sleep_time = None
|
|
149
|
+
while True:
|
|
150
|
+
try:
|
|
151
|
+
return execute_retry()
|
|
152
|
+
except (HTTPError, RequestsConnectionError, RequestsReadTimeout, ChunkedEncodingError) as e:
|
|
153
|
+
retryable_error = False
|
|
154
|
+
if isinstance(e, HTTPError):
|
|
155
|
+
retryable_error = e.response.status_code in RETRY_STATUS_CODES
|
|
156
|
+
error_msg = e.response.status_code
|
|
157
|
+
requested_sleep_time = _get_retry_after_sleep_time(e.response.headers)
|
|
158
|
+
elif isinstance(e, RequestsConnectionError):
|
|
159
|
+
retryable_error = True
|
|
160
|
+
error_msg = str(e)
|
|
161
|
+
else:
|
|
162
|
+
retryable_error = retry_on_read_timeout
|
|
163
|
+
error_msg = str(e)
|
|
164
|
+
|
|
165
|
+
error_msg_set.add(error_msg)
|
|
166
|
+
if retryable_error and retry_number < max_retries:
|
|
167
|
+
retry_number += 1
|
|
168
|
+
sleep_time = 0
|
|
169
|
+
if requested_sleep_time:
|
|
170
|
+
sleep_time = requested_sleep_time
|
|
171
|
+
else:
|
|
172
|
+
sleep_time = backoff_factor * (2**retry_number)
|
|
173
|
+
|
|
174
|
+
logger.warning(
|
|
175
|
+
f"Error in Dagster Cloud request ({error_msg}). Retrying in"
|
|
176
|
+
f" {sleep_time} seconds..."
|
|
177
|
+
)
|
|
178
|
+
time.sleep(sleep_time)
|
|
179
|
+
else:
|
|
180
|
+
# Throw the error straight if no retries were involved
|
|
181
|
+
if max_retries == 0 or not retryable_error:
|
|
182
|
+
if isinstance(e, HTTPError):
|
|
183
|
+
raise DagsterCloudHTTPError(e) from e
|
|
184
|
+
else:
|
|
185
|
+
raise
|
|
186
|
+
else:
|
|
187
|
+
if len(error_msg_set) == 1:
|
|
188
|
+
status_code_msg = str(next(iter(error_msg_set)))
|
|
189
|
+
else:
|
|
190
|
+
status_code_msg = str(error_msg_set)
|
|
191
|
+
raise DagsterCloudAgentServerError(
|
|
192
|
+
f"Max retries ({max_retries}) exceeded, too many"
|
|
193
|
+
f" {status_code_msg} error responses."
|
|
194
|
+
) from e
|
|
195
|
+
except DagsterCloudMaintenanceException as e:
|
|
196
|
+
if time.time() - start_time > e.timeout:
|
|
197
|
+
raise
|
|
198
|
+
|
|
199
|
+
logger.warning(
|
|
200
|
+
"Dagster Cloud is currently unavailable due to scheduled maintenance. Retrying"
|
|
201
|
+
f" in {e.retry_interval} seconds..."
|
|
202
|
+
)
|
|
203
|
+
time.sleep(e.retry_interval)
|
|
204
|
+
except DagsterCloudAgentServerError:
|
|
205
|
+
raise
|
|
206
|
+
except Exception as e:
|
|
207
|
+
raise DagsterCloudAgentServerError(str(e)) from e
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class DagsterCloudGraphQLClient:
|
|
211
|
+
def __init__(
|
|
212
|
+
self,
|
|
213
|
+
url: str,
|
|
214
|
+
session: requests.Session,
|
|
215
|
+
headers: dict[str, Any] | None = None,
|
|
216
|
+
verify: bool = True,
|
|
217
|
+
timeout: int = DEFAULT_TIMEOUT,
|
|
218
|
+
cookies: dict[str, Any] | None = None,
|
|
219
|
+
proxies: dict[str, Any] | None = None,
|
|
220
|
+
max_retries: int = 0,
|
|
221
|
+
backoff_factor: float = DEFAULT_BACKOFF_FACTOR,
|
|
222
|
+
):
|
|
223
|
+
self.url = url
|
|
224
|
+
self.headers = headers
|
|
225
|
+
self.verify = verify
|
|
226
|
+
self.timeout = timeout
|
|
227
|
+
self.cookies = cookies
|
|
228
|
+
self._session = session
|
|
229
|
+
self._proxies = proxies
|
|
230
|
+
self._max_retries = max_retries
|
|
231
|
+
self._backoff_factor = backoff_factor
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def session(self) -> requests.Session:
|
|
235
|
+
return self._session
|
|
236
|
+
|
|
237
|
+
def execute(
|
|
238
|
+
self,
|
|
239
|
+
query: str,
|
|
240
|
+
variable_values: Mapping[str, Any] | None = None,
|
|
241
|
+
headers: Mapping[str, str] | None = None,
|
|
242
|
+
idempotent_mutation: bool = False,
|
|
243
|
+
):
|
|
244
|
+
if "mutation " in query and not idempotent_mutation:
|
|
245
|
+
all_headers = {
|
|
246
|
+
**(self.headers if self.headers is not None else {}),
|
|
247
|
+
**(headers if headers is not None else {}),
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
# mutations can be made idempotent if they use Idempotency-Key header
|
|
251
|
+
retry_on_read_timeout = bool(all_headers.get("Idempotency-Key"))
|
|
252
|
+
else:
|
|
253
|
+
retry_on_read_timeout = True
|
|
254
|
+
|
|
255
|
+
return _retry_loop(
|
|
256
|
+
lambda: self._execute_retry(query, variable_values, headers),
|
|
257
|
+
max_retries=self._max_retries,
|
|
258
|
+
backoff_factor=self._backoff_factor,
|
|
259
|
+
retry_on_read_timeout=retry_on_read_timeout,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
def _execute_retry(
|
|
263
|
+
self,
|
|
264
|
+
query: str,
|
|
265
|
+
variable_values: Mapping[str, Any] | None,
|
|
266
|
+
headers: Mapping[str, Any] | None,
|
|
267
|
+
):
|
|
268
|
+
response = self._session.post(
|
|
269
|
+
self.url,
|
|
270
|
+
headers={
|
|
271
|
+
**(self.headers if self.headers is not None else {}),
|
|
272
|
+
**(headers if headers is not None else {}),
|
|
273
|
+
"Content-type": "application/json",
|
|
274
|
+
},
|
|
275
|
+
cookies=self.cookies,
|
|
276
|
+
timeout=self.timeout,
|
|
277
|
+
verify=self.verify,
|
|
278
|
+
json={
|
|
279
|
+
"query": query,
|
|
280
|
+
"variables": variable_values if variable_values else {},
|
|
281
|
+
},
|
|
282
|
+
proxies=self._proxies,
|
|
283
|
+
)
|
|
284
|
+
try:
|
|
285
|
+
result = response.json()
|
|
286
|
+
if not isinstance(result, dict):
|
|
287
|
+
result = {}
|
|
288
|
+
except ValueError:
|
|
289
|
+
result = {}
|
|
290
|
+
|
|
291
|
+
if "errors" not in result and "data" not in result and "maintenance" not in result:
|
|
292
|
+
response.raise_for_status()
|
|
293
|
+
raise requests.HTTPError("Unexpected GraphQL response", response=response)
|
|
294
|
+
|
|
295
|
+
if "maintenance" in result:
|
|
296
|
+
maintenance_info = result["maintenance"]
|
|
297
|
+
raise DagsterCloudMaintenanceException(
|
|
298
|
+
message=maintenance_info.get("message"),
|
|
299
|
+
timeout=maintenance_info.get("timeout"),
|
|
300
|
+
retry_interval=maintenance_info.get("retry_interval"),
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
if "errors" in result:
|
|
304
|
+
raise DagsterCloudAgentServerError(f"Error in GraphQL response: {result['errors']}")
|
|
305
|
+
|
|
306
|
+
return result
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def get_agent_headers(config_value: dict[str, Any], scope: DagsterCloudInstanceScope):
|
|
310
|
+
return get_dagster_cloud_api_headers(
|
|
311
|
+
config_value["agent_token"],
|
|
312
|
+
scope=scope,
|
|
313
|
+
deployment_name=config_value.get("deployment"),
|
|
314
|
+
additional_headers=config_value.get("headers"),
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class HTTPAdapterWithSocketOptions(HTTPAdapter):
|
|
319
|
+
def __init__(self, *args, **kwargs):
|
|
320
|
+
self.socket_options = kwargs.pop("socket_options", None)
|
|
321
|
+
super().__init__(*args, **kwargs)
|
|
322
|
+
|
|
323
|
+
def init_poolmanager(self, *args, **kwargs):
|
|
324
|
+
if self.socket_options is not None:
|
|
325
|
+
kwargs["socket_options"] = self.socket_options
|
|
326
|
+
super().init_poolmanager(*args, **kwargs)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
@contextmanager
|
|
330
|
+
def create_graphql_requests_session(adapter_kwargs: Mapping[str, Any] | None = None):
|
|
331
|
+
with requests.Session() as session:
|
|
332
|
+
adapter = HTTPAdapterWithSocketOptions(**(adapter_kwargs or {}))
|
|
333
|
+
session.mount("https://", adapter)
|
|
334
|
+
session.mount("http://", adapter)
|
|
335
|
+
yield session
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def create_agent_http_client(
|
|
339
|
+
session: requests.Session,
|
|
340
|
+
config_value: dict[str, Any],
|
|
341
|
+
scope: DagsterCloudInstanceScope = DagsterCloudInstanceScope.DEPLOYMENT,
|
|
342
|
+
):
|
|
343
|
+
return DagsterCloudAgentHttpClient(
|
|
344
|
+
headers=get_agent_headers(config_value, scope=scope),
|
|
345
|
+
verify=config_value.get("verify", True),
|
|
346
|
+
timeout=config_value.get("timeout", DEFAULT_TIMEOUT),
|
|
347
|
+
cookies=config_value.get("cookies", {}),
|
|
348
|
+
# Requests library modifies proxies dictionary so create a copy
|
|
349
|
+
proxies=(
|
|
350
|
+
check.is_dict(config_value.get("proxies")).copy() if config_value.get("proxies") else {}
|
|
351
|
+
),
|
|
352
|
+
session=session,
|
|
353
|
+
max_retries=config_value.get("retries", DEFAULT_RETRIES),
|
|
354
|
+
backoff_factor=config_value.get("backoff_factor", DEFAULT_BACKOFF_FACTOR),
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def create_agent_graphql_client(
|
|
359
|
+
session: requests.Session,
|
|
360
|
+
url: str,
|
|
361
|
+
config_value: dict[str, Any],
|
|
362
|
+
scope: DagsterCloudInstanceScope = DagsterCloudInstanceScope.DEPLOYMENT,
|
|
363
|
+
):
|
|
364
|
+
return DagsterCloudGraphQLClient(
|
|
365
|
+
url=url,
|
|
366
|
+
headers=get_agent_headers(config_value, scope=scope),
|
|
367
|
+
verify=config_value.get("verify", True),
|
|
368
|
+
timeout=config_value.get("timeout", DEFAULT_TIMEOUT),
|
|
369
|
+
cookies=config_value.get("cookies", {}),
|
|
370
|
+
# Requests library modifies proxies dictionary so create a copy
|
|
371
|
+
proxies=(
|
|
372
|
+
check.is_dict(config_value.get("proxies")).copy() if config_value.get("proxies") else {}
|
|
373
|
+
),
|
|
374
|
+
session=session,
|
|
375
|
+
max_retries=config_value.get("retries", DEFAULT_RETRIES),
|
|
376
|
+
backoff_factor=config_value.get("backoff_factor", DEFAULT_BACKOFF_FACTOR),
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _get_retry_after_sleep_time(headers):
|
|
381
|
+
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
|
|
382
|
+
|
|
383
|
+
retry_after = headers.get("Retry-After")
|
|
384
|
+
if retry_after is None:
|
|
385
|
+
return None
|
|
386
|
+
|
|
387
|
+
if re.match(r"^\s*[0-9]+\s*$", retry_after):
|
|
388
|
+
seconds = int(retry_after)
|
|
389
|
+
else:
|
|
390
|
+
retry_date = parsedate_tz(retry_after)
|
|
391
|
+
if retry_date is None:
|
|
392
|
+
return None
|
|
393
|
+
retry_date = mktime_tz(retry_date)
|
|
394
|
+
seconds = retry_date - time.time()
|
|
395
|
+
|
|
396
|
+
return max(seconds, 0)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
@contextmanager
|
|
400
|
+
def create_cloud_webserver_client(
|
|
401
|
+
url: str,
|
|
402
|
+
api_token: str,
|
|
403
|
+
retries=3,
|
|
404
|
+
deployment_name: str | None = None,
|
|
405
|
+
headers: dict[str, Any] | None = None,
|
|
406
|
+
):
|
|
407
|
+
with create_graphql_requests_session(adapter_kwargs={}) as session:
|
|
408
|
+
yield DagsterCloudGraphQLClient(
|
|
409
|
+
session=session,
|
|
410
|
+
url=f"{url}/graphql",
|
|
411
|
+
headers={
|
|
412
|
+
**get_dagster_cloud_api_headers(
|
|
413
|
+
api_token,
|
|
414
|
+
scope=DagsterCloudInstanceScope.DEPLOYMENT,
|
|
415
|
+
deployment_name=deployment_name,
|
|
416
|
+
),
|
|
417
|
+
**(headers if headers else {}),
|
|
418
|
+
},
|
|
419
|
+
max_retries=retries,
|
|
420
|
+
)
|
|
File without changes
|