fal 1.0.7__py3-none-any.whl → 1.1.0__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 +116 -29
- fal/app.py +63 -1
- fal/cli/deploy.py +18 -8
- fal/cli/doctor.py +37 -0
- fal/cli/main.py +2 -2
- fal/sdk.py +6 -2
- fal/toolkit/file/providers/fal.py +1 -0
- fal/workflows.py +1 -1
- {fal-1.0.7.dist-info → fal-1.1.0.dist-info}/METADATA +2 -1
- {fal-1.0.7.dist-info → fal-1.1.0.dist-info}/RECORD +49 -27
- {fal-1.0.7.dist-info → fal-1.1.0.dist-info}/WHEEL +1 -1
- openapi_fal_rest/api/comfy/__init__.py +0 -0
- openapi_fal_rest/api/comfy/create_workflow.py +172 -0
- openapi_fal_rest/api/comfy/delete_workflow.py +175 -0
- openapi_fal_rest/api/comfy/get_workflow.py +181 -0
- openapi_fal_rest/api/comfy/list_user_workflows.py +189 -0
- openapi_fal_rest/api/comfy/update_workflow.py +198 -0
- openapi_fal_rest/api/users/__init__.py +0 -0
- openapi_fal_rest/api/users/get_current_user.py +143 -0
- openapi_fal_rest/api/workflows/{create_or_update_workflow_workflows_post.py → create_workflow.py} +4 -4
- openapi_fal_rest/api/workflows/{get_workflows_workflows_get.py → list_user_workflows.py} +4 -4
- openapi_fal_rest/api/workflows/update_workflow.py +198 -0
- openapi_fal_rest/models/__init__.py +32 -10
- openapi_fal_rest/models/comfy_workflow_detail.py +109 -0
- openapi_fal_rest/models/comfy_workflow_item.py +88 -0
- openapi_fal_rest/models/comfy_workflow_schema.py +119 -0
- openapi_fal_rest/models/{execute_workflow_workflows_user_id_workflow_name_post_json_body_type_0.py → comfy_workflow_schema_extra_data.py} +5 -5
- openapi_fal_rest/models/{execute_workflow_workflows_user_id_workflow_name_post_response_200_type_0.py → comfy_workflow_schema_fal_inputs.py} +5 -5
- openapi_fal_rest/models/comfy_workflow_schema_fal_inputs_dev_info.py +44 -0
- openapi_fal_rest/models/{workflow_detail_contents_type_0.py → comfy_workflow_schema_prompt.py} +5 -5
- openapi_fal_rest/models/current_user.py +138 -0
- openapi_fal_rest/models/customer_details.py +8 -8
- openapi_fal_rest/models/lock_reason.py +3 -0
- openapi_fal_rest/models/page_comfy_workflow_item.py +107 -0
- openapi_fal_rest/models/team_role.py +10 -0
- openapi_fal_rest/models/typed_comfy_workflow.py +85 -0
- openapi_fal_rest/models/typed_comfy_workflow_update.py +95 -0
- openapi_fal_rest/models/typed_workflow_update.py +95 -0
- openapi_fal_rest/models/user_member.py +87 -0
- openapi_fal_rest/models/workflow_contents.py +20 -1
- openapi_fal_rest/models/workflow_contents_metadata.py +44 -0
- openapi_fal_rest/models/workflow_detail.py +18 -59
- openapi_fal_rest/models/workflow_detail_contents.py +44 -0
- openapi_fal_rest/models/workflow_item.py +19 -1
- openapi_fal_rest/api/workflows/execute_workflow_workflows_user_id_workflow_name_post.py +0 -268
- {fal-1.0.7.dist-info → fal-1.1.0.dist-info}/entry_points.txt +0 -0
- {fal-1.0.7.dist-info → fal-1.1.0.dist-info}/top_level.txt +0 -0
- /openapi_fal_rest/api/workflows/{delete_workflow_workflows_user_id_workflow_name_delete.py → delete_workflow.py} +0 -0
- /openapi_fal_rest/api/workflows/{get_workflow_workflows_user_id_workflow_name_get.py → get_workflow.py} +0 -0
fal/_fal_version.py
CHANGED
fal/api.py
CHANGED
|
@@ -5,11 +5,12 @@ import os
|
|
|
5
5
|
import sys
|
|
6
6
|
import threading
|
|
7
7
|
from collections import defaultdict
|
|
8
|
-
from concurrent.futures import ThreadPoolExecutor
|
|
8
|
+
from concurrent.futures import Future, ThreadPoolExecutor
|
|
9
9
|
from contextlib import asynccontextmanager, suppress
|
|
10
10
|
from dataclasses import dataclass, field, replace
|
|
11
11
|
from functools import wraps
|
|
12
12
|
from os import PathLike
|
|
13
|
+
from queue import Queue
|
|
13
14
|
from typing import (
|
|
14
15
|
Any,
|
|
15
16
|
Callable,
|
|
@@ -72,6 +73,9 @@ SERVE_REQUIREMENTS = [
|
|
|
72
73
|
]
|
|
73
74
|
|
|
74
75
|
|
|
76
|
+
THREAD_POOL = ThreadPoolExecutor()
|
|
77
|
+
|
|
78
|
+
|
|
75
79
|
@dataclass
|
|
76
80
|
class FalServerlessError(FalServerlessException):
|
|
77
81
|
message: str
|
|
@@ -83,8 +87,32 @@ class InternalFalServerlessError(FalServerlessException):
|
|
|
83
87
|
|
|
84
88
|
|
|
85
89
|
@dataclass
|
|
86
|
-
class FalMissingDependencyError(FalServerlessError):
|
|
87
|
-
|
|
90
|
+
class FalMissingDependencyError(FalServerlessError): ...
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class SpawnInfo:
|
|
95
|
+
future: Future | None = None
|
|
96
|
+
logs: Queue = field(default_factory=Queue)
|
|
97
|
+
_url_ready: threading.Event = field(default_factory=threading.Event)
|
|
98
|
+
_url: str | None = None
|
|
99
|
+
stream: Any = None
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def return_value(self):
|
|
103
|
+
if self.future is None:
|
|
104
|
+
raise ValueError
|
|
105
|
+
return self.future.result()
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def url(self):
|
|
109
|
+
self._url_ready.wait()
|
|
110
|
+
return self._url
|
|
111
|
+
|
|
112
|
+
@url.setter
|
|
113
|
+
def url(self, value):
|
|
114
|
+
self._url_ready.set()
|
|
115
|
+
self._url = value
|
|
88
116
|
|
|
89
117
|
|
|
90
118
|
@dataclass
|
|
@@ -150,6 +178,15 @@ class Host(Generic[ArgsT, ReturnT]):
|
|
|
150
178
|
"""Run the given function in the isolated environment."""
|
|
151
179
|
raise NotImplementedError
|
|
152
180
|
|
|
181
|
+
def spawn(
|
|
182
|
+
self,
|
|
183
|
+
func: Callable[ArgsT, ReturnT],
|
|
184
|
+
options: Options,
|
|
185
|
+
args: tuple[Any, ...],
|
|
186
|
+
kwargs: dict[str, Any],
|
|
187
|
+
) -> SpawnInfo:
|
|
188
|
+
raise NotImplementedError
|
|
189
|
+
|
|
153
190
|
|
|
154
191
|
def cached(func: Callable[ArgsT, ReturnT]) -> Callable[ArgsT, ReturnT]:
|
|
155
192
|
"""Cache the result of the given function in-memory."""
|
|
@@ -444,12 +481,13 @@ class FalServerlessHost(Host):
|
|
|
444
481
|
return None
|
|
445
482
|
|
|
446
483
|
@_handle_grpc_error()
|
|
447
|
-
def
|
|
484
|
+
def _run(
|
|
448
485
|
self,
|
|
449
486
|
func: Callable[..., ReturnT],
|
|
450
487
|
options: Options,
|
|
451
488
|
args: tuple[Any, ...],
|
|
452
489
|
kwargs: dict[str, Any],
|
|
490
|
+
result_handler: Callable[..., None],
|
|
453
491
|
) -> ReturnT:
|
|
454
492
|
environment_options = options.environment.copy()
|
|
455
493
|
environment_options.setdefault("python_version", active_python())
|
|
@@ -490,8 +528,7 @@ class FalServerlessHost(Host):
|
|
|
490
528
|
machine_requirements=machine_requirements,
|
|
491
529
|
setup_function=setup_function,
|
|
492
530
|
):
|
|
493
|
-
|
|
494
|
-
self._log_printer.print(log)
|
|
531
|
+
result_handler(partial_result)
|
|
495
532
|
|
|
496
533
|
if partial_result.status.state is not HostedRunState.IN_PROGRESS:
|
|
497
534
|
state = partial_result.status.state
|
|
@@ -511,6 +548,46 @@ class FalServerlessHost(Host):
|
|
|
511
548
|
|
|
512
549
|
return cast(ReturnT, return_value)
|
|
513
550
|
|
|
551
|
+
def run(
|
|
552
|
+
self,
|
|
553
|
+
func: Callable[..., ReturnT],
|
|
554
|
+
options: Options,
|
|
555
|
+
args: tuple[Any, ...],
|
|
556
|
+
kwargs: dict[str, Any],
|
|
557
|
+
) -> ReturnT:
|
|
558
|
+
def result_handler(partial_result):
|
|
559
|
+
for log in partial_result.logs:
|
|
560
|
+
self._log_printer.print(log)
|
|
561
|
+
|
|
562
|
+
return self._run(func, options, args, kwargs, result_handler=result_handler)
|
|
563
|
+
|
|
564
|
+
def spawn(
|
|
565
|
+
self,
|
|
566
|
+
func: Callable[..., ReturnT],
|
|
567
|
+
options: Options,
|
|
568
|
+
args: tuple[Any, ...],
|
|
569
|
+
kwargs: dict[str, Any],
|
|
570
|
+
) -> SpawnInfo:
|
|
571
|
+
ret = SpawnInfo()
|
|
572
|
+
|
|
573
|
+
def result_handler(partial_result):
|
|
574
|
+
ret.stream = partial_result.stream
|
|
575
|
+
for log in partial_result.logs:
|
|
576
|
+
if "Access your exposed service at" in log.message:
|
|
577
|
+
ret.url = log.message.rsplit()[-1]
|
|
578
|
+
ret.logs.put(log)
|
|
579
|
+
|
|
580
|
+
THREAD_POOL.submit(
|
|
581
|
+
self._run,
|
|
582
|
+
func,
|
|
583
|
+
options,
|
|
584
|
+
args,
|
|
585
|
+
kwargs,
|
|
586
|
+
result_handler=result_handler,
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
return ret
|
|
590
|
+
|
|
514
591
|
|
|
515
592
|
@dataclass
|
|
516
593
|
class Options:
|
|
@@ -566,8 +643,7 @@ def function(
|
|
|
566
643
|
max_concurrency: int | None = None,
|
|
567
644
|
) -> Callable[
|
|
568
645
|
[Callable[Concatenate[ArgsT], ReturnT]], IsolatedFunction[ArgsT, ReturnT]
|
|
569
|
-
]:
|
|
570
|
-
...
|
|
646
|
+
]: ...
|
|
571
647
|
|
|
572
648
|
|
|
573
649
|
@overload
|
|
@@ -583,8 +659,7 @@ def function(
|
|
|
583
659
|
max_concurrency: int | None = None,
|
|
584
660
|
) -> Callable[
|
|
585
661
|
[Callable[Concatenate[ArgsT], ReturnT]], ServedIsolatedFunction[ArgsT, ReturnT]
|
|
586
|
-
]:
|
|
587
|
-
...
|
|
662
|
+
]: ...
|
|
588
663
|
|
|
589
664
|
|
|
590
665
|
### FalServerlessHost
|
|
@@ -610,8 +685,7 @@ def function(
|
|
|
610
685
|
_scheduler: str | None = None,
|
|
611
686
|
) -> Callable[
|
|
612
687
|
[Callable[Concatenate[ArgsT], ReturnT]], IsolatedFunction[ArgsT, ReturnT]
|
|
613
|
-
]:
|
|
614
|
-
...
|
|
688
|
+
]: ...
|
|
615
689
|
|
|
616
690
|
|
|
617
691
|
@overload
|
|
@@ -636,8 +710,7 @@ def function(
|
|
|
636
710
|
_scheduler: str | None = None,
|
|
637
711
|
) -> Callable[
|
|
638
712
|
[Callable[Concatenate[ArgsT], ReturnT]], ServedIsolatedFunction[ArgsT, ReturnT]
|
|
639
|
-
]:
|
|
640
|
-
...
|
|
713
|
+
]: ...
|
|
641
714
|
|
|
642
715
|
|
|
643
716
|
## conda
|
|
@@ -660,8 +733,7 @@ def function(
|
|
|
660
733
|
max_concurrency: int | None = None,
|
|
661
734
|
) -> Callable[
|
|
662
735
|
[Callable[Concatenate[ArgsT], ReturnT]], IsolatedFunction[ArgsT, ReturnT]
|
|
663
|
-
]:
|
|
664
|
-
...
|
|
736
|
+
]: ...
|
|
665
737
|
|
|
666
738
|
|
|
667
739
|
@overload
|
|
@@ -682,8 +754,7 @@ def function(
|
|
|
682
754
|
max_concurrency: int | None = None,
|
|
683
755
|
) -> Callable[
|
|
684
756
|
[Callable[Concatenate[ArgsT], ReturnT]], ServedIsolatedFunction[ArgsT, ReturnT]
|
|
685
|
-
]:
|
|
686
|
-
...
|
|
757
|
+
]: ...
|
|
687
758
|
|
|
688
759
|
|
|
689
760
|
### FalServerlessHost
|
|
@@ -714,8 +785,7 @@ def function(
|
|
|
714
785
|
_scheduler: str | None = None,
|
|
715
786
|
) -> Callable[
|
|
716
787
|
[Callable[Concatenate[ArgsT], ReturnT]], IsolatedFunction[ArgsT, ReturnT]
|
|
717
|
-
]:
|
|
718
|
-
...
|
|
788
|
+
]: ...
|
|
719
789
|
|
|
720
790
|
|
|
721
791
|
@overload
|
|
@@ -889,6 +959,9 @@ class BaseServable:
|
|
|
889
959
|
yield
|
|
890
960
|
|
|
891
961
|
def _build_app(self) -> FastAPI:
|
|
962
|
+
import json
|
|
963
|
+
import traceback
|
|
964
|
+
|
|
892
965
|
from fastapi import HTTPException, Request
|
|
893
966
|
from fastapi.middleware.cors import CORSMiddleware
|
|
894
967
|
from fastapi.responses import JSONResponse
|
|
@@ -929,6 +1002,15 @@ class BaseServable:
|
|
|
929
1002
|
# If it's not a generic 404, just return the original message.
|
|
930
1003
|
return JSONResponse({"detail": exc.detail}, 404)
|
|
931
1004
|
|
|
1005
|
+
@_app.exception_handler(Exception)
|
|
1006
|
+
async def traceback_logging_exception_handler(request: Request, exc: Exception):
|
|
1007
|
+
print(
|
|
1008
|
+
json.dumps(
|
|
1009
|
+
{"traceback": "".join(traceback.format_exception(exc)[::-1])} # type: ignore
|
|
1010
|
+
)
|
|
1011
|
+
)
|
|
1012
|
+
return JSONResponse({"detail": "Internal Server Error"}, 500)
|
|
1013
|
+
|
|
932
1014
|
routes = self.collect_routes()
|
|
933
1015
|
if not routes:
|
|
934
1016
|
raise ValueError("An application must have at least one route!")
|
|
@@ -978,7 +1060,8 @@ class BaseServable:
|
|
|
978
1060
|
}
|
|
979
1061
|
|
|
980
1062
|
_, pending = await asyncio.wait(
|
|
981
|
-
tasks.keys(),
|
|
1063
|
+
tasks.keys(),
|
|
1064
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
982
1065
|
)
|
|
983
1066
|
if not pending:
|
|
984
1067
|
return
|
|
@@ -1052,6 +1135,14 @@ class IsolatedFunction(Generic[ArgsT, ReturnT]):
|
|
|
1052
1135
|
)
|
|
1053
1136
|
return future
|
|
1054
1137
|
|
|
1138
|
+
def spawn(self, *args: ArgsT.args, **kwargs: ArgsT.kwargs):
|
|
1139
|
+
return self.host.spawn(
|
|
1140
|
+
self.func,
|
|
1141
|
+
self.options,
|
|
1142
|
+
args,
|
|
1143
|
+
kwargs,
|
|
1144
|
+
)
|
|
1145
|
+
|
|
1055
1146
|
def __call__(self, *args: ArgsT.args, **kwargs: ArgsT.kwargs) -> ReturnT:
|
|
1056
1147
|
try:
|
|
1057
1148
|
return self.host.run(
|
|
@@ -1089,14 +1180,12 @@ class IsolatedFunction(Generic[ArgsT, ReturnT]):
|
|
|
1089
1180
|
@overload
|
|
1090
1181
|
def on(
|
|
1091
1182
|
self, host: Host | None = None, *, serve: Literal[False] = False, **config: Any
|
|
1092
|
-
) -> IsolatedFunction[ArgsT, ReturnT]:
|
|
1093
|
-
...
|
|
1183
|
+
) -> IsolatedFunction[ArgsT, ReturnT]: ...
|
|
1094
1184
|
|
|
1095
1185
|
@overload
|
|
1096
1186
|
def on(
|
|
1097
1187
|
self, host: Host | None = None, *, serve: Literal[True], **config: Any
|
|
1098
|
-
) -> ServedIsolatedFunction[ArgsT, ReturnT]:
|
|
1099
|
-
...
|
|
1188
|
+
) -> ServedIsolatedFunction[ArgsT, ReturnT]: ...
|
|
1100
1189
|
|
|
1101
1190
|
def on(self, host: Host | None = None, **config: Any): # type: ignore
|
|
1102
1191
|
host = host or self.host
|
|
@@ -1150,14 +1239,12 @@ class ServedIsolatedFunction(
|
|
|
1150
1239
|
@overload # type: ignore[override,no-overload-impl]
|
|
1151
1240
|
def on( # type: ignore[no-overload-impl]
|
|
1152
1241
|
self, host: Host | None = None, *, serve: Literal[True] = True, **config: Any
|
|
1153
|
-
) -> ServedIsolatedFunction[ArgsT, ReturnT]:
|
|
1154
|
-
...
|
|
1242
|
+
) -> ServedIsolatedFunction[ArgsT, ReturnT]: ...
|
|
1155
1243
|
|
|
1156
1244
|
@overload
|
|
1157
1245
|
def on(
|
|
1158
1246
|
self, host: Host | None = None, *, serve: Literal[False], **config: Any
|
|
1159
|
-
) -> IsolatedFunction[ArgsT, ReturnT]:
|
|
1160
|
-
...
|
|
1247
|
+
) -> IsolatedFunction[ArgsT, ReturnT]: ...
|
|
1161
1248
|
|
|
1162
1249
|
|
|
1163
1250
|
class Server(uvicorn.Server):
|
fal/app.py
CHANGED
|
@@ -4,10 +4,12 @@ import inspect
|
|
|
4
4
|
import json
|
|
5
5
|
import os
|
|
6
6
|
import re
|
|
7
|
+
import time
|
|
7
8
|
import typing
|
|
8
|
-
from contextlib import asynccontextmanager
|
|
9
|
+
from contextlib import asynccontextmanager, contextmanager
|
|
9
10
|
from typing import Any, Callable, ClassVar, TypeVar
|
|
10
11
|
|
|
12
|
+
import httpx
|
|
11
13
|
from fastapi import FastAPI
|
|
12
14
|
|
|
13
15
|
import fal.api
|
|
@@ -69,6 +71,66 @@ def wrap_app(cls: type[App], **kwargs) -> fal.api.IsolatedFunction:
|
|
|
69
71
|
return fn
|
|
70
72
|
|
|
71
73
|
|
|
74
|
+
class EndpointClient:
|
|
75
|
+
def __init__(self, url, endpoint, signature):
|
|
76
|
+
self.url = url
|
|
77
|
+
self.endpoint = endpoint
|
|
78
|
+
self.signature = signature
|
|
79
|
+
|
|
80
|
+
annotations = endpoint.__annotations__ or {}
|
|
81
|
+
self.return_type = annotations.get("return") or None
|
|
82
|
+
|
|
83
|
+
def __call__(self, data):
|
|
84
|
+
with httpx.Client() as client:
|
|
85
|
+
resp = client.post(self.url + self.signature.path, json=dict(data))
|
|
86
|
+
resp.raise_for_status()
|
|
87
|
+
resp_dict = resp.json()
|
|
88
|
+
|
|
89
|
+
if not self.return_type:
|
|
90
|
+
return resp_dict
|
|
91
|
+
|
|
92
|
+
return self.return_type(**resp_dict)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class AppClient:
|
|
96
|
+
def __init__(self, cls, url):
|
|
97
|
+
self.url = url
|
|
98
|
+
self.cls = cls
|
|
99
|
+
|
|
100
|
+
for name, endpoint in inspect.getmembers(cls, inspect.isfunction):
|
|
101
|
+
signature = getattr(endpoint, "route_signature", None)
|
|
102
|
+
if signature is None:
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
setattr(self, name, EndpointClient(self.url, endpoint, signature))
|
|
106
|
+
|
|
107
|
+
@classmethod
|
|
108
|
+
@contextmanager
|
|
109
|
+
def connect(cls, app_cls):
|
|
110
|
+
app = wrap_app(app_cls)
|
|
111
|
+
info = app.spawn()
|
|
112
|
+
try:
|
|
113
|
+
with httpx.Client() as client:
|
|
114
|
+
retries = 100
|
|
115
|
+
while retries:
|
|
116
|
+
resp = client.get(info.url + "/health")
|
|
117
|
+
if resp.is_success:
|
|
118
|
+
break
|
|
119
|
+
elif resp.status_code != 500:
|
|
120
|
+
resp.raise_for_status()
|
|
121
|
+
time.sleep(0.1)
|
|
122
|
+
retries -= 1
|
|
123
|
+
|
|
124
|
+
yield cls(app_cls, info.url)
|
|
125
|
+
finally:
|
|
126
|
+
info.stream.cancel()
|
|
127
|
+
|
|
128
|
+
def health(self):
|
|
129
|
+
with httpx.Client() as client:
|
|
130
|
+
resp = client.get(self.url + "/health")
|
|
131
|
+
resp.raise_for_status()
|
|
132
|
+
return resp.json()
|
|
133
|
+
|
|
72
134
|
|
|
73
135
|
PART_FINDER_RE = re.compile(r"[A-Z][a-z]*")
|
|
74
136
|
|
fal/cli/deploy.py
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from collections import namedtuple
|
|
1
3
|
from pathlib import Path
|
|
2
4
|
|
|
3
5
|
from .parser import FalClientParser, RefAction
|
|
4
6
|
|
|
7
|
+
User = namedtuple("User", ["user_id", "username"])
|
|
8
|
+
|
|
5
9
|
|
|
6
10
|
def _remove_http_and_port_from_url(url):
|
|
7
11
|
# Remove http://
|
|
@@ -20,17 +24,17 @@ def _remove_http_and_port_from_url(url):
|
|
|
20
24
|
return url
|
|
21
25
|
|
|
22
26
|
|
|
23
|
-
def
|
|
27
|
+
def _get_user() -> User:
|
|
24
28
|
import json
|
|
25
29
|
from http import HTTPStatus
|
|
26
30
|
|
|
27
|
-
import openapi_fal_rest.api.
|
|
31
|
+
import openapi_fal_rest.api.users.get_current_user as get_current_user
|
|
28
32
|
|
|
29
33
|
from fal.api import FalServerlessError
|
|
30
34
|
from fal.rest_client import REST_CLIENT
|
|
31
35
|
|
|
32
36
|
try:
|
|
33
|
-
user_details_response =
|
|
37
|
+
user_details_response = get_current_user.sync_detailed(
|
|
34
38
|
client=REST_CLIENT,
|
|
35
39
|
)
|
|
36
40
|
except Exception as e:
|
|
@@ -51,7 +55,7 @@ def _get_user_id() -> str:
|
|
|
51
55
|
if not user_id:
|
|
52
56
|
user_id = full_user_id
|
|
53
57
|
|
|
54
|
-
return user_id
|
|
58
|
+
return User(user_id=user_id, username=user_details_response.parsed.nickname)
|
|
55
59
|
except Exception as e:
|
|
56
60
|
raise FalServerlessError(f"Could not parse the user data: {e}")
|
|
57
61
|
|
|
@@ -77,7 +81,7 @@ def _deploy(args):
|
|
|
77
81
|
[file_path] = options
|
|
78
82
|
file_path = str(file_path)
|
|
79
83
|
|
|
80
|
-
|
|
84
|
+
user = _get_user()
|
|
81
85
|
host = FalServerlessHost(args.host)
|
|
82
86
|
isolated_function, app_name = load_function_from(
|
|
83
87
|
host,
|
|
@@ -103,12 +107,18 @@ def _deploy(args):
|
|
|
103
107
|
"Registered a new revision for function "
|
|
104
108
|
f"'{app_name}' (revision='{app_id}')."
|
|
105
109
|
)
|
|
106
|
-
args.console.print(f"
|
|
110
|
+
args.console.print(f"Playground: https://fal.ai/models/{user.username}/{app_name}")
|
|
111
|
+
args.console.print(f"Endpoint: https://{gateway_host}/{user.username}/{app_name}")
|
|
107
112
|
|
|
108
113
|
|
|
109
114
|
def add_parser(main_subparsers, parents):
|
|
110
115
|
from fal.sdk import ALIAS_AUTH_MODES
|
|
111
116
|
|
|
117
|
+
def valid_auth_option(option):
|
|
118
|
+
if option not in ALIAS_AUTH_MODES:
|
|
119
|
+
raise argparse.ArgumentTypeError(f"{option} is not a auth option")
|
|
120
|
+
return option
|
|
121
|
+
|
|
112
122
|
deploy_help = "Deploy a fal application."
|
|
113
123
|
epilog = (
|
|
114
124
|
"Examples:\n"
|
|
@@ -139,8 +149,8 @@ def add_parser(main_subparsers, parents):
|
|
|
139
149
|
)
|
|
140
150
|
parser.add_argument(
|
|
141
151
|
"--auth",
|
|
142
|
-
|
|
152
|
+
type=valid_auth_option,
|
|
143
153
|
default="private",
|
|
144
|
-
help="Application authentication mode.",
|
|
154
|
+
help="Application authentication mode (private, public).",
|
|
145
155
|
)
|
|
146
156
|
parser.set_defaults(func=_deploy)
|
fal/cli/doctor.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import platform
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _doctor(args):
|
|
6
|
+
import isolate
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
import fal
|
|
10
|
+
|
|
11
|
+
table = Table(show_header=False, show_lines=False, box=None)
|
|
12
|
+
table.add_column("name", no_wrap=True, style="bold")
|
|
13
|
+
table.add_column("value", no_wrap=True)
|
|
14
|
+
|
|
15
|
+
table.add_row("fal", fal.__version__)
|
|
16
|
+
table.add_row("isolate", isolate.__version__)
|
|
17
|
+
|
|
18
|
+
table.add_row("", "")
|
|
19
|
+
table.add_row("python", platform.python_version())
|
|
20
|
+
table.add_row("platform", platform.platform())
|
|
21
|
+
|
|
22
|
+
table.add_row("", "")
|
|
23
|
+
table.add_row("FAL_HOST", fal.flags.GRPC_HOST)
|
|
24
|
+
table.add_row("FAL_KEY", os.getenv("FAL_KEY", "").split(":")[0])
|
|
25
|
+
|
|
26
|
+
args.console.print(table)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def add_parser(main_subparsers, parents):
|
|
30
|
+
doctor_help = "fal version and misc environment information."
|
|
31
|
+
parser = main_subparsers.add_parser(
|
|
32
|
+
"doctor",
|
|
33
|
+
description=doctor_help,
|
|
34
|
+
help=doctor_help,
|
|
35
|
+
parents=parents,
|
|
36
|
+
)
|
|
37
|
+
parser.set_defaults(func=_doctor)
|
fal/cli/main.py
CHANGED
|
@@ -6,7 +6,7 @@ from fal import __version__
|
|
|
6
6
|
from fal.console import console
|
|
7
7
|
from fal.console.icons import CROSS_ICON
|
|
8
8
|
|
|
9
|
-
from . import apps, auth, deploy, keys, run, secrets
|
|
9
|
+
from . import apps, auth, deploy, doctor, keys, run, secrets
|
|
10
10
|
from .debug import debugtools, get_debug_parser
|
|
11
11
|
from .parser import FalParser, FalParserExit
|
|
12
12
|
|
|
@@ -31,7 +31,7 @@ def _get_main_parser() -> argparse.ArgumentParser:
|
|
|
31
31
|
required=True,
|
|
32
32
|
)
|
|
33
33
|
|
|
34
|
-
for cmd in [auth, apps, deploy, run, keys, secrets]:
|
|
34
|
+
for cmd in [auth, apps, deploy, run, keys, secrets, doctor]:
|
|
35
35
|
cmd.add_parser(subparsers, parents)
|
|
36
36
|
|
|
37
37
|
return parser
|
fal/sdk.py
CHANGED
|
@@ -224,6 +224,7 @@ class HostedRunResult(Generic[ResultT]):
|
|
|
224
224
|
status: HostedRunStatus
|
|
225
225
|
logs: list[Log] = field(default_factory=list)
|
|
226
226
|
result: ResultT | None = None
|
|
227
|
+
stream: Any = None
|
|
227
228
|
|
|
228
229
|
|
|
229
230
|
@dataclass
|
|
@@ -569,8 +570,11 @@ class FalServerlessConnection:
|
|
|
569
570
|
request.setup_func.MergeFrom(
|
|
570
571
|
to_serialized_object(setup_function, serialization_method)
|
|
571
572
|
)
|
|
572
|
-
|
|
573
|
-
|
|
573
|
+
stream = self.stub.Run(request)
|
|
574
|
+
for partial_result in stream:
|
|
575
|
+
res = from_grpc(partial_result)
|
|
576
|
+
res.stream = stream
|
|
577
|
+
yield res
|
|
574
578
|
|
|
575
579
|
def create_alias(
|
|
576
580
|
self,
|
|
@@ -98,6 +98,7 @@ class FalCDNFileRepository(FileRepository):
|
|
|
98
98
|
**self.auth_headers,
|
|
99
99
|
"Accept": "application/json",
|
|
100
100
|
"Content-Type": file.content_type,
|
|
101
|
+
"X-Fal-File-Name": file.file_name,
|
|
101
102
|
"X-Fal-Object-Lifecycle-Preference": json.dumps(
|
|
102
103
|
dataclasses.asdict(GLOBAL_LIFECYCLE_PREFERENCE)
|
|
103
104
|
),
|
fal/workflows.py
CHANGED
|
@@ -10,7 +10,7 @@ from typing import Any, Iterator, Union, cast
|
|
|
10
10
|
import graphlib
|
|
11
11
|
import rich
|
|
12
12
|
from openapi_fal_rest.api.workflows import (
|
|
13
|
-
|
|
13
|
+
create_workflow as publish_workflow,
|
|
14
14
|
)
|
|
15
15
|
from openapi_fal_rest.models.http_validation_error import HTTPValidationError
|
|
16
16
|
from pydantic import BaseModel
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: fal
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
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
|
|
@@ -43,6 +43,7 @@ Requires-Dist: pytest <8 ; extra == 'test'
|
|
|
43
43
|
Requires-Dist: pytest-asyncio ; extra == 'test'
|
|
44
44
|
Requires-Dist: pytest-xdist ; extra == 'test'
|
|
45
45
|
Requires-Dist: flaky ; extra == 'test'
|
|
46
|
+
Requires-Dist: boto3 ; extra == 'test'
|
|
46
47
|
|
|
47
48
|
[](https://pypi.org/project/fal)
|
|
48
49
|
[](https://github.com/fal-ai/fal/actions)
|