ob-metaflow-extensions 1.1.162rc0__tar.gz → 1.1.163__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (92) hide show
  1. {ob-metaflow-extensions-1.1.162rc0/ob_metaflow_extensions.egg-info → ob_metaflow_extensions-1.1.163}/PKG-INFO +2 -2
  2. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/__init__.py +27 -18
  3. ob_metaflow_extensions-1.1.163/metaflow_extensions/outerbounds/plugins/aws/__init__.py +4 -0
  4. ob_metaflow_extensions-1.1.163/metaflow_extensions/outerbounds/plugins/aws/assume_role.py +3 -0
  5. ob_metaflow_extensions-1.1.163/metaflow_extensions/outerbounds/plugins/aws/assume_role_decorator.py +65 -0
  6. ob_metaflow_extensions-1.1.163/metaflow_extensions/outerbounds/plugins/ollama/__init__.py +225 -0
  7. ob_metaflow_extensions-1.1.163/metaflow_extensions/outerbounds/plugins/ollama/ollama.py +1924 -0
  8. ob_metaflow_extensions-1.1.163/metaflow_extensions/outerbounds/plugins/ollama/status_card.py +292 -0
  9. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/toplevel/global_aliases_for_metaflow_package.py +23 -2
  10. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163/ob_metaflow_extensions.egg-info}/PKG-INFO +1 -1
  11. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/ob_metaflow_extensions.egg-info/SOURCES.txt +4 -2
  12. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/setup.py +2 -2
  13. ob-metaflow-extensions-1.1.162rc0/metaflow_extensions/outerbounds/plugins/fast_bakery/baker.py +0 -110
  14. ob-metaflow-extensions-1.1.162rc0/metaflow_extensions/outerbounds/plugins/ollama/__init__.py +0 -89
  15. ob-metaflow-extensions-1.1.162rc0/metaflow_extensions/outerbounds/plugins/ollama/ollama.py +0 -813
  16. ob-metaflow-extensions-1.1.162rc0/metaflow_extensions/outerbounds/toplevel/ob_internal.py +0 -1
  17. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/README.md +0 -0
  18. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/__init__.py +0 -0
  19. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/config/__init__.py +0 -0
  20. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/apps/__init__.py +0 -0
  21. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/apps/app_utils.py +0 -0
  22. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/apps/consts.py +0 -0
  23. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/apps/deploy_decorator.py +0 -0
  24. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/apps/supervisord_utils.py +0 -0
  25. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/auth_server.py +0 -0
  26. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/card_utilities/__init__.py +0 -0
  27. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/card_utilities/async_cards.py +0 -0
  28. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/card_utilities/extra_components.py +0 -0
  29. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/card_utilities/injector.py +0 -0
  30. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/checkpoint_datastores/__init__.py +0 -0
  31. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/checkpoint_datastores/coreweave.py +0 -0
  32. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/checkpoint_datastores/nebius.py +0 -0
  33. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/fast_bakery/__init__.py +0 -0
  34. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/fast_bakery/docker_environment.py +0 -0
  35. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/fast_bakery/fast_bakery.py +0 -0
  36. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/fast_bakery/fast_bakery_cli.py +0 -0
  37. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/fast_bakery/fast_bakery_decorator.py +0 -0
  38. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/kubernetes/__init__.py +0 -0
  39. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/kubernetes/kubernetes_client.py +0 -0
  40. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/kubernetes/pod_killer.py +0 -0
  41. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/nim/card.py +0 -0
  42. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/nim/nim_decorator.py +0 -0
  43. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/nim/nim_manager.py +0 -0
  44. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/nim/utils.py +0 -0
  45. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/nvcf/__init__.py +0 -0
  46. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/nvcf/constants.py +0 -0
  47. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/nvcf/exceptions.py +0 -0
  48. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/nvcf/heartbeat_store.py +0 -0
  49. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/nvcf/nvcf.py +0 -0
  50. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/nvcf/nvcf_cli.py +0 -0
  51. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/nvcf/nvcf_decorator.py +0 -0
  52. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/nvcf/utils.py +0 -0
  53. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/nvct/__init__.py +0 -0
  54. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/nvct/exceptions.py +0 -0
  55. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/nvct/nvct.py +0 -0
  56. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/nvct/nvct_cli.py +0 -0
  57. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/nvct/nvct_decorator.py +0 -0
  58. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/nvct/nvct_runner.py +0 -0
  59. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/nvct/utils.py +0 -0
  60. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/ollama/constants.py +0 -0
  61. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/ollama/exceptions.py +0 -0
  62. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/perimeters.py +0 -0
  63. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/profilers/deco_injector.py +0 -0
  64. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/profilers/gpu_profile_decorator.py +0 -0
  65. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/secrets/__init__.py +0 -0
  66. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/secrets/secrets.py +0 -0
  67. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/snowflake/__init__.py +0 -0
  68. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/snowflake/snowflake.py +0 -0
  69. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/snowpark/__init__.py +0 -0
  70. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/snowpark/snowpark.py +0 -0
  71. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/snowpark/snowpark_cli.py +0 -0
  72. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/snowpark/snowpark_client.py +0 -0
  73. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/snowpark/snowpark_decorator.py +0 -0
  74. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/snowpark/snowpark_exceptions.py +0 -0
  75. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/snowpark/snowpark_job.py +0 -0
  76. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/snowpark/snowpark_service_spec.py +0 -0
  77. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/tensorboard/__init__.py +0 -0
  78. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/plugins/torchtune/__init__.py +0 -0
  79. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/profilers/__init__.py +0 -0
  80. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/profilers/gpu.py +0 -0
  81. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/remote_config.py +0 -0
  82. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/toplevel/__init__.py +0 -0
  83. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/toplevel/plugins/azure/__init__.py +0 -0
  84. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/toplevel/plugins/gcp/__init__.py +0 -0
  85. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/toplevel/plugins/kubernetes/__init__.py +0 -0
  86. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/toplevel/plugins/ollama/__init__.py +0 -0
  87. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/toplevel/plugins/snowflake/__init__.py +0 -0
  88. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/metaflow_extensions/outerbounds/toplevel/plugins/torchtune/__init__.py +0 -0
  89. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/ob_metaflow_extensions.egg-info/dependency_links.txt +0 -0
  90. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/ob_metaflow_extensions.egg-info/requires.txt +0 -0
  91. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/ob_metaflow_extensions.egg-info/top_level.txt +0 -0
  92. {ob-metaflow-extensions-1.1.162rc0 → ob_metaflow_extensions-1.1.163}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
