outerbounds 0.3.173rc0__py3-none-any.whl → 0.3.174__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.
@@ -8,7 +8,6 @@ import shutil
8
8
  import subprocess
9
9
 
10
10
  from ..utils import metaflowconfig
11
- from ..apps.app_cli import app
12
11
 
13
12
  APP_READY_POLL_TIMEOUT_SECONDS = 300
14
13
  # Even after our backend validates that the app routes are ready, it takes a few seconds for
@@ -21,6 +20,11 @@ def cli(**kwargs):
21
20
  pass
22
21
 
23
22
 
23
+ @click.group(help="Manage apps")
24
+ def app(**kwargs):
25
+ pass
26
+
27
+
24
28
  @app.command(help="Start an app using a port and a name")
25
29
  @click.option(
26
30
  "-d",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: outerbounds
3
- Version: 0.3.173rc0
3
+ Version: 0.3.174
4
4
  Summary: More Data Science, Less Administration
5
5
  License: Proprietary
6
6
  Keywords: data science,machine learning,MLOps
@@ -29,8 +29,8 @@ Requires-Dist: google-cloud-secret-manager (>=2.20.0,<3.0.0) ; extra == "gcp"
29
29
  Requires-Dist: google-cloud-storage (>=2.14.0,<3.0.0) ; extra == "gcp"
30
30
  Requires-Dist: metaflow-checkpoint (==0.2.1)
31
31
  Requires-Dist: ob-metaflow (==2.15.14.1)
32
- Requires-Dist: ob-metaflow-extensions (==1.1.160rc0)
33
- Requires-Dist: ob-metaflow-stubs (==6.0.3.173rc0)
32
+ Requires-Dist: ob-metaflow-extensions (==1.1.161)
33
+ Requires-Dist: ob-metaflow-stubs (==6.0.3.174)
34
34
  Requires-Dist: opentelemetry-distro (>=0.41b0) ; extra == "otel"
35
35
  Requires-Dist: opentelemetry-exporter-otlp-proto-http (>=1.20.0) ; extra == "otel"
36
36
  Requires-Dist: opentelemetry-instrumentation-requests (>=0.41b0) ; extra == "otel"
@@ -39,23 +39,9 @@ outerbounds/_vendor/yaml/resolver.py,sha256=dPhU1d7G1JCMktPFvNhyqwj2oNvx1yf_Jfa3
39
39
  outerbounds/_vendor/yaml/scanner.py,sha256=ZcI8IngR56PaQ0m27WU2vxCqmDCuRjz-hr7pirbMPuw,52982
40
40
  outerbounds/_vendor/yaml/serializer.py,sha256=8wFZRy9SsQSktF_f9OOroroqsh4qVUe53ry07P9UgCc,4368
41
41
  outerbounds/_vendor/yaml/tokens.py,sha256=JBSu38wihGr4l73JwbfMA7Ks1-X84g8-NskTz7KwPmA,2578
42
- outerbounds/apps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
43
- outerbounds/apps/app_cli.py,sha256=_PNwjrqLXRsQMUVNFEh9QudnFISCLxcDru2G8gBBXgk,17447
44
- outerbounds/apps/app_config.py,sha256=YRjzVf8uKboh2TMEto_VE_Cd_BiSHhJqcqRVjtDlMQQ,11548
45
- outerbounds/apps/artifacts.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
46
- outerbounds/apps/capsule.py,sha256=o5V_xZKeUrBTaPvpkyDkiR45oo9VVvxVRk6kC3BgqRE,13649
47
- outerbounds/apps/code_package/__init__.py,sha256=8McF7pgx8ghvjRnazp2Qktlxi9yYwNiwESSQrk-2oW8,68
48
- outerbounds/apps/code_package/code_packager.py,sha256=SQDBXKwizzpag5GpwoZpvvkyPOodRSQwk2ecAAfO0HI,23316
49
- outerbounds/apps/code_package/examples.py,sha256=aF8qKIJxCVv_ugcShQjqUsXKKKMsm1oMkQIl8w3QKuw,4016
50
- outerbounds/apps/config_schema.yaml,sha256=Rh8mNbZaTu-kfC7rwjoKkk7KvefKZiQNptuojJYpYAo,6715
51
- outerbounds/apps/dependencies.py,sha256=SqvdFQdFZZW0wXX_CHMHCrfE0TwaRkTvGCRbQ2Mx3q0,3935
52
- outerbounds/apps/deployer.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
53
- outerbounds/apps/secrets.py,sha256=27qf04lOBqRjvcswj0ldHOmntP2T6SEjtMJtkJQ_GUg,6100
54
- outerbounds/apps/utils.py,sha256=JymjsgpU0osF-eDvstFS9zkM7bqliJdqAEV7kAjqxCM,7298
55
- outerbounds/apps/validations.py,sha256=AVEw9eCvkzqq1m5ZC8btaWrSR6kWYKzarELfrASuAwQ,1117
56
42
  outerbounds/cli_main.py,sha256=e9UMnPysmc7gbrimq2I4KfltggyU7pw59Cn9aEguVcU,74
57
43
  outerbounds/command_groups/__init__.py,sha256=QPWtj5wDRTINDxVUL7XPqG3HoxHNvYOg08EnuSZB2Hc,21
58
- outerbounds/command_groups/apps_cli.py,sha256=weXYgUbTVIxMSweLVdod_C1laiB32YKwYhf22OfqQJE,20815
44
+ outerbounds/command_groups/apps_cli.py,sha256=v9OlQ1b4BGB-cBZiHB6W5gDocDoMmrQ7zdK11QiJ-B8,20847
59
45
  outerbounds/command_groups/cli.py,sha256=de4_QY1UeoKX6y-IXIbmklAi6bz0DsdBSmAoCg6lq1o,482
60
46
  outerbounds/command_groups/fast_bakery_cli.py,sha256=5kja7v6C651XAY6dsP_IkBPJQgfU4hA4S9yTOiVPhW0,6213
61
47
  outerbounds/command_groups/local_setup_cli.py,sha256=tuuqJRXQ_guEwOuQSIf9wkUU0yg8yAs31myGViAK15s,36364
@@ -69,7 +55,7 @@ outerbounds/utils/metaflowconfig.py,sha256=l2vJbgPkLISU-XPGZFaC8ZKmYFyJemlD6bwB-
69
55
  outerbounds/utils/schema.py,sha256=lMUr9kNgn9wy-sO_t_Tlxmbt63yLeN4b0xQXbDUDj4A,2331
70
56
  outerbounds/utils/utils.py,sha256=4Z8cszNob_8kDYCLNTrP-wWads_S_MdL3Uj3ju4mEsk,501
71
57
  outerbounds/vendor.py,sha256=gRLRJNXtZBeUpPEog0LOeIsl6GosaFFbCxUvR4bW6IQ,5093
72
- outerbounds-0.3.173rc0.dist-info/METADATA,sha256=HWEaZi-PSqjMRwsyth279vExJHyxCYE3_bdfquibBWs,1846
73
- outerbounds-0.3.173rc0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
74
- outerbounds-0.3.173rc0.dist-info/entry_points.txt,sha256=7ye0281PKlvqxu15rjw60zKg2pMsXI49_A8BmGqIqBw,47
75
- outerbounds-0.3.173rc0.dist-info/RECORD,,
58
+ outerbounds-0.3.174.dist-info/METADATA,sha256=iZlYVROqXN-KK7dEtyZASVkm53ICK8Jq4M5fNElIrR8,1837
59
+ outerbounds-0.3.174.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
60
+ outerbounds-0.3.174.dist-info/entry_points.txt,sha256=7ye0281PKlvqxu15rjw60zKg2pMsXI49_A8BmGqIqBw,47
61
+ outerbounds-0.3.174.dist-info/RECORD,,
File without changes
@@ -1,519 +0,0 @@
1
- import json
2
- import os
3
- import sys
4
- from functools import wraps
5
- from typing import Dict, List, Any, Optional, Union
6
- from outerbounds._vendor import click, yaml
7
- from outerbounds.utils import metaflowconfig
8
- from .app_config import (
9
- AppConfig,
10
- AppConfigError,
11
- CODE_PACKAGE_PREFIX,
12
- CAPSULE_DEBUG,
13
- build_config_from_options,
14
- )
15
- from .utils import (
16
- CommaSeparatedListType,
17
- KVPairType,
18
- )
19
- from .validations import deploy_validations
20
- from .code_package import CodePackager
21
- from .capsule import Capsule
22
- import shlex
23
- import time
24
- import uuid
25
- from datetime import datetime
26
-
27
- LOGGER_TIMESTAMP = "magenta"
28
- LOGGER_COLOR = "green"
29
- LOGGER_BAD_COLOR = "red"
30
-
31
-
32
- def _logger(
33
- body="", system_msg=False, head="", bad=False, timestamp=True, nl=True, color=None
34
- ):
35
- if timestamp:
36
- if timestamp is True:
37
- dt = datetime.now()
38
- else:
39
- dt = timestamp
40
- tstamp = dt.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
41
- click.secho(tstamp + " ", fg=LOGGER_TIMESTAMP, nl=False)
42
- if head:
43
- click.secho(head, fg=LOGGER_COLOR, nl=False)
44
- click.secho(
45
- body,
46
- bold=system_msg,
47
- fg=LOGGER_BAD_COLOR if bad else color if color is not None else None,
48
- nl=nl,
49
- )
50
-
51
-
52
- class CliState(object):
53
- pass
54
-
55
-
56
- def _pre_create_debug(app_config: AppConfig, capsule: Capsule, state_dir: str):
57
- if CAPSULE_DEBUG:
58
- os.makedirs(state_dir, exist_ok=True)
59
- debug_path = os.path.join(state_dir, f"debug_{time.time()}.yaml")
60
- with open(
61
- debug_path,
62
- "w",
63
- ) as f:
64
- f.write(
65
- yaml.dump(
66
- {
67
- "app_state": app_config.dump_state(),
68
- "capsule_input": capsule.create_input(),
69
- },
70
- default_flow_style=False,
71
- indent=2,
72
- )
73
- )
74
-
75
-
76
- @click.group()
77
- def cli():
78
- """Outerbounds CLI tool."""
79
- pass
80
-
81
-
82
- @cli.group(
83
- help="Commands related to Deploying/Running/Managing Apps on Outerbounds Platform."
84
- )
85
- @click.pass_context
86
- def app(ctx):
87
- """App-related commands."""
88
- ctx.obj = CliState()
89
- ctx.obj.trace_id = str(uuid.uuid4())
90
- ctx.obj.app_state_dir = os.path.join(os.curdir, ".ob_apps")
91
- profile = os.environ.get("METAFLOW_PROFILE", "")
92
- config_dir = os.path.expanduser(
93
- os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")
94
- )
95
- api_url = metaflowconfig.get_sanitized_url_from_config(
96
- config_dir, profile, "OBP_API_SERVER"
97
- )
98
-
99
- ctx.obj.api_url = api_url
100
- ctx.obj.perimeter = os.environ.get("OB_CURRENT_PERIMETER")
101
- os.makedirs(ctx.obj.app_state_dir, exist_ok=True)
102
-
103
-
104
- def parse_commands(app_config: AppConfig, cli_command_input):
105
- # There can be two modes:
106
- # 1. User passes command via `--` in the CLI
107
- # 2. User passes the `commands` key in the config.
108
- base_commands = []
109
- if len(cli_command_input) > 0:
110
- # TODO: we can be a little more fancy here by allowing the user to just call
111
- # `outerbounds app deploy -- foo.py` and figure out if we need to stuff python
112
- # in front of the command or not. But for sake of dumb simplicity, we can just
113
- # assume what ever the user called on local needs to be called remotely, we can
114
- # just ask them to add the outerbounds command in front of it.
115
- # So the dev ex would be :
116
- # `python foo.py` -> `outerbounds app deploy -- python foo.py`
117
- if type(cli_command_input) == str:
118
- base_commands.append(cli_command_input)
119
- else:
120
- base_commands.append(shlex.join(cli_command_input))
121
- elif app_config.get("commands", None) is not None:
122
- base_commands.extend(app_config.get("commands"))
123
- return base_commands
124
-
125
-
126
- def common_deploy_options(func):
127
- @click.option(
128
- "--name",
129
- type=str,
130
- help="The name of the app to deploy.",
131
- )
132
- @click.option("--port", type=int, help="Port where the app is hosted.")
133
- @click.option(
134
- "--tag",
135
- "tags",
136
- multiple=True,
137
- type=str,
138
- help="The tags of the app to deploy.",
139
- default=None,
140
- )
141
- @click.option(
142
- "--image",
143
- type=str,
144
- help="The Docker image to deploy with the App",
145
- default=None,
146
- )
147
- @click.option(
148
- "--cpu",
149
- type=str,
150
- help="CPU resource request and limit",
151
- default=None,
152
- )
153
- @click.option(
154
- "--memory",
155
- type=str,
156
- help="Memory resource request and limit",
157
- default=None,
158
- )
159
- @click.option(
160
- "--gpu",
161
- type=str,
162
- help="GPU resource request and limit",
163
- default=None,
164
- )
165
- @click.option(
166
- "--disk",
167
- type=str,
168
- help="Storage resource request and limit",
169
- default=None,
170
- )
171
- @click.option(
172
- "--health-check-enabled",
173
- type=bool,
174
- help="Enable health checks",
175
- default=None,
176
- )
177
- @click.option(
178
- "--health-check-path",
179
- type=str,
180
- help="Health check path",
181
- default=None,
182
- )
183
- @click.option(
184
- "--health-check-initial-delay",
185
- type=int,
186
- help="Initial delay seconds for health check",
187
- default=None,
188
- )
189
- @click.option(
190
- "--health-check-period",
191
- type=int,
192
- help="Period seconds for health check",
193
- default=None,
194
- )
195
- @click.option(
196
- "--compute-pools",
197
- type=CommaSeparatedListType,
198
- help="The compute pools to deploy the app to. Example: --compute-pools default,large",
199
- default=None,
200
- )
201
- @click.option(
202
- "--auth-type",
203
- type=click.Choice(["API", "SSO"]),
204
- help="The type of authentication to use for the app.",
205
- default=None,
206
- )
207
- @click.option(
208
- "--public-access/--private-access",
209
- "auth_public",
210
- type=bool,
211
- help="Whether the app is public or not.",
212
- default=None,
213
- )
214
- @click.option(
215
- "--no-deps",
216
- is_flag=True,
217
- help="Do not any dependencies. Directly used the image provided",
218
- default=False,
219
- )
220
- @wraps(func)
221
- def wrapper(*args, **kwargs):
222
- return func(*args, **kwargs)
223
-
224
- return wrapper
225
-
226
-
227
- def common_run_options(func):
228
- """Common options for running and deploying apps."""
229
-
230
- @click.option(
231
- "--config-file",
232
- type=str,
233
- help="The config file to use for the App (YAML or JSON)",
234
- default=None,
235
- )
236
- @click.option(
237
- "--secret",
238
- "secrets",
239
- multiple=True,
240
- type=str,
241
- help="Secrets to deploy with the App",
242
- default=None,
243
- )
244
- @click.option(
245
- "--env",
246
- "envs",
247
- multiple=True,
248
- type=KVPairType,
249
- help="Environment variables to deploy with the App. Use format KEY=VALUE",
250
- default=None,
251
- )
252
- @click.option(
253
- "--package-src-path",
254
- type=str,
255
- help="The path to the source code to deploy with the App.",
256
- default=None,
257
- )
258
- @click.option(
259
- "--package-suffixes",
260
- type=CommaSeparatedListType,
261
- help="The suffixes of the source code to deploy with the App.",
262
- default=None,
263
- )
264
- @click.option(
265
- "--dep-from-task",
266
- type=str,
267
- help="The pathspec of the Task from which to resolve dependencies",
268
- default=None,
269
- )
270
- @click.option(
271
- "--dep-from-run",
272
- type=str,
273
- help="The pathspec of the Run from which to resolve dependencies",
274
- default=None,
275
- )
276
- @click.option(
277
- "--dep-from-requirements",
278
- type=str,
279
- help="Path to requirements.txt file for dependencies",
280
- default=None,
281
- )
282
- @click.option(
283
- "--dep-from-pyproject",
284
- type=str,
285
- help="Path to pyproject.toml file for dependencies",
286
- default=None,
287
- )
288
- # TODO: [FIX ME]: Get better CLI abstraction for pypi/conda dependencies
289
- @wraps(func)
290
- def wrapper(*args, **kwargs):
291
- return func(*args, **kwargs)
292
-
293
- return wrapper
294
-
295
-
296
- def _package_necessary_things(app_config: AppConfig, logger):
297
- # Packaging has a few things to be thought through:
298
- # 1. if `entrypoint_path` exists then should we package the directory
299
- # where the entrypoint lives. For example : if the user calls
300
- # `outerbounds app deploy foo/bar.py` should we package `foo` dir
301
- # or should we package the cwd from which foo/bar.py is being called.
302
- # 2. if the src path is used with the config file then how should we view
303
- # that path ?
304
- # 3. It becomes interesting when users call the deployment with config files
305
- # where there is a `src_path` and then is the src_path relative to the config file
306
- # or is it relative to where the caller command is sitting. Ideally it should work
307
- # like Kustomizations where its relative to where the yaml file sits for simplicity
308
- # of understanding relationships between config files. Ideally users can pass the src_path
309
- # from the command line and that will aliviate any need to package any other directories for
310
- #
311
-
312
- package_dir = app_config.get_state("packaging_directory")
313
- if package_dir is None:
314
- app_config.set_state("code_package_url", None)
315
- app_config.set_state("code_package_key", None)
316
- return
317
- from metaflow.metaflow_config import DEFAULT_DATASTORE
318
-
319
- package = app_config.get_state("package") or {}
320
- suffixes = package.get("suffixes", None)
321
-
322
- packager = CodePackager(
323
- datastore_type=DEFAULT_DATASTORE, code_package_prefix=CODE_PACKAGE_PREFIX
324
- )
325
- package_url, package_key = packager.store(
326
- paths_to_include=[package_dir], file_suffixes=suffixes
327
- )
328
- app_config.set_state("code_package_url", package_url)
329
- app_config.set_state("code_package_key", package_key)
330
- logger("💾 Code Package Saved to : %s" % app_config.get_state("code_package_url"))
331
-
332
-
333
- @app.command(help="Deploy an app to the Outerbounds Platform.")
334
- @common_deploy_options
335
- @common_run_options
336
- @click.pass_context
337
- @click.argument("command", nargs=-1, type=click.UNPROCESSED, required=False)
338
- def deploy(ctx, command, **options):
339
- """Deploy an app to the Outerbounds Platform."""
340
- from functools import partial
341
-
342
- if not ctx.obj.perimeter:
343
- raise AppConfigError("OB_CURRENT_PERIMETER is not set")
344
-
345
- logger = partial(_logger, timestamp=True)
346
- try:
347
- # Create configuration
348
- if options["config_file"]:
349
- # Load from file
350
- app_config = AppConfig.from_file(options["config_file"])
351
-
352
- # Update with any CLI options using the unified method
353
- app_config.update_from_cli_options(options)
354
- else:
355
- # Create from CLI options
356
- config_dict = build_config_from_options(options)
357
- app_config = AppConfig(config_dict)
358
-
359
- # Validate the configuration
360
- app_config.validate()
361
- logger(
362
- f"🚀 Deploying {app_config.get('name')} to the Outerbounds platform...",
363
- color=LOGGER_COLOR,
364
- system_msg=True,
365
- )
366
-
367
- packaging_directory = None
368
- package_src_path = app_config.get("package", {}).get("src_path", None)
369
- if package_src_path:
370
- if os.path.isfile(package_src_path):
371
- raise AppConfigError("src_path must be a directory, not a file")
372
- elif os.path.isdir(package_src_path):
373
- packaging_directory = os.path.abspath(package_src_path)
374
- else:
375
- raise AppConfigError(f"src_path '{package_src_path}' does not exist")
376
- else:
377
- # If src_path is None then we assume then we can assume for the moment
378
- # that we can package the current working directory.
379
- packaging_directory = os.getcwd()
380
-
381
- app_config.set_state("packaging_directory", packaging_directory)
382
- logger(
383
- "📦 Packaging Directory : %s" % app_config.get_state("packaging_directory"),
384
- )
385
- # TODO: Construct the command needed to run the app
386
- # If we are constructing the directory with the src_path
387
- # then we need to add the command from the option otherwise
388
- # we use the command from the entrypoint path and whatever follows `--`
389
- # is the command to run.
390
-
391
- # Set some defaults for the deploy command
392
- app_config.set_deploy_defaults(packaging_directory)
393
-
394
- if options.get("no_deps") == True:
395
- # Setting this in the state will make it skip the fast-bakery step
396
- # of building an image.
397
- app_config.set_state("skip_dependencies", True)
398
- else:
399
- # Check if the user has set the dependencies in the app config
400
- dependencies = app_config.get("dependencies", {})
401
- if len(dependencies) == 0:
402
- # The user has not set any dependencies, so we can sniff the packaging directory
403
- # for a dependencies file.
404
- requirements_file = os.path.join(
405
- packaging_directory, "requirements.txt"
406
- )
407
- pyproject_toml = os.path.join(packaging_directory, "pyproject.toml")
408
- if os.path.exists(pyproject_toml):
409
- app_config.set_state(
410
- "dependencies", {"from_pyproject_toml": pyproject_toml}
411
- )
412
- logger(
413
- "📦 Using dependencies from pyproject.toml: %s" % pyproject_toml
414
- )
415
- elif os.path.exists(requirements_file):
416
- app_config.set_state(
417
- "dependencies", {"from_requirements_file": requirements_file}
418
- )
419
- logger(
420
- "📦 Using dependencies from requirements.txt: %s"
421
- % requirements_file
422
- )
423
-
424
- # Print the configuration
425
- # 1. validate that the secrets for the app exist
426
- # 2. TODO: validate that the compute pool specified in the app exists.
427
- # 3. Building Docker image if necessary (based on parameters)
428
- # - We will bake images with fastbakery and pass it to the deploy command
429
- # TODO: validation logic can be wrapped in try catch so that we can provide
430
- # better error messages.
431
- cache_dir = os.path.join(
432
- ctx.obj.app_state_dir, app_config.get("name", "default")
433
- )
434
- deploy_validations(
435
- app_config,
436
- cache_dir=cache_dir,
437
- logger=logger,
438
- )
439
-
440
- base_commands = parse_commands(app_config, command)
441
-
442
- app_config.set_state("commands", base_commands)
443
-
444
- # TODO: Handle the case where packaging_directory is None
445
- # This would involve:
446
- # 1. Packaging the code:
447
- # - We need to package the code and throw the tarball to some object store
448
- _package_necessary_things(app_config, logger)
449
-
450
- app_config.set_state("perimeter", ctx.obj.perimeter)
451
-
452
- # 2. Convert to the IR that the backend accepts
453
- capsule = Capsule(app_config, ctx.obj.api_url, debug_dir=cache_dir)
454
- _pre_create_debug(app_config, capsule, cache_dir)
455
- # 3. Throw the job into the platform and report deployment status
456
- logger(
457
- f"🚀 Deploying {capsule.capsule_type.lower()} to the platform...",
458
- color=LOGGER_COLOR,
459
- system_msg=True,
460
- )
461
- capsule.create()
462
- capsule.wait_for_terminal_state(logger=logger)
463
- logger(
464
- f"💊 {capsule.capsule_type} {app_config.config['name']} ({capsule.identifier}) deployed successfully! You can access it at {capsule.status.out_of_cluster_url}",
465
- color=LOGGER_COLOR,
466
- system_msg=True,
467
- )
468
-
469
- except AppConfigError as e:
470
- click.echo(f"Error in app configuration: {e}", err=True)
471
- raise e
472
- except Exception as e:
473
- click.echo(f"Error deploying app: {e}", err=True)
474
- raise e
475
-
476
-
477
- @app.command(help="Run an app locally (for testing).")
478
- @common_run_options
479
- @click.pass_context
480
- def run(ctx, **options):
481
- """Run an app locally for testing."""
482
- try:
483
- # Create configuration
484
- if options["config_file"]:
485
- # Load from file
486
- app_config = AppConfig.from_file(options["config_file"])
487
-
488
- # Update with any CLI options using the unified method
489
- app_config.update_from_cli_options(options)
490
- else:
491
- # Create from CLI options
492
- config_dict = build_config_from_options(options)
493
- app_config = AppConfig(config_dict)
494
-
495
- # Validate the configuration
496
- app_config.validate()
497
-
498
- # Print the configuration
499
- click.echo("Running App with configuration:")
500
- click.echo(app_config.to_yaml())
501
-
502
- # TODO: Implement local run logic
503
- # This would involve:
504
- # 1. Setting up the environment
505
- # 2. Running the app locally
506
- # 3. Reporting status
507
-
508
- click.echo(f"App '{app_config.config['name']}' running locally!")
509
-
510
- except AppConfigError as e:
511
- click.echo(f"Error in app configuration: {e}", err=True)
512
- ctx.exit(1)
513
- except Exception as e:
514
- click.echo(f"Error running app: {e}", err=True)
515
- ctx.exit(1)
516
-
517
-
518
- # if __name__ == "__main__":
519
- # cli()