fxn 0.0.42__py3-none-any.whl → 0.0.44__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.
- fxn/__init__.py +2 -1
- fxn/beta/client.py +59 -2
- fxn/beta/remote.py +40 -47
- fxn/c/configuration.py +62 -30
- fxn/c/value.py +3 -2
- fxn/cli/__init__.py +12 -8
- fxn/cli/compile.py +147 -0
- fxn/cli/predictions.py +16 -13
- fxn/cli/predictors.py +35 -4
- fxn/client.py +66 -5
- fxn/{compile/compile.py → compile.py} +22 -16
- fxn/function.py +8 -3
- fxn/logging.py +219 -0
- fxn/{compile/sandbox.py → sandbox.py} +62 -33
- fxn/services/prediction.py +47 -36
- fxn/types/dtype.py +3 -3
- fxn/types/prediction.py +5 -13
- fxn/types/predictor.py +1 -1
- fxn/version.py +1 -1
- {fxn-0.0.42.dist-info → fxn-0.0.44.dist-info}/METADATA +3 -2
- fxn-0.0.44.dist-info/RECORD +47 -0
- {fxn-0.0.42.dist-info → fxn-0.0.44.dist-info}/WHEEL +1 -1
- fxn/compile/__init__.py +0 -7
- fxn/compile/signature.py +0 -183
- fxn-0.0.42.dist-info/RECORD +0 -47
- {fxn-0.0.42.dist-info → fxn-0.0.44.dist-info}/entry_points.txt +0 -0
- {fxn-0.0.42.dist-info → fxn-0.0.44.dist-info/licenses}/LICENSE +0 -0
- {fxn-0.0.42.dist-info → fxn-0.0.44.dist-info}/top_level.txt +0 -0
fxn/client.py
CHANGED
@@ -4,9 +4,9 @@
|
|
4
4
|
#
|
5
5
|
|
6
6
|
from json import loads, JSONDecodeError
|
7
|
-
from pydantic import BaseModel
|
7
|
+
from pydantic import BaseModel, TypeAdapter
|
8
8
|
from requests import request
|
9
|
-
from typing import
|
9
|
+
from typing import AsyncGenerator, Literal, Type, TypeVar
|
10
10
|
|
11
11
|
T = TypeVar("T", bound=BaseModel)
|
12
12
|
|
@@ -19,11 +19,20 @@ class FunctionClient:
|
|
19
19
|
def request (
|
20
20
|
self,
|
21
21
|
*,
|
22
|
-
method: Literal["GET", "POST", "DELETE"],
|
22
|
+
method: Literal["GET", "POST", "PATCH", "DELETE"],
|
23
23
|
path: str,
|
24
|
-
body: dict[str,
|
24
|
+
body: dict[str, object]=None,
|
25
25
|
response_type: Type[T]=None
|
26
26
|
) -> T:
|
27
|
+
"""
|
28
|
+
Make a request to a REST endpoint.
|
29
|
+
|
30
|
+
Parameters:
|
31
|
+
method (str): Request method.
|
32
|
+
path (str): Endpoint path.
|
33
|
+
body (dict): Request JSON body.
|
34
|
+
response_type (Type): Response type.
|
35
|
+
"""
|
27
36
|
response = request(
|
28
37
|
method=method,
|
29
38
|
url=f"{self.api_url}{path}",
|
@@ -40,6 +49,53 @@ class FunctionClient:
|
|
40
49
|
else:
|
41
50
|
error = _ErrorResponse(**data).errors[0].message if isinstance(data, dict) else data
|
42
51
|
raise FunctionAPIError(error, response.status_code)
|
52
|
+
|
53
|
+
async def stream (
|
54
|
+
self,
|
55
|
+
*,
|
56
|
+
method: Literal["GET", "POST", "PATCH", "DELETE"],
|
57
|
+
path: str,
|
58
|
+
body: dict[str, object]=None,
|
59
|
+
response_type: Type[T]=None
|
60
|
+
) -> AsyncGenerator[T, None]:
|
61
|
+
"""
|
62
|
+
Make a request to a REST endpoint and consume the response as a server-sent events stream.
|
63
|
+
|
64
|
+
Parameters:
|
65
|
+
method (str): Request method.
|
66
|
+
path (str): Endpoint path.
|
67
|
+
body (dict): Request JSON body.
|
68
|
+
response_type (Type): Response type.
|
69
|
+
"""
|
70
|
+
response = request(
|
71
|
+
method=method,
|
72
|
+
url=f"{self.api_url}{path}",
|
73
|
+
json=body,
|
74
|
+
headers={
|
75
|
+
"Accept": "text/event-stream",
|
76
|
+
"Authorization": f"Bearer {self.access_key}"
|
77
|
+
},
|
78
|
+
stream=True
|
79
|
+
)
|
80
|
+
event = None
|
81
|
+
data: str = ""
|
82
|
+
for line in response.iter_lines(decode_unicode=True):
|
83
|
+
if line is None:
|
84
|
+
break
|
85
|
+
line: str = line.strip()
|
86
|
+
if line:
|
87
|
+
if line.startswith("event:"):
|
88
|
+
event = line[len("event:"):].strip()
|
89
|
+
elif line.startswith("data:"):
|
90
|
+
line_data = line[len("data:"):].strip()
|
91
|
+
data = f"{data}\n{line_data}"
|
92
|
+
continue
|
93
|
+
if event is not None:
|
94
|
+
yield _parse_sse_event(event, data, response_type)
|
95
|
+
event = None
|
96
|
+
data = ""
|
97
|
+
if event or data:
|
98
|
+
yield _parse_sse_event(event, data, response_type)
|
43
99
|
|
44
100
|
class FunctionAPIError (Exception):
|
45
101
|
|
@@ -55,4 +111,9 @@ class _APIError (BaseModel):
|
|
55
111
|
message: str
|
56
112
|
|
57
113
|
class _ErrorResponse (BaseModel):
|
58
|
-
errors: list[_APIError]
|
114
|
+
errors: list[_APIError]
|
115
|
+
|
116
|
+
def _parse_sse_event (event: str, data: str, type: Type[T]=None) -> T:
|
117
|
+
result = { "event": event, "data": loads(data) }
|
118
|
+
result = TypeAdapter(type).validate_python(result) if type is not None else result
|
119
|
+
return result
|
@@ -5,12 +5,16 @@
|
|
5
5
|
|
6
6
|
from collections.abc import Callable
|
7
7
|
from functools import wraps
|
8
|
+
from inspect import isasyncgenfunction, iscoroutinefunction
|
8
9
|
from pathlib import Path
|
9
|
-
from pydantic import BaseModel, Field
|
10
|
+
from pydantic import BaseModel, ConfigDict, Field
|
11
|
+
from types import ModuleType
|
12
|
+
from typing import Literal
|
10
13
|
|
11
|
-
from ..types import AccessMode, Signature
|
12
14
|
from .sandbox import Sandbox
|
13
|
-
from .
|
15
|
+
from .types import AccessMode
|
16
|
+
|
17
|
+
CompileTarget = Literal["android", "ios", "linux", "macos", "visionos", "wasm", "windows"]
|
14
18
|
|
15
19
|
class PredictorSpec (BaseModel):
|
16
20
|
"""
|
@@ -19,21 +23,26 @@ class PredictorSpec (BaseModel):
|
|
19
23
|
tag: str = Field(description="Predictor tag.")
|
20
24
|
description: str = Field(description="Predictor description. MUST be less than 100 characters long.", min_length=4, max_length=100)
|
21
25
|
sandbox: Sandbox = Field(description="Sandbox to compile the function.")
|
26
|
+
trace_modules: list[ModuleType] = Field(description="Modules to trace and compile.", exclude=True)
|
27
|
+
targets: list[str] | None = Field(description="Targets to compile this predictor for. Pass `None` to compile for our default targets.")
|
22
28
|
access: AccessMode = Field(description="Predictor access.")
|
23
|
-
signature: Signature = Field(description="Predictor signature.")
|
24
29
|
card: str | None = Field(default=None, description="Predictor card (markdown).")
|
25
30
|
media: str | None = Field(default=None, description="Predictor media URL.")
|
26
31
|
license: str | None = Field(default=None, description="Predictor license URL. This is required for public predictors.")
|
32
|
+
model_config = ConfigDict(arbitrary_types_allowed=True, extra="allow", frozen=True)
|
27
33
|
|
28
34
|
def compile (
|
29
35
|
tag: str,
|
30
36
|
*,
|
31
37
|
description: str,
|
32
38
|
sandbox: Sandbox=None,
|
39
|
+
trace_modules: list[ModuleType]=[],
|
40
|
+
targets: list[CompileTarget]=None,
|
33
41
|
access: AccessMode=AccessMode.Private,
|
34
42
|
card: str | Path=None,
|
35
43
|
media: Path=None,
|
36
44
|
license: str=None,
|
45
|
+
**kwargs
|
37
46
|
):
|
38
47
|
"""
|
39
48
|
Create a predictor by compiling a stateless function.
|
@@ -42,6 +51,8 @@ def compile (
|
|
42
51
|
tag (str): Predictor tag.
|
43
52
|
description (str): Predictor description. MUST be less than 100 characters long.
|
44
53
|
sandbox (Sandbox): Sandbox to compile the function.
|
54
|
+
trace_modules (list): Modules to trace and compile.
|
55
|
+
targets (list): Targets to compile this predictor for. Pass `None` to compile for our default targets.
|
45
56
|
access (AccessMode): Predictor access.
|
46
57
|
card (str | Path): Predictor card markdown string or path to card.
|
47
58
|
media (Path): Predictor thumbnail image (jpeg or png) path.
|
@@ -51,25 +62,20 @@ def compile (
|
|
51
62
|
# Check type
|
52
63
|
if not callable(func):
|
53
64
|
raise TypeError("Cannot compile non-function objects")
|
54
|
-
|
55
|
-
|
56
|
-
raise TypeError(f"Function '{func.__name__}' must be a regular function or generator")
|
65
|
+
if isasyncgenfunction(func) or iscoroutinefunction(func):
|
66
|
+
raise TypeError(f"Entrypoint function '{func.__name__}' must be a regular function or generator")
|
57
67
|
# Gather metadata
|
58
|
-
signature = infer_function_signature(func) # throws
|
59
|
-
if isinstance(card, Path):
|
60
|
-
with open(card_content, "r") as f:
|
61
|
-
card_content = f.read()
|
62
|
-
else:
|
63
|
-
card_content = card
|
64
68
|
spec = PredictorSpec(
|
65
69
|
tag=tag,
|
66
70
|
description=description,
|
67
71
|
sandbox=sandbox if sandbox is not None else Sandbox(),
|
72
|
+
trace_modules=trace_modules,
|
73
|
+
targets=targets,
|
68
74
|
access=access,
|
69
|
-
|
70
|
-
card=card_content,
|
75
|
+
card=card.read_text() if isinstance(card, Path) else card,
|
71
76
|
media=None, # INCOMPLETE
|
72
|
-
license=license
|
77
|
+
license=license,
|
78
|
+
**kwargs
|
73
79
|
)
|
74
80
|
# Wrap
|
75
81
|
@wraps(func)
|
fxn/function.py
CHANGED
@@ -30,11 +30,16 @@ class Function:
|
|
30
30
|
predictions: PredictionService
|
31
31
|
beta: BetaClient
|
32
32
|
|
33
|
-
def __init__ (
|
34
|
-
|
33
|
+
def __init__ (
|
34
|
+
self,
|
35
|
+
access_key: str=None,
|
36
|
+
*,
|
37
|
+
api_url: str=None
|
38
|
+
):
|
39
|
+
access_key = access_key or environ.get("FXN_ACCESS_KEY")
|
35
40
|
api_url = api_url or environ.get("FXN_API_URL")
|
36
41
|
self.client = FunctionClient(access_key, api_url)
|
37
42
|
self.users = UserService(self.client)
|
38
43
|
self.predictors = PredictorService(self.client)
|
39
44
|
self.predictions = PredictionService(self.client)
|
40
|
-
self.beta = BetaClient(self.client)
|
45
|
+
self.beta = BetaClient(self.client, predictions=self.predictions)
|
fxn/logging.py
ADDED
@@ -0,0 +1,219 @@
|
|
1
|
+
#
|
2
|
+
# Function
|
3
|
+
# Copyright © 2025 NatML Inc. All Rights Reserved.
|
4
|
+
#
|
5
|
+
|
6
|
+
from contextvars import ContextVar
|
7
|
+
from rich.console import Console, ConsoleOptions, RenderResult
|
8
|
+
from rich.progress import Progress, ProgressColumn, SpinnerColumn, TextColumn
|
9
|
+
from rich.text import Text
|
10
|
+
from rich.traceback import Traceback
|
11
|
+
from types import MethodType
|
12
|
+
|
13
|
+
current_progress = ContextVar("current_progress", default=None)
|
14
|
+
progress_task_stack = ContextVar("progress_task_stack", default=[])
|
15
|
+
|
16
|
+
class CustomSpinnerColumn (SpinnerColumn):
|
17
|
+
|
18
|
+
def __init__ (
|
19
|
+
self,
|
20
|
+
spinner_name="dots",
|
21
|
+
success_text="[bold green]✔[/bold green]",
|
22
|
+
failure_text="[bright_red]✘[/bright_red]",
|
23
|
+
style="",
|
24
|
+
):
|
25
|
+
super().__init__(spinner_name=spinner_name, style=style)
|
26
|
+
self.success_text = success_text
|
27
|
+
self.failure_text = failure_text
|
28
|
+
|
29
|
+
def render (self, task):
|
30
|
+
done_text = (
|
31
|
+
self.failure_text
|
32
|
+
if task.fields.get("status") == "error"
|
33
|
+
else self.success_text
|
34
|
+
)
|
35
|
+
return done_text if task.finished else self.spinner
|
36
|
+
|
37
|
+
class CustomTextColumn (TextColumn):
|
38
|
+
"""Custom text column that changes color based on task status"""
|
39
|
+
|
40
|
+
def __init__ (self, text_format="{task.description}"):
|
41
|
+
super().__init__(text_format)
|
42
|
+
|
43
|
+
def render (self, task):
|
44
|
+
# Indent and color
|
45
|
+
description = task.description
|
46
|
+
indent_level = task.fields.get("indent_level", 0)
|
47
|
+
indent = self.__get_indent(indent_level)
|
48
|
+
task.description = f"{indent}{description}"
|
49
|
+
if task.fields.get("status") == "error":
|
50
|
+
task.description = f"[bright_red]{task.description}[/bright_red]"
|
51
|
+
# Render
|
52
|
+
text = super().render(task)
|
53
|
+
task.description = description
|
54
|
+
# Return
|
55
|
+
return text
|
56
|
+
|
57
|
+
def __get_indent (self, level: int) -> str:
|
58
|
+
if level == 0:
|
59
|
+
return ""
|
60
|
+
indicator = "└── "
|
61
|
+
return " " * len(indicator) * (level - 1) + indicator
|
62
|
+
|
63
|
+
class CustomProgress(Progress):
|
64
|
+
|
65
|
+
def __init__ (
|
66
|
+
self,
|
67
|
+
*columns: ProgressColumn,
|
68
|
+
console=None,
|
69
|
+
auto_refresh=True,
|
70
|
+
refresh_per_second = 10,
|
71
|
+
speed_estimate_period=30,
|
72
|
+
transient=False,
|
73
|
+
redirect_stdout=True,
|
74
|
+
redirect_stderr=True,
|
75
|
+
get_time=None,
|
76
|
+
disable=False,
|
77
|
+
expand=False
|
78
|
+
):
|
79
|
+
default_columns = list(columns) if len(columns) > 0 else [
|
80
|
+
CustomSpinnerColumn(),
|
81
|
+
CustomTextColumn("[progress.description]{task.description}"),
|
82
|
+
]
|
83
|
+
super().__init__(
|
84
|
+
*default_columns,
|
85
|
+
console=console,
|
86
|
+
auto_refresh=auto_refresh,
|
87
|
+
refresh_per_second=refresh_per_second,
|
88
|
+
speed_estimate_period=speed_estimate_period,
|
89
|
+
transient=transient,
|
90
|
+
redirect_stdout=redirect_stdout,
|
91
|
+
redirect_stderr=redirect_stderr,
|
92
|
+
get_time=get_time,
|
93
|
+
disable=disable,
|
94
|
+
expand=expand
|
95
|
+
)
|
96
|
+
self.default_columns = default_columns
|
97
|
+
|
98
|
+
def __enter__ (self):
|
99
|
+
self._token = current_progress.set(self)
|
100
|
+
self._stack_token = progress_task_stack.set([])
|
101
|
+
return super().__enter__()
|
102
|
+
|
103
|
+
def __exit__ (self, exc_type, exc_val, exc_tb):
|
104
|
+
current_progress.reset(self._token)
|
105
|
+
progress_task_stack.reset(self._stack_token)
|
106
|
+
return super().__exit__(exc_type, exc_val, exc_tb)
|
107
|
+
|
108
|
+
def get_renderables (self):
|
109
|
+
for task in self.tasks:
|
110
|
+
task_columns = task.fields.get("columns") or list()
|
111
|
+
self.columns = self.default_columns + task_columns
|
112
|
+
yield self.make_tasks_table([task])
|
113
|
+
|
114
|
+
class CustomProgressTask:
|
115
|
+
|
116
|
+
def __init__ (
|
117
|
+
self,
|
118
|
+
*,
|
119
|
+
loading_text: str,
|
120
|
+
done_text: str=None,
|
121
|
+
columns: list[ProgressColumn]=None
|
122
|
+
):
|
123
|
+
self.loading_text = loading_text
|
124
|
+
self.done_text = done_text
|
125
|
+
self.task_id = None
|
126
|
+
self.columns = columns
|
127
|
+
|
128
|
+
def __enter__ (self):
|
129
|
+
progress = current_progress.get()
|
130
|
+
if progress is not None:
|
131
|
+
self.task_id = progress.add_task(
|
132
|
+
self.loading_text,
|
133
|
+
total=1,
|
134
|
+
columns=self.columns,
|
135
|
+
indent_level=len(progress_task_stack.get())
|
136
|
+
)
|
137
|
+
current_stack = progress_task_stack.get()
|
138
|
+
progress_task_stack.set(current_stack + [self.task_id])
|
139
|
+
return self
|
140
|
+
|
141
|
+
def __exit__ (self, exc_type, exc_val, exc_tb):
|
142
|
+
progress = current_progress.get()
|
143
|
+
if progress is not None and self.task_id is not None:
|
144
|
+
current_task = progress._tasks[self.task_id]
|
145
|
+
progress.update(
|
146
|
+
self.task_id,
|
147
|
+
description=self.done_text or current_task.description,
|
148
|
+
completed=current_task.total,
|
149
|
+
status="error" if exc_type is not None else current_task.fields.get("status")
|
150
|
+
)
|
151
|
+
current_stack = progress_task_stack.get()
|
152
|
+
if current_stack:
|
153
|
+
progress_task_stack.set(current_stack[:-1])
|
154
|
+
self.task_id = None
|
155
|
+
return False
|
156
|
+
|
157
|
+
def update (self, **kwargs):
|
158
|
+
progress = current_progress.get()
|
159
|
+
if progress is None or self.task_id is None:
|
160
|
+
return
|
161
|
+
progress.update(self.task_id, **kwargs)
|
162
|
+
|
163
|
+
def finish (self, message: str):
|
164
|
+
self.done_text = message
|
165
|
+
|
166
|
+
class TracebackMarkupConsole (Console):
|
167
|
+
|
168
|
+
def print(
|
169
|
+
self,
|
170
|
+
*objects,
|
171
|
+
sep = " ",
|
172
|
+
end = "\n",
|
173
|
+
style = None,
|
174
|
+
justify = None,
|
175
|
+
overflow = None,
|
176
|
+
no_wrap = None,
|
177
|
+
emoji = None,
|
178
|
+
markup = None,
|
179
|
+
highlight = None,
|
180
|
+
width = None,
|
181
|
+
height = None,
|
182
|
+
crop = True,
|
183
|
+
soft_wrap = None,
|
184
|
+
new_line_start = False
|
185
|
+
):
|
186
|
+
traceback = objects[0]
|
187
|
+
if isinstance(traceback, Traceback):
|
188
|
+
stack = traceback.trace.stacks[0]
|
189
|
+
original_rich_console = traceback.__rich_console__
|
190
|
+
def __rich_console__ (self: Traceback, console: Console, options: ConsoleOptions) -> RenderResult:
|
191
|
+
for renderable in original_rich_console(console, options):
|
192
|
+
if (
|
193
|
+
isinstance(renderable, Text) and
|
194
|
+
any(part.startswith(f"{stack.exc_type}:") for part in renderable._text)
|
195
|
+
):
|
196
|
+
yield Text.assemble(
|
197
|
+
(f"{stack.exc_type}: ", "traceback.exc_type"),
|
198
|
+
Text.from_markup(stack.exc_value)
|
199
|
+
)
|
200
|
+
else:
|
201
|
+
yield renderable
|
202
|
+
traceback.__rich_console__ = MethodType(__rich_console__, traceback)
|
203
|
+
return super().print(
|
204
|
+
*objects,
|
205
|
+
sep=sep,
|
206
|
+
end=end,
|
207
|
+
style=style,
|
208
|
+
justify=justify,
|
209
|
+
overflow=overflow,
|
210
|
+
no_wrap=no_wrap,
|
211
|
+
emoji=emoji,
|
212
|
+
markup=markup,
|
213
|
+
highlight=highlight,
|
214
|
+
width=width,
|
215
|
+
height=height,
|
216
|
+
crop=crop,
|
217
|
+
soft_wrap=soft_wrap,
|
218
|
+
new_line_start=new_line_start
|
219
|
+
)
|
@@ -4,13 +4,16 @@
|
|
4
4
|
#
|
5
5
|
|
6
6
|
from __future__ import annotations
|
7
|
+
from abc import ABC, abstractmethod
|
7
8
|
from hashlib import sha256
|
8
9
|
from pathlib import Path
|
9
10
|
from pydantic import BaseModel
|
10
11
|
from requests import put
|
12
|
+
from rich.progress import BarColumn, TextColumn
|
11
13
|
from typing import Literal
|
12
14
|
|
13
|
-
from
|
15
|
+
from .function import Function
|
16
|
+
from .logging import CustomProgressTask
|
14
17
|
|
15
18
|
class WorkdirCommand (BaseModel):
|
16
19
|
kind: Literal["workdir"] = "workdir"
|
@@ -20,17 +23,35 @@ class EnvCommand (BaseModel):
|
|
20
23
|
kind: Literal["env"] = "env"
|
21
24
|
env: dict[str, str]
|
22
25
|
|
23
|
-
class
|
24
|
-
kind: Literal["upload_file"] = "upload_file"
|
26
|
+
class UploadableCommand (BaseModel, ABC):
|
25
27
|
from_path: str
|
26
28
|
to_path: str
|
27
29
|
manifest: dict[str, str] | None = None
|
28
30
|
|
29
|
-
|
31
|
+
@abstractmethod
|
32
|
+
def get_files (self) -> list[Path]:
|
33
|
+
pass
|
34
|
+
|
35
|
+
class UploadFileCommand (UploadableCommand):
|
36
|
+
kind: Literal["upload_file"] = "upload_file"
|
37
|
+
|
38
|
+
def get_files (self) -> list[Path]:
|
39
|
+
return [Path(self.from_path).resolve()]
|
40
|
+
|
41
|
+
class UploadDirectoryCommand (UploadableCommand):
|
30
42
|
kind: Literal["upload_dir"] = "upload_dir"
|
31
|
-
|
32
|
-
|
33
|
-
|
43
|
+
|
44
|
+
def get_files (self) -> list[Path]:
|
45
|
+
from_path = Path(self.from_path)
|
46
|
+
assert from_path.is_absolute(), "Cannot upload directory because directory path must be absolute"
|
47
|
+
return [file for file in from_path.rglob("*") if file.is_file()]
|
48
|
+
|
49
|
+
class EntrypointCommand (UploadableCommand):
|
50
|
+
kind: Literal["entrypoint"] = "entrypoint"
|
51
|
+
name: str
|
52
|
+
|
53
|
+
def get_files (self) -> list[Path]:
|
54
|
+
return [Path(self.from_path).resolve()]
|
34
55
|
|
35
56
|
class PipInstallCommand (BaseModel):
|
36
57
|
kind: Literal["pip_install"] = "pip_install"
|
@@ -40,10 +61,6 @@ class AptInstallCommand (BaseModel):
|
|
40
61
|
kind: Literal["apt_install"] = "apt_install"
|
41
62
|
packages: list[str]
|
42
63
|
|
43
|
-
class EntrypointCommand (BaseModel):
|
44
|
-
kind: Literal["entrypoint"] = "entrypoint"
|
45
|
-
path: str
|
46
|
-
|
47
64
|
Command = (
|
48
65
|
WorkdirCommand |
|
49
66
|
EnvCommand |
|
@@ -68,16 +85,14 @@ class Sandbox (BaseModel):
|
|
68
85
|
path (str | Path): Path to change to.
|
69
86
|
"""
|
70
87
|
command = WorkdirCommand(path=str(path))
|
71
|
-
self.commands
|
72
|
-
return self
|
88
|
+
return Sandbox(commands=self.commands + [command])
|
73
89
|
|
74
90
|
def env (self, **env: str) -> Sandbox:
|
75
91
|
"""
|
76
92
|
Set environment variables in the sandbox.
|
77
93
|
"""
|
78
94
|
command = EnvCommand(env=env)
|
79
|
-
self.commands
|
80
|
-
return self
|
95
|
+
return Sandbox(commands=self.commands + [command])
|
81
96
|
|
82
97
|
def upload_file (
|
83
98
|
self,
|
@@ -92,8 +107,7 @@ class Sandbox (BaseModel):
|
|
92
107
|
to_path (str | Path): Remote path to upload file to.
|
93
108
|
"""
|
94
109
|
command = UploadFileCommand(from_path=str(from_path), to_path=str(to_path))
|
95
|
-
self.commands
|
96
|
-
return self
|
110
|
+
return Sandbox(commands=self.commands + [command])
|
97
111
|
|
98
112
|
def upload_directory (
|
99
113
|
self,
|
@@ -108,8 +122,7 @@ class Sandbox (BaseModel):
|
|
108
122
|
to_path (str | Path): Remote path to upload directory to.
|
109
123
|
"""
|
110
124
|
command = UploadDirectoryCommand(from_path=str(from_path), to_path=str(to_path))
|
111
|
-
self.commands
|
112
|
-
return self
|
125
|
+
return Sandbox(commands=self.commands + [command])
|
113
126
|
|
114
127
|
def pip_install (self, *packages: str) -> Sandbox:
|
115
128
|
"""
|
@@ -119,8 +132,7 @@ class Sandbox (BaseModel):
|
|
119
132
|
packages (list): Packages to install.
|
120
133
|
"""
|
121
134
|
command = PipInstallCommand(packages=packages)
|
122
|
-
self.commands
|
123
|
-
return self
|
135
|
+
return Sandbox(commands=self.commands + [command])
|
124
136
|
|
125
137
|
def apt_install (self, *packages: str) -> Sandbox:
|
126
138
|
"""
|
@@ -130,24 +142,41 @@ class Sandbox (BaseModel):
|
|
130
142
|
packages (list): Packages to install.
|
131
143
|
"""
|
132
144
|
command = AptInstallCommand(packages=packages)
|
133
|
-
self.commands
|
134
|
-
|
135
|
-
|
136
|
-
def populate (self, fxn: Function=None) -> Sandbox:
|
145
|
+
return Sandbox(commands=self.commands + [command])
|
146
|
+
|
147
|
+
def populate (self, fxn: Function=None) -> Sandbox: # CHECK # In place
|
137
148
|
"""
|
138
149
|
Populate all metadata.
|
139
150
|
"""
|
140
151
|
fxn = fxn if fxn is not None else Function()
|
152
|
+
entrypoint = next(cmd for cmd in self.commands if isinstance(cmd, EntrypointCommand))
|
153
|
+
entry_path = Path(entrypoint.from_path).resolve()
|
141
154
|
for command in self.commands:
|
142
|
-
if isinstance(command,
|
143
|
-
|
144
|
-
to_path = Path(command.to_path)
|
145
|
-
command.manifest = { str(to_path / from_path.name): self.__upload_file(from_path, fxn=fxn) }
|
146
|
-
elif isinstance(command, UploadDirectoryCommand):
|
155
|
+
if isinstance(command, UploadableCommand):
|
156
|
+
cwd = Path.cwd()
|
147
157
|
from_path = Path(command.from_path)
|
148
158
|
to_path = Path(command.to_path)
|
149
|
-
|
150
|
-
|
159
|
+
if not from_path.is_absolute():
|
160
|
+
from_path = (entry_path / from_path).resolve()
|
161
|
+
command.from_path = str(from_path)
|
162
|
+
files = command.get_files()
|
163
|
+
name = from_path.relative_to(cwd) if from_path.is_relative_to(cwd) else from_path.resolve()
|
164
|
+
with CustomProgressTask(
|
165
|
+
loading_text=f"Uploading [light_slate_blue]{name}[/light_slate_blue]...",
|
166
|
+
done_text=f"Uploaded [light_slate_blue]{name}[/light_slate_blue]",
|
167
|
+
columns=[
|
168
|
+
BarColumn(),
|
169
|
+
TextColumn("{task.completed}/{task.total}")
|
170
|
+
]
|
171
|
+
) as task:
|
172
|
+
manifest = { }
|
173
|
+
for idx, file in enumerate(files):
|
174
|
+
rel_file_path = file.relative_to(from_path) if from_path.is_dir() else file.name
|
175
|
+
dst_path = to_path / rel_file_path
|
176
|
+
checksum = self.__upload_file(file, fxn=fxn)
|
177
|
+
manifest[str(dst_path)] = checksum
|
178
|
+
task.update(total=len(files), completed=idx+1)
|
179
|
+
command.manifest = manifest
|
151
180
|
return self
|
152
181
|
|
153
182
|
def __upload_file (self, path: Path, fxn: Function) -> str:
|
@@ -172,6 +201,6 @@ class Sandbox (BaseModel):
|
|
172
201
|
for chunk in iter(lambda: f.read(4096), b""):
|
173
202
|
hash.update(chunk)
|
174
203
|
return hash.hexdigest()
|
175
|
-
|
204
|
+
|
176
205
|
class _Resource (BaseModel):
|
177
206
|
url: str
|