From aee116a1b4e5383168f226c79aaf22fcc3998a7f Mon Sep 17 00:00:00 2001 From: Yusuf Olokoba Date: Wed, 21 Aug 2024 16:44:13 -0400 Subject: [PATCH] Spring cleaning --- .github/workflows/{edge.yml => fxnc.yml} | 6 +- .github/workflows/pypi.yml | 4 +- Changelog.md | 24 +- README.md | 26 +- fxn/cli/__init__.py | 16 +- fxn/cli/auth.py | 2 +- fxn/cli/predict.py | 7 +- fxn/cli/predictors.py | 41 +-- fxn/function.py | 19 +- fxn/{libs => lib}/__init__.py | 0 fxn/libs/linux/__init__.py | 4 - fxn/libs/macos/__init__.py | 4 - fxn/libs/windows/__init__.py | 4 - fxn/services/prediction.py | 382 ++++++----------------- fxn/services/predictor.py | 71 +---- fxn/services/storage.py | 5 +- fxn/types/__init__.py | 5 +- fxn/types/prediction.py | 4 - fxn/types/predictor.py | 37 +-- fxn/types/value.py | 22 -- fxnc.py | 23 +- pyproject.toml | 10 +- requirements.txt | 2 - 23 files changed, 196 insertions(+), 522 deletions(-) rename .github/workflows/{edge.yml => fxnc.yml} (86%) rename fxn/{libs => lib}/__init__.py (100%) delete mode 100644 fxn/libs/linux/__init__.py delete mode 100644 fxn/libs/macos/__init__.py delete mode 100644 fxn/libs/windows/__init__.py delete mode 100644 fxn/types/value.py diff --git a/.github/workflows/edge.yml b/.github/workflows/fxnc.yml similarity index 86% rename from .github/workflows/edge.yml rename to .github/workflows/fxnc.yml index e0a5d02..373d77f 100644 --- a/.github/workflows/edge.yml +++ b/.github/workflows/fxnc.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/setup-python@v2 with: - python-version: '3.8' + python-version: "3.11" - name: Install dependencies run: | @@ -18,8 +18,8 @@ jobs: python3 -m pip install build twine python3 -m pip install -r requirements.txt - - name: Pull EdgeFunction - run: python3 edgefxn.py + - name: Pull Function C + run: python3 fxnc.py - name: Build Function run: python3 -m build diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 2c98bd3..bd009bb 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -21,8 +21,8 @@ jobs: python3 -m pip install build twine python3 -m pip install -r requirements.txt - - name: Pull EdgeFunction - run: python3 edgefxn.py + - name: Pull Function C + run: python3 fxnc.py - name: Build Function run: python3 -m build diff --git a/Changelog.md b/Changelog.md index ca5bc9e..1234f32 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,6 +1,28 @@ ## 0.0.36 ++ Added `Acceleration.Default` enumeration constant. ++ Added `Acceleration.GPU` enumeration constant for running predictions on the GPU. ++ Added `Acceleration.NPU` enumeration constant forn running predictions on the neural processor. + Fixed crash when using `PIL.Image` values returned by edge predictors. -+ Updated to Function C 0.0.23. ++ Updated to Function C 0.0.26. ++ Removed `Value` type. ++ Removed `PredictorType` enumeration. ++ Removed `fxn.predictors.create` method for creating predictors. [Apply](https://fxn.ai/waitlist) for early access to the new experience. ++ Removed `fxn.predictions.to_object` method. ++ Removed `fxn.predictions.to_value` method. ++ Removed `Predictor.type` field. ++ Removed `Predictor.acceleration` field. ++ Removed `Prediction.type` field. ++ Removed `Acceleration.A40` enumeration constant. ++ Removed `Acceleration.A100` enumeration constant. ++ Removed `fxn create` CLI function. ++ Removed `fxn delete` CLI function. ++ Removed `fxn list` CLI function. ++ Removed `fxn search` CLI function. ++ Removed `fxn retrieve` CLI function. ++ Removed `fxn archive` CLI function. ++ Removed `fxn env` CLI function group. ++ Removed `--raw-outputs` option from `fxn predict` CLI function. ++ Function now requires Python 3.10+. ## 0.0.35 + Updated to Function C 0.0.18. diff --git a/README.md b/README.md index 1018ec8..69bdb95 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![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) -Run AI prediction functions (a.k.a "predictors") in your Python apps. With Function, you can build AI-powered apps by creating and composing GPU-accelerated predictors that run in the cloud. In a few steps: +Run prediction functions (a.k.a "predictors") locally in your Python apps, with full GPU acceleration and zero dependencies. In a few steps: ## Installing Function Function is distributed on PyPi. This distribution contains both the Python client and the command line interface (CLI). To install, open a terminal and run the following command: @@ -22,27 +22,24 @@ Head over to [fxn.ai](https://fxn.ai) to create an account by logging in. Once y ![generate access key](https://raw.githubusercontent.com/fxnai/.github/main/access_key.gif) ## Making a Prediction -Let's run the [`@samples/stable-diffusion`](https://fxn.ai/@samplefxn/stable-diffusion) predictor which accepts a text `prompt` and generates a corresponding image. Run the following Python script: +Let's run the [`@fxn/greeting`](https://fxn.ai/@fxn/greeting) predictor which accepts a `name` and returns a congenial greeting. Run the following Python script: ```py from fxn import Function # Create the Function client -fxn = Function(access_key="") +fxn = Function(access_key="") # Create a prediction prediction = fxn.predictions.create( - tag="@samples/stable-diffusion", - inputs={ - "prompt": "An astronaut riding a horse on Mars" - } + tag="@fxn/greeting", + inputs={ "name": "Peter" } ) -# Show the generated image -image = prediction.results[0] -image.show() +# Print the returned greeting +print(prediction.results[0]) ``` > [!TIP] > Explore public predictors [on Function](https://fxn.ai/explore) or [create your own](https://fxn.ai/waitlist). - +r ## Using the Function CLI Open up a terminal and run the following command: @@ -51,14 +48,9 @@ Open up a terminal and run the following command: fxn auth login # Make a prediction using the Function CLI -fxn predict @samplefxn/stable-diffusion \ - --prompt "An astronaut riding a horse on the moon" +fxn predict @fxn/greeting --name Peter ``` -Within a few seconds, you should see a creepy-looking image pop up 😅: - -![prediction](https://raw.githubusercontent.com/fxnai/.github/main/predict.gif) - ___ ## Useful Links diff --git a/fxn/cli/__init__.py b/fxn/cli/__init__.py index 5b2598d..007e5cc 100644 --- a/fxn/cli/__init__.py +++ b/fxn/cli/__init__.py @@ -9,7 +9,7 @@ from .env import app as env_app from .misc import cli_options from .predict import predict -from .predictors import archive_predictor, create_predictor, delete_predictor, list_predictors, retrieve_predictor, search_predictors +from .predictors import archive_predictor, delete_predictor, list_predictors, retrieve_predictor, search_predictors from ..version import __version__ # Define CLI @@ -26,16 +26,16 @@ # Add subcommands app.add_typer(auth_app, name="auth", help="Login, logout, and check your authentication status.") -app.add_typer(env_app, name="env", help="Manage predictor environment variables.") +#app.add_typer(env_app, name="env", help="Manage predictor environment variables.") # Add top-level commands -app.command(name="create", help="Create a predictor.")(create_predictor) -app.command(name="delete", help="Delete a predictor.")(delete_predictor) +#app.command(name="create", help="Create a predictor.")(create_predictor) +#app.command(name="delete", help="Delete a predictor.")(delete_predictor) app.command(name="predict", help="Make a prediction.", context_settings={ "allow_extra_args": True, "ignore_unknown_options": True })(predict) -app.command(name="list", help="List predictors.")(list_predictors) -app.command(name="search", help="Search predictors.")(search_predictors) -app.command(name="retrieve", help="Retrieve a predictor.")(retrieve_predictor) -app.command(name="archive", help="Archive a predictor.")(archive_predictor) +#app.command(name="list", help="List predictors.")(list_predictors) +#app.command(name="search", help="Search predictors.")(search_predictors) +#app.command(name="retrieve", help="Retrieve a predictor.")(retrieve_predictor) +#app.command(name="archive", help="Archive a predictor.")(archive_predictor) # Run if __name__ == "__main__": diff --git a/fxn/cli/auth.py b/fxn/cli/auth.py index d5ad878..0e05835 100644 --- a/fxn/cli/auth.py +++ b/fxn/cli/auth.py @@ -15,7 +15,7 @@ def login ( access_key: str=Argument(..., help="Function access key.", envvar="FXN_ACCESS_KEY") ): - fxn = Function(access_key) + fxn = Function(access_key=access_key) user = fxn.users.retrieve() user = user.model_dump() if user else None _set_access_key(access_key if user is not None else None) diff --git a/fxn/cli/predict.py b/fxn/cli/predict.py index 11015ae..f786dda 100644 --- a/fxn/cli/predict.py +++ b/fxn/cli/predict.py @@ -18,12 +18,11 @@ def predict ( tag: str = Argument(..., help="Predictor tag."), - raw_outputs: bool = Option(False, "--raw-outputs", help="Output raw Function values instead of converting into plain Python values."), context: Context = 0 ): - run_async(_predict_async(tag, context=context, raw_outputs=raw_outputs)) + run_async(_predict_async(tag, context=context)) -async def _predict_async (tag: str, context: Context, raw_outputs: bool): +async def _predict_async (tag: str, context: Context): with Progress( SpinnerColumn(spinner_name="dots"), TextColumn("[progress.description]{task.description}"), @@ -34,7 +33,7 @@ async def _predict_async (tag: str, context: Context, raw_outputs: bool): inputs = { context.args[i].replace("-", ""): _parse_value(context.args[i+1]) for i in range(0, len(context.args), 2) } # Stream fxn = Function(get_access_key()) - async for prediction in fxn.predictions.stream(tag, inputs=inputs, raw_outputs=raw_outputs, return_binary_path=True): + async for prediction in fxn.predictions.stream(tag, inputs=inputs): # Parse results images = [value for value in prediction.results or [] if isinstance(value, Image.Image)] prediction.results = [_serialize_value(value) for value in prediction.results] if prediction.results is not None else None diff --git a/fxn/cli/predictors.py b/fxn/cli/predictors.py index d5f4fab..3fd8c4a 100644 --- a/fxn/cli/predictors.py +++ b/fxn/cli/predictors.py @@ -5,12 +5,10 @@ from rich import print_json from rich.progress import Progress, SpinnerColumn, TextColumn -from pathlib import Path from typer import Argument, Option -from typing import List from ..function import Function -from ..types import Acceleration, AccessMode, PredictorStatus, PredictorType +from ..types import PredictorStatus from .auth import get_access_key def retrieve_predictor ( @@ -47,43 +45,6 @@ def search_predictors ( predictors = [predictor.model_dump() for predictor in predictors] print_json(data=predictors) -def create_predictor ( - tag: str=Argument(..., help="Predictor tag."), - notebook: Path=Argument(..., help="Path to predictor notebook."), - type: PredictorType=Option(None, case_sensitive=False, help="Predictor type. This defaults to `cloud`."), - edge: bool=Option(False, "--edge", is_flag=True, help="Shorthand for `--type edge`."), - cloud: bool=Option(False, "--cloud", is_flag=True, help="Shorthand for `--type cloud`."), - access: AccessMode=Option(None, case_sensitive=False, help="Predictor access mode. This defaults to `private`."), - description: str=Option(None, help="Predictor description. This must be less than 200 characters long."), - media: Path=Option(None, help="Predictor image path."), - acceleration: Acceleration=Option(None, case_sensitive=False, help="Cloud predictor acceleration. This defaults to `cpu`."), - license: str=Option(None, help="Predictor license URL."), - env: List[str]=Option([], help="Specify a predictor environment variable."), - overwrite: bool=Option(None, "--overwrite", help="Overwrite any existing predictor with the same tag.") -): - with Progress( - SpinnerColumn(spinner_name="dots"), - TextColumn("[progress.description]{task.description}"), - transient=True - ) as progress: - progress.add_task(description="Analyzing Function...", total=None) - fxn = Function(get_access_key()) - type = PredictorType.Cloud if cloud else PredictorType.Edge if edge else type - environment = { e.split("=")[0].strip(): e.split("=")[1].strip() for e in env } - predictor = fxn.predictors.create( - tag=tag, - notebook=notebook, - type=type, - access=access, - description=description, - media=media, - acceleration=acceleration, - environment=environment, - license=license, - overwrite=overwrite - ) - print_json(data=predictor.model_dump()) - def delete_predictor ( tag: str=Argument(..., help="Predictor tag.") ): diff --git a/fxn/function.py b/fxn/function.py index e43b57b..e0047bc 100644 --- a/fxn/function.py +++ b/fxn/function.py @@ -17,8 +17,6 @@ class Function: users (UserService): Manage users. predictors (PredictorService): Manage predictors. predictions (PredictionService): Manage predictions. - environment_variables (EnvironmentVariableService): Manage predictor environment variables. - storage (StorageService): Upload and download files. Constructor: access_key (str): Function access key. @@ -28,15 +26,16 @@ class Function: users: UserService predictors: PredictorService predictions: PredictionService - environment_variables: EnvironmentVariableService - storage: StorageService + #environment_variables: EnvironmentVariableService + #storage: StorageService def __init__ (self, access_key: str=None, api_url: str=None): access_key = access_key or environ.get("FXN_ACCESS_KEY", None) api_url = api_url or environ.get("FXN_API_URL", "https://api.fxn.ai") - self.client = GraphClient(access_key, api_url) - self.users = UserService(self.client) - self.storage = StorageService(self.client) - self.predictors = PredictorService(self.client, self.storage) - self.predictions = PredictionService(self.client, self.storage) - self.environment_variables = EnvironmentVariableService(self.client) \ No newline at end of file + client = GraphClient(access_key, api_url) + storage = StorageService(client) + self.client = client + self.users = UserService(client) + self.predictors = PredictorService(client, storage) + self.predictions = PredictionService(client) + #self.environment_variables = EnvironmentVariableService(self.client) \ No newline at end of file diff --git a/fxn/libs/__init__.py b/fxn/lib/__init__.py similarity index 100% rename from fxn/libs/__init__.py rename to fxn/lib/__init__.py diff --git a/fxn/libs/linux/__init__.py b/fxn/libs/linux/__init__.py deleted file mode 100644 index 172aec6..0000000 --- a/fxn/libs/linux/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# -# Function -# Copyright © 2024 NatML Inc. All Rights Reserved. -# \ No newline at end of file diff --git a/fxn/libs/macos/__init__.py b/fxn/libs/macos/__init__.py deleted file mode 100644 index 172aec6..0000000 --- a/fxn/libs/macos/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# -# Function -# Copyright © 2024 NatML Inc. All Rights Reserved. -# \ No newline at end of file diff --git a/fxn/libs/windows/__init__.py b/fxn/libs/windows/__init__.py deleted file mode 100644 index 172aec6..0000000 --- a/fxn/libs/windows/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# -# Function -# Copyright © 2024 NatML Inc. All Rights Reserved. -# \ No newline at end of file diff --git a/fxn/services/prediction.py b/fxn/services/prediction.py index 5936ad5..7ec776f 100644 --- a/fxn/services/prediction.py +++ b/fxn/services/prediction.py @@ -3,15 +3,13 @@ # Copyright © 2024 NatML Inc. All Rights Reserved. # -from aiohttp import ClientSession from ctypes import byref, cast, c_char_p, c_double, c_int32, c_uint8, c_void_p, create_string_buffer, string_at, CDLL, POINTER from dataclasses import asdict, is_dataclass from datetime import datetime, timezone from importlib import resources from io import BytesIO from json import dumps, loads -from magika import Magika -from numpy import array, dtype, float32, frombuffer, int32, ndarray, zeros +from numpy import array, dtype, int32, ndarray, zeros from numpy.ctypeslib import as_array, as_ctypes_type from numpy.typing import NDArray from pathlib import Path @@ -19,22 +17,17 @@ from platform import machine, system from pydantic import BaseModel from requests import get, post -from tempfile import NamedTemporaryFile from typing import Any, AsyncIterator, Dict, List, Optional, Union -from uuid import uuid4 from urllib.parse import urlparse -from urllib.request import urlopen from ..api import GraphClient from ..c import load_fxnc, FXNConfigurationRef, FXNDtype, FXNPredictionRef, FXNPredictorRef, FXNStatus, FXNValueRef, FXNValueFlags, FXNValueMapRef -from ..types import Dtype, PredictorType, Prediction, PredictionResource, Value, UploadType -from .storage import StorageService +from ..types import Acceleration, Prediction, PredictionResource class PredictionService: - def __init__ (self, client: GraphClient, storage: StorageService): + def __init__ (self, client: GraphClient): self.client = client - self.storage = storage self.__fxnc = PredictionService.__load_fxnc() self.__cache = { } @@ -42,10 +35,8 @@ def create ( self, tag: str, *, - inputs: Dict[str, Union[ndarray, str, float, int, bool, List, Dict[str, Any], Path, Image.Image, Value]] = None, - raw_outputs: bool=False, - return_binary_path: bool=True, - data_url_limit: int=None, + inputs: Optional[Dict[str, Union[ndarray, str, float, int, bool, List, Dict[str, Any], Path, Image.Image]]] = None, + acceleration: Acceleration=Acceleration.Default, client_id: str=None, configuration_id: str=None ) -> Prediction: @@ -54,10 +45,8 @@ def create ( Parameters: tag (str): Predictor tag. - inputs (dict): Input values. This only applies to `CLOUD` predictions. - raw_outputs (bool): Skip converting output values into Pythonic types. This only applies to `CLOUD` predictions. - return_binary_path (bool): Write binary values to file and return a `Path` instead of returning `BytesIO` instance. - data_url_limit (int): Return a data URL if a given output value is smaller than this size in bytes. This only applies to `CLOUD` predictions. + inputs (dict): Input values. + acceleration (Acceleration): Prediction acceleration. client_id (str): Function client identifier. Specify this to override the current client identifier. configuration_id (str): Configuration identifier. Specify this to override the current client configuration identifier. @@ -67,13 +56,10 @@ def create ( # Check if cached if tag in self.__cache: return self.__predict(tag=tag, predictor=self.__cache[tag], inputs=inputs) - # Serialize inputs - key = uuid4().hex - values = { name: self.to_value(value, name, key=key).model_dump(mode="json") for name, value in inputs.items() } if inputs is not None else { } # Query response = post( - f"{self.client.api_url}/predict/{tag}?rawOutputs=true&dataUrlLimit={data_url_limit}", - json=values, + f"{self.client.api_url}/predict/{tag}?rawOutputs=true", + json={ }, headers={ "Authorization": f"Bearer {self.client.access_key}", "fxn-client": client_id if client_id is not None else self.__get_client_id(), @@ -87,26 +73,23 @@ def create ( except Exception as ex: error = prediction["errors"][0]["message"] if "errors" in prediction else str(ex) raise RuntimeError(error) - # Parse prediction - prediction = self.__parse_prediction(prediction, raw_outputs=raw_outputs, return_binary_path=return_binary_path) - # Check edge prediction - if prediction.type != PredictorType.Edge or raw_outputs: + # Check raw prediction + prediction = Prediction(**prediction) + if inputs is None: return prediction - # Load edge predictor - predictor = self.__load(prediction) - self.__cache[tag] = predictor # Create edge prediction - prediction = self.__predict(tag=tag, predictor=predictor, inputs=inputs) if inputs is not None else prediction + predictor = self.__load(prediction, acceleration=acceleration) + self.__cache[tag] = predictor + prediction = self.__predict(tag=tag, predictor=predictor, inputs=inputs) + # Return return prediction - async def stream ( + async def stream ( # INCOMPLETE # Streaming support self, tag: str, *, - inputs: Dict[str, Union[float, int, str, bool, NDArray, List[Any], Dict[str, Any], Path, Image.Image, Value]] = {}, - raw_outputs: bool=False, - return_binary_path: bool=True, - data_url_limit: int=None, + inputs: Dict[str, Union[float, int, str, bool, NDArray, List[Any], Dict[str, Any], Path, Image.Image]] = {}, + acceleration: Acceleration=Acceleration.Default, client_id: str=None, configuration_id: str=None ) -> AsyncIterator[Prediction]: @@ -117,10 +100,8 @@ async def stream ( Parameters: tag (str): Predictor tag. - inputs (dict): Input values. This only applies to `CLOUD` predictions. - raw_outputs (bool): Skip converting output values into Pythonic types. This only applies to `CLOUD` predictions. - return_binary_path (bool): Write binary values to file and return a `Path` instead of returning `BytesIO` instance. - data_url_limit (int): Return a data URL if a given output value is smaller than this size in bytes. This only applies to `CLOUD` predictions. + inputs (dict): Input values. + acceleration (Acceleration): Prediction acceleration. client_id (str): Function client identifier. Specify this to override the current client identifier. configuration_id (str): Configuration identifier. Specify this to override the current client configuration identifier. @@ -131,185 +112,42 @@ async def stream ( if tag in self.__cache: yield self.__predict(tag=tag, predictor=self.__cache[tag], inputs=inputs) return - # Serialize inputs - key = uuid4().hex - values = { name: self.to_value(value, name, key=key).model_dump(mode="json") for name, value in inputs.items() } - # Request - url = f"{self.client.api_url}/predict/{tag}?stream=true&rawOutputs=true&dataUrlLimit={data_url_limit}" - headers = { - "Content-Type": "application/json", - "Authorization": f"Bearer {self.client.access_key}", - "fxn-client": client_id if client_id is not None else self.__get_client_id(), - "fxn-configuration-token": configuration_id if configuration_id is not None else self.__get_configuration_id() - } - async with ClientSession(headers=headers) as session: - async with session.post(url, data=dumps(values)) as response: - async for chunk in response.content.iter_any(): - prediction = loads(chunk) - # Check status - try: - response.raise_for_status() - except Exception as ex: - error = prediction["errors"][0]["message"] if "errors" in prediction else str(ex) - raise RuntimeError(error) - # Parse prediction - prediction = self.__parse_prediction(prediction, raw_outputs=raw_outputs, return_binary_path=return_binary_path) - # Check edge prediction - if prediction.type != PredictorType.Edge or raw_outputs: - yield prediction - continue - # Load edge predictor - predictor = self.__load(prediction) - self.__cache[tag] = predictor - # Create prediction - prediction = self.__predict(tag=tag, predictor=predictor, inputs=inputs) if inputs is not None else prediction - yield prediction - - def to_object ( - self, - value: Value, - return_binary_path: bool=True - ) -> Union[str, float, int, bool, NDArray, list, dict, Image.Image, BytesIO, Path]: - """ - Convert a Function value to a plain object. - - Parameters: - return_binary_path (str): Write binary values to file and return a `Path` instead of returning `BytesIO` instance. - - Returns: - str | float | int | bool | list | dict | ndarray | Image.Image | BytesIO | Path: Plain objectt. - """ - # Null - if value.type == Dtype.null: - return None - # Download - buffer = self.__download_value_data(value.data) - # Array - if value.type in [ - Dtype.int8, Dtype.int16, Dtype.int32, Dtype.int64, - Dtype.uint8, Dtype.uint16, Dtype.uint32, Dtype.uint64, - Dtype.float16, Dtype.float32, Dtype.float64, Dtype.bool - ]: - assert value.shape is not None, "Array value must have a shape specified" - array = frombuffer(buffer.getbuffer(), dtype=value.type).reshape(value.shape) - return array if len(value.shape) > 0 else array.item() - # String - if value.type == Dtype.string: - return buffer.getvalue().decode("utf-8") - # List - if value.type == Dtype.list: - return loads(buffer.getvalue().decode("utf-8")) - # Dict - if value.type == Dtype.dict: - return loads(buffer.getvalue().decode("utf-8")) - # Image - if value.type == Dtype.image: - return Image.open(buffer) - # Binary - if return_binary_path: - with NamedTemporaryFile(mode="wb", delete=False) as f: - f.write(buffer.getbuffer()) - return Path(f.name) - # Return - return buffer - - def to_value ( - self, - object: Union[str, float, int, bool, ndarray, List[Any], Dict[str, any], Path, Image.Image], - name: str, - min_upload_size: int=4096, - key: str=None - ) -> Value: - """ - Convert a plain object to a Function value. - - Parameters: - object (str | float | int | bool | ndarray | list | dict | dataclass | Path | PIL.Image): Input object. - name (str): Value name. - min_upload_size (int): Values larger than this size in bytes will be uploaded. - - Returns: - Value: Function value. - """ - object = self.__try_ensure_serializable(object) - # None - if object is None: - return Value(data=None, type=Dtype.null) - # Value - if isinstance(object, Value): - return object - # Array - if isinstance(object, ndarray): - buffer = BytesIO(object.tobytes()) - data = self.storage.upload(buffer, type=UploadType.Value, name=name, data_url_limit=min_upload_size, key=key) - return Value(data=data, type=object.dtype.name, shape=list(object.shape)) - # String - if isinstance(object, str): - buffer = BytesIO(object.encode()) - data = self.storage.upload(buffer, type=UploadType.Value, name=name, data_url_limit=min_upload_size, key=key) - return Value(data=data, type=Dtype.string) - # Float - if isinstance(object, float): - object = array(object, dtype=float32) - return self.to_value(object, name, min_upload_size=min_upload_size, key=key) - # Boolean - if isinstance(object, bool): - object = array(object, dtype=bool) - return self.to_value(object, name, min_upload_size=min_upload_size, key=key) - # Integer - if isinstance(object, int): - object = array(object, dtype=int32) - return self.to_value(object, name, min_upload_size=min_upload_size, key=key) - # List - if isinstance(object, list): - buffer = BytesIO(dumps(object).encode()) - data = self.storage.upload(buffer, type=UploadType.Value, name=name, data_url_limit=min_upload_size, key=key) - return Value(data=data, type=Dtype.list) - # Dict - if isinstance(object, dict): - buffer = BytesIO(dumps(object).encode()) - data = self.storage.upload(buffer, type=UploadType.Value, name=name, data_url_limit=min_upload_size, key=key) - return Value(data=data, type=Dtype.dict) - # Image - if isinstance(object, Image.Image): - buffer = BytesIO() - format = "PNG" if object.mode == "RGBA" else "JPEG" - object.save(buffer, format=format) - data = self.storage.upload(buffer, type=UploadType.Value, name=name, data_url_limit=min_upload_size, key=key) - return Value(data=data, type=Dtype.image) - # Binary - if isinstance(object, BytesIO): - data = self.storage.upload(object, type=UploadType.Value, name=name, data_url_limit=min_upload_size, key=key) - dtype = self.__get_data_dtype(object) - return Value(data=data, type=dtype) - # Path - if isinstance(object, Path): - assert object.exists(), "Value does not exist at the given path" - assert object.is_file(), "Value path must point to a file, not a directory" - object = object.expanduser().resolve() - data = self.storage.upload(object, type=UploadType.Value, name=name, data_url_limit=min_upload_size, key=key) - dtype = self.__get_data_dtype(object) - return Value(data=data, type=dtype) - # Unsupported - raise RuntimeError(f"Cannot create Function value '{name}' for object {object} of type {type(object)}") + # Create prediction + prediction = self.create( + tag=tag, + client_id=client_id, + configuration_id=configuration_id + ) + # Make single prediction + predictor = self.__load(prediction, acceleration=acceleration) + self.__cache[tag] = predictor + prediction = self.__predict(tag=tag, predictor=predictor, inputs=inputs) + # Yield + yield prediction @classmethod - def __load_fxnc (self) -> Optional[CDLL]: - RESOURCE_MAP = { - "Darwin": ("fxn.libs.macos", "Function.dylib"), - "Windows": ("fxn.libs.windows", "Function.dll"), - } + def __load_fxnc (self) -> Optional[CDLL]: # Get resource - package, resource = RESOURCE_MAP.get(system(), (None, None)) - if package is None or resource is None: + package, resource = None, None + os = system() + if os == "Darwin": + package = f"fxn.lib.macos.{machine()}" + resource = f"Function.dylib" + elif os == "Linux" and False: # INCOMPLETE # Linux + package = f"fxn.lib.linux.{machine()}" + resource = f"libFunction.so" + elif os == "Windows": + package = f"fxn.lib.windows.{machine()}" + resource = f"Function.dll" + else: return None # Load with resources.path(package, resource) as fxnc_path: return load_fxnc(fxnc_path) def __get_client_id (self) -> str: - # Check - if not self.__fxnc: # CHECK # Remove this block once `fxnc` ships Linux binaries + # Fallback if fxnc failed to load + if not self.__fxnc: return { "Darwin": f"macos-{machine()}", "Linux": f"linux-{machine()}", @@ -318,7 +156,8 @@ def __get_client_id (self) -> str: # Get buffer = create_string_buffer(64) status = self.__fxnc.FXNConfigurationGetClientID(buffer, len(buffer)) - assert status.value == FXNStatus.OK, f"Failed to retrieve prediction client identifier with status: {status.value}" + assert status.value == FXNStatus.OK, \ + f"Failed to retrieve prediction client identifier with status: {status.value}" client_id = buffer.value.decode("utf-8") # Return return client_id @@ -330,36 +169,46 @@ def __get_configuration_id (self) -> Optional[str]: # Get buffer = create_string_buffer(2048) status = self.__fxnc.FXNConfigurationGetUniqueID(buffer, len(buffer)) - assert status.value == FXNStatus.OK, f"Failed to retrieve prediction configuration identifier with status: {status.value}" + assert status.value == FXNStatus.OK, \ + f"Failed to retrieve prediction configuration identifier with error: {self.__class__.__status_to_error(status.value)}" uid = buffer.value.decode("utf-8") # Return return uid - def __load (self, prediction: Prediction): - # Load predictor + def __load ( + self, + prediction: Prediction, + *, + acceleration: Acceleration=Acceleration.Default + ) -> type[FXNPredictorRef]: fxnc = self.__fxnc configuration = FXNConfigurationRef() try: # Create configuration status = fxnc.FXNConfigurationCreate(byref(configuration)) - assert status.value == FXNStatus.OK, f"Failed to create {prediction.tag} prediction configuration with status: {status.value}" - # Set tag + assert status.value == FXNStatus.OK, \ + f"Failed to create {prediction.tag} configuration with error: {self.__class__.__status_to_error(status.value)}" status = fxnc.FXNConfigurationSetTag(configuration, prediction.tag.encode()) - assert status.value == FXNStatus.OK, f"Failed to set {prediction.tag} prediction configuration tag with status: {status.value}" - # Set token + assert status.value == FXNStatus.OK, \ + f"Failed to set configuration tag with error: {self.__class__.__status_to_error(status.value)}" status = fxnc.FXNConfigurationSetToken(configuration, prediction.configuration.encode()) - assert status.value == FXNStatus.OK, f"Failed to set {prediction.tag} prediction configuration token with status: {status.value}" - # Add resources + assert status.value == FXNStatus.OK, \ + f"Failed to set configuration token with error: {self.__class__.__status_to_error(status.value)}" + status = fxnc.FXNConfigurationSetAcceleration(configuration, int(acceleration)) + assert status.value == FXNStatus.OK, \ + f"Failed to set configuration acceleration with error: {self.__class__.__status_to_error(status.value)}" for resource in prediction.resources: - if resource.type == "fxn": + if resource.type == "fxn": # CHECK # Remove in fxnc 0.0.27 continue path = self.__get_resource_path(resource) status = fxnc.FXNConfigurationAddResource(configuration, resource.type.encode(), str(path).encode()) - assert status.value == FXNStatus.OK, f"Failed to set prediction configuration resource with type {resource.type} for tag {prediction.tag} with status: {status.value}" + assert status.value == FXNStatus.OK, \ + f"Failed to set prediction configuration resource with type {resource.type} for tag {prediction.tag} with error: {self.__class__.__status_to_error(status.value)}" # Create predictor predictor = FXNPredictorRef() status = fxnc.FXNPredictorCreate(configuration, byref(predictor)) - assert status.value == FXNStatus.OK, f"Failed to create prediction for tag {prediction.tag} with status: {status.value}" + assert status.value == FXNStatus.OK, \ + f"Failed to create prediction for tag {prediction.tag} with error: {self.__class__.__status_to_error(status.value)}" # Return return predictor finally: @@ -370,29 +219,31 @@ def __predict (self, *, tag: str, predictor, inputs: Dict[str, Any]) -> Predicti input_map = FXNValueMapRef() prediction = FXNPredictionRef() try: - # Create input map - status = fxnc.FXNValueMapCreate(byref(input_map)) - assert status.value == FXNStatus.OK, f"Failed to create {tag} prediction because input values could not be provided to the predictor with status: {status.value}" # Marshal inputs + status = fxnc.FXNValueMapCreate(byref(input_map)) + assert status.value == FXNStatus.OK, \ + f"Failed to create {tag} prediction because input values could not be provided to the predictor with error: {self.__class__.__status_to_error(status.value)}" for name, value in inputs.items(): value = self.__to_value(value) fxnc.FXNValueMapSetValue(input_map, name.encode(), value) # Predict status = fxnc.FXNPredictorCreatePrediction(predictor, input_map, byref(prediction)) - assert status.value == FXNStatus.OK, f"Failed to create {tag} prediction with status: {status.value}" + assert status.value == FXNStatus.OK, \ + f"Failed to create {tag} prediction with error: {self.__class__.__status_to_error(status.value)}" # Marshal prediction id = create_string_buffer(256) error = create_string_buffer(2048) latency = c_double() status = fxnc.FXNPredictionGetID(prediction, id, len(id)) - assert status.value == FXNStatus.OK, f"Failed to get {tag} prediction identifier with status: {status.value}" + assert status.value == FXNStatus.OK, \ + f"Failed to get {tag} prediction identifier with error: {self.__class__.__status_to_error(status.value)}" status = fxnc.FXNPredictionGetLatency(prediction, byref(latency)) - assert status.value == FXNStatus.OK, f"Failed to get {tag} prediction latency with status: {status.value}" + assert status.value == FXNStatus.OK, \ + f"Failed to get {tag} prediction latency with error: {self.__class__.__status_to_error(status.value)}" fxnc.FXNPredictionGetError(prediction, error, len(error)) id = id.value.decode("utf-8") latency = latency.value error = error.value.decode("utf-8") - # Marshal logs log_length = c_int32() fxnc.FXNPredictionGetLogLength(prediction, byref(log_length)) logs = create_string_buffer(log_length.value + 1) @@ -403,19 +254,18 @@ def __predict (self, *, tag: str, predictor, inputs: Dict[str, Any]) -> Predicti output_count = c_int32() output_map = FXNValueMapRef() status = fxnc.FXNPredictionGetResults(prediction, byref(output_map)) - assert status.value == FXNStatus.OK, f"Failed to get {tag} prediction results with status: {status.value}" + assert status.value == FXNStatus.OK, f"Failed to get {tag} prediction results with error: {self.__class__.__status_to_error(status.value)}" status = fxnc.FXNValueMapGetSize(output_map, byref(output_count)) - assert status.value == FXNStatus.OK, f"Failed to get {tag} prediction result count with status: {status.value}" + assert status.value == FXNStatus.OK, f"Failed to get {tag} prediction result count with error: {self.__class__.__status_to_error(status.value)}" for idx in range(output_count.value): - # Get name name = create_string_buffer(256) status = fxnc.FXNValueMapGetKey(output_map, idx, name, len(name)) - assert status.value == FXNStatus.OK, f"Failed to get {tag} prediction output name at index {idx} with status: {status.value}" - # Get value + assert status.value == FXNStatus.OK, \ + f"Failed to get {tag} prediction output name at index {idx} with error: {self.__class__.__status_to_error(status.value)}" value = FXNValueRef() status = fxnc.FXNValueMapGetValue(output_map, name, byref(value)) - assert status.value == FXNStatus.OK, f"Failed to get {tag} prediction output value at index {idx} with status: {status.value}" - # Parse + assert status.value == FXNStatus.OK, \ + f"Failed to get {tag} prediction output value at index {idx} with error: {self.__class__.__status_to_error(status.value)}" name = name.value.decode("utf-8") value = self.__to_object(value) results.append(value) @@ -423,7 +273,6 @@ def __predict (self, *, tag: str, predictor, inputs: Dict[str, Any]) -> Predicti return Prediction( id=id, tag=tag, - type=PredictorType.Edge, results=results if not error else None, latency=latency, error=error if error else None, @@ -433,23 +282,12 @@ def __predict (self, *, tag: str, predictor, inputs: Dict[str, Any]) -> Predicti finally: fxnc.FXNPredictionRelease(prediction) fxnc.FXNValueMapRelease(input_map) - - def __parse_prediction ( - self, - data: Dict[str, Any], - *, - raw_outputs: bool, - return_binary_path: bool - ) -> Prediction: - prediction = Prediction(**data) - prediction.results = [Value(**value) for value in prediction.results] if prediction.results is not None else None - prediction.results = [self.to_object(value, return_binary_path=return_binary_path) for value in prediction.results] if prediction.results is not None and not raw_outputs else prediction.results - return prediction def __to_value ( self, value: Union[float, int, bool, str, NDArray, List[Any], Dict[str, Any], Image.Image, bytes, bytearray, memoryview, BytesIO, None] ) -> type[FXNValueRef]: + value = PredictionService.__try_ensure_serializable(value) fxnc = self.__fxnc result = FXNValueRef() if result is None: @@ -487,7 +325,7 @@ def __to_value ( FXNValueFlags.COPY_DATA, byref(result) ) - assert status.value == FXNStatus.OK, f"Failed to create image value with status: {status.value}" + assert status.value == FXNStatus.OK, f"Failed to create image value with error: {self.__class__.__status_to_error(status.value)}" elif isinstance(value, (bytes, bytearray, memoryview, BytesIO)): copy = isinstance(value, memoryview) view = memoryview(value.getvalue() if isinstance(value, BytesIO) else value) if not isinstance(value, memoryview) else value @@ -510,19 +348,19 @@ def __to_object ( fxnc = self.__fxnc dtype = FXNDtype() status = fxnc.FXNValueGetType(value, byref(dtype)) - assert status.value == FXNStatus.OK, f"Failed to get value data type with status: {status.value}" + assert status.value == FXNStatus.OK, f"Failed to get value data type with error: {self.__class__.__status_to_error(status.value)}" dtype = dtype.value # Get data data = c_void_p() status = fxnc.FXNValueGetData(value, byref(data)) - assert status.value == FXNStatus.OK, f"Failed to get value data with status: {status.value}" + assert status.value == FXNStatus.OK, f"Failed to get value data with error: {self.__class__.__status_to_error(status.value)}" # Get shape dims = c_int32() status = fxnc.FXNValueGetDimensions(value, byref(dims)) - assert status.value == FXNStatus.OK, f"Failed to get value dimensions with status: {status.value}" + assert status.value == FXNStatus.OK, f"Failed to get value dimensions with error: {self.__class__.__status_to_error(status.value)}" shape = zeros(dims.value, dtype=int32) status = fxnc.FXNValueGetShape(value, shape.ctypes.data_as(POINTER(c_int32)), dims) - assert status.value == FXNStatus.OK, f"Failed to get value shape with status: {status.value}" + assert status.value == FXNStatus.OK, f"Failed to get value shape with error: {self.__class__.__status_to_error(status.value)}" # Switch if dtype == FXNDtype.NULL: return None @@ -544,29 +382,6 @@ def __to_object ( else: raise RuntimeError(f"Failed to convert Function value to Python value because Function value has unsupported type: {dtype}") - def __get_data_dtype (self, data: Union[Path, BytesIO]) -> Dtype: - magika = Magika() - result = magika.identify_bytes(data.getvalue()) if isinstance(data, BytesIO) else magika.identify_path(data) - group = result.output.group - if group == "image": - return Dtype.image - elif group == "audio": - return Dtype.audio - elif group == "video": - return Dtype.video - elif isinstance(data, Path) and data.suffix in [".obj", ".gltf", ".glb", ".fbx", ".usd", ".usdz", ".blend"]: - return Dtype._3d - else: - return Dtype.binary - - def __download_value_data (self, url: str) -> BytesIO: - if url.startswith("data:"): - with urlopen(url) as response: - return BytesIO(response.read()) - response = get(url) - result = BytesIO(response.content) - return result - def __get_resource_path (self, resource: PredictionResource) -> Path: cache_dir = Path.home() / ".fxn" / "cache" cache_dir.mkdir(exist_ok=True) @@ -584,8 +399,6 @@ def __get_resource_path (self, resource: PredictionResource) -> Path: def __try_ensure_serializable (cls, object: Any) -> Any: if object is None: return object - if isinstance(object, Value): # passthrough - return object if isinstance(object, list): return [cls.__try_ensure_serializable(x) for x in object] if is_dataclass(object) and not isinstance(object, type): @@ -594,6 +407,15 @@ def __try_ensure_serializable (cls, object: Any) -> Any: return object.model_dump(mode="json", by_alias=True) return object + @classmethod + def __status_to_error (cls, status: int) -> str: + if status == FXNStatus.ERROR_INVALID_ARGUMENT: + return "FXN_ERROR_INVALID_ARGUMENT" + elif status == FXNStatus.ERROR_INVALID_OPERATION: + return "FXN_ERROR_INVALID_OPERATION" + elif status == FXNStatus.ERROR_NOT_IMPLEMENTED: + return "FXN_ERROR_NOT_IMPLEMENTED" + return "" PREDICTION_FIELDS = f""" id diff --git a/fxn/services/predictor.py b/fxn/services/predictor.py index 1fcea69..cc0e0f8 100644 --- a/fxn/services/predictor.py +++ b/fxn/services/predictor.py @@ -3,11 +3,10 @@ # Copyright © 2024 NatML Inc. All Rights Reserved. # -from pathlib import Path -from typing import Dict, List, Union +from typing import List from ..api import GraphClient -from ..types import Acceleration, AccessMode, Predictor, PredictorStatus, PredictorType, UploadType +from ..types import Predictor, PredictorStatus from .storage import StorageService from .user import PROFILE_FIELDS @@ -120,70 +119,6 @@ def search ( # Return return predictors - def create ( - self, - tag: str, - notebook: Union[str, Path], - type: PredictorType=None, - access: AccessMode=None, - description: str=None, - media: Union[str, Path]=None, - acceleration: Acceleration=None, - environment: Dict[str, str]=None, - license: str=None, - overwrite: bool=None - ) -> Predictor: - """ - Create a predictor. - - Parameters: - tag (str): Predictor tag. - notebook (str | Path): Predictor notebook path or URL. - type (PredictorType): Predictor type. This defaults to `CLOUD`. - access (AccessMode): Predictor access mode. This defaults to `PRIVATE`. - description (str): Predictor description. This must be under 200 characters long. - media (str | Path): Predictor media path or URL. - acceleration (Acceleration): Predictor acceleration. This only applies for cloud predictors and defaults to `CPU`. - environment (dict): Predictor environment variables. - license (str): Predictor license URL. - overwrite (bool): Overwrite any existing predictor with the same tag. Existing predictor will be deleted. - - Returns: - Predictor: Created predictor. - """ - # Prepare - environment = [{ "name": name, "value": value } for name, value in environment.items()] if environment is not None else [] - notebook = self.storage.upload(notebook, type=UploadType.Notebook) if isinstance(notebook, Path) else notebook - media = self.storage.upload(media, type=UploadType.Media) if isinstance(media, Path) else media - # Query - response = self.client.query(f""" - mutation ($input: CreatePredictorInput!) {{ - createPredictor (input: $input) {{ - {PREDICTOR_FIELDS} - }} - }} - """, - { - "input": { - "tag": tag, - "type": type, - "notebook": notebook, - "access": access, - "description": description, - "media": media, - "acceleration": acceleration, - "environment": environment, - "overwrite": overwrite, - "license": license - } - } - ) - # Create predictor - predictor = response["createPredictor"] - predictor = Predictor(**predictor) if predictor else None - # Return - return predictor - def delete (self, tag: str) -> bool: """ Delete a predictor. @@ -239,14 +174,12 @@ def archive (self, tag: str) -> Predictor: {PROFILE_FIELDS} }} name -type status access created description card media -acceleration signature {{ inputs {{ name diff --git a/fxn/services/storage.py b/fxn/services/storage.py index a74fe6c..4fa1e04 100644 --- a/fxn/services/storage.py +++ b/fxn/services/storage.py @@ -9,7 +9,6 @@ from pathlib import Path from requests import put from rich.progress import open as open_progress, wrap_file -from typing import Union from urllib.parse import urlparse, urlunparse from ..api import GraphClient @@ -45,7 +44,7 @@ def create_upload_url (self, name: str, type: UploadType, key: str=None) -> str: def upload ( self, - file: Union[str, Path, BytesIO], + file: str | Path | BytesIO, *, type: UploadType, name: str=None, @@ -138,7 +137,7 @@ def __simplify_url (self, url: str) -> str: url = urlunparse(parsed_url) return url - def __infer_mime (self, file: Union[str, Path, BytesIO]) -> str: + def __infer_mime (self, file: str | Path | BytesIO) -> str: MAGIC_TO_MIME = { b"\x00\x61\x73\x6d": "application/wasm" } diff --git a/fxn/types/__init__.py b/fxn/types/__init__.py index 53203dc..99fd04c 100644 --- a/fxn/types/__init__.py +++ b/fxn/types/__init__.py @@ -6,8 +6,7 @@ from .dtype import Dtype from .environment import EnvironmentVariable from .prediction import Prediction, PredictionResource -from .predictor import Acceleration, AccessMode, EnumerationMember, Parameter, Predictor, PredictorStatus, PredictorType, Signature +from .predictor import Acceleration, AccessMode, EnumerationMember, Parameter, Predictor, PredictorStatus, Signature from .profile import Profile from .storage import UploadType -from .user import User -from .value import Value \ No newline at end of file +from .user import User \ No newline at end of file diff --git a/fxn/types/prediction.py b/fxn/types/prediction.py index 0a5b586..eaa708b 100644 --- a/fxn/types/prediction.py +++ b/fxn/types/prediction.py @@ -6,8 +6,6 @@ from pydantic import BaseModel, Field from typing import Any, List, Optional -from .predictor import PredictorType - class PredictionResource (BaseModel): """ Prediction resource. @@ -28,7 +26,6 @@ class Prediction (BaseModel): Members: id (str): Prediction identifier. tag (str): Predictor tag. - type (PredictorType): Prediction type. configuration (str): Prediction configuration token. This is only populated for `EDGE` predictions. resources (list): Prediction resources. This is only populated for `EDGE` predictions. results (list): Prediction results. @@ -39,7 +36,6 @@ class Prediction (BaseModel): """ id: str = Field(description="Prediction identifier.") tag: str = Field(description="Predictor tag.") - type: PredictorType = Field(description="Prediction type.") configuration: Optional[str] = Field(default=None, description="Prediction configuration token. This is only populated for `EDGE` predictions.") resources: Optional[List[PredictionResource]] = Field(default=None, description="Prediction resources. This is only populated for `EDGE` predictions.") results: Optional[List[Any]] = Field(default=None, description="Prediction results.") diff --git a/fxn/types/predictor.py b/fxn/types/predictor.py index 865f963..8376fdb 100644 --- a/fxn/types/predictor.py +++ b/fxn/types/predictor.py @@ -3,21 +3,24 @@ # Copyright © 2024 NatML Inc. All Rights Reserved. # -from enum import Enum -from pydantic import AliasChoices, BaseModel, Field -from typing import List, Optional, Tuple, Union +from enum import Enum, IntFlag +from io import BytesIO +from numpy.typing import NDArray +from PIL import Image +from pydantic import AliasChoices, BaseModel, ConfigDict, Field +from typing import Any, Dict, List, Optional, Tuple from .dtype import Dtype from .profile import Profile -from .value import Value -class Acceleration (str, Enum): +class Acceleration (IntFlag): """ Predictor acceleration. """ - CPU = "CPU" - A40 = "A40" - A100 = "A100" + Default = 0, + CPU = 1 << 0, + GPU = 1 << 1, + NPU = 1 << 2 class AccessMode (str, Enum): """ @@ -26,13 +29,6 @@ class AccessMode (str, Enum): Public = "PUBLIC" Private = "PRIVATE" -class PredictorType (str, Enum): - """ - Predictor type. - """ - Cloud = "CLOUD" - Edge = "EDGE" - class PredictorStatus (str, Enum): """ Predictor status. @@ -51,7 +47,7 @@ class EnumerationMember (BaseModel): value (str | int): Enumeration member value. """ name: str = Field(description="Enumeration member name.") - value: Union[str, int] = Field(description="Enumeration member value.") + value: str | int = Field(description="Enumeration member value.") class Parameter (BaseModel): """ @@ -64,7 +60,7 @@ class Parameter (BaseModel): optional (bool): Whether the parameter is optional. range (tuple): Parameter value range for numeric parameters. enumeration (list): Parameter value choices for enumeration parameters. - default_value (Value): Parameter default value. + default_value (str | float | int | bool | ndarray | list | dict | PIL.Image | BytesIO): Parameter default value. value_schema (dict): Parameter JSON schema. This is only populated for `list` and `dict` parameters. """ name: str = Field(description="Parameter name.") @@ -73,8 +69,9 @@ class Parameter (BaseModel): optional: Optional[bool] = Field(default=None, description="Whether the parameter is optional.") range: Optional[Tuple[float, float]] = Field(default=None, description="Parameter value range for numeric parameters.") enumeration: Optional[List[EnumerationMember]] = Field(default=None, description="Parameter value choices for enumeration parameters.") - default_value: Optional[Value] = Field(default=None, description="Parameter default value.", serialization_alias="defaultValue", validation_alias=AliasChoices("default_value", "defaultValue")) + default_value: Optional[str | float | int | bool | NDArray | List[Any] | Dict[str, Any] | Image.Image | BytesIO] = Field(default=None, description="Parameter default value.", serialization_alias="defaultValue", validation_alias=AliasChoices("default_value", "defaultValue")) value_schema: Optional[dict] = Field(default=None, description="Parameter JSON schema. This is only populated for `list` and `dict` parameters.", serialization_alias="schema", validation_alias=AliasChoices("schema", "value_schema")) + model_config = ConfigDict(arbitrary_types_allowed=True) class Signature (BaseModel): """ @@ -95,7 +92,6 @@ class Predictor (BaseModel): tag (str): Predictor tag. owner (Profile): Predictor owner. name (str): Predictor name. - type (PredictorType): Predictor type. status (PredictorStatus): Predictor status. access (AccessMode): Predictor access. signature (Signature): Predictor signature. @@ -103,13 +99,11 @@ class Predictor (BaseModel): description (str): Predictor description. card (str): Predictor card. media (str): Predictor media URL. - acceleration (Acceleration): Predictor acceleration. This only applies to cloud predictors. license (str): Predictor license URL. """ tag: str = Field(description="Predictor tag.") owner: Profile = Field(description="Predictor owner.") name: str = Field(description="Predictor name.") - type: PredictorType = Field(description="Predictor type.") status: PredictorStatus = Field(description="Predictor status.") access: AccessMode = Field(description="Predictor access.") signature: Signature = Field(description="Predictor signature.") @@ -117,5 +111,4 @@ class Predictor (BaseModel): description: Optional[str] = Field(default=None, description="Predictor description.") card: Optional[str] = Field(default=None, description="Predictor card.") media: Optional[str] = Field(default=None, description="Predictor media URL.") - acceleration: Optional[Acceleration] = Field(default=None, description="Predictor acceleration. This only applies to cloud predictors.") license: Optional[str] = Field(default=None, description="Predictor license URL.") \ No newline at end of file diff --git a/fxn/types/value.py b/fxn/types/value.py deleted file mode 100644 index 6517f0e..0000000 --- a/fxn/types/value.py +++ /dev/null @@ -1,22 +0,0 @@ -# -# Function -# Copyright © 2024 NatML Inc. All Rights Reserved. -# - -from pydantic import BaseModel, Field -from typing import List, Optional, Union - -from .dtype import Dtype - -class Value (BaseModel): - """ - Prediction value. - - Members: - data (str): Value URL. This can be a web URL or a data URL. - type (Dtype): Value data type. - shape (list): Value shape. This is `None` if shape information is not available or applicable. - """ - data: Union[str, None] = Field(description="Value URL. This can be a web URL or a data URL.") - type: Dtype = Field(description="Value data type.") - shape: Optional[List[int]] = Field(default=None, description="Value shape. This is `None` if shape information is not available or applicable.") \ No newline at end of file diff --git a/fxnc.py b/fxnc.py index 380c091..dbabcba 100644 --- a/fxnc.py +++ b/fxnc.py @@ -14,6 +14,7 @@ def _download_fxnc (name: str, version: str, path: Path): url = f"https://cdn.fxn.ai/fxnc/{version}/{name}" response = get(url) response.raise_for_status() + path.parent.mkdir(parents=True, exist_ok=True) with open(path, "wb") as f: f.write(response.content) print(f"Wrote {name} {version} to path: {path}") @@ -24,20 +25,18 @@ def _get_latest_version () -> str: release = response.json() return release["tag_name"] -def main (): # CHECK # Linux +def main (): # INCOMPLETE # Linux args = parser.parse_args() version = args.version if args.version else _get_latest_version() - LIB_PATH_BASE = Path("fxn") / "libs" - _download_fxnc( - "Function-macos.dylib", - version, - LIB_PATH_BASE / "macos" / "Function.dylib" - ) - _download_fxnc( - "Function-win64.dll", - version, - LIB_PATH_BASE / "windows" / "Function.dll" - ) + LIB_PATH_BASE = Path("fxn") / "lib" + DOWNLOADS = [ + ("Function-macos-x86_64.dylib", LIB_PATH_BASE / "macos" / "x86_64" / "Function.dylib"), + ("Function-macos-arm64.dylib", LIB_PATH_BASE / "macos" / "arm64" / "Function.dylib"), + ("Function-win-x86_64.dll", LIB_PATH_BASE / "windows" / "x86_64" / "Function.dll"), + ("Function-win-arm64.dll", LIB_PATH_BASE / "windows" / "arm64" / "Function.dll"), + ] + for name, path in DOWNLOADS: + _download_fxnc(name, version, path) if __name__ == "__main__": main() \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index d9f2ad1..985243f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,11 +6,9 @@ [project] name = "fxn" dynamic = ["version"] -description = "Run on-device and cloud AI prediction functions in Python. Register at https://fxn.ai." +description = "Run prediction functions locally in Python. Register at https://fxn.ai." readme = "README.md" dependencies = [ - "aiohttp", - "magika", "numpy", "pillow", "pydantic>=2.0", @@ -18,7 +16,7 @@ dependencies = [ "rich", "typer" ] -requires-python = ">=3.9" +requires-python = ">=3.10" authors = [ { name = "NatML Inc.", email = "hi@fxn.ai" } ] license = { file = "LICENSE" } classifiers = [ @@ -53,9 +51,7 @@ include = ["fxn", "fxn*"] namespaces = false [tool.setuptools.package-data] -"fxn.libs.macos" = ["*.dylib"] -"fxn.libs.windows" = ["*.dll"] -"fxn.libs.linux" = ["*.so"] +"fxn.lib" = ["macos/*/*.dylib", "windows/*/*.dll", "linux/*/*.so"] [tool.setuptools.dynamic] version = { attr = "fxn.version.__version__" } \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index abadc9c..1902f61 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,3 @@ -aiohttp -magika numpy pillow requests