dagster-cloud-cli 1.13.7__tar.gz → 1.13.9__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.
Files changed (88) hide show
  1. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/PKG-INFO +2 -2
  2. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/commands/ci/__init__.py +9 -4
  3. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/commands/ci/checks.py +2 -2
  4. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/commands/deployment/__init__.py +2 -2
  5. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/commands/deployment/alert_policies/commands.py +2 -2
  6. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/commands/organization/__init__.py +2 -2
  7. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/commands/serverless/__init__.py +12 -5
  8. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/commands/workspace/__init__.py +2 -2
  9. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/config/models.py +2 -2
  10. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/config_utils.py +2 -2
  11. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/core/graphql_client.py +3 -0
  12. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/core/pex_builder/deps.py +2 -6
  13. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/core/pex_builder/parse_workspace.py +2 -2
  14. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/core/pex_builder/pex_registry.py +20 -1
  15. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/core/pex_builder/source.py +8 -13
  16. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/docker_utils.py +26 -6
  17. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/gql.py +2 -0
  18. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/types.py +1 -4
  19. dagster_cloud_cli-1.13.9/dagster_cloud_cli/version.py +1 -0
  20. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli_tests/commands/ci/test_ci_template.py +2 -2
  21. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli_tests/config/test_template_loader.py +2 -2
  22. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli_tests/conftest.py +1 -1
  23. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli_tests/test_check.py +1 -1
  24. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli_tests/test_ci_commands.py +1 -1
  25. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli_tests/test_pyproject_toml_support.py +39 -2
  26. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/pyproject.toml +6 -11
  27. dagster_cloud_cli-1.13.7/dagster_cloud_cli/version.py +0 -1
  28. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/.gitignore +0 -0
  29. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/LICENSE +0 -0
  30. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/__init__.py +0 -0
  31. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/commands/__init__.py +0 -0
  32. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/commands/branch_deployment/__init__.py +0 -0
  33. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/commands/ci/report.py +0 -0
  34. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/commands/ci/state.py +0 -0
  35. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/commands/ci/utils.py +0 -0
  36. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/commands/config.py +0 -0
  37. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/commands/deployment/alert_policies/__init__.py +0 -0
  38. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/commands/integration/__init__.py +0 -0
  39. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/commands/integration/atlan/__init__.py +0 -0
  40. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/commands/job/__init__.py +0 -0
  41. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/commands/metrics.py +0 -0
  42. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/commands/organization/saml/__init__.py +0 -0
  43. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/commands/organization/saml/commands.py +0 -0
  44. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/commands/run/__init__.py +0 -0
  45. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/commands/serverless/Dockerfile +0 -0
  46. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/commands/serverless/Dockerfile.dagster-cloud-cli +0 -0
  47. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/config/__init__.py +0 -0
  48. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/config/jinja_template_loader.py +0 -0
  49. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/config/list_merge_strategies.py +0 -0
  50. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/config/loader.py +0 -0
  51. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/config/merger.py +0 -0
  52. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/core/__init__.py +0 -0
  53. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/core/agent_queue.py +0 -0
  54. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/core/alert_types.py +0 -0
  55. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/core/artifacts.py +0 -0
  56. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/core/docker_runner.py +0 -0
  57. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/core/errors.py +0 -0
  58. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/core/headers/__init__.py +0 -0
  59. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/core/headers/auth.py +0 -0
  60. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/core/headers/impl.py +0 -0
  61. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/core/headers/versioning/__init__.py +0 -0
  62. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/core/headers/versioning/constants.py +0 -0
  63. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/core/pex_builder/__init__.py +0 -0
  64. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/core/pex_builder/code_location.py +0 -0
  65. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/core/pex_builder/deploy.py +0 -0
  66. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/core/pex_builder/git_context.py +0 -0
  67. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/core/pex_builder/github_context.py +0 -0
  68. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/core/pex_builder/gitlab_context.py +0 -0
  69. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/core/pex_builder/platforms.py +0 -0
  70. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/core/pex_builder/selftest.py +0 -0
  71. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/core/pex_builder/util.py +0 -0
  72. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/core/workspace.py +0 -0
  73. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/entrypoint.py +0 -0
  74. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/pex_utils.py +0 -0
  75. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/py.typed +0 -0
  76. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/ui.py +0 -0
  77. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli/utils.py +0 -0
  78. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli_tests/__init__.py +0 -0
  79. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli_tests/commands/ci/integration/__init__.py +0 -0
  80. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli_tests/commands/integration/test_atlan_integration_cli.py +0 -0
  81. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli_tests/config/test_config_loader.py +0 -0
  82. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli_tests/config/test_defaults_merging.py +0 -0
  83. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli_tests/test_artifacts.py +0 -0
  84. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli_tests/test_gql.py +0 -0
  85. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli_tests/test_metrics.py +0 -0
  86. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/dagster_cloud_cli_tests/test_utils.py +0 -0
  87. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/tox.ini +0 -0
  88. {dagster_cloud_cli-1.13.7 → dagster_cloud_cli-1.13.9}/uv.lock +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dagster-cloud-cli