- Name: ob-metaflow-extensions
3
- Version: 1.1.162rc0
2
+ Name: ob_metaflow_extensions
3
+ Version: 1.1.163
4
4
  Summary: Outerbounds Platform Extensions for Metaflow
5
5
  Author: Outerbounds, Inc.
6
6
  License: Commercial
@@ -41,6 +41,9 @@ def get_boto3_session(role_arn=None, session_vars=None):
41
41
  import boto3
42
42
  import botocore
43
43
  from metaflow_extensions.outerbounds.plugins.auth_server import get_token
44
+ from metaflow_extensions.outerbounds.plugins.aws.assume_role import (
45
+ OBP_ASSUME_ROLE_ARN_ENV_VAR,
46
+ )
44
47
 
45
48
  from hashlib import sha256
46
49
  from metaflow.util import get_username
@@ -49,6 +52,10 @@ def get_boto3_session(role_arn=None, session_vars=None):
49
52
 
50
53
  token_info = get_token("/generate/aws")
51
54
 
55
+ # Check if the assume_role decorator has set a role ARN via environment variable
56
+ # This takes precedence over CSPR role
57
+ decorator_role_arn = os.environ.get(OBP_ASSUME_ROLE_ARN_ENV_VAR)
58
+
52
59
  # Write token to a file. The file name is derived from the user name
53
60
  # so it works with multiple users on the same machine.
54
61
  #
@@ -69,12 +76,13 @@ def get_boto3_session(role_arn=None, session_vars=None):
69
76
  if token_info.get("cspr_role_arn"):
