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 CHANGED
@@ -4,7 +4,8 @@
4
4
  #
5
5
 
6
6
  from .client import FunctionAPIError
7
- from .compile import *
7
+ from .compile import compile, CompileTarget
8
8
  from .function import Function
9
+ from .sandbox import Sandbox
9
10
  from .types import *
10
11
  from .version import *
fxn/beta/client.py CHANGED
@@ -3,8 +3,17 @@
3
3
  # Copyright © 2025 NatML Inc. All Rights Reserved.
4
4
  #
5
5
 
6
+ from functools import wraps
7
+ from inspect import signature as get_signature, Signature
8
+ from typing import get_origin, Callable, Generator, Iterator, TypeVar
9
+
6
10
  from ..client import FunctionClient
11
+ from ..services import PredictionService as EdgePredictionService
12
+ from ..types import Acceleration
7
13
  from .prediction import PredictionService
14
+ from .remote import RemoteAcceleration
15
+
16
+ F = TypeVar("F", bound=Callable[..., object])
8
17
 
9
18
  class BetaClient:
10
19
  """
@@ -12,5 +21,53 @@ class BetaClient:
12
21
  """
13
22
  predictions: PredictionService
14
23
 
15
- def __init__ (self, client: FunctionClient):
16
- self.predictions = PredictionService(client)
24
+ def __init__ (
25
+ self,
26
+ client: FunctionClient,
27
+ *,
28
+ predictions: EdgePredictionService
29
+ ):
30
+ self.predictions = PredictionService(client)
31
+ self.__edge_predictions = predictions
32
+
33
+ def predict ( # INCOMPLETE # Preload
34
+ self,
35
+ tag: str,
36
+ *,
37
+ remote: bool=False,
38
+ acceleration: Acceleration | RemoteAcceleration="auto",
39
+ preload: bool=True
40
+ ) -> Callable[[F], F]:
41
+ """
42
+ Create a prediction and return results when the decorated function is invoked.
43
+
44
+ Parameters:
45
+ tag (str): Predictor tag.
46
+ remote (bool): Whether to create the prediction remotely.
47
+ acceleration (Acceleration | RemoteAcceleration): Prediction acceleration.
48
+ preload (bool): Whether to preload the predictor on the first run.
49
+ """
50
+ def decorator(func: F) -> F:
51
+ signature = get_signature(func)
52
+ @wraps(func)
53
+ def wrapper(*args, **kwargs):
54
+ bound_args = signature.bind(*args, **kwargs)
55
+ bound_args.apply_defaults()
56
+ stream = (
57
+ signature.return_annotation is not Signature.empty and
58
+ get_origin(signature.return_annotation) in [Iterator, Generator]
59
+ )
60
+ create_func = self.predictions.remote.create if remote else self.__edge_predictions.create
61
+ def _predict (): # INCOMPLETE
62
+ prediction = create_func(
63
+ tag=tag,
64
+ inputs=bound_args.arguments,
65
+ acceleration=acceleration
66
+ )
67
+ if prediction.error:
68
+ raise RuntimeError(prediction.error)
69
+ return tuple(prediction.results) if len(prediction.results) > 1 else prediction.results[0]
70
+ result = _predict()
71
+ return result
72
+ return wrapper
73
+ return decorator
fxn/beta/remote.py CHANGED
@@ -13,7 +13,7 @@ from numpy import array, frombuffer, ndarray
13
13
  from PIL import Image
14
14
  from pydantic import BaseModel, Field
15
15
  from requests import get, put
16
- from typing import Any
16
+ from typing import Literal
17
17
  from urllib.request import urlopen
18
18
 
19
19
  from ..c import Configuration
@@ -21,14 +21,7 @@ from ..client import FunctionClient
21
21
  from ..services import Value
22
22
  from ..types import Dtype, Prediction
23
23
 
24
- class RemoteAcceleration (str, Enum):
25
- """
26
- Remote acceleration.
27
- """
28
- Auto = "auto"
29
- CPU = "cpu"
30
- A40 = "a40"
31
- A100 = "a100"
24
+ RemoteAcceleration = Literal["auto", "cpu", "a40", "a100"]
32
25
 
33
26
  class RemotePredictionService:
