coiled 1.128.3.dev1__tar.gz → 1.130.2.dev11__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 (104) hide show
  1. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/PKG-INFO +1 -1
  2. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/capture_environment.py +57 -82
  3. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/hello/hello.py +1 -1
  4. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/setup/azure.py +85 -8
  5. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/context.py +2 -2
  6. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/core.py +21 -2
  7. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/filestore.py +1 -1
  8. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/pypi_conda_map.py +22 -96
  9. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/software.py +18 -3
  10. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/software_utils.py +33 -11
  11. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/types.py +2 -0
  12. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/v2/cluster.py +0 -1
  13. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/v2/core.py +11 -1
  14. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/v2/widgets/rich.py +2 -1
  15. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/.gitignore +0 -0
  16. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/LICENSE +0 -0
  17. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/README.md +0 -0
  18. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/__init__.py +0 -0
  19. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/__main__.py +0 -0
  20. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/analytics.py +0 -0
  21. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/auth.py +0 -0
  22. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/batch.py +0 -0
  23. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/__init__.py +0 -0
  24. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/batch/__init__.py +0 -0
  25. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/batch/list.py +0 -0
  26. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/batch/logs.py +0 -0
  27. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/batch/run.py +0 -0
  28. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/batch/status.py +0 -0
  29. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/batch/util.py +0 -0
  30. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/batch/wait.py +0 -0
  31. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/cluster/__init__.py +0 -0
  32. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/cluster/azure_logs.py +0 -0
  33. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/cluster/better_logs.py +0 -0
  34. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/cluster/crud.py +0 -0
  35. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/cluster/get_address.py +0 -0
  36. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/cluster/list.py +0 -0
  37. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/cluster/logs.py +0 -0
  38. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/cluster/metrics.py +0 -0
  39. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/cluster/ssh.py +0 -0
  40. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/cluster/utils.py +0 -0
  41. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/config.py +0 -0
  42. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/core.py +0 -0
  43. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/curl.py +0 -0
  44. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/diagnostics.py +0 -0
  45. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/env.py +0 -0
  46. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/file.py +0 -0
  47. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/hello/__init__.py +0 -0
  48. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/hello/examples/__init__.py +0 -0
  49. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/hello/examples/exit.py +0 -0
  50. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/hello/examples/hello_world.py +0 -0
  51. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/hello/examples/nyc_parquet.py +0 -0
  52. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/hello/examples/pytorch.py +0 -0
  53. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/hello/examples/xarray_nwm.py +0 -0
  54. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/hello/scripts/fill_ipython.py +0 -0
  55. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/hello/scripts/nyc_parquet.py +0 -0
  56. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/hello/scripts/pytorch.py +0 -0
  57. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/hello/scripts/xarray_nwm.py +0 -0
  58. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/hello/utils.py +0 -0
  59. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/login.py +0 -0
  60. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/mpi.py +0 -0
  61. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/notebook/__init__.py +0 -0
  62. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/notebook/notebook.py +0 -0
  63. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/package_sync.py +0 -0
  64. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/prefect.py +0 -0
  65. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/prefect_serve.py +0 -0
  66. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/run.py +0 -0
  67. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/setup/__init__.py +0 -0
  68. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/setup/amp.py +0 -0
  69. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/setup/aws.py +0 -0
  70. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/setup/entry.py +0 -0
  71. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/setup/gcp.py +0 -0
  72. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/setup/prometheus.py +0 -0
  73. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/setup/util.py +0 -0
  74. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/sync.py +0 -0
  75. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cli/utils.py +0 -0
  76. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/cluster.py +0 -0
  77. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/coiled.yaml +0 -0
  78. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/compatibility.py +0 -0
  79. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/config.py +0 -0
  80. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/credentials/__init__.py +0 -0
  81. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/credentials/aws.py +0 -0
  82. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/credentials/google.py +0 -0
  83. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/errors.py +0 -0
  84. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/exceptions.py +0 -0
  85. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/extensions/__init__.py +0 -0
  86. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/extensions/prefect/__init__.py +0 -0
  87. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/extensions/prefect/runners.py +0 -0
  88. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/extensions/prefect/workers.py +0 -0
  89. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/function.py +0 -0
  90. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/plugins.py +0 -0
  91. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/prefect.py +0 -0
  92. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/scan.py +0 -0
  93. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/spans.py +0 -0
  94. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/spark.py +0 -0
  95. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/utils.py +0 -0
  96. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/v2/__init__.py +0 -0
  97. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/v2/cluster_comms.py +0 -0
  98. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/v2/cwi_log_link.py +0 -0
  99. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/v2/states.py +0 -0
  100. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/v2/widgets/__init__.py +0 -0
  101. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/v2/widgets/interface.py +0 -0
  102. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/v2/widgets/util.py +0 -0
  103. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/coiled/websockets.py +0 -0
  104. {coiled-1.128.3.dev1 → coiled-1.130.2.dev11}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coiled
