outerbounds 0.3.173rc0__py3-none-any.whl → 0.3.175rc0__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.
@@ -10,12 +10,14 @@ from .app_config import (
10
10
  AppConfigError,
11
11
  CODE_PACKAGE_PREFIX,
12
12
  CAPSULE_DEBUG,
13
- build_config_from_options,
13
+ AuthType,
14
14
  )
15
+ from .cli_to_config import build_config_from_options
15
16
  from .utils import (
16
17
  CommaSeparatedListType,
17
18
  KVPairType,
18
19
  )
20
+ from . import experimental
19
21
  from .validations import deploy_validations
20
22
  from .code_package import CodePackager
21
23
  from .capsule import Capsule
@@ -134,8 +136,8 @@ def common_deploy_options(func):
134
136
  "--tag",
135
137
  "tags",
136
138
  multiple=True,
137
- type=str,
138
- help="The tags of the app to deploy.",
139
+ type=KVPairType,
140
+ help="The tags of the app to deploy. Format KEY=VALUE. Example --tag foo=bar --tag x=y",
139
141
  default=None,
140
142
  )
141
143
  @click.option(
@@ -200,7 +202,7 @@ def common_deploy_options(func):
200
202
  )
201
203
  @click.option(
202
204
  "--auth-type",
203
- type=click.Choice(["API", "SSO"]),
205
+ type=click.Choice(AuthType.enums()),
204
206
  help="The type of authentication to use for the app.",
205
207
  default=None,
206
208
  )
@@ -217,6 +219,18 @@ def common_deploy_options(func):
217
219
  help="Do not any dependencies. Directly used the image provided",
218
220
  default=False,
219
221
  )
222
+ @click.option(
223
+ "--min-replicas",
224
+ type=int,
225
+ help="Minimum number of replicas to deploy",
226
+ default=None,
227
+ )
228
+ @click.option(
229
+ "--max-replicas",
230
+ type=int,
231
+ help="Maximum number of replicas to deploy",
232
+ default=None,
233
+ )
220
234
  @wraps(func)
221
235
  def wrapper(*args, **kwargs):
222
236
  return func(*args, **kwargs)
@@ -261,18 +275,6 @@ def common_run_options(func):
261
275
  help="The suffixes of the source code to deploy with the App.",
262
276
  default=None,
