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,135 @@
1
+ import os
2
+ import pathlib
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+
6
+ import pydantic
7
+
8
+ from dagster_cloud_cli import gql, ui
9
+ from dagster_cloud_cli.config.models import load_dagster_cloud_yaml
10
+ from dagster_cloud_cli.config_utils import TOKEN_ENV_VAR_NAME
11
+
12
+
13
+ def get_validation_errors(validation_error: pydantic.ValidationError) -> list[str]:
14
+ errors = []
15
+ for error in validation_error.errors():
16
+ if "type" in error:
17
+ location = ".".join([str(part) for part in error["loc"] if part != "__root__"])
18
+ if error["type"] == "value_error.missing":
19
+ errors.append(f"expected '{location}': missing required field")
20
+ elif error["type"] == "value_error.extra":
21
+ errors.append(f"unexpected '{location}': unknown field")
22
+ else:
23
+ errors.append(f"{error['type']} at '{location}': {error['msg']}")
24
+ return errors
25
+
26
+
27
+ @dataclass
28
+ class CheckResult:
29
+ errors: list[str] = field(default_factory=list)
30
+ messages: list[str] = field(default_factory=list)
31
+
32
+
33
+ def check_dagster_cloud_yaml(yaml_path: pathlib.Path) -> CheckResult:
34
+ result = CheckResult()
35
+
36
+ if not yaml_path.exists():
37
+ result.errors.append(f"No such file {yaml_path}")
38
+ return result
39
+
40
+ yaml_text = yaml_path.read_text()
41
+ if not yaml_text.strip():
42
+ result.errors.append(f"Unexpected blank file {yaml_path}")
43
+ return result
44
+
45
+ try:
46
+ parsed = load_dagster_cloud_yaml(yaml_path.read_text())
47
+ except pydantic.ValidationError as err:
48
+ for error in get_validation_errors(err):
49
+ result.errors.append(error)
50
+ return result
51
+
52
+ for location in parsed.locations:
53
+ if location.build and location.build.directory:
54
+ build_path = yaml_path.parent / location.build.directory
55
+ if not build_path.is_dir():
56
+ result.errors.append(
57
+ f"Build directory {build_path} not found for location"
58
+ f" {location.location_name} at {build_path.absolute()}"
59
+ )
60
+ return result
61
+
62
+
63
+ class Check(Enum):
64
+ error = "error"
65
+ warn = "warn"
66
+ skip = "skip"
67
+
68
+
69
+ class Verdict(Enum):
70
+ failed = "failed"
71
+ warning = "warning"
72
+ skipped = "skipped"
73
+ passed = "passed"
74
+
75
+
76
+ def handle_result(
77
+ result: CheckResult,
78
+ check: Check,
79
+ prefix_message: str,
80
+ success_message: str,
81
+ failure_message: str,
82
+ ) -> Verdict:
83
+ def full_msg(msg):
84
+ return prefix_message + msg
85
+
86
+ def passed(msg):
87
+ ui.print("✅" + full_msg(msg))
88
+
89
+ def warning(msg):
90
+ ui.print("🟡" + ui.yellow(full_msg(msg)))
91
+
92
+ def failed(msg):
93
+ ui.print("🚫" + ui.red(full_msg(msg)))
94
+
95
+ def print_indented(msgs):
96
+ lines = "\n".join(msgs).splitlines(keepends=False)
97
+ for line in lines:
98
+ ui.print(" | " + line)
99
+
100
+ if check == Check.skip:
101
+ return Verdict.skipped
102
+
103
+ if result.errors:
104
+ if check == Check.error:
105
+ failed(failure_message)
106
+ print_indented(result.messages)
107
+ print_indented(result.errors)
108
+ ui.print("\n")
109
+ return Verdict.failed
110
+ elif check == Check.warn:
111
+ warning(failure_message)
112
+ print_indented(result.messages)
113
+ print_indented(result.errors)
114
+ ui.print("\n")
115
+ return Verdict.warning
116
+
117
+ passed(success_message)
118
+ print_indented(result.messages)
119
+ ui.print("\n")
120
+ return Verdict.passed
121
+
122
+
123
+ def check_connect_dagster_cloud(url) -> CheckResult:
124
+ if TOKEN_ENV_VAR_NAME not in os.environ:
125
+ return CheckResult([f"{TOKEN_ENV_VAR_NAME} not set"])
126
+ result = CheckResult()
127
+ result.messages.append(f"Connecting to {url} using {TOKEN_ENV_VAR_NAME}")
128
+ with gql.graphql_client_from_url(url, os.environ[TOKEN_ENV_VAR_NAME]) as client:
129
+ try:
130
+ gql.get_organization_settings(client)
131
+ result.messages.append("Connection successful")
132
+ except Exception as err:
133
+ result.errors.append(f"Failed to connect to {url}: {err}")
134
+
135
+ return result
@@ -0,0 +1,49 @@
1
+ from dagster_cloud_cli.commands.ci import state
2
+
3
+ STATUS_IMAGES = {
4
+ state.LocationStatus.success: (
5
+ "https://raw.githubusercontent.com/dagster-io/dagster-cloud-action/main/assets/success.png"
6
+ ),
7
+ state.LocationStatus.pending: (
8
+ "https://raw.githubusercontent.com/dagster-io/dagster-cloud-action/main/assets/pending.png"
9
+ ),
10
+ state.LocationStatus.failed: (
11
+ "https://raw.githubusercontent.com/dagster-io/dagster-cloud-action/main/assets/failed.png"
12
+ ),
13
+ }
14
+
15
+ STATUS_MESSAGES = {
16
+ state.LocationStatus.success: "View in Cloud",
17
+ state.LocationStatus.pending: "Building...",
18
+ state.LocationStatus.failed: "Deploy failed",
19
+ }
20
+
21
+
22
+ def markdown_report(location_states: list[state.LocationState]):
23
+ markdown = []
24
+ markdown.append(
25
+ """
26
+ | Location | Status | Link | Updated |
27
+ | ----------------- | --------------- | ------- | --------------- |
28
+ """
29
+ )
30
+ for location_state in location_states:
31
+ if not location_state.selected or not location_state.history:
32
+ continue
33
+ last_status = location_state.history[-1]
34
+ image_url = STATUS_IMAGES[last_status.status]
35
+ message = STATUS_MESSAGES[last_status.status]
36
+ if last_status.status == state.LocationStatus.success or not location_state.status_url:
37
+ link_url = f"{location_state.url}/{location_state.deployment_name}/locations"
38
+ else:
39
+ link_url = location_state.status_url
40
+ status_image = f'[<img src="{image_url}" width=25 height=25/>]({link_url})'
41
+ status_message = f"[{message}]({link_url})"
42
+
43
+ time_str = last_status.timestamp.strftime("%b %d, %Y at %I:%M %p (%Z)")
44
+ markdown.append(
45
+ f"| {location_state.location_name} | {status_image} | {status_message} |"
46
+ f" {time_str} |\n"
47
+ )
48
+
49
+ return "".join(markdown)
@@ -0,0 +1,129 @@
1
+ import datetime
2
+ import json
3
+ import os
4
+ from abc import ABCMeta, abstractmethod
5
+ from enum import Enum
6
+ from typing import Literal
7
+
8
+ from dagster_shared.serdes.objects.models import DefsStateInfo
9
+ from pydantic import BaseModel, Extra, Field
10
+
11
+ from dagster_cloud_cli.config import models
12
+
13
+
14
+ class DockerBuildOutput(BaseModel, extra=Extra.forbid):
15
+ strategy: Literal["docker"] = "docker"
16
+ python_version: str | None = None
17
+ image: str
18
+
19
+
20
+ class PexBuildOutput(BaseModel, extra=Extra.forbid):
21
+ strategy: Literal["python-executable"] = "python-executable"
22
+ python_version: str
23
+ image: str | None # if None we determine the image from the python_version and agent version
24
+ pex_tag: str
25
+
26
+
27
+ class BuildMetadata(BaseModel):
28
+ git_url: str | None
29
+ commit_hash: str | None
30
+ build_config: models.Build | None # copied from dagster_cloud.yaml
31
+
32
+
33
+ class LocationStatus(Enum):
34
+ success = "success"
35
+ pending = "pending"
36
+ failed = "failed"
37
+
38
+
39
+ class StatusChange(BaseModel):
40
+ timestamp: datetime.datetime
41
+ status: LocationStatus
42
+ log: str
43
+
44
+
45
+ class LocationState(BaseModel, extra=Extra.forbid):
46
+ # we intentionally don't save api_token here for security reasons
47
+ url: str
48
+ deployment_name: str
49
+ location_file: str
50
+ location_name: str
51
+ is_branch_deployment: bool
52
+ selected: bool = True
53
+ build: BuildMetadata
54
+ build_output: DockerBuildOutput | PexBuildOutput | None = Field(None, discriminator="strategy")
55
+ defs_state_info: DefsStateInfo | None = None
56
+ status_url: str | None # link to cicd run url when building and dagster cloud url when done
57
+ history: list[StatusChange] = []
58
+ project_dir: str | None = None
59
+
60
+ def add_status_change(self, status: LocationStatus, log: str):
61
+ self.history.append(
62
+ StatusChange(
63
+ timestamp=datetime.datetime.now(datetime.timezone.utc),
64
+ status=status,
65
+ log=log,
66
+ )
67
+ )
68
+
69
+
70
+ class Store(metaclass=ABCMeta):
71
+ @abstractmethod
72
+ def load(self, location_name: str) -> LocationState: ...
73
+
74
+ @abstractmethod
75
+ def save(self, location_state: LocationState): ...
76
+
77
+ @abstractmethod
78
+ def list_locations(self) -> list[LocationState]: ...
79
+
80
+ def list_selected_locations(self) -> list[LocationState]:
81
+ return [location for location in self.list_locations() if location.selected]
82
+
83
+
84
+ class FileStore(Store):
85
+ def __init__(self, statedir: str):
86
+ self.statedir = os.path.abspath(statedir)
87
+ self.location_file_prefix = "location-"
88
+ if not os.path.isdir(self.statedir):
89
+ os.makedirs(self.statedir)
90
+
91
+ def __repr__(self):
92
+ return f"<FileStore(statedir={self.statedir!r})>"
93
+
94
+ def _get_filepath(self, location_name) -> str:
95
+ return os.path.join(self.statedir, f"{self.location_file_prefix}{location_name}.json")
96
+
97
+ def list_locations(self) -> list[LocationState]:
98
+ return [
99
+ self._location_from_file(os.path.join(self.statedir, filename))
100
+ for filename in os.listdir(self.statedir)
101
+ if filename.startswith(self.location_file_prefix)
102
+ ]
103
+
104
+ def load(self, location_name: str) -> LocationState:
105
+ filepath = self._get_filepath(location_name)
106
+ if not filepath:
107
+ raise KeyError(f"No saved state for {location_name} at {filepath}")
108
+ return self._location_from_file(self._get_filepath(location_name))
109
+
110
+ def _location_from_file(self, filepath: str) -> LocationState:
111
+ with open(filepath, encoding="utf-8") as f:
112
+ return LocationState.parse_obj(json.load(f))
113
+
114
+ def save(self, location_state: LocationState):
115
+ filepath = self._get_filepath(location_state.location_name)
116
+ with open(filepath, "w", encoding="utf-8") as f:
117
+ f.write(location_state.json())
118
+
119
+ def deselect(self, location_names: list[str]):
120
+ locations = [self.load(location_name) for location_name in location_names]
121
+ for location in locations:
122
+ location.selected = False
123
+ self.save(location)
124
+
125
+ def select(self, location_names: list[str]):
126
+ locations = [self.load(location_name) for location_name in location_names]
127
+ for location in locations:
128
+ location.selected = True
129
+ self.save(location)
@@ -0,0 +1,22 @@
1
+ from contextlib import contextmanager
2
+ from typing import Any
3
+
4
+ from dagster_shared import check
5
+
6
+ from dagster_cloud_cli import gql
7
+ from dagster_cloud_cli.config_utils import get_user_token
8
+
9
+
10
+ @contextmanager
11
+ def client_from_env(url: str, deployment: str | None = None):
12
+ if deployment:
13
+ url = url + "/" + deployment
14
+ with gql.graphql_client_from_url(
15
+ url, check.not_none(get_user_token()), deployment_name=deployment
16
+ ) as client:
17
+ yield client
18
+
19
+
20
+ def get_registry_info(url: str) -> dict[str, Any]:
21
+ with client_from_env(url) as client:
22
+ return gql.get_ecr_info(client)
@@ -0,0 +1,250 @@
1
+ import json
2
+ import webbrowser
3
+ from enum import Enum
4
+ from http.server import BaseHTTPRequestHandler, HTTPServer
5
+ from urllib import parse
6
+
7
+ from dagster_shared.plus.config import DagsterPlusCliConfig
8
+ from dagster_shared.plus.login_server import start_login_server
9
+ from typer import Argument, Context, Option, Typer
10
+
11
+ from dagster_cloud_cli import gql, ui
12
+ from dagster_cloud_cli.config_utils import (
13
+ available_deployment_names,
14
+ dagster_cloud_options,
15
+ read_config,
16
+ write_config,
17
+ )
18
+
19
+ app = Typer(help="Configure the Dagster Cloud CLI.")
20
+
21
+
22
+ @app.command()
23
+ @dagster_cloud_options()
24
+ def set_deployment(
25
+ ctx: Context,
26
+ deployment: str = Argument(..., autocompletion=available_deployment_names),
27
+ ):
28
+ """Set the default deployment for CLI commands."""
29
+ deployments = available_deployment_names(ctx=ctx)
30
+ if not deployment or deployment not in deployments:
31
+ raise ui.error(f"Deployment {ui.as_code(deployment)} not found")
32
+
33
+ config = read_config()
34
+ new_config = DagsterPlusCliConfig(
35
+ **{**config.__dict__, "default_deployment": deployment},
36
+ )
37
+ write_config(new_config)
38
+
39
+ ui.print(f"Default deployment changed to {ui.as_code(deployment)}")
40
+
41
+
42
+ @app.command()
43
+ def view(
44
+ show_token: bool = Option(
45
+ False,
46
+ "--show-token",
47
+ "-s",
48
+ help="Whether to display the user token in plaintext.",
49
+ ),
50
+ ):
51
+ """View the current CLI configuration."""
52
+ config = read_config()
53
+ config_to_display = {k: v for k, v in config.__dict__.items() if v is not None}
54
+ if not show_token and config.user_token:
55
+ config_to_display["user_token"] = ui.censor_token(config.user_token)
56
+ ui.print_yaml(config_to_display)
57
+
58
+
59
+ app_configure = Typer()
60
+
61
+
62
+ def create_token_callback_handler(nonce: str) -> type[BaseHTTPRequestHandler]:
63
+ class TokenCallbackHandler(BaseHTTPRequestHandler):
64
+ def _send_shared_headers(self):
65
+ self.send_header("Access-Control-Allow-Origin", "*")
66
+ self.send_header("Access-Control-Allow-Methods", "POST,OPTIONS")
67
+ self.send_header("Access-Control-Allow-Headers", "Content-Type")
68
+
69
+ def _error(self, code, message=None):
70
+ self.send_error(code, message)
71
+ self._send_shared_headers()
72
+ self.end_headers()
73
+
74
+ def do_OPTIONS(self):
75
+ self.send_response(200)
76
+ self._send_shared_headers()
77
+ self.end_headers()
78
+
79
+ def do_POST(self):
80
+ url = parse.urlparse(self.path)
81
+
82
+ if url.path != "/callback":
83
+ self._error(404, "Page not found")
84
+ return
85
+
86
+ content = self.rfile.read(int(self.headers["Content-Length"]))
87
+ body = json.loads(content)
88
+
89
+ if not body.get("nonce") or body.get("nonce") != nonce:
90
+ self._error(400, "Invalid nonce")
91
+ return
92
+ if not body.get("token"):
93
+ self._error(400, "No user token provided")
94
+ return
95
+ if not body.get("organization"):
96
+ self._error(400, "No organization provided")
97
+ return
98
+
99
+ organization = body.get("organization")
100
+ token = body.get("token")
101
+
102
+ self.send_response(200)
103
+ self._send_shared_headers()
104
+ response = b'{ "ok": true }'
105
+ self.send_header("Content-length", str(len(response)))
106
+ self.end_headers()
107
+
108
+ self.wfile.write(response)
109
+
110
+ # Pass back result values
111
+ assert isinstance(self.server, TokenServer)
112
+ self.server.set_result(organization=organization, token=token)
113
+ self.server.shutdown()
114
+
115
+ # Overridden so that the webserver doesn't log requests to the console
116
+
117
+ def log_request(self, code="-", size="-"):
118
+ return
119
+
120
+ return TokenCallbackHandler
121
+
122
+
123
+ class TokenServer(HTTPServer):
124
+ organization: str | None = None
125
+ token: str | None = None
126
+
127
+ def __init__(self, host: tuple[str, int], nonce: str):
128
+ super().__init__(host, create_token_callback_handler(nonce))
129
+
130
+ def shutdown(self):
131
+ # Stop serving the token server
132
+ # https://stackoverflow.com/a/36017741
133
+ setattr(self, "_BaseServer__shutdown_request", True)
134
+
135
+ def set_result(self, organization: str, token: str):
136
+ self.organization = organization
137
+ self.token = token
138
+
139
+ def get_organization(self) -> str | None:
140
+ return self.organization
141
+
142
+ def get_token(self) -> str | None:
143
+ return self.token
144
+
145
+
146
+ class SetupAuthMethod(Enum):
147
+ WEB = "web"
148
+ CLI = "cli"
149
+
150
+
151
+ def _settings_method_input(api_token: str):
152
+ if api_token:
153
+ choices = [
154
+ ui.choice(SetupAuthMethod.CLI, "Authenticate using token or keep current settings"),
155
+ ui.choice(SetupAuthMethod.WEB, "Authenticate in browser"),
156
+ ]
157
+ else:
158
+ choices = [
159
+ ui.choice(SetupAuthMethod.WEB, "Authenticate in browser"),
160
+ ui.choice(SetupAuthMethod.CLI, "Authenticate using token"),
161
+ ]
162
+ return ui.list_input(
163
+ "How would you like to authenticate the CLI?",
164
+ choices,
165
+ )
166
+
167
+
168
+ def _setup(organization: str, deployment: str, api_token: str):
169
+ setup_method = _settings_method_input(api_token)
170
+
171
+ if setup_method == SetupAuthMethod.WEB:
172
+ server, url = start_login_server()
173
+
174
+ try:
175
+ webbrowser.open(url, new=0, autoraise=True)
176
+ ui.print(
177
+ f"Opening browser...\nIf a window does not open automatically, visit {url} to"
178
+ " finish authorization"
179
+ )
180
+ except webbrowser.Error as e:
181
+ ui.warn(f"Error launching web browser: {e}\n\nTo finish authorization, visit {url}\n")
182
+
183
+ server.serve_forever()
184
+ new_org = server.get_organization()
185
+ new_api_token = server.get_token()
186
+ ui.print(f"Authorized for organization {ui.as_code(str(new_org))}\n")
187
+ else:
188
+ new_org = ui.input("Dagster Cloud organization:", default=organization or "") or None
189
+ if not api_token:
190
+ deployment_name = deployment if deployment else "prod"
191
+ ui.print(
192
+ "\nTo create a new user token or find an existing token, visit"
193
+ f" https://dagster.cloud/{new_org}/{deployment_name}/org-settings/tokens"
194
+ )
195
+ new_api_token = (
196
+ ui.password_input("Dagster Cloud user token:", default=api_token or "") or None
197
+ )
198
+
199
+ # Attempt to fetch deployment names from server, fallback to a text input upon failure
200
+ deployment_names = []
201
+ if new_org and new_api_token:
202
+ try:
203
+ with gql.graphql_client_from_url(gql.url_from_config(new_org), new_api_token) as client:
204
+ deployments = gql.fetch_full_deployments(client)
205
+ deployment_names = [deployment["deploymentName"] for deployment in deployments]
206
+ except:
207
+ ui.warn(
208
+ "Could not fetch deployment names from server - organization or user token may be"
209
+ " set incorrectly."
210
+ )
211
+
212
+ new_deployment: str | None = None
213
+ if deployment_names:
214
+ options = ["None"] + deployment_names
215
+ new_deployment = ui.list_input(
216
+ "Default deployment:",
217
+ choices=options,
218
+ default=deployment if deployment in options else "None",
219
+ )
220
+ if new_deployment == "None":
221
+ new_deployment = None
222
+ else:
223
+ new_deployment = ui.input("Default deployment:", default=deployment or "") or None
224
+
225
+ write_config(
226
+ DagsterPlusCliConfig(
227
+ organization=new_org,
228
+ default_deployment=new_deployment,
229
+ user_token=new_api_token,
230
+ )
231
+ )
232
+
233
+
234
+ @app.command()
235
+ @dagster_cloud_options(allow_empty=True)
236
+ def setup(organization: str, deployment: str, api_token: str):
237
+ """Populate the CLI configuration."""
238
+ _setup(organization, deployment, api_token)
239
+
240
+
241
+ app_configure = Typer(hidden=True)
242
+
243
+
244
+ # Legacy, to support the old `dagster-cloud configure` path
245
+ # New command is `dagster-cloud config setup`
246
+ @app_configure.command(name="configure", hidden=True)
247
+ @dagster_cloud_options(allow_empty=True)
248
+ def configure_legacy(organization: str, deployment: str, api_token: str):
249
+ """Populate the CLI configuration."""
250
+ _setup(organization, deployment, api_token)
@@ -0,0 +1,108 @@
1
+ from pathlib import Path
2
+ from typing import Annotated
3
+
4
+ import yaml
5
+ from typer import Argument, Typer
6
+
7
+ from dagster_cloud_cli import gql, ui
8
+ from dagster_cloud_cli.config_utils import dagster_cloud_options
9
+ from dagster_cloud_cli.core.artifacts import download_artifact, upload_artifact
10
+ from dagster_cloud_cli.core.headers.auth import DagsterCloudInstanceScope
11
+ from dagster_cloud_cli.utils import create_stub_app
12
+
13
+ try:
14
+ from dagster_cloud_cli.commands.deployment.alert_policies.commands import (
15
+ app as alert_policies_app,
16
+ )
17
+ except ImportError:
18
+ alert_policies_app = create_stub_app("dagster-cloud")
19
+
20
+ app = Typer(help="Customize your Dagster Cloud deployment.")
21
+
22
+ settings_app = Typer(help="Customize your deployment settings.")
23
+ app.add_typer(settings_app, name="settings", no_args_is_help=True)
24
+ app.add_typer(alert_policies_app, name="alert-policies", no_args_is_help=True)
25
+
26
+
27
+ @settings_app.command(name="set-from-file")
28
+ @dagster_cloud_options(allow_empty=True, requires_url=True)
29
+ def set_from_filecommand(
30
+ api_token: str,
31
+ url: str,
32
+ deployment: str | None,
33
+ file_path: Path = Argument(..., readable=True, metavar="SETTINGS_FILE_PATH"),
34
+ ):
35
+ """Set the Dagster Cloud deployment settings from a YAML file."""
36
+ with open(file_path, encoding="utf8") as f:
37
+ settings = {"settings": yaml.safe_load(f) or {}}
38
+ with gql.graphql_client_from_url(url, api_token, deployment_name=deployment) as client:
39
+ gql.set_deployment_settings(client, settings)
40
+
41
+
42
+ @settings_app.command(name="get")
43
+ @dagster_cloud_options(allow_empty=True, requires_url=True)
44
+ def get_command(
45
+ api_token: str,
46
+ url: str,
47
+ deployment: str | None,
48
+ ):
49
+ """Get the Dagster Cloud deployment settings."""
50
+ with gql.graphql_client_from_url(url, api_token, deployment_name=deployment) as client:
51
+ settings = gql.get_deployment_settings(client)
52
+ ui.print_yaml(settings)
53
+
54
+
55
+ @app.command(name="upload-artifact")
56
+ @dagster_cloud_options(allow_empty=True, requires_url=True)
57
+ def upload_artifact_command(
58
+ key: Annotated[
59
+ str,
60
+ Argument(
61
+ help="The key for the artifact being uploaded.",
62
+ ),
63
+ ],
64
+ path: Annotated[
65
+ str,
66
+ Argument(
67
+ help="The path to the file to upload.",
68
+ ),
69
+ ],
70
+ api_token: str,
71
+ url: str,
72
+ ):
73
+ """Upload a deployment scoped artifact."""
74
+ upload_artifact(
75
+ url=url,
76
+ scope=DagsterCloudInstanceScope.DEPLOYMENT,
77
+ api_token=api_token,
78
+ key=key,
79
+ path=path,
80
+ )
81
+
82
+
83
+ @app.command(name="download-artifact")
84
+ @dagster_cloud_options(allow_empty=True, requires_url=True)
85
+ def download_artifact_command(
86
+ key: Annotated[
87
+ str,
88
+ Argument(
89
+ help="The key for the artifact to download.",
90
+ ),
91
+ ],
92
+ path: Annotated[
93
+ str,
94
+ Argument(
95
+ help="Path to the file that the contents should be written to.",
96
+ ),
97
+ ],
98
+ api_token: str,
99
+ url: str,
100
+ ):
101
+ """Download a deployment scoped artifact."""
102
+ download_artifact(
103
+ url=url,
104
+ scope=DagsterCloudInstanceScope.DEPLOYMENT,
105
+ api_token=api_token,
106
+ key=key,
107
+ path=path,
108
+ )