34
27
  """
@@ -43,7 +36,7 @@ class RemotePredictionService:
43
36
  tag: str,
44
37
  *,
45
38
  inputs: dict[str, Value],
46
- acceleration: RemoteAcceleration=RemoteAcceleration.Auto
39
+ acceleration: RemoteAcceleration="auto"
47
40
  ) -> Prediction:
48
41
  """
49
42
  Create a remote prediction.
@@ -74,51 +67,51 @@ class RemotePredictionService:
74
67
 
75
68
  def __to_value (
76
69
  self,
77
- object: Value,
70
+ obj: Value,
78
71
  *,
79
72
  name: str,
80
73
  max_data_url_size: int=4 * 1024 * 1024
81
74
  ) -> RemoteValue:
82
- object = self.__try_ensure_serializable(object)
83
- if object is None:
75
+ obj = self.__try_ensure_serializable(obj)
76
+ if obj is None:
84
77
  return RemoteValue(data=None, type=Dtype.null)
85
- elif isinstance(object, float):
86
- object = array(object, dtype=Dtype.float32)
87
- return self.__to_value(object, name=name, max_data_url_size=max_data_url_size)
88
- elif isinstance(object, bool):
89
- object = array(object, dtype=Dtype.bool)
90
- return self.__to_value(object, name=name, max_data_url_size=max_data_url_size)
91
- elif isinstance(object, int):
92
- object = array(object, dtype=Dtype.int32)
93
- return self.__to_value(object, name=name, max_data_url_size=max_data_url_size)
94
- elif isinstance(object, ndarray):
95
- buffer = BytesIO(object.tobytes())
78
+ elif isinstance(obj, float):
79
+ obj = array(obj, dtype=Dtype.float32)
80
+ return self.__to_value(obj, name=name, max_data_url_size=max_data_url_size)
81
+ elif isinstance(obj, bool):
82
+ obj = array(obj, dtype=Dtype.bool)
83
+ return self.__to_value(obj, name=name, max_data_url_size=max_data_url_size)
84
+ elif isinstance(obj, int):
85
+ obj = array(obj, dtype=Dtype.int32)
86
+ return self.__to_value(obj, name=name, max_data_url_size=max_data_url_size)
87
+ elif isinstance(obj, ndarray):
88
+ buffer = BytesIO(obj.tobytes())
96
89
  data = self.__upload(buffer, name=name, max_data_url_size=max_data_url_size)
97
- return RemoteValue(data=data, type=object.dtype.name, shape=list(object.shape))
98
- elif isinstance(object, str):
99
- buffer = BytesIO(object.encode())
90
+ return RemoteValue(data=data, type=obj.dtype.name, shape=list(obj.shape))
91
+ elif isinstance(obj, str):
92
+ buffer = BytesIO(obj.encode())
100
93
  data = self.__upload(buffer, name=name, mime="text/plain", max_data_url_size=max_data_url_size)
101
94
  return RemoteValue(data=data, type=Dtype.string)
102
- elif isinstance(object, list):
103
- buffer = BytesIO(dumps(object).encode())
95
+ elif isinstance(obj, list):
96
+ buffer = BytesIO(dumps(obj).encode())
104
97
  data = self.__upload(buffer, name=name, mime="application/json", max_data_url_size=max_data_url_size)
105
98
  return RemoteValue(data=data, type=Dtype.list)
106
- elif isinstance(object, dict):
107
- buffer = BytesIO(dumps(object).encode())
99
+ elif isinstance(obj, dict):
100
+ buffer = BytesIO(dumps(obj).encode())
108
101
  data = self.__upload(buffer, name=name, mime="application/json", max_data_url_size=max_data_url_size)
109
102
  return RemoteValue(data=data, type=Dtype.dict)
110
- elif isinstance(object, Image.Image):
103
+ elif isinstance(obj, Image.Image):
111
104
  buffer = BytesIO()
112
- format = "PNG" if object.mode == "RGBA" else "JPEG"
105
+ format = "PNG" if obj.mode == "RGBA" else "JPEG"
113
106
  mime = f"image/{format.lower()}"
114
- object.save(buffer, format=format)
107
+ obj.save(buffer, format=format)
115
108
  data = self.__upload(buffer, name=name, mime=mime, max_data_url_size=max_data_url_size)
116
109
  return RemoteValue(data=data, type=Dtype.image)