3
- Version: 1.13.7
3
+ Version: 1.13.9
4
4
  Project-URL: Homepage, https://github.com/dagster-io/dagster/tree/master/python_modules/libraries/dagster-cloud-cli
5
5
  Author-email: Dagster Labs <hello@dagsterlabs.com>
6
6
  License-Expression: Apache-2.0
7
7
  License-File: LICENSE
8
8
  Requires-Python: <3.15,>=3.10
9
9
  Requires-Dist: click
10
- Requires-Dist: dagster-shared==1.13.7
10
+ Requires-Dist: dagster-shared==1.13.9
11
11
  Requires-Dist: github3-py
12
12
  Requires-Dist: jinja2
13
13
  Requires-Dist: packaging>=20.9
@@ -14,6 +14,7 @@ import typer
14
14
  import yaml
15
15
  from dagster_shared import check
16
16
  from dagster_shared.utils import remove_none_recursively
17
+ from dagster_shared.yaml_utils import safe_load_yaml
17
18
  from jinja2 import TemplateSyntaxError
18
19
  from typer import Typer
19
20
 
@@ -362,11 +363,11 @@ def _create_context_from_values(values_list: list[str], values_file: str | None)
362
363
  values_file_path = pathlib.Path(values_file) if values_file else None
363
364
  if values_file_path:
364
365
  try:
365
- with open(values_file_path) as f:
366
+ with open(values_file_path, encoding="utf-8") as f:
366
367
  if values_file_path.suffix == ".json":
367
368
  context.update(json.load(f))
368
369
  elif values_file_path.suffix in [".yaml", ".yml"]:
369
- context.update(yaml.safe_load(f))
370
+ context.update(safe_load_yaml(f))
370
371
  else:
371
372
  raise ui.error(f"Unsupported values file extension {values_file_path.suffix}")
372
373
  except Exception as err:
