fxn 0.0.40__tar.gz → 0.0.42__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.
Files changed (83) hide show
  1. {fxn-0.0.40 → fxn-0.0.42}/PKG-INFO +4 -4
  2. {fxn-0.0.40 → fxn-0.0.42}/README.md +2 -2
  3. fxn-0.0.42/fxn/__init__.py +10 -0
  4. fxn-0.0.42/fxn/beta/__init__.py +6 -0
  5. fxn-0.0.42/fxn/beta/client.py +16 -0
  6. fxn-0.0.42/fxn/beta/prediction.py +16 -0
  7. fxn-0.0.42/fxn/beta/remote.py +207 -0
  8. fxn-0.0.42/fxn/c/__init__.py +13 -0
  9. fxn-0.0.42/fxn/c/configuration.py +118 -0
  10. fxn-0.0.42/fxn/c/fxnc.py +48 -0
  11. fxn-0.0.42/fxn/c/map.py +64 -0
  12. fxn-0.0.42/fxn/c/prediction.py +76 -0
  13. fxn-0.0.42/fxn/c/predictor.py +59 -0
  14. fxn-0.0.42/fxn/c/stream.py +40 -0
  15. fxn-0.0.42/fxn/c/value.py +223 -0
  16. fxn-0.0.42/fxn/cli/__init__.py +44 -0
  17. {fxn-0.0.40 → fxn-0.0.42}/fxn/cli/auth.py +1 -1
  18. {fxn-0.0.40 → fxn-0.0.42}/fxn/cli/misc.py +1 -1
  19. fxn-0.0.40/fxn/cli/predict.py → fxn-0.0.42/fxn/cli/predictions.py +33 -36
  20. fxn-0.0.42/fxn/cli/predictors.py +18 -0
  21. fxn-0.0.42/fxn/client.py +58 -0
  22. fxn-0.0.42/fxn/compile/__init__.py +7 -0
  23. fxn-0.0.42/fxn/compile/compile.py +80 -0
  24. fxn-0.0.42/fxn/compile/sandbox.py +177 -0
  25. fxn-0.0.42/fxn/compile/signature.py +183 -0
  26. {fxn-0.0.40 → fxn-0.0.42}/fxn/function.py +10 -6
  27. fxn-0.0.42/fxn/lib/__init__.py +4 -0
  28. fxn-0.0.42/fxn/lib/linux/arm64/libFunction.so +0 -0
  29. fxn-0.0.42/fxn/lib/linux/x86_64/libFunction.so +0 -0
  30. fxn-0.0.42/fxn/lib/macos/arm64/Function.dylib +0 -0
  31. fxn-0.0.42/fxn/lib/macos/x86_64/Function.dylib +0 -0
  32. fxn-0.0.42/fxn/lib/windows/arm64/Function.dll +0 -0
  33. fxn-0.0.42/fxn/lib/windows/x86_64/Function.dll +0 -0
  34. fxn-0.0.42/fxn/services/__init__.py +8 -0
  35. fxn-0.0.42/fxn/services/prediction.py +279 -0
  36. fxn-0.0.42/fxn/services/predictor.py +33 -0
  37. fxn-0.0.42/fxn/services/user.py +30 -0
  38. fxn-0.0.42/fxn/types/__init__.py +9 -0
  39. {fxn-0.0.40 → fxn-0.0.42}/fxn/types/dtype.py +1 -1
  40. {fxn-0.0.40 → fxn-0.0.42}/fxn/types/prediction.py +20 -10
  41. {fxn-0.0.40 → fxn-0.0.42}/fxn/types/predictor.py +18 -32
  42. fxn-0.0.42/fxn/types/user.py +29 -0
  43. fxn-0.0.42/fxn/version.py +6 -0
  44. {fxn-0.0.40 → fxn-0.0.42}/fxn.egg-info/PKG-INFO +4 -4
  45. {fxn-0.0.40 → fxn-0.0.42}/fxn.egg-info/SOURCES.txt +10 -7
  46. {fxn-0.0.40 → fxn-0.0.42}/pyproject.toml +1 -1
  47. fxn-0.0.40/fxn/__init__.py +0 -8
  48. fxn-0.0.40/fxn/api/__init__.py +0 -6
  49. fxn-0.0.40/fxn/api/client.py +0 -43
  50. fxn-0.0.40/fxn/c/__init__.py +0 -16
  51. fxn-0.0.40/fxn/c/configuration.py +0 -60
  52. fxn-0.0.40/fxn/c/dtype.py +0 -26
  53. fxn-0.0.40/fxn/c/fxnc.py +0 -28
  54. fxn-0.0.40/fxn/c/map.py +0 -34
  55. fxn-0.0.40/fxn/c/prediction.py +0 -37
  56. fxn-0.0.40/fxn/c/predictor.py +0 -31
  57. fxn-0.0.40/fxn/c/status.py +0 -12
  58. fxn-0.0.40/fxn/c/stream.py +0 -22
  59. fxn-0.0.40/fxn/c/value.py +0 -50
  60. fxn-0.0.40/fxn/c/version.py +0 -13
  61. fxn-0.0.40/fxn/cli/__init__.py +0 -42
  62. fxn-0.0.40/fxn/cli/env.py +0 -40
  63. fxn-0.0.40/fxn/cli/predictors.py +0 -66
  64. fxn-0.0.40/fxn/lib/__init__.py +0 -4
  65. fxn-0.0.40/fxn/lib/linux/arm64/libFunction.so +0 -0
  66. fxn-0.0.40/fxn/lib/linux/x86_64/libFunction.so +0 -0
  67. fxn-0.0.40/fxn/lib/macos/arm64/Function.dylib +0 -0
  68. fxn-0.0.40/fxn/lib/macos/x86_64/Function.dylib +0 -0
  69. fxn-0.0.40/fxn/lib/windows/arm64/Function.dll +0 -0
  70. fxn-0.0.40/fxn/lib/windows/x86_64/Function.dll +0 -0
  71. fxn-0.0.40/fxn/services/__init__.py +0 -8
  72. fxn-0.0.40/fxn/services/prediction.py +0 -450
  73. fxn-0.0.40/fxn/services/predictor.py +0 -206
  74. fxn-0.0.40/fxn/services/user.py +0 -56
  75. fxn-0.0.40/fxn/types/__init__.py +0 -9
  76. fxn-0.0.40/fxn/types/user.py +0 -35
  77. fxn-0.0.40/fxn/version.py +0 -6
  78. {fxn-0.0.40 → fxn-0.0.42}/LICENSE +0 -0
  79. {fxn-0.0.40 → fxn-0.0.42}/fxn.egg-info/dependency_links.txt +0 -0
  80. {fxn-0.0.40 → fxn-0.0.42}/fxn.egg-info/entry_points.txt +0 -0
  81. {fxn-0.0.40 → fxn-0.0.42}/fxn.egg-info/requires.txt +0 -0
  82. {fxn-0.0.40 → fxn-0.0.42}/fxn.egg-info/top_level.txt +0 -0
  83. {fxn-0.0.40 → fxn-0.0.42}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: fxn
