ob-metaflow-extensions 1.1.151__py2.py3-none-any.whl → 1.6.2__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.
Files changed (92) hide show
  1. metaflow_extensions/outerbounds/__init__.py +1 -1
  2. metaflow_extensions/outerbounds/plugins/__init__.py +24 -3
  3. metaflow_extensions/outerbounds/plugins/apps/app_cli.py +0 -0
  4. metaflow_extensions/outerbounds/plugins/apps/core/__init__.py +16 -0
  5. metaflow_extensions/outerbounds/plugins/apps/core/_state_machine.py +506 -0
  6. metaflow_extensions/outerbounds/plugins/apps/core/_vendor/__init__.py +0 -0
  7. metaflow_extensions/outerbounds/plugins/apps/core/_vendor/spinner/__init__.py +4 -0
  8. metaflow_extensions/outerbounds/plugins/apps/core/_vendor/spinner/spinners.py +478 -0
  9. metaflow_extensions/outerbounds/plugins/apps/core/app_config.py +128 -0
  10. metaflow_extensions/outerbounds/plugins/apps/core/app_deploy_decorator.py +333 -0
  11. metaflow_extensions/outerbounds/plugins/apps/core/artifacts.py +0 -0
  12. metaflow_extensions/outerbounds/plugins/apps/core/capsule.py +1029 -0
  13. metaflow_extensions/outerbounds/plugins/apps/core/click_importer.py +24 -0
  14. metaflow_extensions/outerbounds/plugins/apps/core/code_package/__init__.py +3 -0
  15. metaflow_extensions/outerbounds/plugins/apps/core/code_package/code_packager.py +618 -0
  16. metaflow_extensions/outerbounds/plugins/apps/core/code_package/examples.py +125 -0
  17. metaflow_extensions/outerbounds/plugins/apps/core/config/__init__.py +15 -0
  18. metaflow_extensions/outerbounds/plugins/apps/core/config/cli_generator.py +165 -0
  19. metaflow_extensions/outerbounds/plugins/apps/core/config/config_utils.py +966 -0
  20. metaflow_extensions/outerbounds/plugins/apps/core/config/schema_export.py +299 -0
  21. metaflow_extensions/outerbounds/plugins/apps/core/config/typed_configs.py +233 -0
  22. metaflow_extensions/outerbounds/plugins/apps/core/config/typed_init_generator.py +537 -0
  23. metaflow_extensions/outerbounds/plugins/apps/core/config/unified_config.py +1125 -0
  24. metaflow_extensions/outerbounds/plugins/apps/core/config_schema.yaml +337 -0
  25. metaflow_extensions/outerbounds/plugins/apps/core/dependencies.py +115 -0
  26. metaflow_extensions/outerbounds/plugins/apps/core/deployer.py +1300 -0
  27. metaflow_extensions/outerbounds/plugins/apps/core/exceptions.py +341 -0
  28. metaflow_extensions/outerbounds/plugins/apps/core/experimental/__init__.py +89 -0
  29. metaflow_extensions/outerbounds/plugins/apps/core/perimeters.py +123 -0
  30. metaflow_extensions/outerbounds/plugins/apps/core/secrets.py +164 -0
  31. metaflow_extensions/outerbounds/plugins/apps/core/utils.py +233 -0
  32. metaflow_extensions/outerbounds/plugins/apps/core/validations.py +17 -0
  33. metaflow_extensions/outerbounds/plugins/aws/__init__.py +4 -0
  34. metaflow_extensions/outerbounds/plugins/aws/assume_role.py +3 -0
  35. metaflow_extensions/outerbounds/plugins/aws/assume_role_decorator.py +118 -0
  36. metaflow_extensions/outerbounds/plugins/checkpoint_datastores/coreweave.py +9 -77
  37. metaflow_extensions/outerbounds/plugins/checkpoint_datastores/external_chckpt.py +85 -0
  38. metaflow_extensions/outerbounds/plugins/checkpoint_datastores/nebius.py +7 -78
  39. metaflow_extensions/outerbounds/plugins/fast_bakery/baker.py +119 -0
  40. metaflow_extensions/outerbounds/plugins/fast_bakery/docker_environment.py +17 -3
  41. metaflow_extensions/outerbounds/plugins/fast_bakery/fast_bakery.py +1 -0
  42. metaflow_extensions/outerbounds/plugins/kubernetes/kubernetes_client.py +18 -44
  43. metaflow_extensions/outerbounds/plugins/kubernetes/pod_killer.py +374 -0
  44. metaflow_extensions/outerbounds/plugins/nim/card.py +1 -6
  45. metaflow_extensions/outerbounds/plugins/nim/{__init__.py → nim_decorator.py} +13 -49
  46. metaflow_extensions/outerbounds/plugins/nim/nim_manager.py +294 -233
  47. metaflow_extensions/outerbounds/plugins/nim/utils.py +36 -0
  48. metaflow_extensions/outerbounds/plugins/nvcf/constants.py +2 -2
  49. metaflow_extensions/outerbounds/plugins/nvct/nvct_decorator.py +32 -8
  50. metaflow_extensions/outerbounds/plugins/nvct/nvct_runner.py +1 -1
  51. metaflow_extensions/outerbounds/plugins/ollama/__init__.py +171 -16
  52. metaflow_extensions/outerbounds/plugins/ollama/constants.py +1 -0
  53. metaflow_extensions/outerbounds/plugins/ollama/exceptions.py +22 -0
  54. metaflow_extensions/outerbounds/plugins/ollama/ollama.py +1710 -114
  55. metaflow_extensions/outerbounds/plugins/ollama/status_card.py +292 -0
  56. metaflow_extensions/outerbounds/plugins/optuna/__init__.py +49 -0
  57. metaflow_extensions/outerbounds/plugins/profilers/simple_card_decorator.py +96 -0
  58. metaflow_extensions/outerbounds/plugins/s3_proxy/__init__.py +7 -0
  59. metaflow_extensions/outerbounds/plugins/s3_proxy/binary_caller.py +132 -0
  60. metaflow_extensions/outerbounds/plugins/s3_proxy/constants.py +11 -0
  61. metaflow_extensions/outerbounds/plugins/s3_proxy/exceptions.py +13 -0
  62. metaflow_extensions/outerbounds/plugins/s3_proxy/proxy_bootstrap.py +59 -0
  63. metaflow_extensions/outerbounds/plugins/s3_proxy/s3_proxy_api.py +93 -0
  64. metaflow_extensions/outerbounds/plugins/s3_proxy/s3_proxy_decorator.py +250 -0
  65. metaflow_extensions/outerbounds/plugins/s3_proxy/s3_proxy_manager.py +225 -0
  66. metaflow_extensions/outerbounds/plugins/snowflake/snowflake.py +37 -7
  67. metaflow_extensions/outerbounds/plugins/snowpark/snowpark.py +18 -8
  68. metaflow_extensions/outerbounds/plugins/snowpark/snowpark_cli.py +6 -0
  69. metaflow_extensions/outerbounds/plugins/snowpark/snowpark_client.py +45 -18
  70. metaflow_extensions/outerbounds/plugins/snowpark/snowpark_decorator.py +18 -9
  71. metaflow_extensions/outerbounds/plugins/snowpark/snowpark_job.py +10 -4
  72. metaflow_extensions/outerbounds/plugins/torchtune/__init__.py +163 -0
  73. metaflow_extensions/outerbounds/plugins/vllm/__init__.py +255 -0
  74. metaflow_extensions/outerbounds/plugins/vllm/constants.py +1 -0
  75. metaflow_extensions/outerbounds/plugins/vllm/exceptions.py +1 -0
  76. metaflow_extensions/outerbounds/plugins/vllm/status_card.py +352 -0
  77. metaflow_extensions/outerbounds/plugins/vllm/vllm_manager.py +621 -0
  78. metaflow_extensions/outerbounds/remote_config.py +46 -9
  79. metaflow_extensions/outerbounds/toplevel/apps/__init__.py +9 -0
  80. metaflow_extensions/outerbounds/toplevel/apps/exceptions.py +11 -0
  81. metaflow_extensions/outerbounds/toplevel/global_aliases_for_metaflow_package.py +86 -2
  82. metaflow_extensions/outerbounds/toplevel/ob_internal.py +4 -0
  83. metaflow_extensions/outerbounds/toplevel/plugins/optuna/__init__.py +1 -0
  84. metaflow_extensions/outerbounds/toplevel/plugins/torchtune/__init__.py +1 -0
  85. metaflow_extensions/outerbounds/toplevel/plugins/vllm/__init__.py +1 -0
  86. metaflow_extensions/outerbounds/toplevel/s3_proxy.py +88 -0
  87. {ob_metaflow_extensions-1.1.151.dist-info → ob_metaflow_extensions-1.6.2.dist-info}/METADATA +2 -2
  88. ob_metaflow_extensions-1.6.2.dist-info/RECORD +136 -0
  89. metaflow_extensions/outerbounds/plugins/nim/utilities.py +0 -5
  90. ob_metaflow_extensions-1.1.151.dist-info/RECORD +0 -74
  91. {ob_metaflow_extensions-1.1.151.dist-info → ob_metaflow_extensions-1.6.2.dist-info}/WHEEL +0 -0
  92. {ob_metaflow_extensions-1.1.151.dist-info → ob_metaflow_extensions-1.6.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,341 @@
1
+ import json
2
+ from typing import Optional, List, Dict
3
+ from ._state_machine import LogLine
4
+
5
+
6
+ class OuterboundsBackendUnhealthyException(Exception):
7
+ """This exception is raised when the Outerbounds platform is unhealthy (5xx errors) or unreachable."""
8
+
9
+ def __init__(
10
+ self,
11
+ url: str,
12
+ method: str,
13
+ status_code: Optional[int] = None,
14
+ text: Optional[str] = None,
15
+ message: Optional[str] = None,
16
+ ):
17
+ self.url = url
18
+ self.method = method
19
+ self.status_code = status_code
20
+ self.text = text
21
+ self.message = message
22
+ super().__init__(self.message)
23
+
24
+
25
+ class OuterboundsForbiddenException(Exception):
26
+ """This exception is raised when access to an Outerbounds API is forbidden (HTTP 403)."""
27
+
28
+ def __init__(
29
+ self,
30
+ url: str,
31
+ method: str,
32
+ text: str,
33
+ ):
34
+ self.url = url
35
+ self.method = method
36
+ self.text = text
37
+ self.message = (
38
+ f"Access forbidden (HTTP 403) when calling {url}. "
39
+ "This typically means your credentials lack permission for this operation. "
40
+ "Please verify that you outerbounds / metaflow configuration has access to the "
41
+ "correct perimeter.\n"
42
+ "If you believe this is an error, contact your Outerbounds administrator."
43
+ )
44
+ super().__init__(self.message)
45
+
46
+
47
+ class OuterboundsConfigurationException(Exception):
48
+ """This exception is raised when Outerbounds configuration is missing or invalid."""
49
+
50
+ def __init__(self, missing_config: str):
51
+ self.missing_config = missing_config
52
+ self.message = (
53
+ f"Outerbounds configuration '{missing_config}' not found.\n\n"
54
+ "If running locally:\n"
55
+ " - Run: outerbounds configure <magic-string-from-outerbounds-ui>\n"
56
+ "If running remotely (e.g., in a Metaflow task):\n"
57
+ " - Ensure you have the Outerbounds distribution installed (pip install outerbounds)\n"
58
+ " and not the open-source Metaflow. The Outerbounds fork injects configuration\n"
59
+ " into remote tasks automatically which may not be present in open source metaflow.\n"
60
+ )
61
+ super().__init__(self.message)
62
+
63
+
64
+ class CapsuleApiException(Exception):
65
+ def __init__(
66
+ self,
67
+ url: str,
68
+ method: str,
69
+ status_code: int,
70
+ text: str,
71
+ message: Optional[str] = None,
72
+ ):
73
+ self.url = url
74
+ self.method = method
75
+ self.status_code = status_code
76
+ self.text = text
77
+ self.message = message
78
+
79
+ def __str__(self):
80
+ return (
81
+ f"CapsuleApiException: {self.url} [{self.method}]: Status Code: {self.status_code} \n\n {self.text}"
82
+ + (f"\n\n {self.message}" if self.message else "")
83
+ )
84
+
85
+
86
+ class CapsuleDeploymentException(Exception):
87
+ """Base exception for all capsule deployment failures."""
88
+
89
+ def __init__(
90
+ self,
91
+ capsule_id: str,
92
+ message: str,
93
+ ):
94
+ self.capsule_id = capsule_id
95
+ self.message = message
96
+ super().__init__(self.message)
97
+
98
+ def __str__(self):
99
+ return f"CapsuleDeploymentException: [{self.capsule_id}] :: {self.message}"
100
+
101
+
102
+ class CapsuleCrashLoopException(CapsuleDeploymentException):
103
+ """Raised when a worker enters CrashLoopBackOff or Failed state."""
104
+
105
+ def __init__(
106
+ self,
107
+ capsule_id: str,
108
+ worker_id: str,
109
+ logs: Optional[List[LogLine]] = None,
110
+ ):
111
+ self.worker_id = worker_id
112
+ self.logs = logs or []
113
+ message = f"Worker ID ({worker_id}) is crashlooping. Please check the logs for more information."
114
+ super().__init__(capsule_id, message)
115
+
116
+ def __str__(self):
117
+ return f"CapsuleCrashLoopException: [{self.capsule_id}] :: {self.message}"
118
+
119
+
120
+ class CapsuleReadinessException(CapsuleDeploymentException):
121
+ """Raised when capsule fails to meet readiness conditions within timeout."""
122
+
123
+ def __init__(
124
+ self,
125
+ capsule_id: str,
126
+ reason: Optional[str] = None,
127
+ ):
128
+ message = f"Capsule {capsule_id} failed to be ready to serve traffic"
129
+ if reason:
130
+ message += f": {reason}"
131
+ super().__init__(capsule_id, message)
132
+
133
+ def __str__(self):
134
+ return f"CapsuleReadinessException: [{self.capsule_id}] :: {self.message}"
135
+
136
+
137
+ class CapsuleConcurrentUpgradeException(CapsuleDeploymentException):
138
+ """Raised when a concurrent upgrade invalidates the current deployment."""
139
+
140
+ def __init__(
141
+ self,
142
+ capsule_id: str,
143
+ expected_version: str,
144
+ actual_version: str,
145
+ modified_by: Optional[str] = None,
146
+ modified_at: Optional[str] = None,
147
+ ):
148
+ self.expected_version = expected_version
149
+ self.actual_version = actual_version
150
+ self.modified_by = modified_by
151
+ self.modified_at = modified_at
152
+ message = (
153
+ f"A capsule upgrade was triggered outside current deployment instance. "
154
+ f"Expected version: {expected_version}, actual version: {actual_version}"
155
+ )
156
+ if modified_by:
157
+ message += f". Modified by: {modified_by}"
158
+ if modified_at:
159
+ message += f" at {modified_at}"
160
+ super().__init__(capsule_id, message)
161
+
162
+ def __str__(self):
163
+ return (
164
+ f"CapsuleConcurrentUpgradeException: [{self.capsule_id}] :: {self.message}"
165
+ )
166
+
167
+
168
+ class CapsuleDeletedDuringDeploymentException(CapsuleDeploymentException):
169
+ """Raised when a capsule is deleted while deployment is in progress."""
170
+
171
+ def __init__(self, capsule_id: str):
172
+ super().__init__(capsule_id, "Capsule was deleted during deployment")
173
+
174
+
175
+ class CodePackagingException(Exception):
176
+ """Exception raised when code packaging fails."""
177
+
178
+ pass
179
+
180
+
181
+ class AppNotFoundException(Exception):
182
+ pass
183
+
184
+
185
+ class AppCreationFailedException(Exception):
186
+ """Raised when app deployment submission fails due to an API error."""
187
+
188
+ def __init__(
189
+ self,
190
+ app_name: str,
191
+ status_code: int,
192
+ error_text: str,
193
+ ):
194
+ self.status_code = status_code
195
+ self.error_text = error_text
196
+ message = f"Failed to submit app deployment: HTTP {status_code} - {error_text}"
197
+ if status_code == 400:
198
+ # 400 = validation error; the app configuration is invalid and must be fixed.
199
+ message = "Invalid app deployment configuration submitted: "
200
+ try:
201
+ reason_for_failure = json.loads(error_text).get("message", error_text)
202
+ except json.JSONDecodeError:
203
+ reason_for_failure = error_text
204
+ message += reason_for_failure
205
+ message += (
206
+ "\n\nCheck your config file or CLI parameters if deploying via CLI, "
207
+ "or AppDeployer parameters if deploying programmatically."
208
+ )
209
+
210
+ self.message = message
211
+ super().__init__(message)
212
+
213
+ def __str__(self):
214
+ return f"AppCreationFailedException: {self.message}"
215
+
216
+
217
+ class AppDeploymentException(Exception):
218
+ """Base exception for all individual app deployment failures."""
219
+
220
+ def __init__(self, app_id: str, message: str):
221
+ self.app_id = app_id
222
+ self.message = message
223
+ self._deployed_app = None
224
+ super().__init__(self.message)
225
+
226
+ def __str__(self):
227
+ return f"AppDeploymentException: [{self.app_id}] :: {self.message}"
228
+
229
+ @property
230
+ def deployed_app(self):
231
+ from .deployer import DeployedApp
232
+
233
+ return DeployedApp._from_capsule_id(self.app_id)
234
+
235
+
236
+ class AppCrashLoopException(AppDeploymentException):
237
+ """Raised when an app worker enters CrashLoopBackOff or Failed state."""
238
+
239
+ def __init__(
240
+ self,
241
+ app_id: str,
242
+ worker_id: str,
243
+ logs: Optional[List] = None,
244
+ ):
245
+ self.worker_id = worker_id
246
+ self.logs = logs or []
247
+ message = f"Worker ({worker_id}) is crashlooping. Please check the logs for more information."
248
+ super().__init__(app_id, message)
249
+
250
+ def __str__(self):
251
+ return f"AppCrashLoopException: [{self.app_id}] :: {self.message}"
252
+
253
+
254
+ class AppReadinessException(AppDeploymentException):
255
+ """Raised when app fails to meet readiness conditions within timeout."""
256
+
257
+ def __init__(self, app_id: str, reason: Optional[str] = None):
258
+ message = f"App {app_id} failed to be ready to serve traffic"
259
+ if reason:
260
+ message += f": {reason}"
261
+ super().__init__(app_id, message)
262
+
263
+ def __str__(self):
264
+ return f"AppReadinessException: [{self.app_id}] :: {self.message}"
265
+
266
+
267
+ class AppUpgradeInProgressException(AppDeploymentException):
268
+ """Raised when attempting to deploy while another upgrade is already in progress."""
269
+
270
+ def __init__(
271
+ self,
272
+ app_id: str,
273
+ upgrader: Optional[str] = None,
274
+ ):
275
+ self.upgrader = upgrader
276
+ if upgrader:
277
+ message = (
278
+ f"App {app_id} is currently being upgraded by {upgrader}. "
279
+ "Use force_upgrade=True in AppDeployer to override."
280
+ )
281
+ else:
282
+ message = (
283
+ f"App {app_id} is currently being upgraded. "
284
+ "Use force_upgrade=True in AppDeployer to override."
285
+ )
286
+ super().__init__(app_id, message)
287
+
288
+ def __str__(self):
289
+ return f"AppUpgradeInProgressException: [{self.app_id}] :: {self.message}"
290
+
291
+
292
+ class AppConcurrentUpgradeException(AppDeploymentException):
293
+ """Raised when a concurrent upgrade invalidates the current deployment mid-flight."""
294
+
295
+ def __init__(
296
+ self,
297
+ app_id: str,
298
+ expected_version: str,
299
+ actual_version: str,
300
+ modified_by: Optional[str] = None,
301
+ modified_at: Optional[str] = None,
302
+ ):
303
+ self.expected_version = expected_version
304
+ self.actual_version = actual_version
305
+ self.modified_by = modified_by
306
+ self.modified_at = modified_at
307
+
308
+ modifier_info = ""
309
+ if modified_by:
310
+ modifier_info = f" by '{modified_by}'"
311
+ if modified_at:
312
+ modifier_info += f" at {modified_at}"
313
+
314
+ message = (
315
+ f"Another deployment was triggered{modifier_info} while this deployment was in progress.\n\n"
316
+ f"This deployment expected to be working with version '{expected_version}', but the app "
317
+ f"is now at version '{actual_version}'. The current deployment has been invalidated.\n\n"
318
+ "To avoid this in the future, you can either use a unique `name` for each deployment "
319
+ "or coordinate deployments to ensure concurrent upgrades to the same app don't overlap."
320
+ )
321
+ super().__init__(app_id, message)
322
+
323
+ def __str__(self):
324
+ return f"AppConcurrentUpgradeException: [{self.app_id}] :: {self.message}"
325
+
326
+
327
+ class AppDeletedDuringDeploymentException(AppDeploymentException):
328
+ """Raised when an app is deleted while deployment is in progress."""
329
+
330
+ def __init__(self, app_id: str):
331
+ message = (
332
+ f"App '{app_id}' was deleted while this deployment was in progress.\n\n"
333
+ "This can happen when another process or user deletes the app during deployment. "
334
+ "Since apps can be programmatically created and deleted, concurrent operations "
335
+ "may conflict. If you did not intend to delete this app, check for other processes "
336
+ "or users that may be managing deployments in this perimeter."
337
+ )
338
+ super().__init__(app_id, message)
339
+
340
+ def __str__(self):
341
+ return f"AppDeletedDuringDeploymentException: [{self.app_id}] :: {self.message}"
@@ -0,0 +1,89 @@
1
+ import os
2
+ from typing import TYPE_CHECKING
3
+
4
+ if TYPE_CHECKING:
5
+ from ..app_config import AppConfig
6
+
7
+ DEFAULT_BRANCH = "test"
8
+
9
+
10
+ # Account for project / branch and the capsule input.
11
+ def capsule_input_overrides(app_config: "AppConfig", capsule_input: dict):
12
+ project = app_config.get_state("project", None)
13
+ # Update the project/branch related configurations.
14
+ if project is not None:
15
+ branch = app_config.get_state("branch", DEFAULT_BRANCH)
16
+ capsule_input["tags"].extend(
17
+ [dict(key="project", value=project), dict(key="branch", value=branch)]
18
+ )
19
+
20
+ persistence = app_config.get_state("persistence", None)
21
+ if persistence is not None and persistence != "none":
22
+ capsule_input["persistence"] = persistence
23
+
24
+ capsule_input["generateStaticUrl"] = app_config.get_state(
25
+ "generate_static_url", False
26
+ )
27
+
28
+ model_asset_conf = app_config.get_state("models", None)
29
+ data_asset_conf = app_config.get_state("data", None)
30
+ code_info = _code_info(app_config)
31
+ # todo:fix me
32
+ _objects_key = "associatedObjects"
33
+ if model_asset_conf or data_asset_conf or code_info:
34
+ capsule_input[_objects_key] = {}
35
+
36
+ if model_asset_conf:
37
+ capsule_input[_objects_key]["models"] = [
38
+ {"assetId": x["asset_id"], "assetInstanceId": x["asset_instance_id"]}
39
+ for x in model_asset_conf
40
+ ]
41
+ if data_asset_conf:
42
+ capsule_input[_objects_key]["data"] = [
43
+ {"assetId": x["asset_id"], "assetInstanceId": x["asset_instance_id"]}
44
+ for x in data_asset_conf
45
+ ]
46
+ if code_info:
47
+ capsule_input[_objects_key]["code"] = code_info
48
+
49
+ return capsule_input
50
+
51
+
52
+ def _code_info(app_config: "AppConfig"):
53
+ from metaflow.metaflow_git import get_repository_info
54
+
55
+ try:
56
+ from metaflow.metaflow_git import _call_git # type: ignore
57
+ except ImportError:
58
+ # Fallback if _call_git is not available
59
+ def _call_git(args, path=None):
60
+ return "", 1, True
61
+
62
+ package_dirs = app_config.get_state("packaging_directories")
63
+ if package_dirs is None:
64
+ return None
65
+ for directory in package_dirs:
66
+ repo_info = get_repository_info(directory)
67
+ if len(repo_info) == 0:
68
+ continue
69
+ git_log_info, returncode, failed = _call_git(
70
+ ["log", "-1", "--pretty=%B"],
71
+ path=directory,
72
+ )
73
+ repo_url = repo_info["repo_url"]
74
+ if isinstance(repo_url, str):
75
+ _url = (
76
+ repo_url if not repo_url.endswith(".git") else repo_url.rstrip(".git")
77
+ )
78
+ else:
79
+ _url = str(repo_url)
80
+ _code_info = {
81
+ "commitId": repo_info["commit_sha"],
82
+ "commitLink": os.path.join(_url, "commit", str(repo_info["commit_sha"])),
83
+ }
84
+ if not failed and returncode == 0 and isinstance(git_log_info, str):
85
+ _code_info["commitMessage"] = git_log_info.strip()
86
+
87
+ return _code_info
88
+
89
+ return None
@@ -0,0 +1,123 @@
1
+ import os
2
+ import json
3
+ from typing import Tuple, Union
4
+
5
+ import requests
6
+ from .utils import safe_requests_wrapper
7
+ from .exceptions import OuterboundsConfigurationException
8
+
9
+
10
+ class PerimeterExtractor:
11
+
12
+ config = None
13
+
14
+ @classmethod
15
+ def for_ob_cli(
16
+ cls, config_dir: str, profile: str
17
+ ) -> Union[Tuple[str, str], Tuple[None, None]]:
18
+ """
19
+ This function will be called when we are trying to extract the perimeter
20
+ via the ob cli's execution. We will rely on the following logic:
21
+ 1. check environment variables like OB_CURRENT_PERIMETER / OBP_PERIMETER
22
+ 2. run init config to extract the perimeter related configurations.
23
+
24
+ Returns
25
+ -------
26
+ Tuple[str, str] : Tuple containing perimeter name , API server url.
27
+ """
28
+ from outerbounds.utils import metaflowconfig
29
+
30
+ perimeter = None
31
+ api_server = None
32
+ if os.environ.get("OB_CURRENT_PERIMETER") or os.environ.get("OBP_PERIMETER"):
33
+ perimeter = os.environ.get("OB_CURRENT_PERIMETER") or os.environ.get(
34
+ "OBP_PERIMETER"
35
+ )
36
+
37
+ if os.environ.get("OBP_API_SERVER"):
38
+ api_server = os.environ.get("OBP_API_SERVER")
39
+
40
+ if perimeter is None or api_server is None:
41
+ metaflow_config = metaflowconfig.init_config(config_dir, profile)
42
+ perimeter = metaflow_config.get("OBP_PERIMETER")
43
+ api_server = metaflowconfig.get_sanitized_url_from_config(
44
+ config_dir, profile, "OBP_API_SERVER"
45
+ )
46
+
47
+ return perimeter, api_server # type: ignore
48
+
49
+ @classmethod
50
+ def during_programmatic_access(cls) -> Union[Tuple[str, str], Tuple[None, None]]:
51
+ from metaflow.metaflow_config_funcs import init_config
52
+
53
+ clean_url = (
54
+ lambda url: f"https://{url}".rstrip("/")
55
+ if not url.startswith("https://")
56
+ else url
57
+ )
58
+
59
+ config = init_config()
60
+ api_server, perimeter, integrations_url = None, None, None
61
+ perimeter = config.get(
62
+ "OBP_PERIMETER", os.environ.get("OBP_PERIMETER", perimeter)
63
+ )
64
+ if perimeter is None:
65
+ raise OuterboundsConfigurationException("OBP_PERIMETER")
66
+
67
+ api_server = config.get(
68
+ "OBP_API_SERVER", os.environ.get("OBP_API_SERVER", api_server)
69
+ )
70
+
71
+ if api_server is not None and not api_server.startswith("https://"):
72
+ api_server = clean_url(api_server)
73
+
74
+ if api_server is not None:
75
+ return perimeter, api_server
76
+
77
+ integrations_url = config.get(
78
+ "OBP_INTEGRATIONS_URL", os.environ.get("OBP_INTEGRATIONS_URL", None)
79
+ )
80
+
81
+ if integrations_url is not None and not integrations_url.startswith("https://"):
82
+ integrations_url = clean_url(integrations_url)
83
+
84
+ if integrations_url is not None:
85
+ api_server = integrations_url.rstrip("/integrations")
86
+
87
+ if api_server is None:
88
+ raise OuterboundsConfigurationException("OBP_API_SERVER")
89
+
90
+ return perimeter, api_server
91
+
92
+ @classmethod
93
+ def config_during_programmatic_access(cls) -> dict:
94
+ #!HACK: Resolving remote configs is a PITA (all the variable piping we need to do via metaflow)
95
+ # So instead we will just derive the URL. We are in this situation because its a pain
96
+ # to load configurations at arbitrary points in the runtime.
97
+ if cls.config is not None:
98
+ return json.loads(json.dumps(cls.config)) # Return fresh copy
99
+ from metaflow.metaflow_config import SERVICE_HEADERS
100
+
101
+ perimeter, api_server = cls.during_programmatic_access()
102
+ response = safe_requests_wrapper(
103
+ requests.get,
104
+ f"{api_server}/v1/perimeters/{perimeter}/metaflowconfigs/default",
105
+ headers=SERVICE_HEADERS,
106
+ )
107
+ if response.status_code >= 400:
108
+ raise RuntimeError(
109
+ f"Server error: {response.text}. Please reach out to your Outerbounds support team."
110
+ )
111
+ try:
112
+ remote_config = response.json()
113
+
114
+ if not remote_config.get("config"):
115
+ raise json.JSONDecodeError
116
+ except json.JSONDecodeError:
117
+ raise RuntimeError(
118
+ "Exception retrieving remote outerbounds configuration. "
119
+ "Please reach out to Outerbounds suport team with this stack trace."
120
+ )
121
+
122
+ cls.config = remote_config.get("config")
123
+ return json.loads(json.dumps(cls.config)) # Return fresh copy