@@ -832,6 +833,7 @@ def _build_docker(
832
833
  name = location_state.location_name
833
834
  docker_utils.verify_docker()
834
835
  registry_info = utils.get_registry_info(url)
836
+ repo_location = name if registry_info.get("is_harbor") else None
835
837
 
836
838
  docker_image_tag = docker_utils.default_image_tag(
837
839
  location_state.deployment_name, name, location_state.build.commit_hash
@@ -853,15 +855,18 @@ def _build_docker(
853
855
  base_image=docker_base_image,
854
856
  dockerfile_path=dockerfile_path,
855
857
  use_editable_dagster=use_editable_dagster,
858
+ location_name=repo_location,
856
859
  )
857
860
  if retval != 0:
858
861
  raise ui.error(f"Failed to build docker image for location {name}")
859
862
 
860
- retval = docker_utils.upload_image(docker_image_tag, registry_info)
863
+ retval = docker_utils.upload_image(docker_image_tag, registry_info, location_name=repo_location)
861
864
  if retval != 0:
862
865
  raise ui.error(f"Failed to upload docker image for location {name}")
863
866
 
864
- image = f"{registry_info['registry_url']}:{docker_image_tag}"
867
+ image = docker_utils.full_image_ref(
868
+ registry_info["registry_url"], repo_location, docker_image_tag
869
+ )
865
870
  ui.print(f"Built and uploaded image {image} for location {name}")
866
871
 
867
872
  return state.DockerBuildOutput(image=image)
@@ -36,13 +36,13 @@ def check_dagster_cloud_yaml(yaml_path: pathlib.Path) -> CheckResult:
36
36
  result.errors.append(f"No such file {yaml_path}")
37
37
  return result
38
38
 
39
- yaml_text = yaml_path.read_text()
39
+ yaml_text = yaml_path.read_text(encoding="utf-8")
40
40
  if not yaml_text.strip():
41
41
  result.errors.append(f"Unexpected blank file {yaml_path}")
42
42
  return result
43
43
 
44
44
  try:
45
- parsed = load_dagster_cloud_yaml(yaml_path.read_text())
45
+ parsed = load_dagster_cloud_yaml(yaml_path.read_text(encoding="utf-8"))
46
46
  except pydantic.ValidationError as err:
47
47
  for error in get_validation_errors(err):
48
48
  result.errors.append(error)
@@ -1,7 +1,7 @@
1
1
  from pathlib import Path
2
2
  from typing import Annotated
3
3
 
4
- import yaml
4
+ from dagster_shared.yaml_utils import safe_load_yaml
5
5
  from typer import Argument, Typer
6
6
 
7
7
  from dagster_cloud_cli import gql, ui
@@ -34,7 +34,7 @@ def set_from_filecommand(
34
34
  ):
35
35
  """Set the Dagster Cloud deployment settings from a YAML file."""
36
36
  with open(file_path, encoding="utf8") as f:
37
- settings = {"settings": yaml.safe_load(f) or {}}
37
+ settings = {"settings": safe_load_yaml(f) or {}}
38
38
  with gql.graphql_client_from_url(url, api_token, deployment_name=deployment) as client:
39
39
  gql.set_deployment_settings(client, settings)
40
40
 
@@ -1,6 +1,6 @@
1
1
  from pathlib import Path
2
2
 
3
- import yaml
3
+ from dagster_shared.yaml_utils import safe_load_yaml
4
4
  from typer import Option, Typer
5
5
 
6
6
  from dagster_cloud_cli import gql, ui
@@ -42,7 +42,7 @@ def sync_command(
42
42
  """Sync your YAML configured alert policies to Dagster Cloud."""
43
43
  with gql.graphql_client_from_url(url, api_token, deployment_name=deployment) as client:
44
44
  with open(str(alert_policies_file), encoding="utf8") as f:
45
- config = yaml.load(f.read(), Loader=yaml.SafeLoader)
45
+ config = safe_load_yaml(f.read())
46
46
 
47
47
  try:
48
48
  alert_policies = gql.reconcile_alert_policies(client, config)
@@ -1,7 +1,7 @@
1
1
  from pathlib import Path
2
2
  from typing import Annotated
3
3
 
4
- import yaml
4
+ from dagster_shared.yaml_utils import safe_load_yaml
5
5
  from typer import Argument, Typer
6
6
 
7
7
  from dagster_cloud_cli import gql, ui
@@ -43,7 +43,7 @@ def set_from_filecommand(
43
43
  url = gql.url_from_config(organization=organization)
44
44
 
45
45
  with open(file_path, encoding="utf8") as f:
46
- settings = {"settings": yaml.safe_load(f) or {}}
46
+ settings = {"settings": safe_load_yaml(f) or {}}
47
47
  with gql.graphql_client_from_url(url, api_token) as client:
48
48
  gql.set_organization_settings(client, settings)
49
49
 
@@ -212,7 +212,6 @@ def deploy_command(
212
212
  if not source_directory:
213
213
  raise ui.error("No source directory provided.")
214
214
 
215
- _check_source_directory(source_directory)
216
215
  docker_utils.verify_docker()
217
216
 
218
217
  env_vars = kwargs.get("env", [])
@@ -221,21 +220,29 @@ def deploy_command(
221
220
  with gql.graphql_client_from_url(url, api_token, deployment_name=deployment) as client:
222
221
  ecr_info = gql.get_ecr_info(client)
223
222
  registry = ecr_info["registry_url"]
223
+ repo_location = location_name if ecr_info.get("is_harbor") else None
224
224
 
225
225
  image_tag = kwargs.get("image") or docker_utils.default_image_tag(
226
226
  deployment, location_name, kwargs.get("commit_hash")
227
227
  )
228
228
  retval = docker_utils.build_image(
229
- source_directory, image_tag, ecr_info, env_vars, base_image, use_editable_dagster=False
229
+ source_directory,
230
+ image_tag,
231
+ ecr_info,
232
+ env_vars,
233
+ base_image,
234
+ use_editable_dagster=False,
235
+ location_name=repo_location,
230
236
  )
231
237
  if retval != 0:
232
238
  return
233
239
 
234
- retval = docker_utils.upload_image(image_tag, ecr_info)
240
+ retval = docker_utils.upload_image(image_tag, ecr_info, location_name=repo_location)
235
241
  if retval != 0:
236
242
  return
237
243
 
238
- location_args = {**kwargs, "image": f"{registry}:{image_tag}"}
244
+ full_image = docker_utils.full_image_ref(registry, repo_location, image_tag)
245
+ location_args = {**kwargs, "image": full_image}
239
246
  location_document = get_location_document(location_name, location_args)
240
247
  gql.add_or_update_code_location(client, location_document)
241
248
 
@@ -394,7 +401,7 @@ def build_python_dependencies(
394
401
  except pex_builder.deps.DepsBuildFailure as err:
395
402
  errors = [
396
403
  "Could not build dependencies for this project.",
397
- f"Dependencies:\n {' '.join(open(requirements_path).readlines())}",
404
+ f"Dependencies:\n {' '.join(open(requirements_path, encoding='utf-8').readlines())}",
398
405
  err.format_error(),
399
406
  ]
400
407
  raise ui.error("\n".join(errors))
@@ -3,9 +3,9 @@ from collections.abc import Mapping
3
3
  from pathlib import Path
4
4
  from typing import Any
5
5
 
6
- import yaml
7
6
  from dagster_shared import check
8
7
  from dagster_shared.serdes.serdes import deserialize_value
8
+ from dagster_shared.yaml_utils import safe_load_yaml
9
9
  from typer import Argument, Option, Typer
10
10
 
11
11
  from dagster_cloud_cli import gql, ui
@@ -341,7 +341,7 @@ def execute_sync_command(
341
341
  client, workspace, location_load_timeout, agent_heartbeat_timeout, url: str | None = None
342
342
  ):
343
343
  with open(str(workspace), encoding="utf8") as f:
344
- config = yaml.load(f.read(), Loader=yaml.SafeLoader)
344
+ config = safe_load_yaml(f.read())
345
345
  processed_config = format_workspace_config(config)
346
346
 
347
347
  try:
@@ -2,7 +2,7 @@
2
2
 
3
3
  from typing import Any
4
4
 
5
- import yaml
5
+ from dagster_shared.yaml_utils import safe_load_yaml
6
6
  from pydantic import BaseModel, Field, field_validator, model_validator
7
7
 
8
8
 
@@ -73,4 +73,4 @@ class ProcessedDagsterCloudConfig(BaseModel, extra="forbid"):
73
73
 
74
74
 
75
75
  def load_dagster_cloud_yaml(text) -> DagsterCloudYaml:
76
- return DagsterCloudYaml.model_validate(yaml.safe_load(text))
76
+ return DagsterCloudYaml.model_validate(safe_load_yaml(text))
@@ -8,7 +8,6 @@ from typing import TYPE_CHECKING, Any
8
8
  if TYPE_CHECKING:
9
9
  from typer.models import OptionInfo
10
10
 
11
- import yaml
12
11
  from click import Context
13
12
  from dagster_shared.merger import deep_merge_dicts
14
13
  from dagster_shared.plus.config import (
@@ -17,6 +16,7 @@ from dagster_shared.plus.config import (
17
16
  get_dg_config_path,
18
17
  )
19
18
  from dagster_shared.utils import remove_none_recursively
19
+ from dagster_shared.yaml_utils import safe_load_yaml
20
20
  from typer import Option
21
21
 
22
22
  from dagster_cloud_cli import gql, ui
@@ -443,7 +443,7 @@ def get_location_document(name: str | None, kwargs: dict[str, Any]) -> dict[str,
443
443
  location_doc_from_file = {}
444
444
  if location_file:
445
445
  with open(location_file, encoding="utf8") as f:
446
- location_doc_from_file = yaml.safe_load(f.read())
446
+ location_doc_from_file = safe_load_yaml(f.read())
447
447
 
448
448
  if not location_file and not name:
449
449
  raise ui.error(
@@ -39,6 +39,9 @@ RETRY_STATUS_CODES = [
39
39
  409,
40
40
  ]
41
41
 
42
+ # S3 returns sporadic HTTP 500 as a transient error per its retry guidance.
43
+ PRESIGNED_URL_PUT_RETRY_STATUS_CODES = [500, *RETRY_STATUS_CODES]
44
+
42
45
 
43
46
  class DagsterCloudAgentHttpClient:
44
47
  def __init__(
@@ -419,19 +419,15 @@ def get_pyproject_toml_deps(code_directory: str) -> list[str]:
419
419
  except Exception as e:
420
420
  raise ValueError(f"Error parsing pyproject.toml: {e}")
421
421
 
422
- lines = []
423
-
424
422
  # Handle dependencies in [project] section (PEP 621)
425
423
  project_section = pyproject_data.get("project", {})
426
424
  dependencies = project_section.get("dependencies", [])
427
- for dep in dependencies:
428
- lines.append(str(dep))
425
+ lines = [str(dep) for dep in dependencies]
429
426
 
430
427
  # Handle optional dependencies in [project.optional-dependencies]
431
428
  optional_deps = project_section.get("optional-dependencies", {})
432
429
  for group_deps in optional_deps.values():
433
- for dep in group_deps:
434
- lines.append(str(dep))
430
+ lines.extend(str(dep) for dep in group_deps)
435
431
 
436
432
  # Handle legacy [tool.poetry.dependencies] for Poetry projects
437
433
  poetry_section = pyproject_data.get("tool", {}).get("poetry", {})
@@ -1,7 +1,7 @@
1
1
  import os
2
2
  from dataclasses import dataclass
3
3
 
4
- import yaml
4
+ from dagster_shared.yaml_utils import safe_load_yaml
5
5
 
6
6
  from dagster_cloud_cli import ui
7
7
 
@@ -20,7 +20,7 @@ def get_locations(dagster_cloud_yaml_file) -> list[Location]:
20
20
 
21
21
  with open(dagster_cloud_yaml_file, encoding="utf-8") as yaml_file:
22
22
  workspace_contents = yaml_file.read()
23
- workspace_contents_yaml = yaml.safe_load(workspace_contents)
23
+ workspace_contents_yaml = safe_load_yaml(workspace_contents)
24
24
 
25
25
  locations = []
26
26
  for location in workspace_contents_yaml["locations"]:
@@ -5,8 +5,15 @@ from tempfile import TemporaryDirectory
5
5
  from typing import Any
6
6
 
7
7
  import requests
8
+ from requests.adapters import HTTPAdapter
9
+ from urllib3.util.retry import Retry
8
10
 
9
11
  from dagster_cloud_cli import gql, ui
12
+ from dagster_cloud_cli.core.graphql_client import (
13
+ DEFAULT_BACKOFF_FACTOR,
14
+ DEFAULT_RETRIES,
15
+ PRESIGNED_URL_PUT_RETRY_STATUS_CODES,
16
+ )
10
17
 
11
18
  GENERATE_PUT_URL_QUERY = """
12
19
  mutation GenerateServerlessPexUrlMutation($filenames: [String!]!) {
@@ -131,6 +138,18 @@ def upload_files(dagster_cloud_url: str, dagster_cloud_api_token: str, filepaths
131
138
  ui.error(f"Cannot upload files, did not get PUT urls for: {filenames}")
132
139
  return
133
140
 
141
+ session = requests.Session()
142
+ put_retry_adapter = HTTPAdapter(
143
+ max_retries=Retry(
144
+ total=DEFAULT_RETRIES,
145
+ backoff_factor=DEFAULT_BACKOFF_FACTOR,
146
+ status_forcelist=PRESIGNED_URL_PUT_RETRY_STATUS_CODES,
147
+ allowed_methods=["PUT"],
148
+ )
149
+ )
150
+ session.mount("https://", put_retry_adapter)
151
+ session.mount("http://", put_retry_adapter)
152
+
134
153
  # we expect response list to be in the same order as the request
135
154
  for _, filepath, url in zip(filenames, filepaths, urls):
136
155
  if not url:
@@ -139,7 +158,7 @@ def upload_files(dagster_cloud_url: str, dagster_cloud_api_token: str, filepaths
139
158
 
140
159
  ui.print(f"Uploading {filepath} ...")
141
160
  with open(filepath, "rb") as f:
142
- response = requests.put(url, data=f)
161
+ response = session.put(url, data=f)
143
162
  if response.ok:
144
163
  ui.print(f"Upload successful: {filepath}")
145
164
  else:
@@ -58,19 +58,14 @@ def _build_local_package(local_dir: str, build_dir: str, python_interpreter: str
58
58
  ]
59
59
  subprocess.run(command, check=True, cwd=local_dir)
60
60
  elif os.path.exists(os.path.join(local_dir, "pyproject.toml")):
61
- ui.print(f"Building package at {local_dir!r} using {python_interpreter} -m pip install")
62
- # Use pip install with --target to build and install the package to the build directory
63
- # This handles pyproject.toml files properly using modern Python build standards
64
- command = [
65
- python_interpreter,
66
- "-m",
67
- "pip",
68
- "install",
69
- "--target",
70
- build_dir,
71
- "--no-deps", # Don't install dependencies, just the package itself
72
- ".",
73
- ]
61
+ install_args = ["--target", build_dir, "--no-deps", "."]
62
+ uv_path = shutil.which("uv")
63
+ if uv_path:
64
+ ui.print(f"Building package at {local_dir!r} using uv pip install")
65
+ command = [uv_path, "pip", "install", "--python", python_interpreter, *install_args]
66
+ else:
67
+ ui.print(f"Building package at {local_dir!r} using {python_interpreter} -m pip install")
68
+ command = [python_interpreter, "-m", "pip", "install", *install_args]
74
69
  subprocess.run(command, check=True, cwd=local_dir)
75
70
  else:
76
71
  ui.warn(f"No setup.py or pyproject.toml found in {local_dir!r} - will not build.")
@@ -34,6 +34,26 @@ def _template_dockerfile(env_vars, custom_base_image=None) -> Generator[bytes, N
34
34
  yield bytes(dockerfile_content, "utf-8")
35
35
 
36
36
 
37
+ def full_image_ref(
38
+ registry_url: str,
39
+ location_name: str | None,
40
+ image_tag: str,
41
+ ) -> str:
42
+ """Build the full Docker image reference.
43
+
44
+ For Harbor-backed registries the registry URL already contains a
45
+ deployment-scoped prefix (``host/project/depl-prefix``). Each code
46
+ location gets its own repository nested under that prefix, so the
47
+ reference becomes ``host/project/depl-prefix/location:tag``.
48
+
49
+ For legacy ECR registries (or when *location_name* is ``None``) the
50
+ reference falls back to the previous ``registry:tag`` format.
51
+ """
52
+ if location_name:
53
+ return f"{registry_url}/{location_name}:{image_tag}"
54
+ return f"{registry_url}:{image_tag}"
55
+
56
+
37
57
  def build_image(
38
58
  source_directory,
39
59
  image: str,
@@ -42,8 +62,9 @@ def build_image(
42
62
  base_image: str | None,
43
63
  use_editable_dagster: bool,
44
64
  dockerfile_path: str | None = None,
65
+ location_name: str | None = None,
45
66
  ) -> int:
46
- registry = registry_info["registry_url"]
67
+ full_ref = full_image_ref(registry_info["registry_url"], location_name, image)
47
68
 
48
69
  with ExitStack() as stack:
49
70
  if dockerfile_path:
@@ -58,7 +79,7 @@ def build_image(
58
79
  "build",
59
80
  source_directory,
60
81
  "-t",
61
- f"{registry}:{image}",
82
+ full_ref,
62
83
  "-f",
63
84
  file_path,
64
85
  "--platform",
@@ -74,7 +95,7 @@ def build_image(
74
95
  return subprocess.run(cmd, input=build_input, check=True).returncode
75
96
 
76
97
 
77
- def upload_image(image, registry_info) -> int:
98
+ def upload_image(image, registry_info, location_name: str | None = None) -> int:
78
99
  registry = registry_info["registry_url"]
79
100
  aws_token = registry_info["aws_auth_token"]
80
101
  if not registry or not aws_token:
@@ -89,9 +110,8 @@ def upload_image(image, registry_info) -> int:
89
110
  input=password.encode(),
90
111
  check=True,
91
112
  )
92
- return subprocess.call(
93
- ["docker", "push", f"{registry}:{image}"], stderr=sys.stderr, stdout=sys.stdout
94
- )
113
+ full_ref = full_image_ref(registry, location_name, image)
114
+ return subprocess.call(["docker", "push", full_ref], stderr=sys.stderr, stdout=sys.stdout)
95
115
 
96
116
 
97
117
  def default_image_tag(deployment: str, location_name: str, commit_hash: str | None) -> str:
@@ -715,6 +715,7 @@ query CliGetEcrInfo {
715
715
  awsRegion
716
716
  awsAuthToken
717
717
  registryAllowCustomBase
718
+ registryIsHarbor
718
719
  registryUrl
719
720
  }
720
721
  }
@@ -728,6 +729,7 @@ def get_ecr_info(client: DagsterCloudGraphQLClient) -> Any:
728
729
  "aws_region": data["serverless"]["awsRegion"],
729
730
  "aws_auth_token": data["serverless"]["awsAuthToken"],
730
731
  "allow_custom_base": data["serverless"]["registryAllowCustomBase"],
732
+ "is_harbor": data["serverless"].get("registryIsHarbor", False),
731
733
  }
732
734
 
733
735
 
@@ -43,10 +43,7 @@ SubcommandTags = Enum(
43
43
  class CliEventTags:
44
44
  @classmethod
45
45
  def enums(cls):
46
- attrs = []
47
- for attr in cls.__dict__.values():
48
- if isinstance(attr, EnumMeta):
49
- attrs.append(attr)
46
+ attrs = [attr for attr in cls.__dict__.values() if isinstance(attr, EnumMeta)]
50
47
  return attrs
51
48
 
52
49
  @classmethod
@@ -0,0 +1 @@
1
+ __version__ = "1.13.9"
@@ -1,9 +1,9 @@
1
1
  import pathlib
2
2
 
3
- import yaml
4
3
  from dagster_cloud_cli.config.models import ProcessedDagsterCloudConfig
5
4
  from dagster_cloud_cli.entrypoint import app
6
5
  from dagster_cloud_cli_tests.conftest import create_template_file
6
+ from dagster_shared.yaml_utils import safe_load_yaml
7
7
  from typer.testing import CliRunner
8
8
 
9
9
  DAGSTER_CLOUD_YAML_JINJA = """
@@ -71,7 +71,7 @@ def test_ci_template(temp_dir):
71
71
  ],
72
72
  )
73
73
  assert result.exit_code == 0
74
- processed_config = ProcessedDagsterCloudConfig.model_validate(yaml.safe_load(result.stdout))
74
+ processed_config = ProcessedDagsterCloudConfig.model_validate(safe_load_yaml(result.stdout))
75
75
  assert len(processed_config.locations) == 1
76
76
  assert processed_config.locations[0].container_context is not None
77
77
  assert set(processed_config.locations[0].container_context["k8s"]["env_vars"]) == {
@@ -1,6 +1,6 @@
1
- import yaml
2
1
  from dagster_cloud_cli.config.jinja_template_loader import JinjaTemplateLoader
3
2
  from dagster_cloud_cli_tests.conftest import create_template_file
3
+ from dagster_shared.yaml_utils import safe_load_yaml
4
4
 
5
5
 
6
6
  def test_template_loader(tmpdir, monkeypatch):
@@ -41,5 +41,5 @@ def test_template_loader_to_yaml_filter(tmpdir):
41
41
 
42
42
  with create_template_file(tmpdir, "template.jinja", ARTICLES_TEMPLATE) as template_file:
43
43
  result = JinjaTemplateLoader().render(template_file, context)
44
- roundtrip = yaml.safe_load(result)
44
+ roundtrip = safe_load_yaml(result)
45
45
  assert roundtrip == context
@@ -16,7 +16,7 @@ def temp_dir() -> Generator[str, Any, None]:
16
16
  @contextmanager
17
17
  def create_template_file(tmpdir: str, filename: str, text: str) -> Generator[str, Any, None]:
18
18
  file_path = os.path.join(tmpdir, filename)
19
- with open(file_path, "w") as f:
19
+ with open(file_path, "w", encoding="utf-8") as f:
20
20
  f.write(text)
21
21
  yield file_path
22
22
 
@@ -84,7 +84,7 @@ def test_dagster_cloud_yaml_check() -> None:
84
84
  with tempfile.TemporaryDirectory() as tmpdir:
85
85
  os.mkdir(os.path.join(tmpdir, "subdir"))
86
86
  yaml_path = os.path.join(tmpdir, "dagster_cloud.yaml")
87
- with open(yaml_path, "w") as f:
87
+ with open(yaml_path, "w", encoding="utf-8") as f:
88
88
  f.write(text)
89
89
  runner = CliRunner()
90
90
  return runner.invoke(
@@ -51,7 +51,7 @@ def with_dagster_yaml(text):
51
51
  with tempfile.TemporaryDirectory() as tmpdir:
52
52
  os.mkdir(os.path.join(tmpdir, "subdir"))
53
53
  yaml_path = os.path.join(tmpdir, "dagster_cloud.yaml")
54
- with open(yaml_path, "w") as f:
54
+ with open(yaml_path, "w", encoding="utf-8") as f:
55
55
  f.write(text)
56
56
  os.chdir(tmpdir)
57
57
  yield tmpdir
@@ -163,8 +163,9 @@ dependencies = ["requests>=2.25.0"]
163
163
 
164
164
 
165
165
  @patch("subprocess.run")
166
- def test_build_local_package_with_pyproject_toml(mock_run, temp_dir):
167
- """Test building a package with pyproject.toml."""
166
+ @patch("shutil.which", return_value=None)
167
+ def test_build_local_package_with_pyproject_toml(mock_which, mock_run, temp_dir):
168
+ """Test building a package with pyproject.toml falls back to pip when uv is not available."""
168
169
  mock_run.return_value = None
169
170
 
170
171
  pyproject_path = Path(temp_dir) / "pyproject.toml"
@@ -196,6 +197,42 @@ version = "0.1.0"
196
197
  assert command == expected_command
197
198
 
198
199
 
200
+ @patch("subprocess.run")
201
+ @patch("shutil.which", return_value="/usr/bin/uv")
202
+ def test_build_local_package_with_pyproject_toml_uses_uv(mock_which, mock_run, temp_dir):
203
+ """Test building a package with pyproject.toml uses uv when available."""
204
+ mock_run.return_value = None
205
+
206
+ pyproject_path = Path(temp_dir) / "pyproject.toml"
207
+ pyproject_path.write_text("""
208
+ [project]
209
+ name = "test-package"
210
+ version = "0.1.0"
211
+ """)
212
+
213
+ build_dir = Path(temp_dir) / "build"
214
+ build_dir.mkdir()
215
+
216
+ source._build_local_package(temp_dir, str(build_dir), "python") # noqa: SLF001
217
+
218
+ mock_run.assert_called_once()
219
+ args, _kwargs = mock_run.call_args
220
+ command = args[0]
221
+
222
+ expected_command = [
223
+ "/usr/bin/uv",
224
+ "pip",
225
+ "install",
226
+ "--python",
227
+ "python",
228
+ "--target",
229
+ str(build_dir),
230
+ "--no-deps",
231
+ ".",
232
+ ]
233
+ assert command == expected_command
234
+
235
+
199
236
  @patch("subprocess.run")
200
237
  def test_build_local_package_prefers_setup_py(mock_run, temp_dir):
201
238
  """Test that setup.py is preferred over pyproject.toml when both exist."""
@@ -1,20 +1,16 @@
1
1
  [build-system]
2
- requires = [
3
- "hatchling>=1.19.0",
4
- ]
2
+ requires = ["hatchling>=1.19.0"]
5
3
  build-backend = "hatchling.build"
6
4
 
7
5
  [project]
8
6
  name = "dagster-cloud-cli"
9
- version = "1.13.7"
7
+ version = "1.13.9"
10
8
  requires-python = ">=3.10,<3.15"
11
9
  description = ""
12
- authors = [
13
- { name = "Dagster Labs", email = "hello@dagsterlabs.com" },
14
- ]
10
+ authors = [{name = "Dagster Labs", email = "hello@dagsterlabs.com"}]
15
11
  license = "Apache-2.0"
16
12
  dependencies = [
17
- "dagster-shared==1.13.7",
13
+ "dagster-shared==1.13.9",
18
14
  "packaging>=20.9",
19
15
  "questionary",
20
16
  "requests",
@@ -48,6 +44,5 @@ test = [
48
44
  "pip",
49
45
  ]
50
46
 
51
- [tool.uv.sources.dagster-shared]
52
- path = "../../../python_modules/libraries/dagster-shared"
53
- editable = true
47
+ [tool.uv.sources]
48
+ dagster-shared = { path = "../dagster-shared", editable = true }
@@ -1 +0,0 @@
1
- __version__ = "1.13.7"