117
- elif isinstance(object, BytesIO):
118
- data = self.__upload(object, name=name, max_data_url_size=max_data_url_size)
110
+ elif isinstance(obj, BytesIO):
111
+ data = self.__upload(obj, name=name, max_data_url_size=max_data_url_size)
119
112
  return RemoteValue(data=data, type=Dtype.binary)
120
113
  else:
121
- raise ValueError(f"Failed to serialize value '{object}' of type `{type(object)}` because it is not supported")
114
+ raise ValueError(f"Failed to serialize value '{obj}' of type `{type(obj)}` because it is not supported")
122
115
 
123
116
  def __to_object (self, value: RemoteValue) -> Value:
124
117
  if value.type == Dtype.null:
@@ -177,16 +170,16 @@ class RemotePredictionService:
177
170
  return result
178
171
 
179
172
  @classmethod
180
- def __try_ensure_serializable (cls, object: Any) -> Any:
181
- if object is None:
182
- return object
183
- if isinstance(object, list):
184
- return [cls.__try_ensure_serializable(x) for x in object]
185
- if is_dataclass(object) and not isinstance(object, type):
186
- return asdict(object)
187
- if isinstance(object, BaseModel):
188
- return object.model_dump(mode="json", by_alias=True)
189
- return object
173
+ def __try_ensure_serializable (cls, obj: object) -> object:
174
+ if obj is None:
175
+ return obj
176
+ if isinstance(obj, list):
177
+ return [cls.__try_ensure_serializable(x) for x in obj]
178
+ if is_dataclass(obj) and not isinstance(obj, type):
179
+ return asdict(obj)
180
+ if isinstance(obj, BaseModel):
181
+ return obj.model_dump(mode="json", by_alias=True)
182
+ return obj
190
183
 
191
184
  class RemoteValue (BaseModel):
192
185
  data: str | None
fxn/c/configuration.py CHANGED
@@ -16,15 +16,18 @@ class Configuration:
16
16
  def __init__ (self):
17
17
  configuration = c_void_p()
18
18
  status = get_fxnc().FXNConfigurationCreate(byref(configuration))
19
- if status == FXNStatus.OK:
20
- self.__configuration = configuration
21
- else:
19
+ if status != FXNStatus.OK:
22
20
  raise RuntimeError(f"Failed to create configuration with error: {status_to_error(status)}")
21
+ self.__configuration = configuration
23
22
 
24
23
  @property
25
24
  def tag (self) -> str:
26
25
  buffer = create_string_buffer(2048)
27
- status = get_fxnc().FXNConfigurationGetTag(self.__configuration, buffer, len(buffer))
26
+ status = get_fxnc().FXNConfigurationGetTag(
27
+ self.__configuration,
28
+ buffer,
29
+ len(buffer)
30
+ )
28
31
  if status != FXNStatus.OK:
29
32
  raise RuntimeError(f"Failed to get configuration tag with error: {status_to_error(status)}")
30
33
  tag = buffer.value.decode("utf-8")
@@ -40,7 +43,11 @@ class Configuration:
40
43
  @property
41
44
  def token (self) -> str:
42
45
  buffer = create_string_buffer(2048)
43
- status = get_fxnc().FXNConfigurationGetToken(self.__configuration, buffer, len(buffer))
46
+ status = get_fxnc().FXNConfigurationGetToken(
47
+ self.__configuration,
48
+ buffer,
49
+ len(buffer)
50
+ )
44
51
  if status != FXNStatus.OK:
45
52
  raise RuntimeError(f"Failed to get configuration token with error: {status_to_error(status)}")
46
53
  token = buffer.value.decode("utf-8")
@@ -56,26 +63,33 @@ class Configuration:
56
63
  @property
57
64
  def acceleration (self) -> Acceleration:
58
65
  acceleration = c_int()
59
- status = get_fxnc().FXNConfigurationGetAcceleration(self.__configuration, byref(acceleration))
60
- if status == FXNStatus.OK:
61
- return Acceleration(acceleration.value)
62
- else:
66
+ status = get_fxnc().FXNConfigurationGetAcceleration(
67
+ self.__configuration,
68
+ byref(acceleration)
69
+ )
70
+ if status != FXNStatus.OK:
63
71
  raise RuntimeError(f"Failed to get configuration acceleration with error: {status_to_error(status)}")
72
+ return self.__to_acceleration_str(acceleration.value)
64
73
 
65
74
  @acceleration.setter