263
277
  )
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
278
  @click.option(
277
279
  "--dep-from-requirements",
278
280
  type=str,
@@ -333,6 +335,7 @@ def _package_necessary_things(app_config: AppConfig, logger):
333
335
  @app.command(help="Deploy an app to the Outerbounds Platform.")
334
336
  @common_deploy_options
335
337
  @common_run_options
338
+ @experimental.wrapping_cli_options
336
339
  @click.pass_context
337
340
  @click.argument("command", nargs=-1, type=click.UNPROCESSED, required=False)
338
341
  def deploy(ctx, command, **options):
@@ -451,6 +454,7 @@ def deploy(ctx, command, **options):
451
454
 
452
455
  # 2. Convert to the IR that the backend accepts
453
456
  capsule = Capsule(app_config, ctx.obj.api_url, debug_dir=cache_dir)
457
+
454
458
  _pre_create_debug(app_config, capsule, cache_dir)
455
459
  # 3. Throw the job into the platform and report deployment status
456
460
  logger(
@@ -2,90 +2,16 @@ import json
2
2
  import os
3
3
  from outerbounds._vendor import yaml
4
4
  from typing import Dict, Any
5
+ from .cli_to_config import build_config_from_options
5
6
 
6
7
  CODE_PACKAGE_PREFIX = "mf.obp-apps"
7
8
 
8
9
  CAPSULE_DEBUG = os.environ.get("OUTERBOUNDS_CAPSULE_DEBUG", False)
9
10
 
10
11
 
11
- def build_config_from_options(options):
12
- """Build an app configuration from CLI options."""
13
- config = {}
14
-
15
- # Set basic fields
16
- for key in ["name", "port", "image", "compute_pools"]:
17
- if options.get(key):
18
- config[key] = options[key]
19
-
20
- # Handle list fields
21
- if options.get("tags"):
22
- config["tags"] = list(options["tags"])
23
- if options.get("secrets"):
24
- config["secrets"] = list(options["secrets"])
25
-
26
- # Build env dict from key-value pairs
27
- if options.get("envs"):
28
- env_dict = {}
29
- for env_item in options["envs"]:
30
- env_dict.update(env_item)
31
- config["environment"] = env_dict
32
-
33
- # Handle dependencies (only one type allowed)
34
- deps = {}
35
- if options.get("dep_from_task"):
36
- deps["from_task"] = options["dep_from_task"]
37
- elif options.get("dep_from_run"):
38
- deps["from_run"] = options["dep_from_run"]
39
- elif options.get("dep_from_requirements"):
40
- deps["from_requirements_file"] = options["dep_from_requirements"]
41
- elif options.get("dep_from_pyproject"):
42
- deps["from_pyproject_toml"] = options["dep_from_pyproject"]
43
-
44
- # TODO: [FIX ME]: Get better CLI abstraction for pypi/conda dependencies
45
-
46
- if deps:
47
- config["dependencies"] = deps
48
-
49
- # Handle resources
50
- resources = {}
51
- for key in ["cpu", "memory", "gpu", "storage"]:
52
- if options.get(key):
53
- resources[key] = options[key]
54
-
55
- if resources:
56
- config["resources"] = resources
57
-
58
- # Handle health check options
59
- health_check = {}
60
- if options.get("health_check_enabled") is not None:
61
- health_check["enabled"] = options["health_check_enabled"]
62
- if options.get("health_check_path"):
63
- health_check["path"] = options["health_check_path"]
64
- if options.get("health_check_initial_delay") is not None:
65
- health_check["initial_delay_seconds"] = options["health_check_initial_delay"]
66
- if options.get("health_check_period") is not None:
67
- health_check["period_seconds"] = options["health_check_period"]
68
-
69
- if health_check:
70
- config["health_check"] = health_check
71
-
72
- # Handle package options
73
- if options.get("package_src_path") or options.get("package_suffixes"):
74
- config["package"] = {}
75
- if options.get("package_src_path"):
76
- config["package"]["src_path"] = options["package_src_path"]
77
- if options.get("package_suffixes"):
78
- config["package"]["suffixes"] = options["package_suffixes"]
79
-
80
- # Handle auth options
81
- if options.get("auth_type") or options.get("auth_public"):
82
- config["auth"] = {}
83
- if options.get("auth_type"):
84
- config["auth"]["type"] = options["auth_type"]
85
- if options.get("auth_public"):
86
- config["auth"]["public"] = options["auth_public"]
87
-
88
- return config
12
+ class classproperty(property):
13
+ def __get__(self, owner_self, owner_cls):
14
+ return self.fget(owner_cls)
89
15
 
90
16
 
91
17
  class AppConfigError(Exception):
@@ -94,6 +20,19 @@ class AppConfigError(Exception):
94
20
  pass
95
21
 
96
22
 
23
+ class AuthType:
24
+ BROWSER = "Browser"
25
+ API = "API"
26
+
27
+ @classmethod
28
+ def enums(cls):
29
+ return [cls.BROWSER, cls.API]
30
+
31
+ @classproperty
32
+ def default(cls):
33
+ return cls.BROWSER
34
+
35
+
97
36
  class AppConfig:
98
37
  """Class representing an Outerbounds App configuration."""
99
38
 
@@ -140,7 +79,7 @@ class AppConfig:
140
79
  if not self.config["auth"].get("public"):
141
80
  self.config["auth"]["public"] = True
142
81
  if not self.config["auth"].get("type"):
143
- self.config["auth"]["type"] = "SSO"
82
+ self.config["auth"]["type"] = AuthType.BROWSER
144
83
 
145
84
  if not self.config.get("health_check"):
146
85
  self.config["health_check"] = {}
@@ -156,6 +95,23 @@ class AppConfig:
156
95
  if not self.config["resources"].get("disk"):
157
96
  self.config["resources"]["disk"] = "20Gi"
158
97
 
98
+ if not self.config.get("replicas", None):
99
+ self.config["replicas"] = {
100
+ "min": 1,
101
+ "max": 1,
102
+ }
103
+ else:
104
+ max_is_set = self.config["replicas"].get("max", None) is not None
105
+ min_is_set = self.config["replicas"].get("min", None) is not None
106
+ if max_is_set and not min_is_set:
107
+ # If users want to set 0 replicas for min,
108
+ # then they need explicitly specify min to 0
109
+ self.config["replicas"]["min"] = 1 # Atleast set 1 replica
110
+ if min_is_set and not max_is_set:
111
+ # In the situations where we dont have min/max replicas, we can
112
+ # set max to min.
113
+ self.config["replicas"]["max"] = self.config["replicas"].get("min")
114
+
159
115
  def _validate_required_fields(self) -> None:
160
116
  """Validate that all required fields are present."""
161
117
  required_fields = self.schema.get("required", [])
@@ -232,6 +188,35 @@ class AppConfig:
232
188
  f"You can only specify one mode of specifying dependencies. You have specified : {found_types} . Please only set one."
233
189
  )
234
190
 
191
+ # Validate that each tag has exactly one key
192
+ if "tags" in self.config:
193
+ tags = self.config["tags"]
194
+ for tag in tags:
195
+ if not isinstance(tag, dict):
196
+ raise AppConfigError(
197
+ "Each tag must be a dictionary. %s is of type %s"
198
+ % (str(tag), type(tag))
199
+ )
200
+ if len(tag.keys()) != 1:
201
+ raise AppConfigError(
202
+ "Each tag must have exactly one key-value pair. Tag %s has %d key-value pairs."
203
+ % (str(tag), len(tag.keys()))
204
+ )
205
+ if "replicas" in self.config:
206
+ replicas = self.config["replicas"]
207
+ if not isinstance(replicas, dict):
208
+ raise AppConfigError("Replicas must be an object.")
209
+ max_is_set = self.config["replicas"].get("max", None) is not None
210
+ if max_is_set:
211
+ if replicas.get("max") == 0:
212
+ raise AppConfigError("Max replicas must be greater than 0.")
213
+
214
+ if replicas.get("min", 1) > replicas.get("max"):
215
+ raise AppConfigError(
216
+ "Min replicas must be less than max replicas. %s > %s"
217
+ % (replicas.get("min", 1), replicas.get("max", 1))
218
+ )
219
+
235
220
  def to_dict(self) -> Dict[str, Any]:
236
221
  """Return the configuration as a dictionary."""
237
222
  return self.config
@@ -6,7 +6,8 @@ import time
6
6
  import shlex
7
7
  from typing import Optional
8
8
  from .utils import TODOException, safe_requests_wrapper
9
- from .app_config import AppConfig, CAPSULE_DEBUG
9
+ from .app_config import AppConfig, CAPSULE_DEBUG, AuthType
10
+ from . import experimental
10
11
 
11
12
 
12
13
  class CapsuleStateMachine:
@@ -186,9 +187,9 @@ class CapsuleInput:
186
187
  "ephemeralStorage": str(app_config.get_state("resources").get("disk")),
187
188
  **resources,
188
189
  },
