fxn 0.0.40__py3-none-any.whl → 0.0.41__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/c/value.py CHANGED
@@ -3,48 +3,221 @@
3
3
  # Copyright © 2024 NatML Inc. All Rights Reserved.
4
4
  #
5
5
 
6
- from ctypes import c_char_p, c_int, c_int32, c_void_p, CDLL, POINTER, Structure
7
- from .dtype import FXNDtype
8
- from .status import FXNStatus
6
+ from __future__ import annotations
7
+ from collections.abc import Iterable
8
+ from enum import IntFlag
9
+ from ctypes import byref, cast, c_char_p, c_int, c_int32, c_uint8, c_void_p, string_at, POINTER
10
+ from io import BytesIO
11
+ from json import dumps, loads
12
+ from numpy import array, dtype, int32, ndarray, zeros
13
+ from numpy.ctypeslib import as_array, as_ctypes_type
14
+ from PIL import Image
15
+ from typing import final, Any
9
16
 
10
- class FXNValueFlags(c_int):
17
+ from ..types import Dtype
18
+ from .fxnc import get_fxnc, status_to_error, FXNStatus
19
+
20
+ class ValueFlags (IntFlag):
11
21
  NONE = 0
12
22
  COPY_DATA = 1
13
23
 
14
- class FXNValue(Structure): pass
15
-
16
- FXNValueRef = POINTER(FXNValue)
17
-
18
- def _register_fxn_value (fxnc: CDLL) -> CDLL:
19
- # FXNValueRelease
20
- fxnc.FXNValueRelease.argtypes = [FXNValueRef]
21
- fxnc.FXNValueRelease.restype = FXNStatus
22
- # FXNValueGetData
23
- fxnc.FXNValueGetData.argtypes = [FXNValueRef, POINTER(c_void_p)]
24
- fxnc.FXNValueGetData.restype = FXNStatus
25
- # FXNValueGetType
26
- fxnc.FXNValueGetType.argtypes = [FXNValueRef, POINTER(FXNDtype)]
27
- fxnc.FXNValueGetType.restype = FXNStatus
28
- # FXNValueGetDimensions
29
- fxnc.FXNValueGetDimensions.argtypes = [FXNValueRef, POINTER(c_int32)]
30
- fxnc.FXNValueGetDimensions.restype = FXNStatus
31
- # FXNValueGetShape
32
- fxnc.FXNValueGetShape.argtypes = [FXNValueRef, POINTER(c_int32), c_int32]
33
- fxnc.FXNValueGetShape.restype = FXNStatus
34
- # FXNValueCreateArray
35
- fxnc.FXNValueCreateArray.argtypes = [c_void_p, POINTER(c_int32), c_int32, FXNDtype, FXNValueFlags, POINTER(FXNValueRef)]
36
- fxnc.FXNValueCreateArray.restype = FXNStatus
37
- # FXNValueCreateString
38
- fxnc.FXNValueCreateString.argtypes = [c_char_p, POINTER(FXNValueRef)]
39
- fxnc.FXNValueCreateString.restype = FXNStatus
40
- # FXNValueCreateList
41
- fxnc.FXNValueCreateList.argtypes = [c_char_p, POINTER(FXNValueRef)]
42
- fxnc.FXNValueCreateList.restype = FXNStatus
43
- # FXNValueCreateDict
44
- fxnc.FXNValueCreateDict.argtypes = [c_char_p, POINTER(FXNValueRef)]
45
- fxnc.FXNValueCreateDict.restype = FXNStatus
46
- # FXNValueCreateImage
47
- fxnc.FXNValueCreateImage.argtypes = [c_void_p, c_int32, c_int32, c_int32, FXNValueFlags, POINTER(FXNValueRef)]
48
- fxnc.FXNValueCreateImage.restype = FXNStatus
49
- # Return
50
- return fxnc
24
+ @final
25
+ class Value:
26
+
27
+ def __init__ (self, value, *, owner: bool=True):
28
+ self.__value = value
29
+ self.__owner = owner
30
+
31
+ @property
32
+ def data (self):
33
+ data = c_void_p()
34
+ status = get_fxnc().FXNValueGetData(self.__value, byref(data))
35
+ if status == FXNStatus.OK:
36
+ return data
37
+ else:
38
+ raise RuntimeError(f"Failed to get value data with error: {status_to_error(status)}")
39
+
40
+ @property
41
+ def type (self) -> Dtype:
42
+ dtype = c_int()
43
+ status = get_fxnc().FXNValueGetType(self.__value, byref(dtype))
44
+ if status == FXNStatus.OK:
45
+ return _DTYPE_TO_STR.get(dtype.value)
46
+ else:
47
+ raise RuntimeError(f"Failed to get value data type with error: {status_to_error(status)}")
48
+
49
+ @property
50
+ def shape (self) -> list[int] | None:
51
+ if self.type not in _TENSOR_DTYPES:
52
+ return None
53
+ fxnc = get_fxnc()
54
+ dims = c_int32()
55
+ status = fxnc.FXNValueGetDimensions(self.__value, byref(dims))
56
+ if status != FXNStatus.OK:
57
+ raise RuntimeError(f"Failed to get value dimensions with error: {status_to_error(status)}")
58
+ shape = zeros(dims.value, dtype=int32)
59
+ status = fxnc.FXNValueGetShape(self.__value, shape.ctypes.data_as(POINTER(c_int32)), dims)
60
+ if status == FXNStatus.OK:
61
+ return shape.tolist()
62
+ else:
63
+ raise RuntimeError(f"Failed to get value shape with error: {status_to_error(status)}")
64
+
65
+ def to_object (self) -> Any:
66
+ type = self.type
67
+ if type == Dtype.null:
68
+ return None
69
+ elif type in _TENSOR_DTYPES:
70
+ ctype = as_ctypes_type(dtype(type))
71
+ tensor = as_array(cast(self.data, POINTER(ctype)), self.shape)
72
+ return tensor.item() if len(tensor.shape) == 0 else tensor.copy()
73
+ elif type == Dtype.string:
74
+ return cast(self.data, c_char_p).value.decode()
75
+ elif type in [Dtype.list, Dtype.dict]:
76
+ return loads(cast(self.data, c_char_p).value.decode())
77
+ elif type == Dtype.image:
78
+ pixel_buffer = as_array(cast(self.data, POINTER(c_uint8)), self.shape)
79
+ return Image.fromarray(pixel_buffer.squeeze()).copy()
80
+ elif type == Dtype.binary:
81
+ return BytesIO(string_at(self.data, self.shape[0]))
82
+ else:
83
+ raise RuntimeError(f"Failed to convert Function value to object because value has unsupported type: {type}")
84
+
85
+ def __enter__ (self):
86
+ return self
87
+
88
+ def __exit__ (self, exc_type, exc_value, traceback):
89
+ self.__release()
90
+
91
+ def __release (self):
92
+ if self.__value and self.__owner:
93
+ get_fxnc().FXNValueRelease(self.__value)
94
+ self.__value = None
95
+
96
+ @classmethod
97
+ def create_array (
98
+ cls,
99
+ data: ndarray,
100
+ *,
101
+ flags: ValueFlags=ValueFlags.NONE
102
+ ) -> Value:
103
+ dtype = _STR_TO_DTYPE.get(data.dtype.name)
104
+ if dtype is None:
105
+ raise RuntimeError(f"Failed to create array value because data type is not supported: {data.dtype}")
106
+ value = c_void_p()
107
+ status = get_fxnc().FXNValueCreateArray(
108
+ data.ctypes.data_as(c_void_p),
109
+ data.ctypes.shape_as(c_int32),
110
+ len(data.shape),
111
+ dtype,
112
+ flags,
113
+ byref(value)
114
+ )
115
+ if status == FXNStatus.OK:
116
+ return Value(value)
117
+ else:
118
+ raise RuntimeError(f"Failed to create array value with error: {status_to_error(status)}")
119
+
120
+ @classmethod
121
+ def create_string (cls, data: str) -> Value:
122
+ value = c_void_p()
123
+ status = get_fxnc().FXNValueCreateString(data.encode(), byref(value))
124
+ if status == FXNStatus.OK:
125
+ return Value(value)
126
+ else:
127
+ raise RuntimeError(f"Failed to create string value with error: {status_to_error(status)}")
128
+
129
+ @classmethod
130
+ def create_list (cls, data: Iterable[Any]) -> Value:
131
+ value = c_void_p()
132
+ status = get_fxnc().FXNValueCreateList(dumps(data).encode(), byref(value))
133
+ if status == FXNStatus.OK:
134
+ return Value(value)
135
+ else:
136
+ raise RuntimeError(f"Failed to create list value with error: {status_to_error(status)}")
137
+
138
+ @classmethod
139
+ def create_dict (cls, data: dict[str, Any]) -> Value:
140
+ value = c_void_p()
141
+ status = get_fxnc().FXNValueCreateDict(dumps(data).encode(), byref(value))
142
+ if status == FXNStatus.OK:
143
+ return Value(value)
144
+ else:
145
+ raise RuntimeError(f"Failed to create dict value with error: {status_to_error(status)}")
146
+
147
+ @classmethod
148
+ def create_image (cls, image: Image.Image) -> Value:
149
+ value = c_void_p()
150
+ pixel_buffer = array(image)
151
+ status = get_fxnc().FXNValueCreateImage(
152
+ pixel_buffer.ctypes.data_as(c_void_p),
153
+ image.width,
154
+ image.height,
155
+ pixel_buffer.shape[2],
156
+ ValueFlags.COPY_DATA,
157
+ byref(value)
158
+ )
159
+ if status == FXNStatus.OK:
160
+ return Value(value)
161
+ else:
162
+ raise RuntimeError(f"Failed to create image value with error: {status_to_error(status)}")
163
+
164
+ @classmethod
165
+ def create_binary (
166
+ cls,
167
+ data: memoryview,
168
+ *,
169
+ flags: ValueFlags=ValueFlags.NONE
170
+ ) -> Value:
171
+ buffer = (c_uint8 * len(data)).from_buffer(data)
172
+ value = c_void_p()
173
+ status = get_fxnc().FXNValueCreateBinary(buffer, len(data), flags, byref(value))
174
+ if status == FXNStatus.OK:
175
+ return Value(value)
176
+ else:
177
+ raise RuntimeError(f"Failed to create binary value with error: {status_to_error(status)}")
178
+
179
+ @classmethod
180
+ def create_null (cls) -> Value:
181
+ value = c_void_p()
182
+ status = get_fxnc().FXNValueCreateNull(byref(value))
183
+ if status == FXNStatus.OK:
184
+ return Value(value)
185
+ else:
186
+ raise RuntimeError(f"Failed to create null value with error: {status_to_error(status)}")
187
+
188
+
189
+ _STR_TO_DTYPE = {
190
+ Dtype.null: 0,
191
+ Dtype.float16: 1,
192
+ Dtype.float32: 2,
193
+ Dtype.float64: 3,
194
+ Dtype.int8: 4,
195
+ Dtype.int16: 5,
196
+ Dtype.int32: 6,
197
+ Dtype.int64: 7,
198
+ Dtype.uint8: 8,
199
+ Dtype.uint16: 9,
200
+ Dtype.uint32: 10,
201
+ Dtype.uint64: 11,
202
+ Dtype.bool: 12,
203
+ Dtype.string: 13,
204
+ Dtype.list: 14,
205
+ Dtype.dict: 15,
206
+ Dtype.image: 16,
207
+ Dtype.binary: 17,
208
+ }
209
+ _DTYPE_TO_STR = { value: key for key, value in _STR_TO_DTYPE.items() }
210
+ _TENSOR_DTYPES = {
211
+ Dtype.float16,
212
+ Dtype.float32,
213
+ Dtype.float64,
214
+ Dtype.int8,
215
+ Dtype.int16,
216
+ Dtype.int32,
217
+ Dtype.int64,
218
+ Dtype.uint8,
219
+ Dtype.uint16,
220
+ Dtype.uint32,
221
+ Dtype.uint64,
222
+ Dtype.bool,
223
+ }
fxn/cli/__init__.py CHANGED
@@ -8,7 +8,7 @@ from typer import Typer
8
8
  from .auth import app as auth_app