3
- Version: 0.0.40
3
+ Version: 0.0.42
4
4
  Summary: Run prediction functions locally in Python. Register at https://fxn.ai.
5
5
  Author-email: "NatML Inc." <hi@fxn.ai>
6
6
  License: Apache License
@@ -227,7 +227,7 @@ Requires-Dist: typer
227
227
 
228
228
  ![function logo](https://raw.githubusercontent.com/fxnai/.github/main/logo_wide.png)
229
229
 
230
- [![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fdiscord.com%2Fapi%2Finvites%2Fy5vwgXkz2f%3Fwith_counts%3Dtrue&query=%24.approximate_member_count&logo=discord&logoColor=white&label=Function%20community)](https://fxn.ai/community)
230
+ [![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fdiscord.com%2Fapi%2Finvites%2Fy5vwgXkz2f%3Fwith_counts%3Dtrue&query=%24.approximate_member_count&logo=discord&logoColor=white&label=Function%20community)](https://discord.gg/fxn)
231
231
 
232
232
  Run prediction functions (a.k.a "predictors") locally in your Python apps, with full GPU acceleration and zero dependencies.
233
233
 
@@ -286,7 +286,7 @@ ___
286
286
 
287
287
  ## Useful Links
288
288
  - [Discover predictors to use in your apps](https://fxn.ai/explore).
289
- - [Join our Discord community](https://fxn.ai/community).
289
+ - [Join our Discord community](https://discord.gg/fxn).
290
290
  - [Check out our docs](https://docs.fxn.ai).
291
291
  - Learn more about us [on our blog](https://blog.fxn.ai).
292
292
  - Reach out to us at [hi@fxn.ai](mailto:hi@fxn.ai).
@@ -2,7 +2,7 @@
2
2
 
3
3
  ![function logo](https://raw.githubusercontent.com/fxnai/.github/main/logo_wide.png)
4
4
 
5
- [![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fdiscord.com%2Fapi%2Finvites%2Fy5vwgXkz2f%3Fwith_counts%3Dtrue&query=%24.approximate_member_count&logo=discord&logoColor=white&label=Function%20community)](https://fxn.ai/community)
5
+ [![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fdiscord.com%2Fapi%2Finvites%2Fy5vwgXkz2f%3Fwith_counts%3Dtrue&query=%24.approximate_member_count&logo=discord&logoColor=white&label=Function%20community)](https://discord.gg/fxn)
6
6
 
7
7
  Run prediction functions (a.k.a "predictors") locally in your Python apps, with full GPU acceleration and zero dependencies.
8
8
 
@@ -61,7 +61,7 @@ ___
61
61
 
62
62
  ## Useful Links
63
63
  - [Discover predictors to use in your apps](https://fxn.ai/explore).
64
- - [Join our Discord community](https://fxn.ai/community).
64
+ - [Join our Discord community](https://discord.gg/fxn).
65
65
  - [Check out our docs](https://docs.fxn.ai).
66
66
  - Learn more about us [on our blog](https://blog.fxn.ai).
67
67
  - Reach out to us at [hi@fxn.ai](mailto:hi@fxn.ai).
@@ -0,0 +1,10 @@
1
+ #
2
+ # Function
3
+ # Copyright © 2025 NatML Inc. All Rights Reserved.
4
+ #
5
+
6
+ from .client import FunctionAPIError
7
+ from .compile import *
8
+ from .function import Function
9
+ from .types import *
10
+ from .version import *
@@ -0,0 +1,6 @@
1
+ #
2
+ # Function
3
+ # Copyright © 2025 NatML Inc. All Rights Reserved.
4
+ #
5
+
6
+ from .remote import RemoteAcceleration
@@ -0,0 +1,16 @@
1
+ #
2
+ # Function
3
+ # Copyright © 2025 NatML Inc. All Rights Reserved.
4
+ #
5
+
6
+ from ..client import FunctionClient
7
+ from .prediction import PredictionService
8
+
9
+ class BetaClient:
10
+ """
11
+ Client for incubating features.
12
+ """
13
+ predictions: PredictionService
14
+
15
+ def __init__ (self, client: FunctionClient):
16
+ self.predictions = PredictionService(client)
@@ -0,0 +1,16 @@
1
+ #
2
+ # Function
3
+ # Copyright © 2025 NatML Inc. All Rights Reserved.
4
+ #
5
+
6
+ from ..client import FunctionClient
7
+ from .remote import RemotePredictionService
8
+
9
+ class PredictionService:
10
+ """
11
+ Make predictions.
12
+ """
13
+ remote: RemotePredictionService
14
+
15
+ def __init__ (self, client: FunctionClient):
16
+ self.remote = RemotePredictionService(client)
@@ -0,0 +1,207 @@
1
+ #
2
+ # Function
3
+ # Copyright © 2025 NatML Inc. All Rights Reserved.
4
+ #
5
+
6
+ from __future__ import annotations
7
+ from base64 import b64encode
8
+ from dataclasses import asdict, is_dataclass
9
+ from enum import Enum
10
+ from io import BytesIO
11
+ from json import dumps, loads
12
+ from numpy import array, frombuffer, ndarray
13
+ from PIL import Image
14
+ from pydantic import BaseModel, Field
15
+ from requests import get, put
16
+ from typing import Any
17
+ from urllib.request import urlopen
18
+
19
+ from ..c import Configuration
20
+ from ..client import FunctionClient
21
+ from ..services import Value
22
+ from ..types import Dtype, Prediction
23
+
24
+ class RemoteAcceleration (str, Enum):
25
+ """
26
+ Remote acceleration.
27
+ """
28
+ Auto = "auto"
29
+ CPU = "cpu"
30
+ A40 = "a40"
31
+ A100 = "a100"
32
+
33
+ class RemotePredictionService:
34
+ """
35
+ Make remote predictions.
36
+ """
37
+
38
+ def __init__ (self, client: FunctionClient):
39
+ self.client = client
40
+
41
+ def create (
42
+ self,
43
+ tag: str,
44
+ *,
45
+ inputs: dict[str, Value],
46
+ acceleration: RemoteAcceleration=RemoteAcceleration.Auto
47
+ ) -> Prediction:
48
+ """
49
+ Create a remote prediction.
50
+
51
+ Parameters:
52
+ tag (str): Predictor tag.
53
+ inputs (dict): Input values.
54
+ acceleration (RemoteAcceleration): Prediction acceleration.
55
+
56
+ Returns:
57
+ Prediction: Created prediction.
58
+ """
59
+ input_map = { name: self.__to_value(value, name=name).model_dump(mode="json") for name, value in inputs.items() }
60
+ prediction = self.client.request(
61
+ method="POST",
62
+ path="/predictions/remote",
63
+ body={
64
+ "tag": tag,
65
+ "inputs": input_map,
66
+ "acceleration": acceleration,
67
+ "clientId": Configuration.get_client_id()
68
+ },
69
+ response_type=RemotePrediction
70
+ )
71
+ results = list(map(self.__to_object, prediction.results)) if prediction.results is not None else None
72
+ prediction = Prediction(**{ **prediction.model_dump(), "results": results })
73
+ return prediction
74
+
75
+ def __to_value (
76
+ self,
77
+ object: Value,
78
+ *,
79
+ name: str,
80
+ max_data_url_size: int=4 * 1024 * 1024
81
+ ) -> RemoteValue:
82
+ object = self.__try_ensure_serializable(object)
83
+ if object is None:
84
+ 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())
96
+ 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())
100
+ data = self.__upload(buffer, name=name, mime="text/plain", max_data_url_size=max_data_url_size)
101
+ return RemoteValue(data=data, type=Dtype.string)
102
+ elif isinstance(object, list):
103
+ buffer = BytesIO(dumps(object).encode())
104
+ data = self.__upload(buffer, name=name, mime="application/json", max_data_url_size=max_data_url_size)
105
+ return RemoteValue(data=data, type=Dtype.list)
106
+ elif isinstance(object, dict):
107
+ buffer = BytesIO(dumps(object).encode())
108
+ data = self.__upload(buffer, name=name, mime="application/json", max_data_url_size=max_data_url_size)
109
+ return RemoteValue(data=data, type=Dtype.dict)
110
+ elif isinstance(object, Image.Image):
111
+ buffer = BytesIO()
112
+ format = "PNG" if object.mode == "RGBA" else "JPEG"
113
+ mime = f"image/{format.lower()}"
114
+ object.save(buffer, format=format)
115
+ data = self.__upload(buffer, name=name, mime=mime, max_data_url_size=max_data_url_size)
116
+ 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)
119
+ return RemoteValue(data=data, type=Dtype.binary)
120
+ else:
121
+ raise ValueError(f"Failed to serialize value '{object}' of type `{type(object)}` because it is not supported")
122
+
123
+ def __to_object (self, value: RemoteValue) -> Value:
124
+ if value.type == Dtype.null:
125
+ return None
126
+ buffer = self.__download(value.data)
127
+ if value.type in [
128
+ Dtype.int8, Dtype.int16, Dtype.int32, Dtype.int64,
129
+ Dtype.uint8, Dtype.uint16, Dtype.uint32, Dtype.uint64,
130
+ Dtype.float16, Dtype.float32, Dtype.float64, Dtype.bool
131
+ ]:
132
+ assert value.shape is not None, "Array value must have a shape specified"
133
+ array = frombuffer(buffer.getbuffer(), dtype=value.type).reshape(value.shape)
134
+ return array if len(value.shape) > 0 else array.item()
135
+ elif value.type == Dtype.string:
136
+ return buffer.getvalue().decode("utf-8")
137
+ elif value.type in [Dtype.list, Dtype.dict]:
138
+ return loads(buffer.getvalue().decode("utf-8"))
139
+ elif value.type == Dtype.image:
140
+ return Image.open(buffer)
141
+ elif value.type == Dtype.binary:
142
+ return buffer
143
+ else:
144
+ raise ValueError(f"Failed to deserialize value with type `{value.type}` because it is not supported")
145
+
146
+ def __upload (
147
+ self,
148
+ data: BytesIO,
149
+ *,
150
+ name: str,
151
+ mime: str="application/octet-stream",
152
+ max_data_url_size: int=4 * 1024 * 1024
153
+ ) -> str:
154
+ if data.getbuffer().nbytes <= max_data_url_size:
155
+ encoded_data = b64encode(data.getvalue()).decode("ascii")
156
+ return f"data:{mime};base64,{encoded_data}"
157
+ value = self.client.request(
158
+ method="POST",
159
+ path="/values",
160
+ body={ "name": name },
161
+ response_type=CreateValueResponse
162
+ )
163
+ put(
164
+ value.upload_url,
165
+ data=data,
166
+ headers={ "Content-Type": mime }
167
+ ).raise_for_status()
168
+ return value.download_url
169
+
170
+ def __download (self, url: str) -> BytesIO:
171
+ if url.startswith("data:"):
172
+ with urlopen(url) as response:
173
+ return BytesIO(response.read())
174
+ response = get(url)
175
+ response.raise_for_status()
176
+ result = BytesIO(response.content)
177
+ return result
178
+
179
+ @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
190
+
191
+ class RemoteValue (BaseModel):
192
+ data: str | None
193
+ type: Dtype
194
+ shape: list[int] | None = None
195
+
196
+ class RemotePrediction (BaseModel):
197
+ id: str
198
+ tag: str
199
+ created: str
200
+ results: list[RemoteValue] | None
201
+ latency: float | None
202
+ error: str | None
203
+ logs: str | None
204
+
205
+ class CreateValueResponse (BaseModel):
206
+ upload_url: str = Field(validation_alias="uploadUrl")
207
+ download_url: str = Field(validation_alias="downloadUrl")
@@ -0,0 +1,13 @@
1
+ #
2
+ # Function
3
+ # Copyright © 2025 NatML Inc. All Rights Reserved.
4
+ #
5
+
6
+ # https://github.com/fxnai/fxnc
7
+
8
+ from .configuration import Configuration
9
+ from .map import ValueMap
10
+ from .prediction import Prediction
11
+ from .predictor import Predictor
12
+ from .stream import PredictionStream
13
+ from .value import Value, ValueFlags
@@ -0,0 +1,118 @@
1
+ #
2
+ # Function
3
+ # Copyright © 2025 NatML Inc. All Rights Reserved.
4
+ #
5
+
6
+ from ctypes import byref, c_int, c_void_p, create_string_buffer
7
+ from pathlib import Path
8
+ from typing import final
9
+
10
+ from ..types import Acceleration
11
+ from .fxnc import get_fxnc, status_to_error, FXNStatus
12
+
13
+ @final
14
+ class Configuration:
15
+
16
+ def __init__ (self):
17
+ configuration = c_void_p()
18
+ status = get_fxnc().FXNConfigurationCreate(byref(configuration))
19
+ if status == FXNStatus.OK:
20
+ self.__configuration = configuration
21
+ else:
22
+ raise RuntimeError(f"Failed to create configuration with error: {status_to_error(status)}")
23
+
24
+ @property
25
+ def tag (self) -> str:
26
+ buffer = create_string_buffer(2048)
27
+ status = get_fxnc().FXNConfigurationGetTag(self.__configuration, buffer, len(buffer))
28
+ if status != FXNStatus.OK:
29
+ raise RuntimeError(f"Failed to get configuration tag with error: {status_to_error(status)}")
30
+ tag = buffer.value.decode("utf-8")
31
+ return tag if tag else None
32
+
33
+ @tag.setter
34
+ def tag (self, tag: str):
35
+ tag = tag.encode() if tag is not None else None
36
+ status = get_fxnc().FXNConfigurationSetTag(self.__configuration, tag)
37
+ if status != FXNStatus.OK:
38
+ raise RuntimeError(f"Failed to set configuration tag with error: {status_to_error(status)}")
39
+
40
+ @property
41
+ def token (self) -> str:
42
+ buffer = create_string_buffer(2048)
43
+ status = get_fxnc().FXNConfigurationGetToken(self.__configuration, buffer, len(buffer))
44
+ if status != FXNStatus.OK:
45
+ raise RuntimeError(f"Failed to get configuration token with error: {status_to_error(status)}")
46
+ token = buffer.value.decode("utf-8")
47
+ return token if token else None
48
+
49
+ @token.setter
50
+ def token (self, token: str):
51
+ token = token.encode() if token is not None else None
52
+ status = get_fxnc().FXNConfigurationSetToken(self.__configuration, token)
53
+ if status != FXNStatus.OK:
54
+ raise RuntimeError(f"Failed to set configuration token with error: {status_to_error(status)}")
55
+
56
+ @property
57
+ def acceleration (self) -> Acceleration:
58
+ acceleration = c_int()
59
+ status = get_fxnc().FXNConfigurationGetAcceleration(self.__configuration, byref(acceleration))
60
+ if status == FXNStatus.OK:
61
+ return Acceleration(acceleration.value)
62
+ else:
63
+ raise RuntimeError(f"Failed to get configuration acceleration with error: {status_to_error(status)}")
64
+
65
+ @acceleration.setter
66
+ def acceleration (self, acceleration: Acceleration):
67
+ status = get_fxnc().FXNConfigurationSetAcceleration(self.__configuration, acceleration.value)
68
+ if status != FXNStatus.OK:
69
+ raise RuntimeError(f"Failed to set configuration acceleration with error: {status_to_error(status)}")
70
+
71
+ @property
72
+ def device (self):
73
+ 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:
78
+ raise RuntimeError(f"Failed to get configuration device with error: {status_to_error(status)}")
79
+
80
+ @device.setter
81
+ def device (self, device):
82
+ status = get_fxnc().FXNConfigurationSetDevice(self.__configuration, device)
83
+ if status != FXNStatus.OK:
84
+ raise RuntimeError(f"Failed to set configuration device with error: {status_to_error(status)}")
85
+
86
+ def add_resource (self, type: str, path: Path):
87
+ status = get_fxnc().FXNConfigurationAddResource(self.__configuration, type.encode(), str(path).encode())
88
+ if status != FXNStatus.OK:
89
+ raise RuntimeError(f"Failed to add configuration resource with error: {status_to_error(status)}")
90
+
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
+ @classmethod
103
+ def get_unique_id (cls) -> str:
104
+ buffer = create_string_buffer(2048)
105
+ status = get_fxnc().FXNConfigurationGetUniqueID(buffer, len(buffer))
106
+ if status == FXNStatus.OK:
107
+ return buffer.value.decode("utf-8")
108
+ else:
109
+ raise RuntimeError(f"Failed to retrieve configuration identifier with error: {status_to_error(status)}")
110
+
111
+ @classmethod
112
+ def get_client_id (cls) -> str:
113
+ buffer = create_string_buffer(64)
114
+ status = get_fxnc().FXNConfigurationGetClientID(buffer, len(buffer))
115
+ if status == FXNStatus.OK:
116
+ return buffer.value.decode("utf-8")
117
+ else:
118
+ raise RuntimeError(f"Failed to retrieve client identifier with error: {status_to_error(status)}")
@@ -0,0 +1,48 @@
1
+ #
2
+ # Function
3
+ # Copyright © 2025 NatML Inc. All Rights Reserved.
4
+ #
5
+
6
+ from ctypes import CDLL
7
+ from enum import IntEnum
8
+ from importlib import resources
9
+ from platform import machine, system
10
+
11
+ _fxnc: CDLL = None
12
+
13
+ class FXNStatus(IntEnum):
14
+ OK = 0
15
+ ERROR_INVALID_ARGUMENT = 1
16
+ ERROR_INVALID_OPERATION = 2
17
+ ERROR_NOT_IMPLEMENTED = 3
18
+
19
+ def get_fxnc () -> CDLL:
20
+ global _fxnc
21
+ _fxnc = _fxnc if _fxnc is not None else _load_fxnc()
22
+ return _fxnc
23
+
24
+ def set_fxnc (fxnc: CDLL):
25
+ global _fxnc
26
+ _fxnc = fxnc
27
+
28
+ def _load_fxnc () -> CDLL:
29
+ os = system().lower()
30
+ os = "macos" if os == "darwin" else os
31
+ arch = machine().lower()
32
+ arch = "arm64" if arch == "aarch64" else arch
33
+ arch = "x86_64" if arch in ["x64", "amd64"] else arch
34
+ package = f"fxn.lib.{os}.{arch}"
35
+ resource = "libFunction.so"
36
+ resource = "Function.dylib" if os == "macos" else resource
37
+ resource = "Function.dll" if os == "windows" else resource
38
+ with resources.path(package, resource) as path:
39
+ return CDLL(str(path))
40
+
41
+ def status_to_error (status: int) -> str:
42
+ if status == FXNStatus.ERROR_INVALID_ARGUMENT:
43
+ return "FXN_ERROR_INVALID_ARGUMENT"
44
+ elif status == FXNStatus.ERROR_INVALID_OPERATION:
45
+ return "FXN_ERROR_INVALID_OPERATION"
46
+ elif status == FXNStatus.ERROR_NOT_IMPLEMENTED:
47
+ return "FXN_ERROR_NOT_IMPLEMENTED"
48
+ return ""
@@ -0,0 +1,64 @@
1
+ #
2
+ # Function
3
+ # Copyright © 2025 NatML Inc. All Rights Reserved.
4
+ #
5
+
6
+ from ctypes import byref, c_int, c_int32, c_void_p, create_string_buffer
7
+ from pathlib import Path
8
+ from typing import final
9
+
10
+ from .fxnc import get_fxnc, status_to_error, FXNStatus
11
+ from .value import Value
12
+
13
+ @final
14
+ class ValueMap:
15
+
16
+ def __init__ (self, map=None, *, owner: bool=True):
17
+ if map is None:
18
+ map = c_void_p()
19
+ owner = True
20
+ status = get_fxnc().FXNValueMapCreate(byref(map))
21
+ if status != FXNStatus.OK:
22
+ raise RuntimeError(f"Failed to create value map with error: {status_to_error(status)}")
23
+ self.__map = map
24
+ self.__owner = owner
25
+
26
+ def key (self, index: int) -> str:
27
+ buffer = create_string_buffer(256)
28
+ status = get_fxnc().FXNValueMapGetKey(self.__map, index, buffer, len(buffer))
29
+ if status == FXNStatus.OK:
30
+ return buffer.value.decode("utf-8")
31
+ else:
32
+ raise RuntimeError(f"Failed to get value map key at index {index} with error: {status_to_error(status)}")
33
+
34
+ def __getitem__ (self, key: str) -> Value | None:
35
+ value = c_void_p()
36
+ status = get_fxnc().FXNValueMapGetValue(self.__map, key.encode(), byref(value))
37
+ if status == FXNStatus.OK:
38
+ return Value(value, owner=False)
39
+ else:
40
+ raise RuntimeError(f"Failed to get value map value for key '{key}' with error: {status_to_error(status)}")
41
+
42
+ def __setitem__ (self, key: str, value: Value):
43
+ status = get_fxnc().FXNValueMapSetValue(self.__map, key.encode(), value._Value__value)
44
+ if status != FXNStatus.OK:
45
+ raise RuntimeError(f"Failed to set value map value for key '{key}' with error: {status_to_error(status)}")
46
+
47
+ def __len__ (self) -> int:
48
+ count = c_int32()
49
+ status = get_fxnc().FXNValueMapGetSize(self.__map, byref(count))
50
+ if status == FXNStatus.OK:
51
+ return count.value
52
+ else:
53
+ raise RuntimeError(f"Failed to get value map size with error: {status_to_error(status)}")
54
+
55
+ def __enter__ (self):
56
+ return self
57
+
58
+ def __exit__ (self, exc_type, exc_value, traceback):
59
+ self.__release()
60
+
61
+ def __release (self):
62
+ if self.__map and self.__owner:
63
+ get_fxnc().FXNValueMapRelease(self.__map)
64
+ self.__map = None
@@ -0,0 +1,76 @@
1
+ #
2
+ # Function
3
+ # Copyright © 2025 NatML Inc. All Rights Reserved.
4
+ #
5
+
6
+ from ctypes import byref, c_double, c_int32, c_void_p, create_string_buffer
7
+ from pathlib import Path
8
+ from typing import final
9
+
10
+ from .fxnc import get_fxnc, status_to_error, FXNStatus
11
+ from .map import ValueMap
12
+
13
+ @final
14
+ class Prediction:
15
+
16
+ def __init__ (self, prediction):
17
+ self.__prediction = prediction
18
+
19
+ @property
20
+ def id (self) -> str:
21
+ id = create_string_buffer(256)
22
+ status = get_fxnc().FXNPredictionGetID(self.__prediction, id, len(id))
23
+ if status == FXNStatus.OK:
24
+ return id.value.decode("utf-8")
25
+ else:
26
+ raise RuntimeError(f"Failed to get prediction id with error: {status_to_error(status)}")
27
+
28
+ @property
29
+ def latency (self) -> float:
30
+ latency = c_double()
31
+ status = get_fxnc().FXNPredictionGetLatency(self.__prediction, byref(latency))
32
+ if status == FXNStatus.OK:
33
+ return latency.value
34
+ else:
35
+ raise RuntimeError(f"Failed to get prediction latency with error: {status_to_error(status)}")
36
+
37
+ @property
38
+ def results (self) -> ValueMap | None:
39
+ map = c_void_p()
40
+ status = get_fxnc().FXNPredictionGetResults(self.__prediction, byref(map))
41
+ if status != FXNStatus.OK:
42
+ raise RuntimeError(f"Failed to get prediction results with error: {status_to_error(status)}")
43
+ map = ValueMap(map, owner=False)
44
+ return map if len(map) > 0 else None
45
+
46
+ @property
47
+ def error (self) -> str | None:
48
+ error = create_string_buffer(2048)
49
+ get_fxnc().FXNPredictionGetError(self.__prediction, error, len(error))
50
+ error = error.value.decode("utf-8")
51
+ return error if error else None
52
+
53
+ @property
54
+ def logs (self) -> str:
55
+ fxnc = get_fxnc()
56
+ log_length = c_int32()
57
+ status = fxnc.FXNPredictionGetLogLength(self.__prediction, byref(log_length))
58
+ if status != FXNStatus.OK:
59
+ raise RuntimeError(f"Failed to get prediction log length with error: {status_to_error(status)}")
60
+ logs = create_string_buffer(log_length.value + 1)
61
+ status = fxnc.FXNPredictionGetLogs(self.__prediction, logs, len(logs))
62
+ if status == FXNStatus.OK:
63
+ return logs.value.decode("utf-8")
64
+ else:
65
+ raise RuntimeError(f"Failed to get prediction logs with error: {status_to_error(status)}")
66
+
67
+ def __enter__ (self):
68
+ return self
69
+
70
+ def __exit__ (self, exc_type, exc_value, traceback):
71
+ self.__release()
72
+
73
+ def __release (self):
74
+ if self.__prediction:
75
+ get_fxnc().FXNPredictionRelease(self.__prediction)
76
+ self.__prediction = None