66
75
  def acceleration (self, acceleration: Acceleration):
67
- status = get_fxnc().FXNConfigurationSetAcceleration(self.__configuration, acceleration.value)
76
+ status = get_fxnc().FXNConfigurationSetAcceleration(
77
+ self.__configuration,
78
+ self.__to_acceleration_int(acceleration)
79
+ )
68
80
  if status != FXNStatus.OK:
69
81
  raise RuntimeError(f"Failed to set configuration acceleration with error: {status_to_error(status)}")
70
82
 
71
83
  @property
72
84
  def device (self):
73
85
  device = c_void_p()
74
- status = get_fxnc().FXNConfigurationGetDevice(self.__configuration, byref(device))
75
- if status == FXNStatus.OK:
76
- return device if device.value else None
77
- else:
86
+ status = get_fxnc().FXNConfigurationGetDevice(
87
+ self.__configuration,
88
+ byref(device)
89
+ )
90
+ if status != FXNStatus.OK:
78
91
  raise RuntimeError(f"Failed to get configuration device with error: {status_to_error(status)}")
92
+ return device if device.value else None
79
93
 
80
94
  @device.setter
81
95
  def device (self, device):
@@ -84,29 +98,21 @@ class Configuration:
84
98
  raise RuntimeError(f"Failed to set configuration device with error: {status_to_error(status)}")
85
99
 
86
100
  def add_resource (self, type: str, path: Path):
87
- status = get_fxnc().FXNConfigurationAddResource(self.__configuration, type.encode(), str(path).encode())
101
+ status = get_fxnc().FXNConfigurationAddResource(
102
+ self.__configuration,
103
+ type.encode(),
104
+ str(path).encode()
105
+ )
88
106
  if status != FXNStatus.OK:
89
107
  raise RuntimeError(f"Failed to add configuration resource with error: {status_to_error(status)}")
90
108
 
91
- def __enter__ (self):
92
- return self
93
-
94
- def __exit__ (self, exc_type, exc_value, traceback):
95
- self.__release()
96
-
97
- def __release (self):
98
- if self.__configuration:
99
- get_fxnc().FXNConfigurationRelease(self.__configuration)
100
- self.__configuration = None
101
-
102
109
  @classmethod
103
110
  def get_unique_id (cls) -> str:
104
111
  buffer = create_string_buffer(2048)
105
112
  status = get_fxnc().FXNConfigurationGetUniqueID(buffer, len(buffer))
106
- if status == FXNStatus.OK:
107
- return buffer.value.decode("utf-8")
108
- else:
113
+ if status != FXNStatus.OK:
109
114
  raise RuntimeError(f"Failed to retrieve configuration identifier with error: {status_to_error(status)}")
115
+ return buffer.value.decode("utf-8")
110
116
 
111
117
  @classmethod
112
118
  def get_client_id (cls) -> str:
@@ -115,4 +121,30 @@ class Configuration:
115
121
  if status == FXNStatus.OK:
116
122
  return buffer.value.decode("utf-8")
117
123
  else:
118
- raise RuntimeError(f"Failed to retrieve client identifier with error: {status_to_error(status)}")
124
+ raise RuntimeError(f"Failed to retrieve client identifier with error: {status_to_error(status)}")
125
+
126
+ def __enter__ (self):
127
+ return self
128
+
129
+ def __exit__ (self, exc_type, exc_value, traceback):
130
+ self.__release()
131
+
132
+ def __release (self):
133
+ if self.__configuration:
134
+ get_fxnc().FXNConfigurationRelease(self.__configuration)
135
+ self.__configuration = None
136
+
137
+ def __to_acceleration_int (self, value: Acceleration) -> int:
138
+ match value:
139
+ case "auto": return 0
140
+ case "cpu": return 1
141
+ case "gpu": return 2
142
+ case "npu": return 4
143
+
144
+ def __to_acceleration_str (self, value: int) -> Acceleration:
145
+ match value:
146
+ case 0: return "auto"
147
+ case 1: return "cpu"
148
+ case 2: return "gpu"
149
+ case 4: return "npu"
150
+ case _: return None
fxn/c/value.py CHANGED
@@ -48,7 +48,7 @@ class Value:
48
48
 
49
49
  @property
50
50
  def shape (self) -> list[int] | None:
51
- if self.type not in _TENSOR_DTYPES:
51
+ if self.type not in _TENSOR_ISH_DTYPES:
52
52
  return None
