ob-metaflow-extensions 1.1.175rc1__py2.py3-none-any.whl → 1.1.175rc3__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.

Potentially problematic release.


This version of ob-metaflow-extensions might be problematic. Click here for more details.

Files changed (28) hide show
  1. metaflow_extensions/outerbounds/plugins/__init__.py +1 -1
  2. metaflow_extensions/outerbounds/plugins/apps/app_deploy_decorator.py +112 -0
  3. metaflow_extensions/outerbounds/plugins/apps/core/__init__.py +9 -0
  4. metaflow_extensions/outerbounds/plugins/apps/core/app_cli.py +42 -374
  5. metaflow_extensions/outerbounds/plugins/apps/core/app_config.py +45 -225
  6. metaflow_extensions/outerbounds/plugins/apps/core/capsule.py +22 -6
  7. metaflow_extensions/outerbounds/plugins/apps/core/code_package/code_packager.py +16 -15
  8. metaflow_extensions/outerbounds/plugins/apps/core/config/__init__.py +12 -0
  9. metaflow_extensions/outerbounds/plugins/apps/core/config/cli_generator.py +161 -0
  10. metaflow_extensions/outerbounds/plugins/apps/core/config/config_utils.py +828 -0
  11. metaflow_extensions/outerbounds/plugins/apps/core/config/schema_export.py +285 -0
  12. metaflow_extensions/outerbounds/plugins/apps/core/config/typed_configs.py +104 -0
  13. metaflow_extensions/outerbounds/plugins/apps/core/config/typed_init_generator.py +317 -0
  14. metaflow_extensions/outerbounds/plugins/apps/core/config/unified_config.py +994 -0
  15. metaflow_extensions/outerbounds/plugins/apps/core/config_schema.yaml +217 -211
  16. metaflow_extensions/outerbounds/plugins/apps/core/dependencies.py +3 -3
  17. metaflow_extensions/outerbounds/plugins/apps/core/deployer.py +132 -0
  18. metaflow_extensions/outerbounds/plugins/apps/core/experimental/__init__.py +4 -36
  19. metaflow_extensions/outerbounds/plugins/apps/core/perimeters.py +44 -2
  20. metaflow_extensions/outerbounds/plugins/apps/core/validations.py +4 -9
  21. metaflow_extensions/outerbounds/toplevel/global_aliases_for_metaflow_package.py +1 -0
  22. metaflow_extensions/outerbounds/toplevel/ob_internal.py +1 -0
  23. {ob_metaflow_extensions-1.1.175rc1.dist-info → ob_metaflow_extensions-1.1.175rc3.dist-info}/METADATA +1 -1
  24. {ob_metaflow_extensions-1.1.175rc1.dist-info → ob_metaflow_extensions-1.1.175rc3.dist-info}/RECORD +26 -20
  25. metaflow_extensions/outerbounds/plugins/apps/core/cli_to_config.py +0 -99
  26. metaflow_extensions/outerbounds/plugins/apps/core/config_schema_autogen.json +0 -336
  27. {ob_metaflow_extensions-1.1.175rc1.dist-info → ob_metaflow_extensions-1.1.175rc3.dist-info}/WHEEL +0 -0
  28. {ob_metaflow_extensions-1.1.175rc1.dist-info → ob_metaflow_extensions-1.1.175rc3.dist-info}/top_level.txt +0 -0
@@ -339,7 +339,7 @@ STEP_DECORATORS_DESC = [
339
339
  ("nim", ".nim.nim_decorator.NimDecorator"),
340
340
  ("ollama", ".ollama.OllamaDecorator"),
341
341
  ("vllm", ".vllm.VLLMDecorator"),
342
- ("app_deploy", ".apps.deploy_decorator.WorkstationAppDeployDecorator"),
342
+ ("app_deploy", ".apps.app_deploy_decorator.AppDeployDecorator"),
343
343
  ]
344
344
 