9
9
  from .env import app as env_app
10
10
  from .misc import cli_options
11
- from .predict import predict
11
+ from .predictions import create_prediction
12
12
  from .predictors import archive_predictor, delete_predictor, list_predictors, retrieve_predictor, search_predictors
13
13
  from ..version import __version__
14
14
 
@@ -31,7 +31,11 @@ app.add_typer(auth_app, name="auth", help="Login, logout, and check your authent
31
31
  # Add top-level commands
32
32
  #app.command(name="create", help="Create a predictor.")(create_predictor)
33
33
  #app.command(name="delete", help="Delete a predictor.")(delete_predictor)
34
- app.command(name="predict", help="Make a prediction.", context_settings={ "allow_extra_args": True, "ignore_unknown_options": True })(predict)
34
+ app.command(
35
+ name="predict",
36
+ help="Make a prediction.",
37
+ context_settings={ "allow_extra_args": True, "ignore_unknown_options": True }
38
+ )(create_prediction)
35
39
  #app.command(name="list", help="List predictors.")(list_predictors)
36
40
  #app.command(name="search", help="Search predictors.")(search_predictors)
37
41
  #app.command(name="retrieve", help="Retrieve a predictor.")(retrieve_predictor)
@@ -5,7 +5,7 @@
5
5
 
6
6
  from asyncio import run as run_async
7
7
  from io import BytesIO
8
- from numpy import ndarray
8
+ 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
@@ -14,34 +14,30 @@ from tempfile import mkstemp
14
14
  from typer import Argument, Context, Option
15
15
 
16
16
  from ..function import Function
17
+ from ..types import Prediction
17
18
  from .auth import get_access_key
18
19
 
19
- def predict (
20
- tag: str = Argument(..., help="Predictor tag."),
20
+ def create_prediction (
21
+ tag: str=Argument(..., help="Predictor tag."),
22
+ quiet: bool=Option(False, "--quiet", help="Suppress verbose logging when creating the prediction."),
21
23
  context: Context = 0
22
24
  ):
23
- run_async(_predict_async(tag, context=context))
25
+ run_async(_predict_async(tag, quiet=quiet, context=context))
24
26
 
25
- async def _predict_async (tag: str, context: Context):
27
+ async def _predict_async (tag: str, quiet: bool, context: Context):
28
+ # Preload
29
+ fxn = Function(get_access_key())
30
+ fxn.predictions.create(tag, inputs={ }, verbose=not quiet)
31
+ # Predict
26
32
  with Progress(
27
33
  SpinnerColumn(spinner_name="dots"),
28
34
  TextColumn("[progress.description]{task.description}"),
29
35
  transient=True
30
36
  ) as progress:
31
37
  progress.add_task(description="Running Function...", total=None)
32
- # Parse inputs
33
38
  inputs = { context.args[i].replace("-", ""): _parse_value(context.args[i+1]) for i in range(0, len(context.args), 2) }
34
- # Stream
35
- fxn = Function(get_access_key())
36
- async for prediction in fxn.predictions.stream(tag, inputs=inputs):
37
- # Parse results
38
- images = [value for value in prediction.results or [] if isinstance(value, Image.Image)]
39
- prediction.results = [_serialize_value(value) for value in prediction.results] if prediction.results is not None else None
40
- # Print
41
- print_json(data=prediction.model_dump())
42
- # Show images
43
- for image in images:
44
- image.show()
39
+ prediction = fxn.predictions.create(tag, inputs=inputs)
40
+ _log_prediction(prediction)
45
41
 
46
42
  def _parse_value (value: str):
47
43
  """
@@ -70,33 +66,34 @@ def _parse_value (value: str):
70
66
  pass
71
67
  # File
72
68
  if value.startswith("@"):
73
- return Path(value[1:])
69
+ path = Path(value[1:]).expanduser().resolve()
70
+ if path.suffix in [".txt", ".md"]:
71
+ with open(path) as f:
72
+ return f.read()
73
+ elif path.suffix in [".jpg", ".png"]:
74
+ return Image.open(path)
75
+ else:
76
+ with open(path, "rb") as f:
77
+ return BytesIO(f.read())
74
78
  # String
75
79
  return value
76
-
80
+
81
+ def _log_prediction (prediction: Prediction):
82
+ images = [value for value in prediction.results or [] if isinstance(value, Image.Image)]
83
+ prediction.results = [_serialize_value(value) for value in prediction.results] if prediction.results is not None else None
84
+ print_json(data=prediction.model_dump())
85
+ for image in images:
86
+ image.show()
87
+
77
88
  def _serialize_value (value):
78
- # Convert ndarray to list
79
89
  if isinstance(value, ndarray):
80
- return value.tolist()
81
- # Write image
90
+ return array_repr(value)
82
91
  if isinstance(value, Image.Image):
83
92
  _, path = mkstemp(suffix=".png" if value.mode == "RGBA" else ".jpg")
84
93
  value.save(path)
85
94
  return path
86
- # Serialize `BytesIO`
87
95
  if isinstance(value, BytesIO):
88
96
  return str(value)
89
- # Serialize `Path`
90
97
  if isinstance(value, PurePath):
91
98
  return str(value)
92
- # Return
93
- return value
94
-
95
- def _prediction_dict_factory (kv_pairs):
96
- # Check if value
97
- VALUE_KEYS = ["data", "type", "shape"]
98
- keys = [k for k, _ in kv_pairs]
99
- is_value = all(k in keys for k in VALUE_KEYS)
100
- kv_pairs = [(k, v) for k, v in kv_pairs if v is not None] if is_value else kv_pairs
101
- # Construct
102
- return dict(kv_pairs)
99
+ return value
fxn/client.py ADDED
@@ -0,0 +1,46 @@
1
+ #
2
+ # Function
3
+ # Copyright © 2024 NatML Inc. All Rights Reserved.
4
+ #
5
+
6
+ from requests import request
7
+ from typing import Any, Literal
8
+
9
+ class FunctionClient:
10
+
11
+ def __init__(self, access_key: str, api_url: str | None) -> None:
12
+ self.access_key = access_key
13
+ self.api_url = api_url or "https://api.fxn.ai/v1"
14
+
15
+ def request (
16
+ self,
17
+ *,
18
+ method: Literal["GET", "POST", "DELETE"],
19
+ path: str,
20
+ body: dict[str, Any]=None
21
+ ) -> dict[str, Any] | list[Any]:
22
+ response = request(
23
+ method=method,
24
+ url=f"{self.api_url}{path}",
25
+ json=body,
26
+ headers={ "Authorization": f"Bearer {self.access_key}" }
27
+ )
28
+ data = None
29
+ try:
30
+ data = response.json()
31
+ except Exception as ex:
32
+ raise FunctionAPIError(str(ex), response.status_code)
33
+ if not response.ok:
34
+ error = data["errors"][0]["message"] if "errors" in data else str(ex)
35
+ raise FunctionAPIError(error, response.status_code)
36
+ return data
37
+
38
+ class FunctionAPIError (Exception):
39
+
40
+ def __init__(self, message: str, status_code: int):
41
+ super().__init__(message)
42
+ self.message = message
43
+ self.status_code = status_code
44
+
45
+ def __str__(self):
46
+ return f"FunctionAPIError: {self.message} (Status Code: {self.status_code})"
fxn/function.py CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  from os import environ
7
7
 
8
- from .api import GraphClient
8
+ from .client import FunctionClient
9
9
  from .services import PredictionService, PredictorService, UserService
10
10
 
11
11
  class Function:
@@ -22,15 +22,15 @@ class Function:
22
22
  access_key (str): Function access key.
23
23
  api_url (str): Function API URL.
24
24
  """
25
- client: GraphClient
25
+ client: FunctionClient
26
26
  users: UserService
27
27
  predictors: PredictorService
28
28
  predictions: PredictionService
29
29
 
30
30
  def __init__ (self, access_key: str=None, api_url: str=None):
31
31
  access_key = access_key or environ.get("FXN_ACCESS_KEY", None)
32
- api_url = api_url or environ.get("FXN_API_URL", "https://api.fxn.ai")
33
- self.client = GraphClient(access_key, api_url)
32
+ api_url = api_url or environ.get("FXN_API_URL")
33
+ self.client = FunctionClient(access_key, api_url)
34
34
  self.users = UserService(self.client)
35
35
  self.predictors = PredictorService(self.client)
36
36
  self.predictions = PredictionService(self.client)
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
fxn/services/__init__.py CHANGED
@@ -3,6 +3,6 @@
3
3
  # Copyright © 2024 NatML Inc. All Rights Reserved.
4
4
  #
5
5
 
6
- from .user import UserService, PROFILE_FIELDS, USER_FIELDS
7
- from .predictor import PredictorService, PREDICTOR_FIELDS
8
- from .prediction import PredictionService, PREDICTION_FIELDS
6
+ from .user import UserService
7
+ from .predictor import PredictorService
8
+ from .prediction import PredictionService, Value