53
53
  fxnc = get_fxnc()
54
54
  dims = c_int32()
@@ -220,4 +220,5 @@ _TENSOR_DTYPES = {
220
220
  Dtype.uint32,
221
221
  Dtype.uint64,
222
222
  Dtype.bool,
223
- }
223
+ }
224
+ _TENSOR_ISH_DTYPES = _TENSOR_DTYPES | { Dtype.image }
fxn/cli/__init__.py CHANGED
@@ -3,17 +3,19 @@
3
3
  # Copyright © 2025 NatML Inc. All Rights Reserved.
4
4
  #
5
5
 
6
- from typer import Typer
6
+ import typer
7
7
 
8
8
  from .auth import app as auth_app
9
- #from .compile import compile_predictor
9
+ from .compile import compile_predictor
10
10
  from .misc import cli_options
11
11
  from .predictions import create_prediction
12
- from .predictors import retrieve_predictor
12
+ from .predictors import archive_predictor, delete_predictor, retrieve_predictor
13
+ from ..logging import TracebackMarkupConsole
13
14
  from ..version import __version__
14
15
 
15
16
  # Define CLI
16
- app = Typer(
17
+ typer.main.console_stderr = TracebackMarkupConsole()
18
+ app = typer.Typer(
17
19
  name=f"Function CLI {__version__}",
18
20
  no_args_is_help=True,
19
21
  pretty_exceptions_show_locals=False,
@@ -33,11 +35,13 @@ app.command(
33
35
  help="Make a prediction.",
34
36
  context_settings={ "allow_extra_args": True, "ignore_unknown_options": True }
35
37
  )(create_prediction)
36
- # app.command(
37
- # name="compile",
38
- # help="Create a predictor by compiling a Python function."
39
- # )(compile_predictor)
38
+ app.command(
39
+ name="compile",
40
+ help="Create a predictor by compiling a Python function."
41
+ )(compile_predictor)
40
42
  app.command(name="retrieve", help="Retrieve a predictor.")(retrieve_predictor)
43
+ app.command(name="archive", help="Archive a predictor.")(archive_predictor)
44
+ app.command(name="delete", help="Delete a predictor.")(delete_predictor)
41
45
 
42
46
  # Run
43
47
  if __name__ == "__main__":
fxn/cli/compile.py ADDED
@@ -0,0 +1,147 @@
1
+ #
2
+ # Function
3
+ # Copyright © 2025 NatML Inc. All Rights Reserved.
4
+ #
5
+
6
+ from asyncio import run as run_async
7
+ from importlib.util import module_from_spec, spec_from_file_location
8
+ from inspect import getmembers, getmodulename, isfunction
9
+ from pathlib import Path
10
+ from pydantic import BaseModel
11
+ from rich import print as print_rich
12
+ from rich.progress import SpinnerColumn, TextColumn
13
+ import sys
14
+ from typer import Argument, Option
15
+ from typing import Callable, Literal
16
+ from urllib.parse import urlparse, urlunparse
17
+
18
+ from ..compile import PredictorSpec
19
+ from ..function import Function
20
+ from ..sandbox import EntrypointCommand
21
+ from ..logging import CustomProgress, CustomProgressTask
22
+ from .auth import get_access_key
23
+
24
+ class CompileError (Exception):
25
+ pass
26
+
27
+ def compile_predictor (
28
+ path: str=Argument(..., help="Predictor path.")
29
+ ):
30
+ run_async(_compile_predictor_async(path))
31
+
32
+ async def _compile_predictor_async (path: str):
33
+ fxn = Function(get_access_key())
34
+ path: Path = Path(path).resolve()
35
+ with CustomProgress():
36
+ # Load
37
+ with CustomProgressTask(loading_text="Loading predictor...") as task:
38
+ func = _load_predictor_func(path)
39
+ entrypoint = EntrypointCommand(from_path=str(path), to_path="./", name=func.__name__)
40
+ spec: PredictorSpec = func.__predictor_spec
41
+ task.finish(f"Loaded prediction function: [bold cyan]{spec.tag}[/bold cyan]")
42
+ # Populate
43
+ sandbox = spec.sandbox
44
+ sandbox.commands.append(entrypoint)
45
+ with CustomProgressTask(loading_text="Uploading sandbox...", done_text="Uploaded sandbox"):
46
+ sandbox.populate(fxn=fxn)
47
+ # Compile
48
+ with CustomProgressTask(loading_text="Running codegen...", done_text="Completed codegen"):
49
+ with CustomProgressTask(loading_text="Creating predictor..."):
50
+ predictor = fxn.client.request(
51
+ method="POST",
52
+ path="/predictors",
53
+ body=spec.model_dump(mode="json", exclude=spec.model_extra.keys(), by_alias=True),
54
+ response_type=_Predictor
55
+ )
56
+ with ProgressLogQueue() as task_queue:
57
+ async for event in fxn.client.stream(
58
+ method="POST",
59
+ path=f"/predictors/{predictor.tag}/compile",
60
+ body={ },
61
+ response_type=_LogEvent | _ErrorEvent
62
+ ):
63
+ if isinstance(event, _LogEvent):
64
+ task_queue.push_log(event)
65
+ elif isinstance(event, _ErrorEvent):
66
+ task_queue.push_error(event)
67
+ raise CompileError(event.data.error)
68
+ predictor_url = _compute_predictor_url(fxn.client.api_url, spec.tag)
69
+ print_rich(f"\n[bold spring_green3]🎉 Predictor is now being compiled.[/bold spring_green3] Check it out at [link={predictor_url}]{predictor_url}[/link]")
70
+
71
+ def _load_predictor_func (path: str) -> Callable[...,object]:
72
+ if "" not in sys.path:
73
+ sys.path.insert(0, "")
74
+ path: Path = Path(path).resolve()
75
+ sys.path.insert(0, str(path.parent))
76
+ name = getmodulename(path)
77
+ spec = spec_from_file_location(name, path)
78
+ module = module_from_spec(spec)
79
+ sys.modules[name] = module
80
+ spec.loader.exec_module(module)
81
+ main_func = next(func for _, func in getmembers(module, isfunction) if hasattr(func, "__predictor_spec"))
82
+ return main_func
83
+
84
+ def _compute_predictor_url (api_url: str, tag: str) -> str:
85
+ parsed_url = urlparse(api_url)
86
+ hostname_parts = parsed_url.hostname.split(".")
87
+ if hostname_parts[0] == "api":
88
+ hostname_parts.pop(0)
89
+ hostname = ".".join(hostname_parts)
90
+ netloc = hostname if not parsed_url.port else f"{hostname}:{parsed_url.port}"
91
+ predictor_url = urlunparse(parsed_url._replace(netloc=netloc, path=f"{tag}"))
92
+ return predictor_url
93
+
94
+ class _Predictor (BaseModel):
95
+ tag: str
96
+
97
+ class _LogData (BaseModel):
98
+ message: str
99
+ level: int = 0
100
+ status: Literal["success", "error"] = "success"
101
+ update: bool = False
102
+
103
+ class _LogEvent (BaseModel):
104
+ event: Literal["log"]
105
+ data: _LogData
106
+
107
+ class _ErrorData (BaseModel):
108
+ error: str
109
+
110
+ class _ErrorEvent (BaseModel):
111
+ event: Literal["error"]
112
+ data: _ErrorData
113
+
114
+ class ProgressLogQueue:
115
+
116
+ def __init__ (self):
117
+ self.queue: list[tuple[int, CustomProgressTask]] = []
118
+
119
+ def push_log (self, event: _LogEvent):
120
+ # Check for update
121
+ if event.data.update and self.queue:
122
+ current_level, current_task = self.queue[-1]
123
+ current_task.update(description=event.data.message, status=event.data.status)
124
+ return
125
+ # Pop
126
+ while self.queue:
127
+ current_level, current_task = self.queue[-1]
128
+ if event.data.level > current_level:
129
+ break
130
+ current_task.__exit__(None, None, None)
131
+ self.queue.pop()
132
+ task = CustomProgressTask(loading_text=event.data.message)
133
+ task.__enter__()
134
+ self.queue.append((event.data.level, task))
135
+
136
+ def push_error (self, error: _ErrorEvent):
137
+ while self.queue:
138
+ _, current_task = self.queue.pop()
139
+ current_task.__exit__(RuntimeError, None, None)
140
+
141
+ def __enter__ (self):
142
+ return self
143
+
144
+ def __exit__ (self, exc_type, exc_value, traceback):
145
+ while self.queue:
146
+ _, current_task = self.queue.pop()
147
+ current_task.__exit__(None, None, None)
fxn/cli/predictions.py CHANGED
@@ -9,11 +9,11 @@ from numpy import array_repr, ndarray
9
9
  from pathlib import Path, PurePath
10
10
  from PIL import Image
11
11
  from rich import print_json
12
- from rich.progress import Progress, SpinnerColumn, TextColumn
13
12
  from tempfile import mkstemp
14
13
  from typer import Argument, Context, Option
15
14
 
16
15
  from ..function import Function
16
+ from ..logging import CustomProgress, CustomProgressTask
17
17
  from ..types import Prediction
18
18
  from .auth import get_access_key
19
19
 
@@ -26,18 +26,21 @@ def create_prediction (
26
26
 
27
27
  async def _predict_async (tag: str, quiet: bool, context: Context):
28
28
  # Preload
29
- fxn = Function(get_access_key())
30
- fxn.predictions.create(tag, inputs={ }, verbose=not quiet)
31
- # Predict
32
- with Progress(
33
- SpinnerColumn(spinner_name="dots"),
34
- TextColumn("[progress.description]{task.description}"),
35
- transient=True
36
- ) as progress:
37
- progress.add_task(description="Running Function...", total=None)
38
- inputs = { context.args[i].replace("-", ""): _parse_value(context.args[i+1]) for i in range(0, len(context.args), 2) }
39
- prediction = fxn.predictions.create(tag, inputs=inputs)
40
- _log_prediction(prediction)
29
+ with CustomProgress(transient=True, disable=quiet):
30
+ fxn = Function(get_access_key())
31
+ with CustomProgressTask(
32
+ loading_text="Preloading predictor...",
33
+ done_text="Preloaded predictor"
34
+ ):
35
+ fxn.predictions.create(tag, inputs={ })
36
+ with CustomProgressTask(loading_text="Making prediction..."):
37
+ inputs = { }
38
+ for i in range(0, len(context.args), 2):
39
+ name = context.args[i].replace("-", "")
40
+ value = _parse_value(context.args[i+1])
41
+ inputs[name] = value
42
+ prediction = fxn.predictions.create(tag, inputs=inputs)
43
+ _log_prediction(prediction)
41
44
 
42
45
  def _parse_value (value: str):
43
46
  """
fxn/cli/predictors.py CHANGED
@@ -7,12 +7,43 @@ from rich import print_json
7
7
  from typer import Argument
8
8
 
9
9
  from ..function import Function
10
+ from ..logging import CustomProgress, CustomProgressTask
10
11
  from .auth import get_access_key
11
12
 
12
13
  def retrieve_predictor (
13
14
  tag: str=Argument(..., help="Predictor tag.")
14
15
  ):
15
- fxn = Function(get_access_key())
16
- predictor = fxn.predictors.retrieve(tag)
17
- predictor = predictor.model_dump() if predictor else None
18
- print_json(data=predictor)
16
+ with CustomProgress(transient=True):
17
+ with CustomProgressTask(loading_text="Retrieving predictor..."):
18
+ fxn = Function(get_access_key())
19
+ predictor = fxn.predictors.retrieve(tag)
20
+ predictor = predictor.model_dump() if predictor else None
21
+ print_json(data=predictor)
22
+
23
+ def archive_predictor (
24
+ tag: str=Argument(..., help="Predictor tag.")
25
+ ):
26
+ with CustomProgress():
27
+ with CustomProgressTask(
28
+ loading_text="Archiving predictor...",
29
+ done_text=f"Archived predictor: [bold dark_orange]{tag}[/bold dark_orange]"
30
+ ):
31
+ fxn = Function(get_access_key())
32
+ fxn.client.request(
33
+ method="POST",
34
+ path=f"/predictors/{tag}/archive"
35
+ )
36
+
37
+ def delete_predictor (
38
+ tag: str=Argument(..., help="Predictor tag.")
39
+ ):
40
+ with CustomProgress():
41
+ with CustomProgressTask(
42
+ loading_text="Deleting predictor...",
43
+ done_text=f"Deleted predictor: [bold red]{tag}[/bold red]"
44
+ ):
45
+ fxn = Function(get_access_key())
46
+ fxn.client.request(
47
+ method="DELETE",
48
+ path=f"/predictors/{tag}"
49
+ )