189
- "autoscalingConfig": { # TODO [FIX ME]: Make this configurable from top level
190
- "minReplicas": 1,
191
- "maxReplicas": 1,
190
+ "autoscalingConfig": {
191
+ "minReplicas": app_config.get_state("replicas", {}).get("min", 1),
192
+ "maxReplicas": app_config.get_state("replicas", {}).get("max", 1),
192
193
  },
193
194
  **_scheduling_config,
194
195
  "containerStartupConfig": {
@@ -202,14 +203,18 @@ class CapsuleInput:
202
203
  "authType": app_config.get_state("auth").get("type"),
203
204
  "publicToDeployment": app_config.get_state("auth").get("public"),
204
205
  },
205
- "tags": app_config.get_state("tags", []),
206
+ "tags": [
207
+ dict(key=k, value=v)
208
+ for tag in app_config.get_state("tags", [])
209
+ for k, v in tag.items()
210
+ ],
206
211
  "port": app_config.get_state("port"),
207
212
  "displayName": app_config.get_state("name"),
208
213
  }
209
214
 
210
215
 
211
- def create_capsule(app_config: AppConfig, api_url: str, request_headers: dict):
212
- _data = json.dumps(CapsuleInput.from_app_config(app_config))
216
+ def create_capsule(capsule_input: dict, api_url: str, request_headers: dict):
217
+ _data = json.dumps(capsule_input)
213
218
  response = safe_requests_wrapper(
214
219
  requests.post,
215
220
  api_url,
@@ -273,18 +278,6 @@ def delete_capsule(capsule_id: str, api_url: str, request_headers: dict):
273
278
  return response.json()
274
279
 
275
280
 
276
- class StatusTrail:
277
- def __init__(self, capsule_id: str):
278
- self._capsule_id = capsule_id
279
- self._status_trail = []
280
-
281
- def add_status(self, status: dict):
282
- self._status_trail.append({"timestamp": time.time(), "status": status})
283
-
284
- def get_status_trail(self):
285
- return self._status_trail
286
-
287
-
288
281
  class Capsule:
289
282
 
290
283
  status: CapsuleStateMachine
@@ -318,10 +311,10 @@ class Capsule:
318
311
 
319
312
  @property
320
313
  def capsule_type(self):
321
- auth_type = self._app_config.get_state("auth", {}).get("type", "SSO")
322
- if auth_type == "SSO":
314
+ auth_type = self._app_config.get_state("auth", {}).get("type", AuthType.default)
315
+ if auth_type == AuthType.BROWSER:
323
316
  return "App"
324
- elif auth_type == "API":
317
+ elif auth_type == AuthType.API:
325
318
  return "Endpoint"
326
319
  else:
327
320
  raise TODOException(f"Unknown auth type: {auth_type}")
@@ -331,11 +324,13 @@ class Capsule:
331
324
  return self._app_config.get_state("name")
332
325
 
333
326
  def create_input(self):
334
- return CapsuleInput.from_app_config(self._app_config)
327
+ return experimental.capsule_input_overrides(
328
+ self._app_config, CapsuleInput.from_app_config(self._app_config)
329
+ )
335
330
 
336
331
  def create(self):
337
332
  capsule_response = create_capsule(
338
- self._app_config, self._base_url, self._request_headers
333
+ self.create_input(), self._base_url, self._request_headers
339
334
  )
340
335
  self.identifier = capsule_response.get("id")
341
336
  return self.identifier
@@ -346,18 +341,14 @@ class Capsule:
346
341
 
347
342
  def wait_for_terminal_state(self, logger=print):
348
343
  state_machine = CapsuleStateMachine(self.identifier)
344
+ logger(
345
+ "💊 Waiting for %s %s to be ready to serve traffic"
346
+ % (self.capsule_type.lower(), self.identifier)
347
+ )
349
348
  for i in range(self._create_timeout):
350
349
  capsule_response = self.get()
351
350
  state_machine.add_status(capsule_response.get("status", {}))
352
351
  time.sleep(1)
353
- if state_machine.is_completely_new_capsule() and i == 0:
354
- logger(
355
- "🔧 🛠️ Creating new %s with id %s"
356
- % (self.capsule_type.lower(), self.identifier)
357
- )
358
- elif not state_machine.is_completely_new_capsule() and i == 0:
359
- logger("🔧 🛠️ Updating %s %s" % (self.capsule_type, self.identifier))
360
-
361
352
  state_machine.report_current_status(logger)
362
353
  if state_machine.ready_to_serve_traffic:
363
354
  logger(
@@ -0,0 +1,91 @@
1
+ from . import experimental
2
+
3
+
4
+ def build_config_from_options(options):
5
+ """Build an app configuration from CLI options."""
6
+ config = {}
7
+
8
+ # Set basic fields
9
+ for key in ["name", "port", "image", "compute_pools"]:
10
+ if options.get(key):
11
+ config[key] = options[key]
12
+
13
+ # Handle list fields
14
+ if options.get("tags"):
15
+ config["tags"] = list(options["tags"])
16
+ if options.get("secrets"):
17
+ config["secrets"] = list(options["secrets"])
18
+
19
+ # Build env dict from key-value pairs
20
+ if options.get("envs"):
21
+ env_dict = {}
22
+ for env_item in options["envs"]:
23
+ env_dict.update(env_item)
24
+ config["environment"] = env_dict
25
+
26
+ # Handle dependencies (only one type allowed)
27
+ deps = {}
28
+ if options.get("dep_from_task"):
29
+ deps["from_task"] = options["dep_from_task"]
30
+ elif options.get("dep_from_run"):
31
+ deps["from_run"] = options["dep_from_run"]
32
+ elif options.get("dep_from_requirements"):
33
+ deps["from_requirements_file"] = options["dep_from_requirements"]
34
+ elif options.get("dep_from_pyproject"):
35
+ deps["from_pyproject_toml"] = options["dep_from_pyproject"]
36
+
37
+ # TODO: [FIX ME]: Get better CLI abstraction for pypi/conda dependencies
38
+
39
+ if deps:
40
+ config["dependencies"] = deps
41
+
42
+ # Handle resources
43
+ resources = {}
44
+ for key in ["cpu", "memory", "gpu", "storage"]:
45
+ if options.get(key):
46
+ resources[key] = options[key]
47
+
48
+ if resources:
49
+ config["resources"] = resources
50
+
51
+ # Handle health check options
52
+ health_check = {}
53
+ if options.get("health_check_enabled") is not None:
54
+ health_check["enabled"] = options["health_check_enabled"]
55
+ if options.get("health_check_path"):
56
+ health_check["path"] = options["health_check_path"]
57
+ if options.get("health_check_initial_delay") is not None:
58
+ health_check["initial_delay_seconds"] = options["health_check_initial_delay"]
59
+ if options.get("health_check_period") is not None:
60
+ health_check["period_seconds"] = options["health_check_period"]
61
+
62
+ if health_check:
63
+ config["health_check"] = health_check
64
+
65
+ # Handle package options
66
+ if options.get("package_src_path") or options.get("package_suffixes"):
67
+ config["package"] = {}
68
+ if options.get("package_src_path"):
69
+ config["package"]["src_path"] = options["package_src_path"]
70
+ if options.get("package_suffixes"):
71
+ config["package"]["suffixes"] = options["package_suffixes"]
72
+
73
+ # Handle auth options
74
+ if options.get("auth_type") or options.get("auth_public"):
75
+ config["auth"] = {}
76
+ if options.get("auth_type"):
77
+ config["auth"]["type"] = options["auth_type"]
78
+ if options.get("auth_public"):
79
+ config["auth"]["public"] = options["auth_public"]
80
+
81
+ replicas = {}
82
+ if options.get("min_replicas"):
83
+ replicas["min"] = options["min_replicas"]
84
+ if options.get("max_replicas"):
85
+ replicas["max"] = options["max_replicas"]
86
+ if len(replicas) > 0:
87
+ config["replicas"] = replicas
88
+
89
+ config.update(experimental.build_config_from_options(options))
90
+
91
+ return config
@@ -2,7 +2,9 @@
2
2
  title: Outerbounds App Configuration Schema
3
3
  description: |
4
4
  Schema for defining Outerbounds Apps configuration. This schema is what we will end up using on the CLI/programmatic interface.
5
- All the properties in this schema will then translate into an IR that will have resolved information from the
5
+ How to read this schema:
6
+ 1. If the a property has `allow_union`:true then it will allow overrides from the cli.
7
+ 2. If a property has `experimental` set to true then a lot its validations may-be skipped and parsing handled somewhere else.
6
8
  version: 1.0.0
7
9
  type: object
8
10
  required:
@@ -27,8 +29,10 @@ properties:
27
29
  type: array
28
30
  description: The tags of the app to deploy.
29
31
  items:
30
- type: string
31
- example: ["production", "v1.0"]
32
+ type: object
33
+ example:
34
+ - foo: bar
35
+ - x: y
32
36
  image: # Only used in `deploy` command
33
37
  allow_union: true # We will overrwite the image if specified on the CLI.
34
38
  type: string
@@ -141,6 +145,20 @@ properties:
141
145
  description: Storage resource request and limit.
142
146
  example: "1Gi"
143
147
  default: "10Gi"
148
+ replicas:
149
+ allow_union: true
150
+ type: object
151
+ description: |
152
+ The number of replicas to deploy the app with.
153
+ properties:
154
+ min:
155
+ type: integer
156
+ description: The minimum number of replicas to deploy the app with.
157
+ example: 1
158
+ max:
159
+ type: integer
160
+ description: The maximum number of replicas to deploy the app with.
161
+ example: 10
144
162
  health_check: # Can be used in `run` command
145
163
  type: object
146
164
  # `allow_union` property means that any object in this field will be done a union with the config file if something is provided on commanline.
@@ -183,7 +201,7 @@ properties:
183
201
  type: string
184
202
  description: |
185
203
  The type of authentication to use for the app.
186
- enum: [API, SSO]
204
+ enum: [API, Browser]
187
205
  public:
188
206
  type: boolean
189
207
  description: |
@@ -192,3 +210,41 @@ properties:
192
210
  # There is an allowed perimeters property
193
211
  # But that needs a little more thought on how
194
212
  # to expose.
213
+
214
+ # ------------------------------------ EXPERIMENTAL ------------------------------------
215
+ project:
216
+ type: string
217
+ description: The project name to deploy the app to.
218
+ experimental: true
219
+ allow-union: true
220
+ branch:
221
+ type: string
222
+ description: The branch name to deploy the app to.
223
+ experimental: true
224
+ allow-union: true
225
+
226
+ models: #
227
+ type: array
228
+ description: model asset ids to include with the deployment. NO CLI Option for this Now.
229
+ experimental: true
230
+ allow-union: true
231
+ items:
232
+ type: object
233
+ properties:
234
+ asset_id:
235
+ type: string
236
+ asset_instance_id:
237
+ type: string
238
+ data: #
239
+ type: array
240
+ description: data asset ids to include with the deployment.
241
+ experimental: true
242
+ allow-union: true
243
+ items:
244
+ type: object
245
+ properties:
246
+ asset_id:
247
+ type: string
248
+ asset_instance_id:
249
+ type: string
250
+ # ------------------------------------ EXPERIMENTAL ------------------------------------
@@ -0,0 +1,103 @@
1
+ from functools import wraps
2
+ from outerbounds._vendor import click
3
+ import os
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from ..app_config import AppConfig
8
+
9
+ DEFAULT_BRANCH = "test"
10
+
11
+
12
+ def wrapping_cli_options(func):
13
+ @click.option(
14
+ "--project",
15
+ type=str,
16
+ help="The flow project the app/endpoint belongs to",
17
+ default=None,
18
+ )
19
+ @click.option(
20
+ "--branch",
21
+ type=str,
22
+ help="The branch the app/endpoint belongs to",
23
+ default=None,
24
+ )
25
+ @wraps(func)
26
+ def wrapper(*args, **kwargs):
27
+ return func(*args, **kwargs)
28
+
29
+ return wrapper
30
+
31
+
32
+ def build_config_from_options(options):
33
+ """Build an app configuration from CLI options."""
34
+ keys = [
35
+ "project",
36
+ "branch",
37
+ ]
38
+ config = {}
39
+ for key in keys:
40
+ if options.get(key):
41
+ config[key] = options.get(key)
42
+
43
+ return config
44
+
45
+
46
+ # Account for project / branch and the capsule input.
47
+ def capsule_input_overrides(app_config: "AppConfig", capsule_input: dict):
48
+ project = app_config.get_state("project", None)
49
+ # Update the project/branch related configurations.
50
+ if project is not None:
51
+ branch = app_config.get_state("branch", DEFAULT_BRANCH)
52
+ capsule_input["tags"].extend(
53
+ [dict(key="project", value=project), dict(key="branch", value=branch)]
54
+ )
55
+
56
+ model_asset_conf = app_config.get_state("models", None)
57
+ data_asset_conf = app_config.get_state("data", None)
58
+ code_info = _code_info(app_config)
59
+ # todo:fix me
60
+ _objects_key = "associatedObjects"
61
+ if model_asset_conf or data_asset_conf or code_info:
62
+ capsule_input[_objects_key] = {}
63
+
64
+ if model_asset_conf:
65
+ capsule_input[_objects_key]["models"] = [
66
+ {"assetId": x["asset_id"], "assetInstanceId": x["asset_instance_id"]}
67
+ for x in model_asset_conf
68
+ ]
69
+ if data_asset_conf:
70
+ capsule_input[_objects_key]["data"] = [
71
+ {"assetId": x["asset_id"], "assetInstanceId": x["asset_instance_id"]}
72
+ for x in data_asset_conf
73
+ ]
74
+ if code_info:
75
+ capsule_input[_objects_key]["code"] = code_info
76
+
77
+ return capsule_input
78
+
79
+
80
+ def _code_info(app_config: "AppConfig"):
81
+ from metaflow.metaflow_git import get_repository_info, _call_git
82
+
83
+ repo_info = get_repository_info(app_config.get_state("packaging_directory", None))
84
+ if len(repo_info) == 0:
85
+ return None
86
+
87
+ git_log_info, returncode, failed = _call_git(
88
+ ["log", "-1", "--pretty=%B"],
89
+ path=app_config.get_state("packaging_directory", None),
90
+ )
91
+ _url = (
92
+ repo_info["repo_url"]
93
+ if not repo_info["repo_url"].endswith(".git")
94
+ else repo_info["repo_url"].rstrip(".git")
95
+ )
96
+ _code_info = {
97
+ "commitId": repo_info["commit_sha"],
98
+ "commitLink": os.path.join(_url, "commit", repo_info["commit_sha"]),
99
+ }
100
+ if not failed and returncode == 0:
101
+ _code_info["commitMessage"] = git_log_info.strip()
102
+
103
+ return _code_info
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: outerbounds
3
- Version: 0.3.173rc0
3
+ Version: 0.3.175rc0
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.162rc0)
33
+ Requires-Dist: ob-metaflow-stubs (==6.0.3.175rc0)
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"
@@ -40,16 +40,18 @@ outerbounds/_vendor/yaml/scanner.py,sha256=ZcI8IngR56PaQ0m27WU2vxCqmDCuRjz-hr7pi
40
40
  outerbounds/_vendor/yaml/serializer.py,sha256=8wFZRy9SsQSktF_f9OOroroqsh4qVUe53ry07P9UgCc,4368
41
41
  outerbounds/_vendor/yaml/tokens.py,sha256=JBSu38wihGr4l73JwbfMA7Ks1-X84g8-NskTz7KwPmA,2578
42
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
43
+ outerbounds/apps/app_cli.py,sha256=oUsPGDr3jnW5l7h4Mp1I4WnRZAanXN1se9_aQ1ybhNQ,17559
44
+ outerbounds/apps/app_config.py,sha256=KBmW9grhiuG9XZG-R0GZkM-024cjj6ztGzOX_2wZW34,11291
45
45
  outerbounds/apps/artifacts.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
46
- outerbounds/apps/capsule.py,sha256=o5V_xZKeUrBTaPvpkyDkiR45oo9VVvxVRk6kC3BgqRE,13649
46
+ outerbounds/apps/capsule.py,sha256=n23uJi9877IVnArgqdq_ES_c2vAql8rzVm7jlTYUr1M,13351
47
+ outerbounds/apps/cli_to_config.py,sha256=IjPHeH1lC7l9WPkNLQIWU7obRS3G0G8aTNVZemUvmuw,3168
47
48
  outerbounds/apps/code_package/__init__.py,sha256=8McF7pgx8ghvjRnazp2Qktlxi9yYwNiwESSQrk-2oW8,68
48
49
  outerbounds/apps/code_package/code_packager.py,sha256=SQDBXKwizzpag5GpwoZpvvkyPOodRSQwk2ecAAfO0HI,23316
49
50
  outerbounds/apps/code_package/examples.py,sha256=aF8qKIJxCVv_ugcShQjqUsXKKKMsm1oMkQIl8w3QKuw,4016
50
- outerbounds/apps/config_schema.yaml,sha256=Rh8mNbZaTu-kfC7rwjoKkk7KvefKZiQNptuojJYpYAo,6715
51
+ outerbounds/apps/config_schema.yaml,sha256=D-qopf3mGZusa4n5GIbrssoJHS3v96_yponFEM127b4,8275
51
52
  outerbounds/apps/dependencies.py,sha256=SqvdFQdFZZW0wXX_CHMHCrfE0TwaRkTvGCRbQ2Mx3q0,3935
52
53
  outerbounds/apps/deployer.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
+ outerbounds/apps/experimental/__init__.py,sha256=12L_FzZyzv162uo4I6cmlrxat7feUtIu_kxbObTJZTA,3059
53
55
  outerbounds/apps/secrets.py,sha256=27qf04lOBqRjvcswj0ldHOmntP2T6SEjtMJtkJQ_GUg,6100
54
56
  outerbounds/apps/utils.py,sha256=JymjsgpU0osF-eDvstFS9zkM7bqliJdqAEV7kAjqxCM,7298
55
57
  outerbounds/apps/validations.py,sha256=AVEw9eCvkzqq1m5ZC8btaWrSR6kWYKzarELfrASuAwQ,1117
@@ -69,7 +71,7 @@ outerbounds/utils/metaflowconfig.py,sha256=l2vJbgPkLISU-XPGZFaC8ZKmYFyJemlD6bwB-
69
71
  outerbounds/utils/schema.py,sha256=lMUr9kNgn9wy-sO_t_Tlxmbt63yLeN4b0xQXbDUDj4A,2331
70
72
  outerbounds/utils/utils.py,sha256=4Z8cszNob_8kDYCLNTrP-wWads_S_MdL3Uj3ju4mEsk,501
71
73
  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,,
74
+ outerbounds-0.3.175rc0.dist-info/METADATA,sha256=c69bE_dY1O_vbcTLpQ0FWj2xL0rF1sIvuI-e3A8MTXA,1846
75
+ outerbounds-0.3.175rc0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
76
+ outerbounds-0.3.175rc0.dist-info/entry_points.txt,sha256=7ye0281PKlvqxu15rjw60zKg2pMsXI49_A8BmGqIqBw,47
77
+ outerbounds-0.3.175rc0.dist-info/RECORD,,