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.
Files changed (71) hide show
  1. dagster_cloud_cli/__init__.py +5 -0
  2. dagster_cloud_cli/commands/__init__.py +0 -0
  3. dagster_cloud_cli/commands/branch_deployment/__init__.py +154 -0
  4. dagster_cloud_cli/commands/ci/__init__.py +1207 -0
  5. dagster_cloud_cli/commands/ci/checks.py +135 -0
  6. dagster_cloud_cli/commands/ci/report.py +49 -0
  7. dagster_cloud_cli/commands/ci/state.py +129 -0
  8. dagster_cloud_cli/commands/ci/utils.py +22 -0
  9. dagster_cloud_cli/commands/config.py +250 -0
  10. dagster_cloud_cli/commands/deployment/__init__.py +108 -0
  11. dagster_cloud_cli/commands/deployment/alert_policies/__init__.py +0 -0
  12. dagster_cloud_cli/commands/deployment/alert_policies/commands.py +52 -0
  13. dagster_cloud_cli/commands/integration/__init__.py +6 -0
  14. dagster_cloud_cli/commands/integration/atlan/__init__.py +94 -0
  15. dagster_cloud_cli/commands/job/__init__.py +117 -0
  16. dagster_cloud_cli/commands/metrics.py +114 -0
  17. dagster_cloud_cli/commands/organization/__init__.py +134 -0
  18. dagster_cloud_cli/commands/organization/saml/__init__.py +0 -0
  19. dagster_cloud_cli/commands/organization/saml/commands.py +85 -0
  20. dagster_cloud_cli/commands/run/__init__.py +27 -0
  21. dagster_cloud_cli/commands/serverless/Dockerfile +38 -0
  22. dagster_cloud_cli/commands/serverless/Dockerfile.dagster-cloud-cli +20 -0
  23. dagster_cloud_cli/commands/serverless/__init__.py +560 -0
  24. dagster_cloud_cli/commands/workspace/__init__.py +358 -0
  25. dagster_cloud_cli/config/__init__.py +6 -0
  26. dagster_cloud_cli/config/jinja_template_loader.py +45 -0
  27. dagster_cloud_cli/config/list_merge_strategies.py +77 -0
  28. dagster_cloud_cli/config/loader.py +23 -0
  29. dagster_cloud_cli/config/merger.py +92 -0
  30. dagster_cloud_cli/config/models.py +76 -0
  31. dagster_cloud_cli/config_utils.py +506 -0
  32. dagster_cloud_cli/core/__init__.py +0 -0
  33. dagster_cloud_cli/core/agent_queue.py +3 -0
  34. dagster_cloud_cli/core/alert_types.py +26 -0
  35. dagster_cloud_cli/core/artifacts.py +192 -0
  36. dagster_cloud_cli/core/docker_runner.py +57 -0
  37. dagster_cloud_cli/core/errors.py +32 -0
  38. dagster_cloud_cli/core/graphql_client.py +420 -0
  39. dagster_cloud_cli/core/headers/__init__.py +0 -0
  40. dagster_cloud_cli/core/headers/auth.py +16 -0
  41. dagster_cloud_cli/core/headers/impl.py +31 -0
  42. dagster_cloud_cli/core/headers/versioning/__init__.py +0 -0
  43. dagster_cloud_cli/core/headers/versioning/constants.py +2 -0
  44. dagster_cloud_cli/core/pex_builder/__init__.py +8 -0
  45. dagster_cloud_cli/core/pex_builder/code_location.py +126 -0
  46. dagster_cloud_cli/core/pex_builder/deploy.py +255 -0
  47. dagster_cloud_cli/core/pex_builder/deps.py +495 -0
  48. dagster_cloud_cli/core/pex_builder/git_context.py +29 -0
  49. dagster_cloud_cli/core/pex_builder/github_context.py +178 -0
  50. dagster_cloud_cli/core/pex_builder/gitlab_context.py +30 -0
  51. dagster_cloud_cli/core/pex_builder/parse_workspace.py +39 -0
  52. dagster_cloud_cli/core/pex_builder/pex_registry.py +148 -0
  53. dagster_cloud_cli/core/pex_builder/platforms.py +6105 -0
  54. dagster_cloud_cli/core/pex_builder/selftest.py +14 -0
  55. dagster_cloud_cli/core/pex_builder/source.py +166 -0
  56. dagster_cloud_cli/core/pex_builder/util.py +165 -0
  57. dagster_cloud_cli/core/workspace.py +145 -0
  58. dagster_cloud_cli/docker_utils.py +98 -0
  59. dagster_cloud_cli/entrypoint.py +209 -0
  60. dagster_cloud_cli/gql.py +1025 -0
  61. dagster_cloud_cli/pex_utils.py +180 -0
  62. dagster_cloud_cli/py.typed +0 -0
  63. dagster_cloud_cli/types.py +78 -0
  64. dagster_cloud_cli/ui.py +101 -0
  65. dagster_cloud_cli/utils.py +107 -0
  66. dagster_cloud_cli/version.py +1 -0
  67. dagster_cloud_cli-1.12.16.dist-info/METADATA +31 -0
  68. dagster_cloud_cli-1.12.16.dist-info/RECORD +71 -0
  69. dagster_cloud_cli-1.12.16.dist-info/WHEEL +5 -0
  70. dagster_cloud_cli-1.12.16.dist-info/entry_points.txt +3 -0
  71. 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