outerbounds 0.3.174__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.
@@ -0,0 +1,125 @@
1
+ """
2
+ Examples demonstrating how to use the code packager abstraction.
3
+
4
+ This file provides usage examples for the code packager classes.
5
+ These examples are for documentation purposes and are not meant to be run directly.
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ from io import BytesIO
11
+ from typing import List, Dict, Any, Callable, Optional
12
+
13
+ from .code_packager import CodePackager
14
+
15
+
16
+ def basic_usage_example(datastore_type: str = "s3") -> None:
17
+ """
18
+ Basic usage example with ContentAddressedStore.
19
+
20
+ This example shows how to:
21
+ 1. Define paths to include in a package
22
+ 2. Create a package using CodePackager's default packaging
23
+ 3. Store the package using ContentAddressedStore directly
24
+ 4. Generate a download command
25
+
26
+ Parameters
27
+ ----------
28
+ datastore_type : str, default "s3"
29
+ The type of datastore to use: "s3", "azure", "gs", or "local"
30
+ """
31
+ # Define the paths to include in the package
32
+ paths_to_include = ["./"]
33
+
34
+ # Define which file suffixes to include
35
+ file_suffixes = [".py", ".md"]
36
+
37
+ # Create metadata for the package
38
+ metadata = {"example": True, "timestamp": "2023-01-01T00:00:00Z"}
39
+
40
+ # Initialize the packager with datastore configuration
41
+ packager = CodePackager(
42
+ datastore_type=datastore_type,
43
+ code_package_prefix="my-custom-prefix", # Optional
44
+ )
45
+
46
+ # Store the package with packaging parameters
47
+ package_url, package_key = packager.store(
48
+ paths_to_include=paths_to_include,
49
+ file_suffixes=file_suffixes,
50
+ metadata=metadata,
51
+ )
52
+
53
+ # Generate a download command
54
+ download_cmd = CodePackager.get_download_cmd(
55
+ package_url=package_url,
56
+ datastore_type=datastore_type,
57
+ target_file="my_package.tar",
58
+ )
59
+
60
+ # Generate complete package commands for downloading and setup
61
+ package_commands = packager.get_package_commands(
62
+ code_package_url=package_url,
63
+ target_file="my_package.tar",
64
+ working_dir="my_app",
65
+ )
66
+
67
+ # Print some information
68
+ print(f"Package URL: {package_url}")
69
+ print(f"Package Key: {package_key}")
70
+ print(f"Download Command: {download_cmd}")
71
+ print(f"Complete package commands: {package_commands}")
72
+
73
+
74
+ def usage_with_custom_package_function(datastore_type: str = "s3") -> None:
75
+ """
76
+ Example of using the CodePackager with a custom package creation function.
77
+
78
+ Parameters
79
+ ----------
80
+ datastore_type : str, default "s3"
81
+ The type of datastore to use: "s3", "azure", "gs", or "local"
82
+ """
83
+
84
+ # Define a custom package function
85
+ def create_custom_package():
86
+ # This is a simple example - in real use, you might create a more complex package
87
+ from io import BytesIO
88
+ import tarfile
89
+ import time
90
+
91
+ buf = BytesIO()
92
+ with tarfile.open(fileobj=buf, mode="w:gz") as tar:
93
+ # Add a simple file to the tarball
94
+ content = b"print('Hello, custom package!')"
95
+ info = tarfile.TarInfo(name="hello.py")
96
+ info.size = len(content)
97
+ info.mtime = int(time.time())
98
+ file_object = BytesIO(content)
99
+ tar.addfile(info, file_object)
100
+
101
+ return bytearray(buf.getvalue())
102
+
103
+ # Initialize the packager with datastore configuration
104
+ packager = CodePackager(datastore_type=datastore_type)
105
+
106
+ # Store the package with the custom package function
107
+ package_url, package_key = packager.store(package_create_fn=create_custom_package)
108
+
109
+ # Generate a download command
110
+ download_cmd = CodePackager.get_download_cmd(
111
+ package_url=package_url,
112
+ datastore_type=datastore_type,
113
+ target_file="custom_package.tar",
114
+ )
115
+
116
+ # Generate complete package commands
117
+ package_commands = packager.get_package_commands(
118
+ code_package_url=package_url,
119
+ )
120
+
121
+ # Print some information
122
+ print(f"Package URL: {package_url}")
123
+ print(f"Package Key: {package_key}")
124
+ print(f"Download Command: {download_cmd}")
125
+ print(f"Complete commands: {package_commands}")
@@ -0,0 +1,250 @@
1
+
2
+ title: Outerbounds App Configuration Schema
3
+ description: |
4
+ Schema for defining Outerbounds Apps configuration. This schema is what we will end up using on the CLI/programmatic interface.
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.
8
+ version: 1.0.0
9
+ type: object
10
+ required:
11
+ - name
12
+ - port
13
+ properties:
14
+ name: # Only used in `deploy` command
15
+ allow_union: true # Allow overriding name from the CLI.
16
+ type: string
17
+ description: The name of the app to deploy.
18
+ maxLength: 20 # todo: check if we should allow a larger length.
19
+ example: "myapp"
20
+ port: # Only used in `deploy` command
21
+ allow_union: false
22
+ type: integer
23
+ description: Port where the app is hosted. When deployed this will be port on which we will deploy the app.
24
+ minimum: 1
25
+ maximum: 65535
26
+ example: 8000
27
+ tags: # Only used in `deploy` command
28
+ allow_union: true
29
+ type: array
30
+ description: The tags of the app to deploy.
31
+ items:
32
+ type: object
33
+ example:
34
+ - foo: bar
35
+ - x: y
36
+ image: # Only used in `deploy` command
37
+ allow_union: true # We will overrwite the image if specified on the CLI.
38
+ type: string
39
+ description: The Docker image to deploy with the App.
40
+ example: "python:3.10-slim"
41
+ secrets: # Used in `run` command
42
+ allow_union: true
43
+ type: array
44
+ description: Outerbounds integrations to attach to the app. You can use the value you set in the `@secrets` decorator in your code.
45
+ items:
46
+ type: string
47
+ example: ["outerbounds.hf-token"]
48
+ environment: # Used in `run` command
49
+ # Todo: So this part might not be best on the CLI. We should probably have a better way to handle this.
50
+ # In simplicity, we can just JSON dump anything that looks like a dict/list/
51
+ allow_union: true
52
+ type: object
53
+ description: Environment variables to deploy with the App.
54
+ additionalProperties:
55
+ oneOf:
56
+ - type: string
57
+ - type: number
58
+ - type: boolean
59
+ - type: object
60
+ - type: array # When users give arrays, or objects, we need to JSON dump them. Users need to be aware of this.
61
+ example:
62
+ DEBUG: true
63
+ DATABASE_CONFIG: {"host": "localhost", "port": 5432}
64
+ ALLOWED_ORIGINS: ["http://localhost:3000", "https://myapp.com"]
65
+ dependencies: # Used in `run` command
66
+ allow_union: false
67
+ type: object
68
+ description: |
69
+ The dependencies to attach to the app. Only one of the properties can be specified.
70
+ properties:
71
+ from_requirements_file:
72
+ type: string
73
+ description: The path to the requirements.txt file to attach to the app.
74
+ example: "requirements.txt"
75
+ from_pyproject_toml:
76
+ type: string
77
+ description: The path to the pyproject.toml file to attach to the app.
78
+ example: "pyproject.toml"
79
+ python:
80
+ type: string
81
+ description: |
82
+ The Python version to use for the app.
83
+ example: "3.10"
84
+ pypi:
85
+ type: object
86
+ description: |
87
+ A dictionary of pypi dependencies to attach to the app.
88
+ The key is the package name and the value is the version.
89
+ example:
90
+ numpy: 1.23.0
91
+ pandas: ""
92
+ conda:
93
+ type: object
94
+ description: |
95
+ A dictionary of pypi dependencies to attach to the app.
96
+ The key is the package name and the value is the version.
97
+ example:
98
+ numpy: 1.23.0
99
+ pandas: ""
100
+ package:
101
+ allow_union: false
102
+ type: object
103
+ description: |
104
+ Configurations associated with packaging the app.
105
+ properties:
106
+ src_path:
107
+ type: string
108
+ description: The path to the source code to deploy with the App.
109
+ example: "./"
110
+ suffixes:
111
+ type: array
112
+ description: |
113
+ A list of suffixes to add to the source code to deploy with the App.
114
+ items:
115
+ type: string
116
+ example: [".py", ".ipynb"]
117
+
118
+ commands: # Used in `run` command
119
+ allow_union: false
120
+ type: array
121
+ description: A list of commands to run the app with. Cannot be configured from the CLI. Only used in `run` command.
122
+ items:
123
+ type: string
124
+ example: ["python app.py", "python app.py --foo bar"]
125
+ resources: # Only used in `deploy` command
126
+ allow_union: true # You can override CPU/Memory/GPU/Storage from the CLI.
127
+ type: object
128
+ properties:
129
+ cpu:
130
+ type: string
131
+ description: CPU resource request and limit.
132
+ example: "500m"
133
+ default: "1"
134
+ memory:
135
+ type: string
136
+ description: Memory resource request and limit.
137
+ example: "512Mi"
138
+ default: "4Gi"
139
+ gpu:
140
+ type: string
141
+ description: GPU resource request and limit.
142
+ example: "1"
143
+ storage:
144
+ type: string
145
+ description: Storage resource request and limit.
146
+ example: "1Gi"
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
162
+ health_check: # Can be used in `run` command
163
+ type: object
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.
165
+ # If it is set to false, then we should throw an exception if the CLI is trying to override something specified in the config file.
166
+ # We will only allow unions in certains options. The rest will not allow any unions and only need to be specified in one place.
167
+ allow_union: false
168
+ properties:
169
+ enabled:
170
+ type: boolean
171
+ description: Whether to enable health checks.
172
+ example: true
173
+ default: false
174
+ path:
175
+ type: string
176
+ description: The path for health checks.
177
+ example: "/health"
178
+ initial_delay_seconds:
179
+ type: integer
180
+ description: Number of seconds to wait before performing the first health check.
181
+ example: 10
182
+ period_seconds:
183
+ type: integer
184
+ description: How often to perform the health check.
185
+ example: 30
186
+ compute_pools: # Only used in `deploy` command
187
+ allow_union: true
188
+ type: array
189
+ description: |
190
+ A list of compute pools to deploy the app to.
191
+ items:
192
+ type: string
193
+ example: ["default", "large"]
194
+ auth: # Only used in `deploy` command
195
+ allow_union: false
196
+ type: object
197
+ description: |
198
+ Auth related configurations.
199
+ properties:
200
+ type:
201
+ type: string
202
+ description: |
203
+ The type of authentication to use for the app.
204
+ enum: [API, Browser]
205
+ public:
206
+ type: boolean
207
+ description: |
208
+ Whether the app is public or not.
209
+ default: true
210
+ # There is an allowed perimeters property
211
+ # But that needs a little more thought on how
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,115 @@
1
+ import copy
2
+ import json
3
+ import os
4
+ import shutil
5
+ import sys
6
+ import tempfile
7
+ from hashlib import sha256
8
+ from typing import List, Optional, Callable, Any
9
+ from .app_config import AppConfig
10
+ from .utils import TODOException
11
+ from metaflow.metaflow_config import (
12
+ get_pinned_conda_libs,
13
+ DEFAULT_DATASTORE,
14
+ KUBERNETES_CONTAINER_IMAGE,
15
+ )
16
+ from collections import namedtuple
17
+
18
+ BakingStatus = namedtuple(
19
+ "BakingStatus", ["image_should_be_baked", "python_path", "resolved_image"]
20
+ )
21
+
22
+
23
+ class ImageBakingException(Exception):
24
+ pass
25
+
26
+
27
+ def _safe_open_file(path: str):
28
+ if not os.path.exists(path):
29
+ raise TODOException(f"File does not exist: {path}")
30
+ try:
31
+ with open(path, "r") as f:
32
+ return f.read()
33
+ except Exception as e:
34
+ raise TODOException(f"Failed to open file: {e}")
35
+
36
+
37
+ def bake_deployment_image(
38
+ app_config: AppConfig,
39
+ cache_file_path: str,
40
+ logger: Optional[Callable[[str], Any]] = None,
41
+ ) -> BakingStatus:
42
+ # When do we bake an image?
43
+ # 1. When the user has specified something like `pypi`/`conda`
44
+ # 2, When the user has specified something like `from_requirements`/ `from_pyproject`
45
+ # TODO: add parsers for the pyproject/requirements stuff.
46
+ from metaflow.ob_internal import bake_image
47
+ from metaflow.plugins.pypi.parsers import (
48
+ requirements_txt_parser,
49
+ pyproject_toml_parser,
50
+ )
51
+
52
+ image = app_config.get("image", KUBERNETES_CONTAINER_IMAGE)
53
+ python_version = "%d.%d.%d" % sys.version_info[:3]
54
+
55
+ dependencies = app_config.get_state("dependencies", {})
56
+ pypi_packages = {}
57
+ conda_packages = {}
58
+
59
+ parsed_packages = {}
60
+
61
+ if dependencies.get("from_requirements_file"):
62
+ parsed_packages = requirements_txt_parser(
63
+ _safe_open_file(dependencies.get("from_requirements_file"))
64
+ )
65
+ pypi_packages = parsed_packages.get("packages", {})
66
+ python_version = parsed_packages.get("python_version", python_version)
67
+
68
+ elif dependencies.get("from_pyproject_toml"):
69
+ parsed_packages = pyproject_toml_parser(
70
+ _safe_open_file(dependencies.get("from_pyproject_toml"))
71
+ )
72
+ pypi_packages = parsed_packages.get("packages", {})
73
+ python_version = parsed_packages.get("python_version", python_version)
74
+
75
+ elif "pypi" in dependencies:
76
+ pypi_packages = dependencies.get("pypi", {})
77
+
78
+ if "conda" in dependencies:
79
+ conda_packages = dependencies.get("conda", {})
80
+ if "python" in dependencies:
81
+ python_version = dependencies.get("python", python_version)
82
+
83
+ python_packages_exist = len(pypi_packages) > 0 or len(conda_packages) > 0
84
+ if (not python_packages_exist) or app_config.get_state("skip_dependencies", False):
85
+ # Inform the user that no dependencies are being used.
86
+ if app_config.get_state("skip_dependencies", False):
87
+ logger(
88
+ "⏭️ Skipping baking dependencies into the image based on the --no-deps flag."
89
+ )
90
+ # TODO: Handle this a little more nicely.
91
+ return BakingStatus(
92
+ image_should_be_baked=False, resolved_image=image, python_path="python"
93
+ )
94
+
95
+ pinned_conda_libs = get_pinned_conda_libs(python_version, DEFAULT_DATASTORE)
96
+ pypi_packages.update(pinned_conda_libs)
97
+ _reference = app_config.get("name", "default")
98
+ # `image` cannot be None. If by change it is none, FB will fart.
99
+ fb_response = bake_image(
100
+ cache_file_path=cache_file_path,
101
+ pypi_packages=pypi_packages,
102
+ conda_packages=conda_packages,
103
+ ref=_reference,
104
+ python=python_version,
105
+ base_image=image,
106
+ logger=logger,
107
+ )
108
+ if fb_response.failure:
109
+ raise ImageBakingException(f"Failed to bake image: {fb_response.response}")
110
+
111
+ return BakingStatus(
112
+ image_should_be_baked=True,
113
+ resolved_image=fb_response.container_image,
114
+ python_path=fb_response.python_path,
115
+ )
File without changes
@@ -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