345
345
  TOGGLE_STEP_DECORATOR = [
@@ -0,0 +1,112 @@
1
+ from metaflow.exception import MetaflowException
2
+ from metaflow.decorators import StepDecorator
3
+ from metaflow import current
4
+ from .core import AppDeployer, apps
5
+ from .core.perimeters import PerimeterExtractor
6
+ import os
7
+ import hashlib
8
+
9
+
10
+ class AppDeployDecorator(StepDecorator):
11
+
12
+ """
13
+ MF Add To Current
14
+ -----------------
15
+ apps -> metaflow_extensions.outerbounds.plugins.apps.core.apps
16
+
17
+ @@ Returns
18
+ ----------
19
+ apps
20
+ The object carrying the Deployer class to deploy apps.
21
+ """
22
+
23
+ name = "app_deploy"
24
+ defaults = {}
25
+
26
+ package_url = None
27
+ package_sha = None
28
+
29
+ def step_init(self, flow, graph, step, decos, environment, flow_datastore, logger):
30
+ self.logger = logger
31
+ self.environment = environment
32
+ self.step = step
33
+ self.flow_datastore = flow_datastore
34
+
35
+ def _resolve_package_url_and_sha(self):
36
+ return os.environ.get("METAFLOW_CODE_URL", self.package_url), os.environ.get(
37
+ "METAFLOW_CODE_SHA", self.package_sha
38
+ )
39
+
40
+ def task_pre_step(
41
+ self,
42
+ step_name,
43
+ task_datastore,
44
+ metadata,
45
+ run_id,
46
+ task_id,
47
+ flow,
48
+ graph,
49
+ retry_count,
50
+ max_user_code_retries,
51
+ ubf_context,
52
+ inputs,
53
+ ):
54
+ perimeter, api_server = PerimeterExtractor.during_metaflow_execution()
55
+ package_url, package_sha = self._resolve_package_url_and_sha()
56
+ if package_url is None or package_sha is None:
57
+ raise MetaflowException(
58
+ "METAFLOW_CODE_URL or METAFLOW_CODE_SHA is not set. "
59
+ "Please set METAFLOW_CODE_URL and METAFLOW_CODE_SHA in your environment."
60
+ )
61
+ default_name = "-".join(current.pathspec.split("/")).lower()
62
+ image = os.environ.get("FASTBAKERY_IMAGE", None)
63
+
64
+ hash_key = hashlib.sha256(package_url.encode()).hexdigest()[:6]
65
+
66
+ default_name = (
67
+ (current.flow_name + "-" + current.step_name)[:12] + "-" + hash_key
68
+ ).lower()
69
+
70
+ AppDeployer._set_state(
71
+ perimeter,
72
+ api_server,
73
+ code_package_url=package_url,
74
+ code_package_key=package_sha,
75
+ name=default_name,
76
+ image=image,
77
+ )
78
+ current._update_env(
79
+ {
80
+ "apps": apps(),
81
+ }
82
+ )
83
+
84
+ def task_post_step(
85
+ self, step_name, flow, graph, retry_count, max_user_code_retries
86
+ ):
87
+ pass
88
+
89
+ def runtime_init(self, flow, graph, package, run_id):
90
+ # Set some more internal state.
91
+ self.flow = flow
92
+ self.graph = graph
93
+ self.package = package
94
+ self.run_id = run_id
95
+
96
+ def runtime_task_created(
97
+ self, task_datastore, task_id, split_index, input_paths, is_cloned, ubf_context
98
+ ):
99
+ # To execute the Kubernetes job, the job container needs to have
100
+ # access to the code package. We store the package in the datastore
101
+ # which the pod is able to download as part of it's entrypoint.
102
+ if not is_cloned:
103
+ self._save_package_once(self.flow_datastore, self.package)
104
+
105
+ @classmethod
106
+ def _save_package_once(cls, flow_datastore, package):
107
+ if cls.package_url is None:
108
+ cls.package_url, cls.package_sha = flow_datastore.save_data(
109
+ [package.blob], len_hint=1
110
+ )[0]
111
+ os.environ["METAFLOW_CODE_URL"] = cls.package_url
112
+ os.environ["METAFLOW_CODE_SHA"] = cls.package_sha
@@ -1 +1,10 @@
1
1
  from . import app_cli
2
+ from . import config
3
+ from .deployer import AppDeployer, apps
4
+ from .config.typed_configs import (
5
+ ReplicaConfigDict,
6
+ ResourceConfigDict,
7
+ AuthConfigDict,
8
+ DependencyConfigDict,
9
+ PackageConfigDict,
10
+ )
@@ -28,13 +28,11 @@ from .app_config import (
28
28
  CAPSULE_DEBUG,
29
29
  AuthType,
30
30
  )
31
+ from .config import auto_cli_options, CoreConfig
31
32
  from .perimeters import PerimeterExtractor
32
- from .cli_to_config import build_config_from_options
33
33
  from .utils import (
34
34
  MultiStepSpinner,
35
35
  )
36
- from . import experimental
37
- from .validations import deploy_validations
38
36
  from .code_package import CodePackager
39
37
  from .capsule import (
40
38
  CapsuleDeployer,
@@ -101,101 +99,6 @@ class KeyValuePair(click.ParamType):
101
99
  return "KV-PAIR"
102
100
 
103
101
 
104
- class MountMetaflowArtifact(click.ParamType):
105
- name = "MOUNT-METAFLOW-ARTIFACT" # type: ignore
106
-
107
- def convert(self, value, param, ctx):
108
- """
109
- Convert a string like "flow=MyFlow,artifact=my_model,path=/tmp/abc" or
110
- "pathspec=MyFlow/123/foo/345/my_model,path=/tmp/abc" to a dict.
111
- """
112
- artifact_dict = {}
113
- parts = value.split(",")
114
-
115
- for part in parts:
116
- if "=" not in part:
117
- self.fail(
118
- f"Invalid format in part '{part}'. Expected 'key=value'", param, ctx
119
- )
120
-
121
- key, val = part.split("=", 1)
122
- artifact_dict[key.strip()] = val.strip()
123
-
124
- # Validate required fields
125
- if "pathspec" in artifact_dict:
126
- if "path" not in artifact_dict:
127
- self.fail(
128
- "When using 'pathspec', you must also specify 'path'", param, ctx
129
- )
130
-
131
- # Return as pathspec format
132
- return {
133
- "pathspec": artifact_dict["pathspec"],
134
- "path": artifact_dict["path"],
135
- }
136
- elif (
137
- "flow" in artifact_dict
138
- and "artifact" in artifact_dict
139
- and "path" in artifact_dict
140
- ):
141
- # Return as flow/artifact format
142
- result = {
143
- "flow": artifact_dict["flow"],
144
- "artifact": artifact_dict["artifact"],
145
- "path": artifact_dict["path"],
146
- }
147
-
148
- # Add optional namespace if provided
149
- if "namespace" in artifact_dict:
150
- result["namespace"] = artifact_dict["namespace"]
151
-
152
- return result
153
- else:
154
- self.fail(
155
- "Invalid format. Must be either 'flow=X,artifact=Y,path=Z' or 'pathspec=X,path=Z'",
156
- param,
157
- ctx,
158
- )
159
-
160
- def __str__(self):
161
- return repr(self)
162
-
163
- def __repr__(self):
164
- return "MOUNT-METAFLOW-ARTIFACT"
165
-
166
-
167
- class MountSecret(click.ParamType):
168
- name = "MOUNT-SECRET" # type: ignore
169
-
170
- def convert(self, value, param, ctx):
171
- """
172
- Convert a string like "id=my_secret,path=/tmp/secret" to a dict.
173
- """
174
- secret_dict = {}
175
- parts = value.split(",")
176
-
177
- for part in parts:
178
- if "=" not in part:
179
- self.fail(
180
- f"Invalid format in part '{part}'. Expected 'key=value'", param, ctx
181
- )
182
-
183
- key, val = part.split("=", 1)
184
- secret_dict[key.strip()] = val.strip()
185
-
186
- # Validate required fields
187
- if "id" in secret_dict and "path" in secret_dict:
188
- return {"id": secret_dict["id"], "path": secret_dict["path"]}
189
- else:
190
- self.fail("Invalid format. Must be 'key=X,path=Y'", param, ctx)
191
-
192
- def __str__(self):
193
- return repr(self)
194
-
195
- def __repr__(self):
196
- return "MOUNT-SECRET"
197
-
198
-
199
102
  class CommaSeparatedList(click.ParamType):
200
103
  name = "COMMA-SEPARATED-LIST" # type: ignore
201
104
 
@@ -209,11 +112,9 @@ class CommaSeparatedList(click.ParamType):
209
112
  return "COMMA-SEPARATED-LIST"
210
113
 
211
114
 
212
- KVPairType = KeyValuePair()
213
- MetaflowArtifactType = MountMetaflowArtifact()
214
- SecretMountType = MountSecret()
215
- CommaSeparatedListType = CommaSeparatedList()
216
- KVDictType = KeyValueDictPair()
115
+ KVPairType = KeyValuePair() # used for --tag and --env
116
+ CommaSeparatedListType = CommaSeparatedList() # used for --compute-pools
117
+ KVDictType = KeyValueDictPair() # only Used for the list/delete commands for tags
217
118
 
218
119
 
219
120
  class ColorTheme:
@@ -224,10 +125,13 @@ class ColorTheme:
224
125
  DEBUG_COLOR = "yellow"
225
126
 
226
127
  TL_HEADER_COLOR = "magenta"
227
- ROW_COLOR = "bright_white"
128
+ # Use a color that is readable in both light and dark mode.
129
+ # "white" can be hard to see in light mode, so use "black" for rows.
130
+ # Alternatively, "reset" (default terminal color) is safest.
131
+ ROW_COLOR = "reset"
228
132
 
229
133
  INFO_KEY_COLOR = "green"
230
- INFO_VALUE_COLOR = "bright_white"
134
+ INFO_VALUE_COLOR = "reset"
231
135
 
232
136
 
233
137
  NativeList = list
@@ -293,7 +197,6 @@ def _pre_create_debug(
293
197
  capsule: CapsuleDeployer,
294
198
  state_dir: str,
295
199
  options: Dict[str, Any],
296
- cli_parsed_config: Dict[str, Any],
297
200
  ):
298
201
  if CAPSULE_DEBUG:
299
202
  os.makedirs(state_dir, exist_ok=True)
@@ -309,7 +212,6 @@ def _pre_create_debug(
309
212
  "capsule_input": capsule.create_input(), # This is the input that is passed to the capsule deploy API
310
213
  "deploy_response": capsule._capsule_deploy_response, # This is the response from the capsule deploy API
311
214
  "cli_options": options, # These are the actual options passing down to the CLI
312
- "cli_parsed_config": cli_parsed_config, # This is the config object that is created after parsing options from the CLI
313
215
  },
314
216
  indent=2,
315
217
  default=str,
@@ -404,25 +306,18 @@ def app(ctx):
404
306
  os.makedirs(ctx.obj.app_state_dir, exist_ok=True)
405
307
 
406
308
 
407
- def parse_commands(app_config: AppConfig, cli_command_input):
309
+ def parse_cli_commands(cli_command_input):
408
310
  # There can be two modes:
409
311
  # 1. User passes command via `--` in the CLI
410
312
  # 2. User passes the `commands` key in the config.
313
+ # This function parses the command for mode 1.
411
314
  base_commands = []
412
315
  if len(cli_command_input) > 0:
413
- # TODO: we can be a little more fancy here by allowing the user to just call
414
- # `outerbounds app deploy -- foo.py` and figure out if we need to stuff python
415
- # in front of the command or not. But for sake of dumb simplicity, we can just
416
- # assume what ever the user called on local needs to be called remotely, we can
417
- # just ask them to add the outerbounds command in front of it.
418
- # So the dev ex would be :
419
- # `python foo.py` -> `outerbounds app deploy -- python foo.py`
420
316
  if type(cli_command_input) == str:
421
317
  base_commands.append(cli_command_input)
422
318
  else:
423
319
  base_commands.append(shlex.join(cli_command_input))
424
- elif app_config.get("commands", None) is not None:
425
- base_commands.extend(app_config.get("commands"))
320
+
426
321
  return base_commands
427
322
 
428
323
 
@@ -466,194 +361,6 @@ def deployment_instance_options(func):
466
361
  return wrapper
467
362
 
468
363
 
469
- def common_deploy_options(func):
470
- @click.option(
471
- "--name",
472
- type=str,
473
- help="The name of the app to deploy.",
474
- )
475
- @click.option("--port", type=int, help="Port where the app is hosted.")
476
- @click.option(
477
- "--tag",
478
- "tags",
479
- multiple=True,
480
- type=KVPairType,
481
- help="The tags of the app to deploy. Format KEY=VALUE. Example --tag foo=bar --tag x=y",
482
- default=None,
483
- )
484
- @click.option(
485
- "--image",
486
- type=str,
487
- help="The Docker image to deploy with the App",
488
- default=None,
489
- )
490
- @click.option(
491
- "--cpu",
492
- type=str,
493
- help="CPU resource request and limit",
494
- default=None,
495
- )
496
- @click.option(
497
- "--memory",
498
- type=str,
499
- help="Memory resource request and limit",
500
- default=None,
501
- )
502
- @click.option(
503
- "--gpu",
504
- type=str,
505
- help="GPU resource request and limit",
506
- default=None,
507
- )
508
- @click.option(
509
- "--disk",
510
- type=str,
511
- help="Storage resource request and limit",
512
- default=None,
513
- )
514
- @click.option(
515
- "--health-check-enabled",
516
- type=bool,
517
- help="Enable health checks",
518
- default=None,
519
- )
520
- @click.option(
521
- "--health-check-path",
522
- type=str,
523
- help="Health check path",
524
- default=None,
525
- )
526
- @click.option(
527
- "--health-check-initial-delay",
528
- type=int,
529
- help="Initial delay seconds for health check",
530
- default=None,
531
- )
532
- @click.option(
533
- "--health-check-period",
534
- type=int,
535
- help="Period seconds for health check",
536
- default=None,
537
- )
538
- @click.option(
539
- "--compute-pools",
540
- type=CommaSeparatedListType,
541
- help="The compute pools to deploy the app to. Example: --compute-pools default,large",
542
- default=None,
543
- )
544
- @click.option(
545
- "--auth-type",
546
- type=click.Choice(AuthType.enums()),
547
- help="The type of authentication to use for the app.",
548
- default=None,
549
- )
550
- @click.option(
551
- "--public-access/--private-access",
552
- "auth_public",
553
- type=bool,
554
- help="Whether the app is public or not.",
555
- default=None,
556
- )
557
- @click.option(
558
- "--no-deps",
559
- is_flag=True,
560
- help="Do not any dependencies. Directly used the image provided",
561
- default=False,
562
- )
563
- @click.option(
564
- "--min-replicas",
565
- type=int,
566
- help="Minimum number of replicas to deploy",
567
- default=None,
568
- )
569
- @click.option(
570
- "--max-replicas",
571
- type=int,
572
- help="Maximum number of replicas to deploy",
573
- default=None,
574
- )
575
- @click.option(
576
- "--description",
577
- type=str,
578
- help="The description of the app to deploy.",
579
- default=None,
580
- )
581
- @click.option(
582
- "--app-type",
583
- type=str,
584
- help="The type of app to deploy.",
585
- default=None,
586
- )
587
- @click.option(
588
- "--force-upgrade",
589
- is_flag=True,
590
- help="Force upgrade the app even if it is currently being upgraded.",
591
- default=False,
592
- )
593
- @wraps(func)
594
- def wrapper(*args, **kwargs):
595
- return func(*args, **kwargs)
596
-
597
- return wrapper
598
-
599
-
600
- def common_run_options(func):
601
- """Common options for running and deploying apps."""
602
-
603
- @click.option(
604
- "--config-file",
605
- type=str,
606
- help="The config file to use for the App (YAML or JSON)",
607
- default=None,
608
- )
609
- @click.option(
610
- "--secret",
611
- "secrets",
612
- multiple=True,
613
- type=str,
614
- help="Secrets to deploy with the App",
615
- default=None,
616
- )
617
- @click.option(
618
- "--env",
619
- "envs",
620
- multiple=True,
621
- type=KVPairType,
622
- help="Environment variables to deploy with the App. Use format KEY=VALUE",
623
- default=None,
624
- )
625
- @click.option(
626
- "--package-src-path",
627
- type=str,
628
- help="The path to the source code to deploy with the App.",
629
- default=None,
630
- )
631
- @click.option(
632
- "--package-suffixes",
633
- type=CommaSeparatedListType,
634
- help="The suffixes of the source code to deploy with the App.",
635
- default=None,
636
- )
637
- @click.option(
638
- "--dep-from-requirements",
639
- type=str,
640
- help="Path to requirements.txt file for dependencies",
641
- default=None,
642
- )
643
- @click.option(
644
- "--dep-from-pyproject",
645
- type=str,
646
- help="Path to pyproject.toml file for dependencies",
647
- default=None,
648
- )
649
- # TODO: [FIX ME]: Get better CLI abstraction for pypi/conda dependencies
650
- @wraps(func)
651
- def wrapper(*args, **kwargs):
652
- return func(*args, **kwargs)
653
-
654
- return wrapper
655
-
656
-
657
364
  def _package_necessary_things(app_config: AppConfig, logger):
658
365
  # Packaging has a few things to be thought through:
659
366
  # 1. if `entrypoint_path` exists then should we package the directory
@@ -692,10 +399,14 @@ def _package_necessary_things(app_config: AppConfig, logger):
692
399
 
693
400
 
694
401
  @app.command(help="Deploy an app to the Outerbounds Platform.")
695
- @common_deploy_options
696
- @common_run_options
402
+ @click.option(
403
+ "--config-file",
404
+ type=str,
405
+ help="The config file to use for the App (YAML or JSON)",
406
+ default=None,
407
+ )
697
408
  @deployment_instance_options
698
- @experimental.wrapping_cli_options
409
+ @auto_cli_options()
699
410
  @click.pass_context
700
411
  @click.argument("command", nargs=-1, type=click.UNPROCESSED, required=False)
701
412
  def deploy(
@@ -715,8 +426,10 @@ def deploy(
715
426
  raise AppConfigError("OB_CURRENT_PERIMETER is not set")
716
427
  _current_instance_debug_dir = None
717
428
  logger = partial(_logger, timestamp=True)
429
+
430
+ base_commands = parse_cli_commands(command)
431
+ options["commands"] = base_commands
718
432
  try:
719
- _cli_parsed_config = build_config_from_options(options)
720
433
  # Create configuration
721
434
  if options["config_file"]:
722
435
  # Load from file
@@ -726,10 +439,10 @@ def deploy(
726
439
  app_config.update_from_cli_options(options)
727
440
  else:
728
441
  # Create from CLI options
729
- app_config = AppConfig(_cli_parsed_config)
442
+ app_config = AppConfig.from_cli(options)
730
443
 
731
444
  # Validate the configuration
732
- app_config.validate()
445
+ app_config.commit()
733
446
  logger(
734
447
  f"🚀 Deploying {app_config.get('name')} to the Outerbounds platform...",
735
448
  color=ColorTheme.INFO_COLOR,
@@ -754,23 +467,23 @@ def deploy(
754
467
  logger(
755
468
  "📦 Packaging Directory : %s" % app_config.get_state("packaging_directory"),
756
469
  )
757
- # TODO: Construct the command needed to run the app
758
- # If we are constructing the directory with the src_path
759
- # then we need to add the command from the option otherwise
760
- # we use the command from the entrypoint path and whatever follows `--`
761
- # is the command to run.
762
-
763
- # Set some defaults for the deploy command
764
- app_config.set_deploy_defaults(packaging_directory)
765
470
 
766
- if options.get("no_deps") == True:
471
+ if app_config.get("no_deps", False):
767
472
  # Setting this in the state will make it skip the fast-bakery step
768
473
  # of building an image.
769
474
  app_config.set_state("skip_dependencies", True)
770
475
  else:
771
476
  # Check if the user has set the dependencies in the app config
772
477
  dependencies = app_config.get("dependencies", {})
773
- if len(dependencies) == 0:
478
+
479
+ if all(
480
+ [
481
+ dependencies.get("from_pyproject_toml", None) is None,
482
+ dependencies.get("from_requirements_file", None) is None,
483
+ dependencies.get("pypi", None) is None,
484
+ dependencies.get("conda", None) is None,
485
+ ]
486
+ ):
774
487
  # The user has not set any dependencies, so we can sniff the packaging directory
775
488
  # for a dependencies file.
776
489
  requirements_file = os.path.join(
@@ -798,8 +511,6 @@ def deploy(
798
511
  # 2. TODO: validate that the compute pool specified in the app exists.
799
512
  # 3. Building Docker image if necessary (based on parameters)
800
513
  # - We will bake images with fastbakery and pass it to the deploy command
801
- # TODO: validation logic can be wrapped in try catch so that we can provide
802
- # better error messages.
803
514
  cache_dir = os.path.join(
804
515
  ctx.obj.app_state_dir, app_config.get("name", "default")
805
516
  )
@@ -808,11 +519,6 @@ def deploy(
808
519
  for m in msg:
809
520
  logger(m, **kwargs)
810
521
 
811
- deploy_validations(
812
- app_config,
813
- cache_dir=cache_dir,
814
- logger=logger,
815
- )
816
522
  image_spinner = None
817
523
  img_logger = _non_spinner_logger
818
524
  if not no_loader:
@@ -830,10 +536,6 @@ def deploy(
830
536
  if image_spinner:
831
537
  image_spinner.stop()
832
538
 
833
- base_commands = parse_commands(app_config, command)
834
-
835
- app_config.set_state("commands", base_commands)
836
-
837
539
  # TODO: Handle the case where packaging_directory is None
838
540
  # This would involve:
839
541
  # 1. Packaging the code:
@@ -889,7 +591,6 @@ def deploy(
889
591
  capsule,
890
592
  _current_instance_debug_dir,
891
593
  options,
892
- _cli_parsed_config,
893
594
  )
894
595
 
895
596
  if len(currently_present_capsules) > 0:
@@ -1043,6 +744,14 @@ def list(ctx, project, branch, name, tags, format, auth_type):
1043
744
  print_table(table_data, headers)
1044
745
 
1045
746
 
747
+ @app.command()
748
+ @auto_cli_options()
749
+ @click.pass_context
750
+ @click.argument("command", nargs=-1, type=click.UNPROCESSED, required=False)
751
+ def deploy2(ctx, **kwargs):
752
+ pass
753
+
754
+
1046
755
  @app.command(help="Delete an app/apps from the Outerbounds Platform.")
1047
756
  @click.option("--name", type=str, help="Filter app to delete by name")
1048
757
  @click.option("--id", "cap_id", type=str, help="Filter app to delete by id")
@@ -1118,47 +827,6 @@ def delete(ctx, name, cap_id, project, branch, tags, auto_approve):
1118
827
  time.sleep(0.5 + random.random() * 2) # delay to avoid rate limiting
1119
828
 
1120
829
 
1121
- @app.command(help="Run an app locally (for testing).")
1122
- @common_run_options
1123
- @click.pass_context
1124
- def run(ctx, **options):
1125
- """Run an app locally for testing."""
1126
- try:
1127
- # Create configuration
1128
- if options["config_file"]:
1129
- # Load from file
1130
- app_config = AppConfig.from_file(options["config_file"])
1131
-
1132
- # Update with any CLI options using the unified method
1133
- app_config.update_from_cli_options(options)
1134
- else:
1135
- # Create from CLI options
1136
- config_dict = build_config_from_options(options)
1137
- app_config = AppConfig(config_dict)
1138
-
1139
- # Validate the configuration
1140
- app_config.validate()
1141
-
1142
- # Print the configuration
1143
- click.echo("Running App with configuration:")
1144
- click.echo(app_config.to_yaml())
1145
-
1146
- # TODO: Implement local run logic
1147
- # This would involve:
1148
- # 1. Setting up the environment
1149
- # 2. Running the app locally
1150
- # 3. Reporting status
1151
-
1152
- click.echo(f"App '{app_config.config['name']}' running locally!")
1153
-
1154
- except AppConfigError as e:
1155
- click.echo(f"Error in app configuration: {e}", err=True)
1156
- ctx.exit(1)
1157
- except Exception as e:
1158
- click.echo(f"Error running app: {e}", err=True)
1159
- ctx.exit(1)
1160
-
1161
-
1162
830
  @app.command(
1163
831
  help="Get detailed information about an app from the Outerbounds Platform."
1164
832
  )