fxn 0.0.39__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/__init__.py +6 -9
- fxn/c/configuration.py +113 -55
- fxn/c/fxnc.py +41 -21
- fxn/c/map.py +59 -29
- fxn/c/prediction.py +71 -32
- fxn/c/predictor.py +55 -26
- fxn/c/stream.py +36 -17
- fxn/c/value.py +214 -41
- fxn/cli/__init__.py +6 -2
- fxn/cli/{predict.py → predictions.py} +32 -35
- fxn/client.py +46 -0
- fxn/function.py +4 -4
- fxn/lib/linux/arm64/libFunction.so +0 -0
- fxn/lib/linux/x86_64/libFunction.so +0 -0
- fxn/lib/macos/arm64/Function.dylib +0 -0
- fxn/lib/macos/x86_64/Function.dylib +0 -0
- fxn/lib/windows/arm64/Function.dll +0 -0
- fxn/lib/windows/x86_64/Function.dll +0 -0
- fxn/services/__init__.py +3 -3
- fxn/services/prediction.py +179 -351
- fxn/services/predictor.py +10 -186
- fxn/services/user.py +12 -41
- fxn/types/__init__.py +1 -1
- fxn/types/prediction.py +8 -8
- fxn/types/predictor.py +18 -21
- fxn/types/user.py +8 -14
- fxn/version.py +1 -1
- {fxn-0.0.39.dist-info → fxn-0.0.41.dist-info}/METADATA +7 -7
- fxn-0.0.41.dist-info/RECORD +40 -0
- {fxn-0.0.39.dist-info → fxn-0.0.41.dist-info}/WHEEL +1 -1
- fxn/api/__init__.py +0 -6
- fxn/api/client.py +0 -43
- fxn/c/dtype.py +0 -26
- fxn/c/status.py +0 -12
- fxn/c/version.py +0 -13
- fxn-0.0.39.dist-info/RECORD +0 -44
- {fxn-0.0.39.dist-info → fxn-0.0.41.dist-info}/LICENSE +0 -0
- {fxn-0.0.39.dist-info → fxn-0.0.41.dist-info}/entry_points.txt +0 -0
- {fxn-0.0.39.dist-info → fxn-0.0.41.dist-info}/top_level.txt +0 -0
fxn/c/value.py
CHANGED
@@ -3,48 +3,221 @@
|
|
3
3
|
# Copyright © 2024 NatML Inc. All Rights Reserved.
|
4
4
|
#
|
5
5
|
|
6
|
-
from
|
7
|
-
from .
|
8
|
-
from
|
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
|
-
|
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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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 .
|
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(
|
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
|
20
|
-
tag: str
|
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
|
-
|
35
|
-
|
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
|
-
|
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
|
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
|
-
|
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 .
|
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:
|
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"
|
33
|
-
self.client =
|
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
|
7
|
-
from .predictor import PredictorService
|
8
|
-
from .prediction import PredictionService,
|
6
|
+
from .user import UserService
|
7
|
+
from .predictor import PredictorService
|
8
|
+
from .prediction import PredictionService, Value
|