70
77
  cspr_role = token_info["cspr_role_arn"]
71
78
 
72
- if cspr_role:
73
- # If CSPR role is set, we set it as the default role to assume
74
- # for the AWS SDK. We do this by writing an AWS config file
79
+ # If assume_role decorator is used, prioritize it over CSPR role
80
+ effective_role = decorator_role_arn or cspr_role
81
+ if effective_role:
82
+ # If we have either a decorator role or CSPR role, set up AWS config
75
83
  # with two profiles. One to get credentials for the task role
76
84
  # in exchange for the OIDC token, and second to assume the
77
- # CSPR role using the task role credentials.
85
+ # effective role using the task role credentials.
78
86
  import configparser
79
87
  from io import StringIO
80
88
 
@@ -86,9 +94,9 @@ def get_boto3_session(role_arn=None, session_vars=None):
86
94
  "web_identity_token_file": token_file,
87
95
  }
88
96
 
89
- # CSPR role profile (default)
90
- aws_config["profile cspr"] = {
91
- "role_arn": cspr_role,
97
+ # Effective role profile (decorator role or CSPR role)
98
+ aws_config["profile effective"] = {
99
+ "role_arn": effective_role,
92
100
  "source_profile": "task",
93
101
  }
94
102
 
@@ -104,7 +112,7 @@ def get_boto3_session(role_arn=None, session_vars=None):
104
112
  tmp_aws_config_file = f.name
105
113
  os.rename(tmp_aws_config_file, aws_config_file)
106
114
  os.environ["AWS_CONFIG_FILE"] = aws_config_file
107
- os.environ["AWS_PROFILE"] = "cspr"
115
+ os.environ["AWS_PROFILE"] = "effective"
108
116
  else:
109
117
  os.environ["AWS_WEB_IDENTITY_TOKEN_FILE"] = token_file
110
118
  os.environ["AWS_ROLE_ARN"] = token_info["role_arn"]
@@ -116,21 +124,22 @@ def get_boto3_session(role_arn=None, session_vars=None):
116
124
  if token_info.get("region"):
117
125
  os.environ["AWS_DEFAULT_REGION"] = token_info["region"]
118
126
 
119
- if cspr_role:
120
- # The generated AWS config will be used here since we set the
121
- # AWS_CONFIG_FILE environment variable above.
122
- if role_arn == USE_CSPR_ROLE_ARN_IF_SET:
123
- # Otherwise start from the default profile, assuming CSPR role
124
- session = boto3.session.Session(profile_name="cspr")
127
+ if effective_role:
128
+ if role_arn == decorator_role_arn or role_arn == USE_CSPR_ROLE_ARN_IF_SET:
129
+ # We have either a decorator role or CSPR role, use the effective profile
130
+ # The generated AWS config will be used here since we set the
131
+ # AWS_CONFIG_FILE environment variable above.
132
+ session = boto3.session.Session(profile_name="effective")
125
133
  else:
126
134
  session = boto3.session.Session(profile_name="task")
127
135
  else:
136
+ # No decorator role or CSPR role, use default session
128
137
  # Not using AWS config, just AWS_WEB_IDENTITY_TOKEN_FILE + AWS_ROLE_ARN
129
138
  session = boto3.session.Session()
130
139
 
131
- if role_arn and role_arn != USE_CSPR_ROLE_ARN_IF_SET:
140
+ if role_arn and role_arn != USE_CSPR_ROLE_ARN_IF_SET and decorator_role_arn != None:
132
141
  # If the user provided a role_arn, we assume that role
133
- # using the task role credentials. CSPR role is not used.
142
+ # using the task role credentials. This overrides everything else.
134
143
  fetcher = botocore.credentials.AssumeRoleCredentialFetcher(
135
144
  client_creator=session._session.create_client,
136
145
  source_credentials=session._session.get_credentials(),
@@ -146,8 +155,8 @@ def get_boto3_session(role_arn=None, session_vars=None):
146
155
  else:
147
156
  # If the user didn't provide a role_arn, or if the role_arn
148
157
  # is set to USE_CSPR_ROLE_ARN_IF_SET, we return the default
149
- # session which would use the CSPR role if it is set on the
150
- # server, and the task role otherwise.
158
+ # session which would use the effective role (decorator or CSPR) if set,
159
+ # or the task role otherwise.
151
160
  return session
152
161
 
153
162
 
@@ -0,0 +1,4 @@
1
+ from .assume_role_decorator import assume_role
2
+ from .assume_role import OBP_ASSUME_ROLE_ARN_ENV_VAR
3
+
4
+ __mf_promote_submodules__ = ["plugins.aws"]
@@ -0,0 +1,3 @@
1
+ # Environment variable name used by the assume_role decorator to pass
2
+ # the role ARN to AWS client functions
3
+ OBP_ASSUME_ROLE_ARN_ENV_VAR = "OBP_ASSUME_ROLE_ARN"
@@ -0,0 +1,65 @@
1
+ from metaflow.user_configs.config_decorators import (
2
+ MutableFlow,
3
+ MutableStep,
4
+ CustomFlowDecorator,
5
+ )
6
+ from .assume_role import OBP_ASSUME_ROLE_ARN_ENV_VAR
7
+
8
+
9
+ class assume_role(CustomFlowDecorator):
10
+ """
11
+ Flow-level decorator for assuming AWS IAM roles.
12
+
13
+ When applied to a flow, all steps in the flow will automatically use the specified IAM role-arn
14
+ as their source principal.
15
+
16
+ Usage:
17
+ ------
18
+ @assume_role(role_arn="arn:aws:iam::123456789012:role/my-iam-role")
19
+ class MyFlow(FlowSpec):
20
+ @step
21
+ def start(self):
22
+ import boto3
23
+ client = boto3.client("dynamodb") # Automatically uses the role in the flow decorator
24
+ self.next(self.end)
25
+
26
+ @step
27
+ def end(self):
28
+ from metaflow import get_aws_client
29
+ client = get_aws_client("dynamodb") # Automatically uses the role in the flow decorator
30
+ """
31
+
32
+ def init(self, *args, **kwargs):
33
+ self.role_arn = kwargs.get("role_arn", None)
34
+
35
+ if self.role_arn is None:
36
+ raise ValueError(
37
+ "`role_arn` keyword argument is required for the assume_role decorator"
38
+ )
39
+
40
+ if not self.role_arn.startswith("arn:aws:iam::"):
41
+ raise ValueError(
42
+ "`role_arn` must be a valid AWS IAM role ARN starting with 'arn:aws:iam::'"
43
+ )
44
+
45
+ def evaluate(self, mutable_flow: MutableFlow) -> None:
46
+ """
47
+ This method is called by Metaflow to apply the decorator to the flow.
48
+ It sets up environment variables that will be used by the AWS client
49
+ to automatically assume the specified role.
50
+ """
51
+ # Import environment decorator at runtime to avoid circular imports
52
+ from metaflow import environment
53
+
54
+ # Set the role ARN as an environment variable that will be picked up
55
+ # by the get_aws_client function
56
+ def _setup_role_assumption(step: MutableStep) -> None:
57
+ # We'll inject the role assumption by adding an environment decorator
58
+ # The role will be available through an environment variable
59
+ step.add_decorator(
60
+ environment, vars={OBP_ASSUME_ROLE_ARN_ENV_VAR: self.role_arn}
61
+ )
62
+
63
+ # Apply the role assumption setup to all steps in the flow
64
+ for _, step in mutable_flow.steps:
65
+ _setup_role_assumption(step)
@@ -0,0 +1,225 @@
1
+ from metaflow.decorators import StepDecorator
2
+ from metaflow import current
3
+ import functools
4
+ import os
5
+ import threading
6
+
7
+ from .ollama import OllamaManager, OllamaRequestInterceptor
8
+ from .status_card import OllamaStatusCard
9
+ from ..card_utilities.injector import CardDecoratorInjector
10
+
11
+ __mf_promote_submodules__ = ["plugins.ollama"]
12
+
13
+
14
+ class OllamaDecorator(StepDecorator, CardDecoratorInjector):
15
+ """
16
+ This decorator is used to run Ollama APIs as Metaflow task sidecars.
17
+
18
+ User code call
19
+ --------------
20
+ @ollama(
21
+ models=[...],
22
+ ...
23
+ )
24
+
25
+ Valid backend options
26
+ ---------------------
27
+ - 'local': Run as a separate process on the local task machine.
28
+ - (TODO) 'managed': Outerbounds hosts and selects compute provider.
29
+ - (TODO) 'remote': Spin up separate instance to serve Ollama models.
30
+
31
+ Valid model options
32
+ -------------------
33
+ Any model here https://ollama.com/search, e.g. 'llama3.2', 'llama3.3'
34
+
35
+ Parameters
36
+ ----------
37
+ models: list[str]
38
+ List of Ollama containers running models in sidecars.
39
+ backend: str
40
+ Determines where and how to run the Ollama process.
41
+ force_pull: bool
42
+ Whether to run `ollama pull` no matter what, or first check the remote cache in Metaflow datastore for this model key.
43
+ cache_update_policy: str
44
+ Cache update policy: "auto", "force", or "never".
45
+ force_cache_update: bool
46
+ Simple override for "force" cache update policy.
47
+ debug: bool
48
+ Whether to turn on verbose debugging logs.
49
+ circuit_breaker_config: dict
50
+ Configuration for circuit breaker protection. Keys: failure_threshold, recovery_timeout, reset_timeout.
51
+ timeout_config: dict
52
+ Configuration for various operation timeouts. Keys: pull, stop, health_check, install, server_startup.
53
+ """
54
+
55
+ name = "ollama"
56
+ defaults = {
57
+ "models": [],
58
+ "backend": "local",
59
+ "force_pull": False,
60
+ "cache_update_policy": "auto", # "auto", "force", "never"
61
+ "force_cache_update": False, # Simple override for "force"
62
+ "debug": False,
63
+ "circuit_breaker_config": {
64
+ "failure_threshold": 3,
65
+ "recovery_timeout": 60,
66
+ "reset_timeout": 30,
67
+ },
68
+ "timeout_config": {
69
+ "pull": 600, # 10 minutes for model pulls
70
+ "stop": 30, # 30 seconds for model stops
71
+ "health_check": 5, # 5 seconds for health checks
72
+ "install": 60, # 1 minute for Ollama installation
73
+ "server_startup": 300, # 5 minutes for server startup
74
+ },
75
+ "card_refresh_interval": 10, # seconds - how often to update the status card
76
+ }
77
+
78
+ def step_init(
79
+ self, flow, graph, step_name, decorators, environment, flow_datastore, logger
80
+ ):
81
+ super().step_init(
82
+ flow, graph, step_name, decorators, environment, flow_datastore, logger
83
+ )
84
+ self.flow_datastore_backend = flow_datastore._storage_impl
85
+
86
+ # Attach the ollama status card
87
+ self.attach_card_decorator(
88
+ flow,
89
+ step_name,
90
+ "ollama_status",
91
+ "blank",
92
+ refresh_interval=self.attributes["card_refresh_interval"],
93
+ )
94
+
95
+ def task_decorate(
96
+ self, step_func, flow, graph, retry_count, max_user_code_retries, ubf_context
97
+ ):
98
+ @functools.wraps(step_func)
99
+ def ollama_wrapper():
100
+ self.ollama_manager = None
101
+ self.request_interceptor = None
102
+ self.status_card = None
103
+ self.card_monitor_thread = None
104
+
105
+ try:
106
+ # Initialize status card and monitoring
107
+ self.status_card = OllamaStatusCard(
108
+ refresh_interval=self.attributes["card_refresh_interval"]
109
+ )
110
+
111
+ # Start card monitoring in background
112
+ def monitor_card():
113
+ try:
114
+ self.status_card.on_startup(current.card["ollama_status"])
115
+
116
+ while not getattr(
117
+ self.card_monitor_thread, "_stop_event", False
118
+ ):
119
+ try:
120
+ # Trigger card update with current data
121
+ self.status_card.on_update(
122
+ current.card["ollama_status"], None
123
+ )
124
+ import time
125
+
126
+ time.sleep(self.attributes["card_refresh_interval"])
127
+ except Exception as e:
128
+ if self.attributes["debug"]:
129
+ print(f"[@ollama] Card monitoring error: {e}")
130
+ break
131
+ except Exception as e:
132
+ if self.attributes["debug"]:
133
+ print(f"[@ollama] Card monitor thread error: {e}")
134
+ self.status_card.on_error(current.card["ollama_status"], str(e))
135
+
136
+ self.card_monitor_thread = threading.Thread(
137
+ target=monitor_card, daemon=True
138
+ )
139
+ self.card_monitor_thread._stop_event = False
140
+ self.card_monitor_thread.start()
141
+
142
+ # Initialize OllamaManager with status card
143
+ self.ollama_manager = OllamaManager(
144
+ models=self.attributes["models"],
145
+ backend=self.attributes["backend"],
146
+ flow_datastore_backend=self.flow_datastore_backend,
147
+ force_pull=self.attributes["force_pull"],
148
+ cache_update_policy=self.attributes["cache_update_policy"],
149
+ force_cache_update=self.attributes["force_cache_update"],
150
+ debug=self.attributes["debug"],
151
+ circuit_breaker_config=self.attributes["circuit_breaker_config"],
152
+ timeout_config=self.attributes["timeout_config"],
153
+ status_card=self.status_card,
154
+ )
155
+
156
+ # Install request protection by monkey-patching ollama package
157
+ self.request_interceptor = OllamaRequestInterceptor(
158
+ self.ollama_manager.circuit_breaker, self.attributes["debug"]
159
+ )
160
+ self.request_interceptor.install_protection()
161
+
162
+ if self.attributes["debug"]:
163
+ print(
164
+ "[@ollama] OllamaManager initialized and request protection installed"
165
+ )
166
+
167
+ except Exception as e:
168
+ if self.status_card:
169
+ self.status_card.add_event(
170
+ "error", f"Initialization failed: {str(e)}"
171
+ )
172
+ try:
173
+ self.status_card.on_error(current.card["ollama_status"], str(e))
174
+ except:
175
+ pass
176
+ print(f"[@ollama] Error initializing OllamaManager: {e}")
177
+ raise
178
+
179
+ try:
180
+ if self.status_card:
181
+ self.status_card.add_event("info", "Starting user step function")
182
+ step_func()
183
+ if self.status_card:
184
+ self.status_card.add_event(
185
+ "success", "User step function completed successfully"
186
+ )
187
+ finally:
188
+ # Remove request protection first (before terminating models)
189
+ if self.request_interceptor:
190
+ self.request_interceptor.remove_protection()
191
+ if self.attributes["debug"]:
192
+ print("[@ollama] Request protection removed")
193
+
194
+ # Then cleanup ollama manager (while card monitoring is still active)
195
+ if self.ollama_manager:
196
+ self.ollama_manager.terminate_models()
197
+
198
+ # Give the card a moment to render the final shutdown events
199
+ if self.card_monitor_thread and self.status_card:
200
+ import time
201
+
202
+ # Trigger one final card update to capture all shutdown events
203
+ try:
204
+ self.status_card.on_update(current.card["ollama_status"], None)
205
+ except Exception as e:
206
+ if self.attributes["debug"]:
207
+ print(f"[@ollama] Final card update error: {e}")
208
+ time.sleep(2) # Allow final events to be rendered
209
+
210
+ # Now stop card monitoring
211
+ if self.card_monitor_thread:
212
+ self.card_monitor_thread._stop_event = True
213
+
214
+ if self.ollama_manager and self.attributes["debug"]:
215
+ print(
216
+ f"[@ollama] process statuses: {self.ollama_manager.processes}"
217
+ )
218
+ print(
219
+ f"[@ollama] process runtime stats: {self.ollama_manager.stats}"
220
+ )
221
+ print(
222
+ f"[@ollama] Circuit Breaker status: {self.ollama_manager.circuit_breaker.get_status()}"
223
+ )
224
+
225
+ return ollama_wrapper