fal 1.3.1__py3-none-any.whl → 1.3.3__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.
Potentially problematic release.
This version of fal might be problematic. Click here for more details.
- fal/_fal_version.py +2 -2
- fal/api.py +2 -0
- fal/app.py +12 -0
- fal/apps.py +9 -0
- fal/cli/_utils.py +34 -0
- fal/cli/deploy.py +59 -9
- fal/cli/parser.py +11 -7
- fal/cli/run.py +11 -1
- fal/exceptions/__init__.py +6 -1
- fal/exceptions/_base.py +7 -0
- fal/files.py +81 -0
- fal/sdk.py +36 -0
- fal/utils.py +2 -1
- {fal-1.3.1.dist-info → fal-1.3.3.dist-info}/METADATA +3 -2
- {fal-1.3.1.dist-info → fal-1.3.3.dist-info}/RECORD +18 -16
- {fal-1.3.1.dist-info → fal-1.3.3.dist-info}/WHEEL +1 -1
- {fal-1.3.1.dist-info → fal-1.3.3.dist-info}/entry_points.txt +0 -0
- {fal-1.3.1.dist-info → fal-1.3.3.dist-info}/top_level.txt +0 -0
fal/_fal_version.py
CHANGED
fal/api.py
CHANGED
|
@@ -425,6 +425,7 @@ class FalServerlessHost(Host):
|
|
|
425
425
|
application_name: str | None = None,
|
|
426
426
|
application_auth_mode: Literal["public", "shared", "private"] | None = None,
|
|
427
427
|
metadata: dict[str, Any] | None = None,
|
|
428
|
+
deployment_strategy: Literal["recreate", "rolling"] = "recreate",
|
|
428
429
|
) -> str | None:
|
|
429
430
|
environment_options = options.environment.copy()
|
|
430
431
|
environment_options.setdefault("python_version", active_python())
|
|
@@ -477,6 +478,7 @@ class FalServerlessHost(Host):
|
|
|
477
478
|
application_auth_mode=application_auth_mode,
|
|
478
479
|
machine_requirements=machine_requirements,
|
|
479
480
|
metadata=metadata,
|
|
481
|
+
deployment_strategy=deployment_strategy,
|
|
480
482
|
):
|
|
481
483
|
for log in partial_result.logs:
|
|
482
484
|
self._log_printer.print(log)
|
fal/app.py
CHANGED
|
@@ -17,6 +17,7 @@ from fastapi import FastAPI
|
|
|
17
17
|
import fal.api
|
|
18
18
|
from fal._serialization import include_modules_from
|
|
19
19
|
from fal.api import RouteSignature
|
|
20
|
+
from fal.exceptions import RequestCancelledException
|
|
20
21
|
from fal.logging import get_logger
|
|
21
22
|
from fal.toolkit.file.providers import fal as fal_provider_module
|
|
22
23
|
|
|
@@ -272,6 +273,17 @@ class App(fal.api.BaseServable):
|
|
|
272
273
|
)
|
|
273
274
|
return response
|
|
274
275
|
|
|
276
|
+
@app.exception_handler(RequestCancelledException)
|
|
277
|
+
async def value_error_exception_handler(
|
|
278
|
+
request, exc: RequestCancelledException
|
|
279
|
+
):
|
|
280
|
+
from fastapi.responses import JSONResponse
|
|
281
|
+
|
|
282
|
+
# A 499 status code is not an officially recognized HTTP status code,
|
|
283
|
+
# but it is sometimes used by servers to indicate that a client has closed
|
|
284
|
+
# the connection without receiving a response
|
|
285
|
+
return JSONResponse({"detail": str(exc)}, 499)
|
|
286
|
+
|
|
275
287
|
def _add_extra_routes(self, app: FastAPI):
|
|
276
288
|
@app.get("/health")
|
|
277
289
|
def health():
|
fal/apps.py
CHANGED
|
@@ -97,6 +97,15 @@ class RequestHandle:
|
|
|
97
97
|
else:
|
|
98
98
|
raise ValueError(f"Unknown status: {data['status']}")
|
|
99
99
|
|
|
100
|
+
def cancel(self) -> None:
|
|
101
|
+
"""Cancel an async inference request."""
|
|
102
|
+
url = (
|
|
103
|
+
_QUEUE_URL_FORMAT.format(app_id=self.app_id)
|
|
104
|
+
+ f"/requests/{self.request_id}/cancel"
|
|
105
|
+
)
|
|
106
|
+
response = _HTTP_CLIENT.put(url, headers=self._creds.to_headers())
|
|
107
|
+
response.raise_for_status()
|
|
108
|
+
|
|
100
109
|
def iter_events(
|
|
101
110
|
self,
|
|
102
111
|
*,
|
fal/cli/_utils.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from fal.files import find_pyproject_toml, parse_pyproject_toml
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def is_app_name(app_ref: tuple[str, str | None]) -> bool:
|
|
7
|
+
is_single_file = app_ref[1] is None
|
|
8
|
+
is_python_file = app_ref[0].endswith(".py")
|
|
9
|
+
|
|
10
|
+
return is_single_file and not is_python_file
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_app_data_from_toml(app_name):
|
|
14
|
+
toml_path = find_pyproject_toml()
|
|
15
|
+
|
|
16
|
+
if toml_path is None:
|
|
17
|
+
raise ValueError("No pyproject.toml file found.")
|
|
18
|
+
|
|
19
|
+
fal_data = parse_pyproject_toml(toml_path)
|
|
20
|
+
apps = fal_data.get("apps", {})
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
app_data = apps[app_name]
|
|
24
|
+
except KeyError:
|
|
25
|
+
raise ValueError(f"App {app_name} not found in pyproject.toml")
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
app_ref = app_data["ref"]
|
|
29
|
+
except KeyError:
|
|
30
|
+
raise ValueError(f"App {app_name} does not have a ref key in pyproject.toml")
|
|
31
|
+
|
|
32
|
+
app_auth = app_data.get("auth", "private")
|
|
33
|
+
|
|
34
|
+
return app_ref, app_auth
|
fal/cli/deploy.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
from collections import namedtuple
|
|
3
3
|
from pathlib import Path
|
|
4
|
+
from typing import Optional, Union
|
|
4
5
|
|
|
6
|
+
from ._utils import get_app_data_from_toml, is_app_name
|
|
5
7
|
from .parser import FalClientParser, RefAction
|
|
6
8
|
|
|
7
9
|
User = namedtuple("User", ["user_id", "username"])
|
|
@@ -60,11 +62,13 @@ def _get_user() -> User:
|
|
|
60
62
|
raise FalServerlessError(f"Could not parse the user data: {e}")
|
|
61
63
|
|
|
62
64
|
|
|
63
|
-
def
|
|
65
|
+
def _deploy_from_reference(
|
|
66
|
+
app_ref: tuple[Optional[Union[Path, str]], ...], app_name: str, auth: str, args
|
|
67
|
+
):
|
|
64
68
|
from fal.api import FalServerlessError, FalServerlessHost
|
|
65
69
|
from fal.utils import load_function_from
|
|
66
70
|
|
|
67
|
-
file_path, func_name =
|
|
71
|
+
file_path, func_name = app_ref
|
|
68
72
|
if file_path is None:
|
|
69
73
|
# Try to find a python file in the current directory
|
|
70
74
|
options = list(Path(".").glob("*.py"))
|
|
@@ -77,24 +81,27 @@ def _deploy(args):
|
|
|
77
81
|
)
|
|
78
82
|
|
|
79
83
|
[file_path] = options
|
|
80
|
-
file_path = str(file_path)
|
|
84
|
+
file_path = str(file_path) # type: ignore
|
|
81
85
|
|
|
82
86
|
user = _get_user()
|
|
83
87
|
host = FalServerlessHost(args.host)
|
|
84
88
|
loaded = load_function_from(
|
|
85
89
|
host,
|
|
86
|
-
file_path,
|
|
87
|
-
func_name,
|
|
90
|
+
file_path, # type: ignore
|
|
91
|
+
func_name, # type: ignore
|
|
88
92
|
)
|
|
89
93
|
isolated_function = loaded.function
|
|
90
|
-
app_name =
|
|
91
|
-
app_auth =
|
|
94
|
+
app_name = app_name or loaded.app_name # type: ignore
|
|
95
|
+
app_auth = auth or loaded.app_auth or "private"
|
|
96
|
+
deployment_strategy = args.strategy or "default"
|
|
97
|
+
|
|
92
98
|
app_id = host.register(
|
|
93
99
|
func=isolated_function.func,
|
|
94
100
|
options=isolated_function.options,
|
|
95
101
|
application_name=app_name,
|
|
96
102
|
application_auth_mode=app_auth,
|
|
97
103
|
metadata=isolated_function.options.host.get("metadata", {}),
|
|
104
|
+
deployment_strategy=deployment_strategy,
|
|
98
105
|
)
|
|
99
106
|
|
|
100
107
|
if app_id:
|
|
@@ -119,6 +126,26 @@ def _deploy(args):
|
|
|
119
126
|
)
|
|
120
127
|
|
|
121
128
|
|
|
129
|
+
def _deploy(args):
|
|
130
|
+
# my-app
|
|
131
|
+
if is_app_name(args.app_ref):
|
|
132
|
+
# we do not allow --app-name and --auth to be used with app name
|
|
133
|
+
if args.app_name or args.auth:
|
|
134
|
+
raise ValueError("Cannot use --app-name or --auth with app name reference.")
|
|
135
|
+
|
|
136
|
+
app_name = args.app_ref[0]
|
|
137
|
+
app_ref, app_auth = get_app_data_from_toml(app_name)
|
|
138
|
+
file_path, func_name = RefAction.split_ref(app_ref)
|
|
139
|
+
|
|
140
|
+
# path/to/myfile.py::MyApp
|
|
141
|
+
else:
|
|
142
|
+
file_path, func_name = args.app_ref
|
|
143
|
+
app_name = args.app_name
|
|
144
|
+
app_auth = args.auth
|
|
145
|
+
|
|
146
|
+
_deploy_from_reference((file_path, func_name), app_name, app_auth, args)
|
|
147
|
+
|
|
148
|
+
|
|
122
149
|
def add_parser(main_subparsers, parents):
|
|
123
150
|
from fal.sdk import ALIAS_AUTH_MODES
|
|
124
151
|
|
|
@@ -127,14 +154,22 @@ def add_parser(main_subparsers, parents):
|
|
|
127
154
|
raise argparse.ArgumentTypeError(f"{option} is not a auth option")
|
|
128
155
|
return option
|
|
129
156
|
|
|
130
|
-
deploy_help =
|
|
157
|
+
deploy_help = (
|
|
158
|
+
"Deploy a fal application. "
|
|
159
|
+
"If no app reference is provided, the command will look for a "
|
|
160
|
+
"pyproject.toml file with a [tool.fal.apps] section and deploy the "
|
|
161
|
+
"application specified with the provided app name."
|
|
162
|
+
)
|
|
163
|
+
|
|
131
164
|
epilog = (
|
|
132
165
|
"Examples:\n"
|
|
133
166
|
" fal deploy\n"
|
|
134
167
|
" fal deploy path/to/myfile.py\n"
|
|
135
168
|
" fal deploy path/to/myfile.py::MyApp\n"
|
|
136
169
|
" fal deploy path/to/myfile.py::MyApp --app-name myapp --auth public\n"
|
|
170
|
+
" fal deploy my-app\n"
|
|
137
171
|
)
|
|
172
|
+
|
|
138
173
|
parser = main_subparsers.add_parser(
|
|
139
174
|
"deploy",
|
|
140
175
|
parents=[*parents, FalClientParser(add_help=False)],
|
|
@@ -142,21 +177,36 @@ def add_parser(main_subparsers, parents):
|
|
|
142
177
|
help=deploy_help,
|
|
143
178
|
epilog=epilog,
|
|
144
179
|
)
|
|
180
|
+
|
|
145
181
|
parser.add_argument(
|
|
146
182
|
"app_ref",
|
|
147
183
|
nargs="?",
|
|
148
184
|
action=RefAction,
|
|
149
185
|
help=(
|
|
150
|
-
"Application reference.
|
|
186
|
+
"Application reference. Either a file path or a file path and a "
|
|
187
|
+
"function name separated by '::'. If no reference is provided, the "
|
|
188
|
+
"command will look for a pyproject.toml file with a [tool.fal.apps] "
|
|
189
|
+
"section and deploy the application specified with the provided app name.\n"
|
|
190
|
+
"File path example: path/to/myfile.py::MyApp\n"
|
|
191
|
+
"App name example: my-app\n"
|
|
151
192
|
),
|
|
152
193
|
)
|
|
194
|
+
|
|
153
195
|
parser.add_argument(
|
|
154
196
|
"--app-name",
|
|
155
197
|
help="Application name to deploy with.",
|
|
156
198
|
)
|
|
199
|
+
|
|
157
200
|
parser.add_argument(
|
|
158
201
|
"--auth",
|
|
159
202
|
type=valid_auth_option,
|
|
160
203
|
help="Application authentication mode (private, public).",
|
|
161
204
|
)
|
|
205
|
+
parser.add_argument(
|
|
206
|
+
"--strategy",
|
|
207
|
+
choices=["default", "rolling"],
|
|
208
|
+
help="Deployment strategy.",
|
|
209
|
+
default="default",
|
|
210
|
+
)
|
|
211
|
+
|
|
162
212
|
parser.set_defaults(func=_deploy)
|
fal/cli/parser.py
CHANGED
|
@@ -14,14 +14,18 @@ class RefAction(argparse.Action):
|
|
|
14
14
|
kwargs.setdefault("default", (None, None))
|
|
15
15
|
super().__init__(*args, **kwargs)
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
@classmethod
|
|
18
|
+
def split_ref(cls, value):
|
|
19
|
+
if isinstance(value, tuple):
|
|
20
|
+
return value
|
|
21
|
+
|
|
22
|
+
if value.find("::") > 1:
|
|
23
|
+
return value.split("::", 1)
|
|
24
24
|
|
|
25
|
+
return value, None
|
|
26
|
+
|
|
27
|
+
def __call__(self, parser, args, values, option_string=None): # noqa: ARG002
|
|
28
|
+
file_path, obj_path = self.split_ref(values)
|
|
25
29
|
setattr(args, self.dest, (file_path, obj_path))
|
|
26
30
|
|
|
27
31
|
|
fal/cli/run.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from ._utils import get_app_data_from_toml, is_app_name
|
|
1
2
|
from .parser import FalClientParser, RefAction
|
|
2
3
|
|
|
3
4
|
|
|
@@ -6,7 +7,16 @@ def _run(args):
|
|
|
6
7
|
from fal.utils import load_function_from
|
|
7
8
|
|
|
8
9
|
host = FalServerlessHost(args.host)
|
|
9
|
-
|
|
10
|
+
|
|
11
|
+
if is_app_name(args.func_ref):
|
|
12
|
+
app_name = args.func_ref[0]
|
|
13
|
+
app_ref, _ = get_app_data_from_toml(app_name)
|
|
14
|
+
file_path, func_name = RefAction.split_ref(app_ref)
|
|
15
|
+
else:
|
|
16
|
+
file_path, func_name = args.func_ref
|
|
17
|
+
|
|
18
|
+
loaded = load_function_from(host, file_path, func_name)
|
|
19
|
+
|
|
10
20
|
isolated_function = loaded.function
|
|
11
21
|
# let our exc handlers handle UserFunctionException
|
|
12
22
|
isolated_function.reraise = False
|
fal/exceptions/__init__.py
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from ._base import
|
|
3
|
+
from ._base import (
|
|
4
|
+
AppException, # noqa: F401
|
|
5
|
+
FalServerlessException, # noqa: F401
|
|
6
|
+
FieldException, # noqa: F401
|
|
7
|
+
RequestCancelledException, # noqa: F401
|
|
8
|
+
)
|
|
4
9
|
from ._cuda import CUDAOutOfMemoryException # noqa: F401
|
fal/exceptions/_base.py
CHANGED
|
@@ -49,3 +49,10 @@ class FieldException(FalServerlessException):
|
|
|
49
49
|
}
|
|
50
50
|
]
|
|
51
51
|
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class RequestCancelledException(FalServerlessException):
|
|
56
|
+
"""Exception raised when the request is cancelled by the client."""
|
|
57
|
+
|
|
58
|
+
message: str = "Request cancelled by the client."
|
fal/files.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from functools import lru_cache
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any, Dict, Optional, Sequence, Tuple, Union
|
|
4
|
+
|
|
5
|
+
import tomli
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@lru_cache
|
|
9
|
+
def _load_toml(path: Union[Path, str]) -> Dict[str, Any]:
|
|
10
|
+
with open(path, "rb") as f:
|
|
11
|
+
return tomli.load(f)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@lru_cache
|
|
15
|
+
def _cached_resolve(path: Path) -> Path:
|
|
16
|
+
return path.resolve()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@lru_cache
|
|
20
|
+
def find_project_root(srcs: Optional[Sequence[str]]) -> Tuple[Path, str]:
|
|
21
|
+
"""Return a directory containing .git, or pyproject.toml.
|
|
22
|
+
|
|
23
|
+
That directory will be a common parent of all files and directories
|
|
24
|
+
passed in `srcs`.
|
|
25
|
+
|
|
26
|
+
If no directory in the tree contains a marker that would specify it's the
|
|
27
|
+
project root, the root of the file system is returned.
|
|
28
|
+
|
|
29
|
+
Returns a two-tuple with the first element as the project root path and
|
|
30
|
+
the second element as a string describing the method by which the
|
|
31
|
+
project root was discovered.
|
|
32
|
+
"""
|
|
33
|
+
if not srcs:
|
|
34
|
+
srcs = [str(_cached_resolve(Path.cwd()))]
|
|
35
|
+
|
|
36
|
+
path_srcs = [_cached_resolve(Path(Path.cwd(), src)) for src in srcs]
|
|
37
|
+
|
|
38
|
+
# A list of lists of parents for each 'src'. 'src' is included as a
|
|
39
|
+
# "parent" of itself if it is a directory
|
|
40
|
+
src_parents = [
|
|
41
|
+
list(path.parents) + ([path] if path.is_dir() else []) for path in path_srcs
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
common_base = max(
|
|
45
|
+
set.intersection(*(set(parents) for parents in src_parents)),
|
|
46
|
+
key=lambda path: path.parts,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
for directory in (common_base, *common_base.parents):
|
|
50
|
+
if (directory / ".git").exists():
|
|
51
|
+
return directory, ".git directory"
|
|
52
|
+
|
|
53
|
+
if (directory / "pyproject.toml").is_file():
|
|
54
|
+
pyproject_toml = _load_toml(directory / "pyproject.toml")
|
|
55
|
+
if "fal" in pyproject_toml.get("tool", {}):
|
|
56
|
+
return directory, "pyproject.toml"
|
|
57
|
+
|
|
58
|
+
return directory, "file system root"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def find_pyproject_toml(
|
|
62
|
+
path_search_start: Optional[Tuple[str, ...]] = None,
|
|
63
|
+
) -> Optional[str]:
|
|
64
|
+
"""Find the absolute filepath to a pyproject.toml if it exists"""
|
|
65
|
+
path_project_root, _ = find_project_root(path_search_start)
|
|
66
|
+
path_pyproject_toml = path_project_root / "pyproject.toml"
|
|
67
|
+
|
|
68
|
+
if path_pyproject_toml.is_file():
|
|
69
|
+
return str(path_pyproject_toml)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def parse_pyproject_toml(path_config: str) -> Dict[str, Any]:
|
|
73
|
+
"""Parse a pyproject toml file, pulling out relevant parts for fal.
|
|
74
|
+
|
|
75
|
+
If parsing fails, will raise a tomli.TOMLDecodeError.
|
|
76
|
+
"""
|
|
77
|
+
pyproject_toml = _load_toml(path_config)
|
|
78
|
+
config: Dict[str, Any] = pyproject_toml.get("tool", {}).get("fal", {})
|
|
79
|
+
config = {k.replace("--", "").replace("-", "_"): v for k, v in config.items()}
|
|
80
|
+
|
|
81
|
+
return config
|
fal/sdk.py
CHANGED
|
@@ -275,6 +275,33 @@ class KeyScope(enum.Enum):
|
|
|
275
275
|
raise ValueError(f"Unknown KeyScope: {proto}")
|
|
276
276
|
|
|
277
277
|
|
|
278
|
+
class DeploymentStrategy(enum.Enum):
|
|
279
|
+
RECREATE = "recreate"
|
|
280
|
+
ROLLING = "rolling"
|
|
281
|
+
|
|
282
|
+
@staticmethod
|
|
283
|
+
def from_proto(
|
|
284
|
+
proto: isolate_proto.DeploymentStrategy.ValueType | None,
|
|
285
|
+
) -> DeploymentStrategy:
|
|
286
|
+
if proto is None:
|
|
287
|
+
return DeploymentStrategy.RECREATE
|
|
288
|
+
|
|
289
|
+
if proto is isolate_proto.DeploymentStrategy.RECREATE:
|
|
290
|
+
return DeploymentStrategy.RECREATE
|
|
291
|
+
elif proto is isolate_proto.DeploymentStrategy.ROLLING:
|
|
292
|
+
return DeploymentStrategy.ROLLING
|
|
293
|
+
else:
|
|
294
|
+
raise ValueError(f"Unknown DeploymentStrategy: {proto}")
|
|
295
|
+
|
|
296
|
+
def to_proto(self) -> isolate_proto.DeploymentStrategy.ValueType:
|
|
297
|
+
if self is DeploymentStrategy.RECREATE:
|
|
298
|
+
return isolate_proto.DeploymentStrategy.RECREATE
|
|
299
|
+
elif self is DeploymentStrategy.ROLLING:
|
|
300
|
+
return isolate_proto.DeploymentStrategy.ROLLING
|
|
301
|
+
else:
|
|
302
|
+
raise ValueError(f"Unknown DeploymentStrategy: {self}")
|
|
303
|
+
|
|
304
|
+
|
|
278
305
|
@from_grpc.register(isolate_proto.ApplicationInfo)
|
|
279
306
|
def _from_grpc_application_info(
|
|
280
307
|
message: isolate_proto.ApplicationInfo,
|
|
@@ -457,6 +484,7 @@ class FalServerlessConnection:
|
|
|
457
484
|
serialization_method: str = _DEFAULT_SERIALIZATION_METHOD,
|
|
458
485
|
machine_requirements: MachineRequirements | None = None,
|
|
459
486
|
metadata: dict[str, Any] | None = None,
|
|
487
|
+
deployment_strategy: Literal["recreate", "rolling"] = "recreate",
|
|
460
488
|
) -> Iterator[isolate_proto.RegisterApplicationResult]:
|
|
461
489
|
wrapped_function = to_serialized_object(function, serialization_method)
|
|
462
490
|
if machine_requirements:
|
|
@@ -488,6 +516,13 @@ class FalServerlessConnection:
|
|
|
488
516
|
struct_metadata = isolate_proto.Struct()
|
|
489
517
|
struct_metadata.update(metadata)
|
|
490
518
|
|
|
519
|
+
if deployment_strategy == "default":
|
|
520
|
+
deployment_strategy = "recreate"
|
|
521
|
+
|
|
522
|
+
deployment_strategy_proto = DeploymentStrategy[
|
|
523
|
+
deployment_strategy.upper()
|
|
524
|
+
].to_proto()
|
|
525
|
+
|
|
491
526
|
request = isolate_proto.RegisterApplicationRequest(
|
|
492
527
|
function=wrapped_function,
|
|
493
528
|
environments=environments,
|
|
@@ -495,6 +530,7 @@ class FalServerlessConnection:
|
|
|
495
530
|
application_name=application_name,
|
|
496
531
|
auth_mode=auth_mode,
|
|
497
532
|
metadata=struct_metadata,
|
|
533
|
+
deployment_strategy=deployment_strategy_proto,
|
|
498
534
|
)
|
|
499
535
|
for partial_result in self.stub.RegisterApplication(request):
|
|
500
536
|
yield from_grpc(partial_result)
|
fal/utils.py
CHANGED
|
@@ -55,10 +55,11 @@ def load_function_from(
|
|
|
55
55
|
fal._serialization.include_package_from_path(file_path)
|
|
56
56
|
|
|
57
57
|
target = module[function_name]
|
|
58
|
-
endpoints =
|
|
58
|
+
endpoints = ["/"]
|
|
59
59
|
if isinstance(target, type) and issubclass(target, App):
|
|
60
60
|
app_name = target.app_name
|
|
61
61
|
app_auth = target.app_auth
|
|
62
|
+
endpoints = target.get_endpoints() or ["/"]
|
|
62
63
|
target = wrap_app(target, host=host)
|
|
63
64
|
|
|
64
65
|
if not isinstance(target, IsolatedFunction):
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: fal
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.3
|
|
4
4
|
Summary: fal is an easy-to-use Serverless Python Framework
|
|
5
5
|
Author: Features & Labels <support@fal.ai>
|
|
6
6
|
Requires-Python: >=3.8
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
Requires-Dist: isolate[build]<1.14.0,>=0.13.0
|
|
9
|
-
Requires-Dist: isolate-proto==0.5.
|
|
9
|
+
Requires-Dist: isolate-proto==0.5.3
|
|
10
10
|
Requires-Dist: grpcio==1.64.0
|
|
11
11
|
Requires-Dist: dill==0.3.7
|
|
12
12
|
Requires-Dist: cloudpickle==3.0.0
|
|
@@ -35,6 +35,7 @@ Requires-Dist: pillow<11,>=10.2.0
|
|
|
35
35
|
Requires-Dist: pyjwt[crypto]<3,>=2.8.0
|
|
36
36
|
Requires-Dist: uvicorn<1,>=0.29.0
|
|
37
37
|
Requires-Dist: cookiecutter
|
|
38
|
+
Requires-Dist: tomli
|
|
38
39
|
Requires-Dist: importlib-metadata>=4.4; python_version < "3.10"
|
|
39
40
|
Provides-Extra: dev
|
|
40
41
|
Requires-Dist: fal[test]; extra == "dev"
|
|
@@ -1,39 +1,41 @@
|
|
|
1
1
|
fal/__init__.py,sha256=wXs1G0gSc7ZK60-bHe-B2m0l_sA6TrFk4BxY0tMoLe8,784
|
|
2
2
|
fal/__main__.py,sha256=4JMK66Wj4uLZTKbF-sT3LAxOsr6buig77PmOkJCRRxw,83
|
|
3
|
-
fal/_fal_version.py,sha256=
|
|
3
|
+
fal/_fal_version.py,sha256=VriGPi1kVXIBM0YGAuhpE803XR-FNq1JvTW1Kz2us08,411
|
|
4
4
|
fal/_serialization.py,sha256=rD2YiSa8iuzCaZohZwN_MPEB-PpSKbWRDeaIDpTEjyY,7653
|
|
5
5
|
fal/_version.py,sha256=EBGqrknaf1WygENX-H4fBefLvHryvJBBGtVJetaB0NY,266
|
|
6
|
-
fal/api.py,sha256=
|
|
7
|
-
fal/app.py,sha256=
|
|
8
|
-
fal/apps.py,sha256=
|
|
6
|
+
fal/api.py,sha256=xOPRO8-Y-7tgab_yKkQ2Lh_n4l8av5zd7srs9PCgJ5U,42020
|
|
7
|
+
fal/app.py,sha256=mBBwTi6IldCEN-IEeznpEwyjUydqB4HkVCu49J3Vsfw,17639
|
|
8
|
+
fal/apps.py,sha256=lge7-HITzI20l1oXdlkAzqxdMVtXRfnACIylKRWgCNQ,7151
|
|
9
9
|
fal/container.py,sha256=V7riyyq8AZGwEX9QaqRQDZyDN_bUKeRKV1OOZArXjL0,622
|
|
10
|
+
fal/files.py,sha256=QgfYfMKmNobMPufrAP_ga1FKcIAlSbw18Iar1-0qepo,2650
|
|
10
11
|
fal/flags.py,sha256=oWN_eidSUOcE9wdPK_77si3A1fpgOC0UEERPsvNLIMc,842
|
|
11
12
|
fal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
13
|
fal/rest_client.py,sha256=kGBGmuyHfX1lR910EoKCYPjsyU8MdXawT_cW2q8Sajc,568
|
|
13
|
-
fal/sdk.py,sha256=
|
|
14
|
+
fal/sdk.py,sha256=ND3nwZRQDEIC130zbTfTaP0fYpR2KEkQ10e3zvOXylQ,21391
|
|
14
15
|
fal/sync.py,sha256=ZuIJA2-hTPNANG9B_NNJZUsO68EIdTH0dc9MzeVE2VU,4340
|
|
15
|
-
fal/utils.py,sha256=
|
|
16
|
+
fal/utils.py,sha256=9q_QrQBlQN3nZYA1kEGRfhJWi4RjnO4H1uQswfaei9w,2146
|
|
16
17
|
fal/workflows.py,sha256=jx3tGy2R7cN6lLvOzT6lhhlcjmiq64iZls2smVrmQj0,14657
|
|
17
18
|
fal/auth/__init__.py,sha256=r8iA2-5ih7-Fik3gEC4HEWNFbGoxpYnXpZu1icPIoS0,3561
|
|
18
19
|
fal/auth/auth0.py,sha256=rSG1mgH-QGyKfzd7XyAaj1AYsWt-ho8Y_LZ-FUVWzh4,5421
|
|
19
20
|
fal/auth/local.py,sha256=sndkM6vKpeVny6NHTacVlTbiIFqaksOmw0Viqs_RN1U,1790
|
|
20
21
|
fal/cli/__init__.py,sha256=padK4o0BFqq61kxAA1qQ0jYr2SuhA2mf90B3AaRkmJA,37
|
|
22
|
+
fal/cli/_utils.py,sha256=DSfHZ6qna4jLffs-N4F3XbV-9ydF_OSlP2-GRounWsY,911
|
|
21
23
|
fal/cli/apps.py,sha256=-DDp-Gvxz5kHho5YjAhbri8vOny_9cftAI_wP2KR5nU,8175
|
|
22
24
|
fal/cli/auth.py,sha256=--MhfHGwxmtHbRkGioyn1prKn_U-pBzbz0G_QeZou-U,1352
|
|
23
25
|
fal/cli/create.py,sha256=a8WDq-nJLFTeoIXqpb5cr7GR7YR9ZZrQCawNm34KXXE,627
|
|
24
26
|
fal/cli/debug.py,sha256=u_urnyFzSlNnrq93zz_GXE9FX4VyVxDoamJJyrZpFI0,1312
|
|
25
|
-
fal/cli/deploy.py,sha256=
|
|
27
|
+
fal/cli/deploy.py,sha256=JCTQRNzbPt7Bn7lR8byJ38Ff-vQ2BQoSdzmdp9OlF3A,6790
|
|
26
28
|
fal/cli/doctor.py,sha256=U4ne9LX5gQwNblsYQ27XdO8AYDgbYjTO39EtxhwexRM,983
|
|
27
29
|
fal/cli/keys.py,sha256=trDpA3LJu9S27qE_K8Hr6fKLK4vwVzbxUHq8TFrV4pw,3157
|
|
28
30
|
fal/cli/main.py,sha256=_Wh_DQc02qwh-ZN7v41lZm0lDR1WseViXVOcqUlyWLg,2009
|
|
29
|
-
fal/cli/parser.py,sha256=
|
|
30
|
-
fal/cli/run.py,sha256=
|
|
31
|
+
fal/cli/parser.py,sha256=edCqFWYAQSOhrxeEK9BtFRlTEUAlG2JUDjS_vhZ_nHE,2868
|
|
32
|
+
fal/cli/run.py,sha256=uscbLBfTe8-UAbqh8h1iWuGD_G9UNNrj3k8bF5rtzy4,1201
|
|
31
33
|
fal/cli/secrets.py,sha256=740msFm7d41HruudlcfqUXlFl53N-WmChsQP9B9M9Po,2572
|
|
32
34
|
fal/console/__init__.py,sha256=ernZ4bzvvliQh5SmrEqQ7lA5eVcbw6Ra2jalKtA7dxg,132
|
|
33
35
|
fal/console/icons.py,sha256=De9MfFaSkO2Lqfne13n3PrYfTXJVIzYZVqYn5BWsdrA,108
|
|
34
36
|
fal/console/ux.py,sha256=KMQs3UHQvVHDxDQQqlot-WskVKoMQXOE3jiVkkfmIMY,356
|
|
35
|
-
fal/exceptions/__init__.py,sha256=
|
|
36
|
-
fal/exceptions/_base.py,sha256=
|
|
37
|
+
fal/exceptions/__init__.py,sha256=m2okJEpax11mnwmoqO_pCGtbt-FvzKiiuMhKo2ok-_8,270
|
|
38
|
+
fal/exceptions/_base.py,sha256=LwzpMaW_eYQEC5s26h2qGXbNA-S4bOqC8s-bMCX6HjE,1491
|
|
37
39
|
fal/exceptions/_cuda.py,sha256=q5EPFYEb7Iyw03cHrQlRHnH5xOvjwTwQdM6a9N3GB8k,1494
|
|
38
40
|
fal/exceptions/auth.py,sha256=gxRago5coI__vSIcdcsqhhq1lRPkvCnwPAueIaXTAdw,329
|
|
39
41
|
fal/logging/__init__.py,sha256=snqprf7-sKw6oAATS_Yxklf-a3XhLg0vIHICPwLp6TM,1583
|
|
@@ -123,8 +125,8 @@ openapi_fal_rest/models/workflow_node_type.py,sha256=-FzyeY2bxcNmizKbJI8joG7byRi
|
|
|
123
125
|
openapi_fal_rest/models/workflow_schema.py,sha256=4K5gsv9u9pxx2ItkffoyHeNjBBYf6ur5bN4m_zePZNY,2019
|
|
124
126
|
openapi_fal_rest/models/workflow_schema_input.py,sha256=2OkOXWHTNsCXHWS6EGDFzcJKkW5FIap-2gfO233EvZQ,1191
|
|
125
127
|
openapi_fal_rest/models/workflow_schema_output.py,sha256=EblwSPAGfWfYVWw_WSSaBzQVju296is9o28rMBAd0mc,1196
|
|
126
|
-
fal-1.3.
|
|
127
|
-
fal-1.3.
|
|
128
|
-
fal-1.3.
|
|
129
|
-
fal-1.3.
|
|
130
|
-
fal-1.3.
|
|
128
|
+
fal-1.3.3.dist-info/METADATA,sha256=49757swSphQt8uqSMj5h83T79CeFwOWi0J0iDQHIKe8,3787
|
|
129
|
+
fal-1.3.3.dist-info/WHEEL,sha256=UvcQYKBHoFqaQd6LKyqHw9fxEolWLQnlzP0h_LgJAfI,91
|
|
130
|
+
fal-1.3.3.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
|
|
131
|
+
fal-1.3.3.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
|
|
132
|
+
fal-1.3.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|