3
- Version: 1.128.3.dev1
3
+ Version: 1.130.2.dev11
4
4
  Summary: Python client for coiled.io dask clusters
5
5
  Project-URL: Homepage, https://coiled.io
6
6
  Maintainer-email: Coiled <info@coiled.io>
@@ -1,6 +1,4 @@
1
- import asyncio
2
1
  import contextlib
3
- import logging
4
2
  import platform
5
3
  import sys
6
4
  import typing
@@ -16,9 +14,12 @@ from typing_extensions import Literal
16
14
  from coiled.context import track_context
17
15
  from coiled.scan import scan_prefix
18
16
  from coiled.software_utils import (
17
+ ANY_AVAILABLE,
18
+ PYTHON_VERSION,
19
19
  check_pip_happy,
20
20
  create_wheels_for_local_python,
21
21
  create_wheels_for_packages,
22
+ get_lockfile,
22
23
  partition_ignored_packages,
23
24
  partition_local_packages,
24
25
  partition_local_python_code_packages,
@@ -36,10 +37,6 @@ from coiled.v2.core import CloudV2
36
37
  from coiled.v2.widgets.rich import CONSOLE_WIDTH, print_rich_package_table
37
38
  from coiled.v2.widgets.util import simple_progress, use_rich_widget
38
39
 
39
- PYTHON_VERSION = platform.python_version_tuple()
40
- ANY_AVAILABLE = "ANY-AVAILABLE"
41
-
42
-
43
40
  logger = getLogger("coiled.package_sync")
44
41
 
45
42
 
@@ -69,49 +66,53 @@ async def approximate_packages(
69
66
  architecture: ArchitectureTypesEnum = ArchitectureTypesEnum.X86_64,
70
67
  pip_check_errors: Optional[Dict[str, List[str]]] = None,
71
68
  gpu_enabled: bool = False,
69
+ use_uv_installer: bool = True,
70
+ lockfile_path: Optional[Path] = None,
72
71
  ) -> typing.List[ResolvedPackageInfo]:
73
72
  user_conda_installed_python = next((p for p in packages if p["name"] == "python"), None)
74
- user_conda_installed_pip = next(
75
- (i for i, p in enumerate(packages) if p["name"] == "pip" and p["source"] == "conda"),
76
- None,
77
- )
78
- if not user_conda_installed_pip:
79
- # This means pip was installed by pip, or the system
80
- # package manager
81
- # Insert a conda version of pip to be installed first, it will
82
- # then be used to install the users version of pip
83
- pip = next(
84
- (p for p in packages if p["name"] == "pip" and p["source"] == "pip"),
73
+ # Only add pip if we need it
74
+ if not use_uv_installer:
75
+ user_conda_installed_pip = next(
76
+ (i for i, p in enumerate(packages) if p["name"] == "pip" and p["source"] == "conda"),
85
77
  None,
86
78
  )
87
- if not pip:
88
- # insert a modern version and hope it does not introduce conflicts
89
- packages.append({
90
- "name": "pip",
91
- "path": None,
92
- "source": "conda",
93
- "channel_url": "https://conda.anaconda.org/conda-forge/",
94
- "channel": "conda-forge",
95
- "subdir": "noarch",
96
- "conda_name": "pip",
97
- "version": "22.3.1",
98
- "wheel_target": None,
99
- "requested": False,
100
- })
101
- else:
102
- # insert the users pip version and hope it exists on conda-forge
103
- packages.append({
104
- "name": "pip",
105
- "path": None,
106
- "source": "conda",
107
- "channel_url": "https://conda.anaconda.org/conda-forge/",
108
- "channel": "conda-forge",
109
- "subdir": "noarch",
110
- "conda_name": "pip",
111
- "version": pip["version"],
112
- "wheel_target": None,
113
- "requested": True,
114
- })
79
+ if not user_conda_installed_pip:
80
+ # This means pip was installed by pip, or the system
81
+ # package manager
82
+ # Insert a conda version of pip to be installed first, it will
83
+ # then be used to install the users version of pip
84
+ pip = next(
85
+ (p for p in packages if p["name"] == "pip" and p["source"] == "pip"),
86
+ None,
87
+ )
88
+ if not pip:
89
+ # insert a modern version and hope it does not introduce conflicts
90
+ packages.append({
91
+ "name": "pip",
92
+ "path": None,
93
+ "source": "conda",
94
+ "channel_url": "https://conda.anaconda.org/conda-forge/",
95
+ "channel": "conda-forge",
96
+ "subdir": "noarch",
97
+ "conda_name": "pip",
98
+ "version": "22.3.1",
99
+ "wheel_target": None,
100
+ "requested": False,
101
+ })
102
+ else:
103
+ # insert the users pip version and hope it exists on conda-forge
104
+ packages.append({
105
+ "name": "pip",
106
+ "path": None,
107
+ "source": "conda",
108
+ "channel_url": "https://conda.anaconda.org/conda-forge/",
109
+ "channel": "conda-forge",
110
+ "subdir": "noarch",
111
+ "conda_name": "pip",
112
+ "version": pip["version"],
113
+ "wheel_target": None,
114
+ "requested": True,
115
+ })
115
116
  coiled_selected_python = None
116
117
  if not user_conda_installed_python:
117
118
  # insert a special python package
@@ -162,6 +163,8 @@ async def approximate_packages(
162
163
  architecture=architecture,
163
164
  pip_check_errors=pip_check_errors,
164
165
  gpu_enabled=gpu_enabled,
166
+ lockfile_name=lockfile_path.name if lockfile_path else None,
167
+ lockfile_content=lockfile_path.read_text() if lockfile_path else None,
165
168
  )
166
169
  finalized_packages: typing.List[ResolvedPackageInfo] = []
167
170
  finalized_packages.extend(await create_wheels_for_local_python(local_python_code, progress=progress))
@@ -208,6 +211,7 @@ async def create_environment_approximation(
208
211
  progress: Optional[Progress] = None,
209
212
  architecture: ArchitectureTypesEnum = ArchitectureTypesEnum.X86_64,
210
213
  gpu_enabled: bool = False,
214
+ use_uv_installer: bool = True,
211
215
  ) -> typing.List[ResolvedPackageInfo]:
212
216
  packages = await scan_prefix(progress=progress)
213
217
  pip_check_errors = await check_pip_happy(progress)
@@ -237,6 +241,8 @@ async def create_environment_approximation(
237
241
  architecture=architecture,
238
242
  pip_check_errors=pip_check_errors,
239
243
  gpu_enabled=gpu_enabled,
244
+ use_uv_installer=use_uv_installer,
245
+ lockfile_path=get_lockfile(),
240
246
  )
241
247
  return result
242
248
 
@@ -259,7 +265,7 @@ async def scan_and_create(
259
265
  ):
260
266
  use_widget = force_rich_widget or (show_widget and use_rich_widget())
261
267
 
262
- local_env_name = Path(sys.prefix).name
268
+ local_env_name = str(get_lockfile() or Path(sys.prefix).name)
263
269
  if use_widget:
264
270
  progress = Progress(TextColumn("[progress.description]{task.description}"), BarColumn(), TimeElapsedColumn())
265
271
  live = Live(Panel(progress, title=f"[green]Package Sync for {local_env_name}", width=CONSOLE_WIDTH))
@@ -268,6 +274,9 @@ async def scan_and_create(
268
274
  progress = None
269
275
 
270
276
  with live:
277
+ # We do this even with lockfiles because some early checks happen
278
+ # on this endpoint to prevent people getting delayed quota errors
279
+ # TODO: Add a lighter weight endpoint that does just these checks
271
280
  with simple_progress("Fetching latest package priorities", progress):
272
281
  logger.info(f"Resolving your local {local_env_name} Python environment...")
273
282
  async with (
@@ -306,6 +315,7 @@ async def scan_and_create(
306
315
  architecture=architecture,
307
316
  gpu_enabled=gpu_enabled,
308
317
  conda_extras=package_sync_conda_extras,
318
+ use_uv_installer=use_uv_installer,
309
319
  )
310
320
 
311
321
  if not package_sync_only:
@@ -367,6 +377,7 @@ async def scan_and_create(
367
377
  # default region in declarative service create_software_environment
368
378
  region_name=region_name,
369
379
  use_uv_installer=use_uv_installer,
380
+ lockfile_path=get_lockfile(),
370
381
  )
371
382
  if use_widget:
372
383
  print_rich_package_table(packages_with_notes, packages_with_errors)
@@ -427,39 +438,3 @@ If you use pip, venv, uv, pixi, etc. create a new environment and then:
427
438
 
428
439
  See https://docs.coiled.io/user_guide/software/package_sync_best_practices.html
429
440
  for more best practices. If that doesn't solve your issue, please contact support@coiled.io.""")
430
-
431
-
432
- if __name__ == "__main__":
433
- from logging import basicConfig
434
-
435
- basicConfig(level=logging.INFO)
436
-
437
- from rich.console import Console
438
- from rich.table import Table
439
-
440
- async def run():
441
- async with CloudV2(asynchronous=True) as cloud:
442
- return await create_environment_approximation(
443
- cloud=cloud,
444
- priorities={
445
- ("dask", "conda"): PackageLevelEnum.CRITICAL,
446
- ("twisted", "conda"): PackageLevelEnum.IGNORE,
447
- ("graphviz", "conda"): PackageLevelEnum.LOOSE,
448
- ("icu", "conda"): PackageLevelEnum.LOOSE,
449
- },
450
- )
451
-
452
- result = asyncio.run(run())
453
-
454
- table = Table(title="Packages")
455
- keys = ("name", "source", "include", "client_version", "specifier", "error", "note")
456
-
457
- for key in keys:
458
- table.add_column(key)
459
-
460
- for pkg in result:
461
- row_values = [str(pkg.get(key, "")) for key in keys]
462
- table.add_row(*row_values)
463
- console = Console()
464
- console.print(table)
465
- console.print(table)
@@ -295,7 +295,7 @@ Choose any computation you'd like to run:
295
295
  Yee-haw you've done all my examples 🎉
296
296
  Now you can:
297
297
  - Try Coiled in your own use case
298
- - [Talk to us](https://calendly.com/d/cmph-386-cjt/coiled-help-getting-started)
298
+ - [Ask us questions](mailto:support@coiled.io)
299
299
  - Explore the [docs](https://docs.coiled.io?utm_source=coiled-hello&utm_medium=finished) to see all the other things Coiled can do
300
300
  """), # noqa
301
301
  border_style="green",
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import datetime
2
4
  import json
3
5
  import os
@@ -6,7 +8,6 @@ import shutil
6
8
  import subprocess
7
9
  import sys
8
10
  import time
9
- from typing import Optional
10
11
 
11
12
  import click
12
13
  import httpx
@@ -158,6 +159,12 @@ coiled curl -X POST "${SETUP_ENDPOINT}" --json --data "{\\"credentials\\": {\\"t
158
159
  is_flag=True,
159
160
  help="If there's existing enterprise application, add new secret rather than replacing any existing ones.",
160
161
  )
162
+ @click.option(
163
+ "--update-role-definitions",
164
+ default=False,
165
+ is_flag=True,
166
+ help="If Role Definitions already exist, then update them; default is to leave them unchanged if they exist.",
167
+ )
161
168
  @click.option(
162
169
  "--save-script",
163
170
  is_flag=True,
@@ -175,8 +182,22 @@ coiled curl -X POST "${SETUP_ENDPOINT}" --json --data "{\\"credentials\\": {\\"t
175
182
  "service principal for Coiled to use."
176
183
  ),
177
184
  )
185
+ @click.option(
186
+ "--refresh-for-app-id", default=None, help="Refresh the secret key used by Coiled for specified Application ID."
187
+ )
178
188
  @click.command(context_settings=CONTEXT_SETTINGS)
179
- def azure_setup(subscription, resource_group, region, account, iam_user, keep_existing_access, save_script, ship_token):
189
+ def azure_setup(
190
+ subscription,
191
+ resource_group,
192
+ region,
193
+ account,
194
+ iam_user,
195
+ keep_existing_access,
196
+ update_role_definitions,
197
+ save_script,
198
+ ship_token,
199
+ refresh_for_app_id,
200
+ ):
180
201
  print(
181
202
  "Coiled on Azure is currently in [bold]public beta[/bold], "
182
203
  "please contact [link]support@coiled.io[/link] if you have any questions or problems."
@@ -264,6 +285,17 @@ def azure_setup(subscription, resource_group, region, account, iam_user, keep_ex
264
285
  f"with [green]{region}[/green] as the default region\n"
265
286
  )
266
287
 
288
+ if refresh_for_app_id:
289
+ refresh_app_creds(
290
+ app_id=refresh_for_app_id,
291
+ coiled_account=coiled_account,
292
+ sub_id=sub_id,
293
+ rg_name=rg_name,
294
+ region=region,
295
+ keep_existing_keys=True,
296
+ )
297
+ return
298
+
267
299
  if ship_token:
268
300
  enable_providers(creds, sub_id)
269
301
  ship_token_creds(
@@ -289,7 +321,15 @@ def azure_setup(subscription, resource_group, region, account, iam_user, keep_ex
289
321
  app_name = iam_user or f"coiled-{coiled_account}-app"
290
322
  try:
291
323
  if not setup_with_service_principal(
292
- creds, app_name, sub_id, rg_name, rg_id, coiled_account, region, keep_existing_keys=keep_existing_access
324
+ creds,
325
+ app_name,
326
+ sub_id,
327
+ rg_name,
328
+ rg_id,
329
+ coiled_account,
330
+ region,
331
+ keep_existing_keys=keep_existing_access,
332
+ update_role_definitions=update_role_definitions,
293
333
  ):
294
334
  coiled.add_interaction(action="CoiledSetup", success=False)
295
335
  except Exception as e:
@@ -301,7 +341,15 @@ def azure_setup(subscription, resource_group, region, account, iam_user, keep_ex
301
341
 
302
342
 
303
343
  def setup_with_service_principal(
304
- creds, app_name, sub_id, rg_name, rg_id, coiled_account, region, keep_existing_keys: bool = False
344
+ creds,
345
+ app_name,
346
+ sub_id,
347
+ rg_name,
348
+ rg_id,
349
+ coiled_account,
350
+ region,
351
+ keep_existing_keys: bool = False,
352
+ update_role_definitions: bool = False,
305
353
  ):
306
354
  prompt = f"Create [green]{app_name}[/green] service principal and grant Coiled access to your Azure subscription?"
307
355
  if not Confirm.ask(prompt, default=True):
@@ -341,12 +389,16 @@ def setup_with_service_principal(
341
389
 
342
390
  az_cli_wrapper(
343
391
  f"az role definition create --role-definition @{role_def_path}",
344
- command_if_exists=f"az role definition update --role-definition @{role_def_path}",
392
+ command_if_exists=f"az role definition update --role-definition @{role_def_path}"
393
+ if update_role_definitions
394
+ else None,
345
395
  )
346
396
  print(f" [bright_black]Creating/updating role definition {LOG_ROLE_NAME}...")
347
397
  az_cli_wrapper(
348
398
  f"az role definition create --role-definition @{log_role_def_path}",
349
- command_if_exists=f"az role definition update --role-definition @{log_role_def_path}",
399
+ command_if_exists=f"az role definition update --role-definition @{log_role_def_path}"
400
+ if update_role_definitions
401
+ else None,
350
402
  )
351
403
 
352
404
  print(f" [bright_black]Assigning '{RG_ROLE_NAME}' role to service principal on '{rg_name}' resource group...")
@@ -378,6 +430,31 @@ def setup_with_service_principal(
378
430
  return True
379
431
 
380
432
 
433
+ def refresh_app_creds(app_id, keep_existing_keys, coiled_account, sub_id, rg_name, region):
434
+ print(f" [bright_black]Resetting/retrieving credentials for {app_id}...")
435
+ cred_reset_opts = "--append" if keep_existing_keys else ""
436
+ app_creds_json = az_cli_wrapper(f"az ad app credential reset --id {app_id} {cred_reset_opts}")
437
+ app_creds = json.loads(app_creds_json)
438
+
439
+ creds_to_submit = {
440
+ "tenant_id": app_creds["tenant"],
441
+ "client_id": app_creds["appId"],
442
+ "client_secret": app_creds["password"],
443
+ }
444
+
445
+ print("Sending Azure credentials to Coiled... ", end="")
446
+ submit_azure_credentials(
447
+ coiled_account=coiled_account,
448
+ sub_id=sub_id,
449
+ rg_name=rg_name,
450
+ region=region,
451
+ creds_to_submit=creds_to_submit,
452
+ )
453
+ print(f"Azure credentials have been updated for {coiled_account} using Azure app {app_id} and {rg_name}!")
454
+
455
+ coiled.add_interaction(action="RefreshCloudCredentials", success=True)
456
+
457
+
381
458
  def submit_azure_credentials(coiled_account, sub_id, rg_name, region, creds_to_submit, check_after: bool = False):
382
459
  with coiled.Cloud(account=coiled_account) as cloud:
383
460
  setup_endpoint = f"/api/v2/cloud-credentials/{coiled_account}/azure"
@@ -399,13 +476,13 @@ def strip_output(output: str) -> str:
399
476
  return output.strip(' \n"')
400
477
 
401
478
 
402
- def get_cli_path() -> Optional[str]:
479
+ def get_cli_path() -> str | None:
403
480
  return shutil.which("az")
404
481
 
405
482
 
406
483
  def az_cli_wrapper(
407
484
  command: str,
408
- command_if_exists: str = "",
485
+ command_if_exists: str | None = "",
409
486
  show_stdout: bool = False,
410
487
  interactive: bool = False,
411
488
  ):
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
- import asyncio
4
3
  import functools
4
+ import inspect
5
5
  import random
6
6
  import string
7
7
  from contextlib import contextmanager, nullcontext
@@ -103,7 +103,7 @@ def get_trace_context(func: Union[SyncFuncType, AsyncFuncType]):
103
103
 
104
104
 
105
105
  def track_context(func: F) -> F:
106
- if asyncio.iscoroutinefunction(func):
106
+ if inspect.iscoroutinefunction(func):
107
107
 
108
108
  @functools.wraps(func)
109
109
  async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
@@ -924,6 +924,7 @@ class Cloud(Generic[IsAsynchronous]):
924
924
  include_local_code: bool = False,
925
925
  ignore_local_packages: List[str] | None = None,
926
926
  use_uv_installer: bool = True,
927
+ lockfile_path: Union[str, pathlib.Path, None] = None,
927
928
  ) -> SoftwareEnvironmentAlias | None:
928
929
  if name is None and conda is not None and isinstance(conda, dict) and "name" in conda:
929
930
  name = conda["name"]
@@ -943,8 +944,20 @@ class Cloud(Generic[IsAsynchronous]):
943
944
  raise TypeError("The build backend does not support specifying both packages and a container")
944
945
  if container and include_local_code:
945
946
  raise TypeError("The build backend does not support including local code when using a container")
946
- if conda or pip:
947
- senv = await create_env_spec(conda=conda, pip=pip)
947
+ if lockfile_path:
948
+ if pip or conda:
949
+ raise TypeError("The build backend does not support specifying both a lockfile and packages")
950
+ lockfile_path = pathlib.Path(lockfile_path)
951
+ if not lockfile_path.exists():
952
+ raise FileNotFoundError(f"Lockfile not found: {lockfile_path}")
953
+ if not lockfile_path.name.endswith(("uv.lock", "pylock.toml", "conda-lock.yml")):
954
+ logger.warning(
955
+ "The specified lockfile does not appear to be generated by a supported tool "
956
+ "(uv, pip, conda-lock). Proceeding anyway."
957
+ )
958
+
959
+ if conda or pip or lockfile_path:
960
+ senv = await create_env_spec(conda=conda, pip=pip, lockfile_path=lockfile_path)
948
961
  if include_local_code:
949
962
  prefix = await scan_prefix()
950
963
  packages, _ = partition_ignored_packages(
@@ -1030,6 +1043,8 @@ class Cloud(Generic[IsAsynchronous]):
1030
1043
  architecture: ArchitectureTypesEnum,
1031
1044
  pip_check_errors: Dict[str, List[str]] | None = None,
1032
1045
  gpu_enabled: bool = False,
1046
+ lockfile_name: str | None = None,
1047
+ lockfile_content: str | None = None,
1033
1048
  ) -> List[ApproximatePackageResult]:
1034
1049
  response = await self._do_request(
1035
1050
  "POST",
@@ -1046,6 +1061,8 @@ class Cloud(Generic[IsAsynchronous]):
1046
1061
  "index_urls": get_index_urls(),
1047
1062
  "pip_check_errors": pip_check_errors,
1048
1063
  "gpu_enabled": gpu_enabled,
1064
+ "lockfile_name": lockfile_name,
1065
+ "lockfile_content": lockfile_content,
1049
1066
  },
1050
1067
  )
1051
1068
  if response.status >= 400:
@@ -1133,6 +1150,8 @@ class Cloud(Generic[IsAsynchronous]):
1133
1150
  "architecture": architecture,
1134
1151
  "region_name": region_name,
1135
1152
  "enable_experimental_installer": use_uv_installer,
1153
+ "lockfile_content": senv.get("lockfile_content") if senv else None,
1154
+ "lockfile_name": senv.get("lockfile_name") if senv else None,
1136
1155
  }
1137
1156
  if senv:
1138
1157
  payload["packages"] = senv["packages"]
@@ -422,7 +422,7 @@ class FilestoreManagerWithoutHttp:
422
422
  for chunk in response.iter_bytes(chunk_size=8192):
423
423
  f.write(chunk)
424
424
  return # Success, exit function
425
- except (httpx.RemoteProtocolError, httpx.ReadTimeout, httpx.ConnectError) as e:
425
+ except (httpx.RemoteProtocolError, httpx.ReadTimeout, httpx.ConnectError, httpx.HTTPStatusError) as e:
426
426
  if attempt < max_retries - 1:
427
427
  wait_time = 2**attempt # Exponential backoff: 1s, 2s, 4s
428
428
  if verbose: