fal 1.49.1__py3-none-any.whl → 1.57.2__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.
- fal/_fal_version.py +2 -2
- fal/_serialization.py +1 -0
- fal/api/__init__.py +1 -0
- fal/api/api.py +32 -2
- fal/api/apps.py +23 -1
- fal/api/client.py +72 -1
- fal/api/deploy.py +16 -28
- fal/api/keys.py +31 -0
- fal/api/runners.py +10 -0
- fal/api/secrets.py +29 -0
- fal/app.py +50 -14
- fal/cli/_utils.py +11 -3
- fal/cli/api.py +4 -2
- fal/cli/apps.py +56 -2
- fal/cli/deploy.py +17 -3
- fal/cli/files.py +16 -24
- fal/cli/keys.py +47 -50
- fal/cli/queue.py +12 -10
- fal/cli/run.py +11 -7
- fal/cli/runners.py +189 -27
- fal/cli/secrets.py +28 -30
- fal/files.py +32 -8
- fal/logging/__init__.py +0 -5
- fal/sdk.py +39 -23
- fal/sync.py +22 -12
- fal/toolkit/__init__.py +10 -0
- fal/toolkit/compilation.py +220 -0
- fal/toolkit/file/file.py +10 -9
- fal/utils.py +65 -31
- fal/workflows.py +6 -2
- {fal-1.49.1.dist-info → fal-1.57.2.dist-info}/METADATA +6 -6
- {fal-1.49.1.dist-info → fal-1.57.2.dist-info}/RECORD +35 -33
- fal/rest_client.py +0 -25
- {fal-1.49.1.dist-info → fal-1.57.2.dist-info}/WHEEL +0 -0
- {fal-1.49.1.dist-info → fal-1.57.2.dist-info}/entry_points.txt +0 -0
- {fal-1.49.1.dist-info → fal-1.57.2.dist-info}/top_level.txt +0 -0
fal/app.py
CHANGED
|
@@ -38,6 +38,7 @@ from fal.toolkit.file.providers.fal import LIFECYCLE_PREFERENCE
|
|
|
38
38
|
|
|
39
39
|
REALTIME_APP_REQUIREMENTS = ["websockets", "msgpack"]
|
|
40
40
|
REQUEST_ID_KEY = "x-fal-request-id"
|
|
41
|
+
REQUEST_ENDPOINT_KEY = "x-fal-endpoint"
|
|
41
42
|
DEFAULT_APP_FILES_IGNORE = [
|
|
42
43
|
r"\.pyc$",
|
|
43
44
|
r"__pycache__/",
|
|
@@ -103,9 +104,7 @@ async def _set_logger_labels(
|
|
|
103
104
|
code = await res.code()
|
|
104
105
|
assert str(code) == "StatusCode.OK", str(code)
|
|
105
106
|
except BaseException:
|
|
106
|
-
|
|
107
|
-
# logger.debug("Failed to set logger labels", exc_info=True)
|
|
108
|
-
pass
|
|
107
|
+
logger.debug("Failed to set logger labels", exc_info=True)
|
|
109
108
|
|
|
110
109
|
|
|
111
110
|
def wrap_app(cls: type[App], **kwargs) -> IsolatedFunction:
|
|
@@ -136,6 +135,7 @@ def wrap_app(cls: type[App], **kwargs) -> IsolatedFunction:
|
|
|
136
135
|
local_python_modules=cls.local_python_modules,
|
|
137
136
|
machine_type=cls.machine_type,
|
|
138
137
|
num_gpus=cls.num_gpus,
|
|
138
|
+
regions=cls.regions,
|
|
139
139
|
**cls.host_kwargs,
|
|
140
140
|
**kwargs,
|
|
141
141
|
metadata=metadata,
|
|
@@ -374,6 +374,7 @@ class App(BaseServable):
|
|
|
374
374
|
local_python_modules: ClassVar[list[str]] = []
|
|
375
375
|
machine_type: ClassVar[str | list[str]] = "S"
|
|
376
376
|
num_gpus: ClassVar[int | None] = None
|
|
377
|
+
regions: ClassVar[Optional[list[str]]] = None
|
|
377
378
|
host_kwargs: ClassVar[dict[str, Any]] = {
|
|
378
379
|
"_scheduler": "nomad",
|
|
379
380
|
"_scheduler_options": {
|
|
@@ -393,6 +394,7 @@ class App(BaseServable):
|
|
|
393
394
|
max_concurrency: ClassVar[Optional[int]] = None
|
|
394
395
|
concurrency_buffer: ClassVar[Optional[int]] = None
|
|
395
396
|
concurrency_buffer_perc: ClassVar[Optional[int]] = None
|
|
397
|
+
scaling_delay: ClassVar[Optional[int]] = None
|
|
396
398
|
max_multiplexing: ClassVar[Optional[int]] = None
|
|
397
399
|
kind: ClassVar[Optional[str]] = None
|
|
398
400
|
image: ClassVar[Optional[ContainerImage]] = None
|
|
@@ -436,21 +438,21 @@ class App(BaseServable):
|
|
|
436
438
|
if cls.concurrency_buffer_perc is not None:
|
|
437
439
|
cls.host_kwargs["concurrency_buffer_perc"] = cls.concurrency_buffer_perc
|
|
438
440
|
|
|
441
|
+
if cls.scaling_delay is not None:
|
|
442
|
+
cls.host_kwargs["scaling_delay"] = cls.scaling_delay
|
|
443
|
+
|
|
439
444
|
if cls.max_multiplexing is not None:
|
|
440
445
|
cls.host_kwargs["max_multiplexing"] = cls.max_multiplexing
|
|
441
446
|
|
|
442
447
|
if cls.kind is not None:
|
|
443
448
|
cls.host_kwargs["kind"] = cls.kind
|
|
444
|
-
if cls.kind == "container" and cls.app_files:
|
|
445
|
-
raise ValueError("app_files is not supported for container apps.")
|
|
446
449
|
|
|
447
450
|
if cls.image is not None:
|
|
448
451
|
cls.host_kwargs["image"] = cls.image
|
|
449
452
|
|
|
450
|
-
cls.
|
|
453
|
+
cls.host_kwargs["health_check_path"] = cls.get_health_check_endpoint()
|
|
451
454
|
|
|
452
|
-
|
|
453
|
-
raise ValueError("app_files is not supported for container apps.")
|
|
455
|
+
cls.app_name = getattr(cls, "app_name") or app_name
|
|
454
456
|
|
|
455
457
|
if cls.__init__ is not App.__init__:
|
|
456
458
|
raise ValueError(
|
|
@@ -474,6 +476,24 @@ class App(BaseServable):
|
|
|
474
476
|
if (signature := getattr(endpoint, "route_signature", None))
|
|
475
477
|
]
|
|
476
478
|
|
|
479
|
+
@classmethod
|
|
480
|
+
def get_health_check_endpoint(cls) -> Optional[str]:
|
|
481
|
+
paths = [
|
|
482
|
+
signature.path
|
|
483
|
+
for _, endpoint in inspect.getmembers(cls, inspect.isfunction)
|
|
484
|
+
if (signature := getattr(endpoint, "route_signature", None))
|
|
485
|
+
and signature.is_health_check
|
|
486
|
+
]
|
|
487
|
+
if len(paths) > 1:
|
|
488
|
+
raise ValueError(
|
|
489
|
+
f"Multiple health check endpoints found: {', '.join(paths)}. "
|
|
490
|
+
"An app can only have one health check endpoint."
|
|
491
|
+
)
|
|
492
|
+
elif len(paths) == 1:
|
|
493
|
+
return paths[0]
|
|
494
|
+
else:
|
|
495
|
+
return None
|
|
496
|
+
|
|
477
497
|
def collect_routes(self) -> dict[RouteSignature, Callable[..., Any]]:
|
|
478
498
|
return {
|
|
479
499
|
signature: endpoint
|
|
@@ -483,6 +503,8 @@ class App(BaseServable):
|
|
|
483
503
|
|
|
484
504
|
@asynccontextmanager
|
|
485
505
|
async def lifespan(self, app: fastapi.FastAPI):
|
|
506
|
+
os.environ["FAL_RUNNER_STATE"] = "SETUP"
|
|
507
|
+
|
|
486
508
|
# We want to not do any directory changes for container apps,
|
|
487
509
|
# since we don't have explicit checks to see the kind of app
|
|
488
510
|
# We check for app_files here and check kind and app_files earlier
|
|
@@ -491,9 +513,13 @@ class App(BaseServable):
|
|
|
491
513
|
_include_app_files_path(self.local_file_path, self.app_files_context_dir)
|
|
492
514
|
_print_python_packages()
|
|
493
515
|
await _call_any_fn(self.setup)
|
|
516
|
+
|
|
517
|
+
os.environ["FAL_RUNNER_STATE"] = "RUNNING"
|
|
518
|
+
|
|
494
519
|
try:
|
|
495
520
|
yield
|
|
496
521
|
finally:
|
|
522
|
+
os.environ["FAL_RUNNER_STATE"] = "STOPPING"
|
|
497
523
|
await _call_any_fn(self.teardown)
|
|
498
524
|
|
|
499
525
|
def health(self):
|
|
@@ -587,12 +613,18 @@ class App(BaseServable):
|
|
|
587
613
|
return await call_next(request)
|
|
588
614
|
|
|
589
615
|
request_id = request.headers.get(REQUEST_ID_KEY)
|
|
590
|
-
|
|
616
|
+
request_endpoint = request.headers.get(REQUEST_ENDPOINT_KEY)
|
|
617
|
+
|
|
618
|
+
if request_id is None and request_endpoint is None:
|
|
591
619
|
return await call_next(request)
|
|
592
620
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
621
|
+
labels_to_set = {}
|
|
622
|
+
if request_id:
|
|
623
|
+
labels_to_set["fal_request_id"] = request_id
|
|
624
|
+
if request_endpoint:
|
|
625
|
+
labels_to_set["fal_endpoint"] = request_endpoint
|
|
626
|
+
|
|
627
|
+
await _set_logger_labels(labels_to_set, channel=self.isolate_channel)
|
|
596
628
|
|
|
597
629
|
async def _unset_at_end():
|
|
598
630
|
await _set_logger_labels({}, channel=self.isolate_channel) # type: ignore
|
|
@@ -636,7 +668,7 @@ class App(BaseServable):
|
|
|
636
668
|
|
|
637
669
|
|
|
638
670
|
def endpoint(
|
|
639
|
-
path: str, *, is_websocket: bool = False
|
|
671
|
+
path: str, *, is_websocket: bool = False, is_health_check: bool = False
|
|
640
672
|
) -> Callable[[EndpointT], EndpointT]:
|
|
641
673
|
"""Designate the decorated function as an application endpoint."""
|
|
642
674
|
|
|
@@ -646,7 +678,11 @@ def endpoint(
|
|
|
646
678
|
f"Can't set multiple routes for the same function: {callable.__name__}"
|
|
647
679
|
)
|
|
648
680
|
|
|
649
|
-
callable.route_signature = RouteSignature(
|
|
681
|
+
callable.route_signature = RouteSignature( # type: ignore
|
|
682
|
+
path=path,
|
|
683
|
+
is_websocket=is_websocket,
|
|
684
|
+
is_health_check=is_health_check,
|
|
685
|
+
)
|
|
650
686
|
return callable
|
|
651
687
|
|
|
652
688
|
return marker_fn
|
fal/cli/_utils.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import copy
|
|
3
4
|
from typing import Any, Optional
|
|
4
5
|
|
|
5
6
|
from fal.project import find_project_root, find_pyproject_toml, parse_pyproject_toml
|
|
@@ -22,7 +23,13 @@ def is_app_name(app_ref: tuple[str, str | None]) -> bool:
|
|
|
22
23
|
|
|
23
24
|
def get_app_data_from_toml(
|
|
24
25
|
app_name,
|
|
25
|
-
) -> tuple[
|
|
26
|
+
) -> tuple[
|
|
27
|
+
str,
|
|
28
|
+
Optional[AuthModeLiteral],
|
|
29
|
+
Optional[DeploymentStrategyLiteral],
|
|
30
|
+
bool,
|
|
31
|
+
Optional[str],
|
|
32
|
+
]:
|
|
26
33
|
toml_path = find_pyproject_toml()
|
|
27
34
|
|
|
28
35
|
if toml_path is None:
|
|
@@ -32,7 +39,7 @@ def get_app_data_from_toml(
|
|
|
32
39
|
apps = fal_data.get("apps", {})
|
|
33
40
|
|
|
34
41
|
try:
|
|
35
|
-
app_data: dict[str, Any] = apps[app_name]
|
|
42
|
+
app_data: dict[str, Any] = copy.deepcopy(apps[app_name])
|
|
36
43
|
except KeyError:
|
|
37
44
|
raise ValueError(f"App {app_name} not found in pyproject.toml")
|
|
38
45
|
|
|
@@ -49,6 +56,7 @@ def get_app_data_from_toml(
|
|
|
49
56
|
app_deployment_strategy: Optional[DeploymentStrategyLiteral] = app_data.pop(
|
|
50
57
|
"deployment_strategy", None
|
|
51
58
|
)
|
|
59
|
+
app_team: Optional[str] = app_data.pop("team", None)
|
|
52
60
|
|
|
53
61
|
app_reset_scale: bool
|
|
54
62
|
if "no_scale" in app_data:
|
|
@@ -62,4 +70,4 @@ def get_app_data_from_toml(
|
|
|
62
70
|
if len(app_data) > 0:
|
|
63
71
|
raise ValueError(f"Found unexpected keys in pyproject.toml: {app_data}")
|
|
64
72
|
|
|
65
|
-
return app_ref, app_auth, app_deployment_strategy, app_reset_scale
|
|
73
|
+
return app_ref, app_auth, app_deployment_strategy, app_reset_scale, app_team
|
fal/cli/api.py
CHANGED
|
@@ -2,8 +2,6 @@ import re
|
|
|
2
2
|
|
|
3
3
|
import rich
|
|
4
4
|
|
|
5
|
-
import fal.apps
|
|
6
|
-
|
|
7
5
|
# = or := only
|
|
8
6
|
KV_SPLIT_RE = re.compile(r"(=|:=)")
|
|
9
7
|
|
|
@@ -24,6 +22,8 @@ def _api(args):
|
|
|
24
22
|
|
|
25
23
|
|
|
26
24
|
def stream_run(model_id: str, params: dict):
|
|
25
|
+
import fal.apps
|
|
26
|
+
|
|
27
27
|
res = fal.apps.stream(model_id, params) # type: ignore
|
|
28
28
|
for line in res:
|
|
29
29
|
if isinstance(line, str):
|
|
@@ -41,6 +41,8 @@ def queue_run(model_id: str, params: dict):
|
|
|
41
41
|
from rich.panel import Panel
|
|
42
42
|
from rich.text import Text
|
|
43
43
|
|
|
44
|
+
import fal.apps
|
|
45
|
+
|
|
44
46
|
handle = fal.apps.submit(model_id, params) # type: ignore
|
|
45
47
|
logs = [] # type: ignore
|
|
46
48
|
|
fal/cli/apps.py
CHANGED
|
@@ -14,6 +14,12 @@ from .parser import FalClientParser, SinceAction, get_output_parser
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
15
15
|
from fal.sdk import AliasInfo, ApplicationInfo
|
|
16
16
|
|
|
17
|
+
CODE_SPECIFIC_SCALING_PARAMS = [
|
|
18
|
+
"max_multiplexing",
|
|
19
|
+
"startup_timeout",
|
|
20
|
+
"machine_types",
|
|
21
|
+
]
|
|
22
|
+
|
|
17
23
|
|
|
18
24
|
def _apps_table(apps: list[AliasInfo]):
|
|
19
25
|
from rich.table import Table
|
|
@@ -25,6 +31,7 @@ def _apps_table(apps: list[AliasInfo]):
|
|
|
25
31
|
table.add_column("Min Concurrency")
|
|
26
32
|
table.add_column("Max Concurrency")
|
|
27
33
|
table.add_column("Concurrency Buffer")
|
|
34
|
+
table.add_column("Scaling Delay")
|
|
28
35
|
table.add_column("Max Multiplexing")
|
|
29
36
|
table.add_column("Keep Alive")
|
|
30
37
|
table.add_column("Request Timeout")
|
|
@@ -48,6 +55,7 @@ def _apps_table(apps: list[AliasInfo]):
|
|
|
48
55
|
str(app.min_concurrency),
|
|
49
56
|
str(app.max_concurrency),
|
|
50
57
|
concurrency_buffer_str,
|
|
58
|
+
str(app.scaling_delay),
|
|
51
59
|
str(app.max_multiplexing),
|
|
52
60
|
str(app.keep_alive),
|
|
53
61
|
str(app.request_timeout),
|
|
@@ -169,6 +177,7 @@ def _scale(args):
|
|
|
169
177
|
and args.min_concurrency is None
|
|
170
178
|
and args.concurrency_buffer is None
|
|
171
179
|
and args.concurrency_buffer_perc is None
|
|
180
|
+
and args.scaling_delay is None
|
|
172
181
|
and args.request_timeout is None
|
|
173
182
|
and args.startup_timeout is None
|
|
174
183
|
and args.machine_types is None
|
|
@@ -185,6 +194,7 @@ def _scale(args):
|
|
|
185
194
|
min_concurrency=args.min_concurrency,
|
|
186
195
|
concurrency_buffer=args.concurrency_buffer,
|
|
187
196
|
concurrency_buffer_perc=args.concurrency_buffer_perc,
|
|
197
|
+
scaling_delay=args.scaling_delay,
|
|
188
198
|
request_timeout=args.request_timeout,
|
|
189
199
|
startup_timeout=args.startup_timeout,
|
|
190
200
|
machine_types=args.machine_types,
|
|
@@ -194,6 +204,18 @@ def _scale(args):
|
|
|
194
204
|
|
|
195
205
|
args.console.print(table)
|
|
196
206
|
|
|
207
|
+
code_specific_changes = set()
|
|
208
|
+
for param in CODE_SPECIFIC_SCALING_PARAMS:
|
|
209
|
+
if getattr(args, param) is not None:
|
|
210
|
+
code_specific_changes.add(f"[bold]{param}[/bold]")
|
|
211
|
+
|
|
212
|
+
if len(code_specific_changes) > 0:
|
|
213
|
+
args.console.print(
|
|
214
|
+
"[bold yellow]Note:[/bold yellow] Please be aware that "
|
|
215
|
+
f"{', '.join(code_specific_changes)} will be reset on the next deployment. "
|
|
216
|
+
"See https://docs.fal.ai/serverless/deployment-operations/scale-your-application#code-specific-settings-reset-on-deploy for details." # noqa: E501
|
|
217
|
+
)
|
|
218
|
+
|
|
197
219
|
|
|
198
220
|
def _add_scale_parser(subparsers, parents):
|
|
199
221
|
scale_help = "Scale application."
|
|
@@ -237,6 +259,11 @@ def _add_scale_parser(subparsers, parents):
|
|
|
237
259
|
type=int,
|
|
238
260
|
help="Concurrency buffer %",
|
|
239
261
|
)
|
|
262
|
+
parser.add_argument(
|
|
263
|
+
"--scaling-delay",
|
|
264
|
+
type=int,
|
|
265
|
+
help="Scaling delay (seconds).",
|
|
266
|
+
)
|
|
240
267
|
parser.add_argument(
|
|
241
268
|
"--request-timeout",
|
|
242
269
|
type=int,
|
|
@@ -262,6 +289,32 @@ def _add_scale_parser(subparsers, parents):
|
|
|
262
289
|
parser.set_defaults(func=_scale)
|
|
263
290
|
|
|
264
291
|
|
|
292
|
+
def _rollout(args):
|
|
293
|
+
client = SyncServerlessClient(host=args.host, team=args.team)
|
|
294
|
+
client.apps.rollout(args.app_name, force=args.force)
|
|
295
|
+
args.console.log(f"Rolled out application {args.app_name}")
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _add_rollout_parser(subparsers, parents):
|
|
299
|
+
rollout_help = "Rollout application."
|
|
300
|
+
parser = subparsers.add_parser(
|
|
301
|
+
"rollout",
|
|
302
|
+
description=rollout_help,
|
|
303
|
+
help=rollout_help,
|
|
304
|
+
parents=parents,
|
|
305
|
+
)
|
|
306
|
+
parser.add_argument(
|
|
307
|
+
"app_name",
|
|
308
|
+
help="Application name.",
|
|
309
|
+
)
|
|
310
|
+
parser.add_argument(
|
|
311
|
+
"--force",
|
|
312
|
+
action="store_true",
|
|
313
|
+
help="Force rollout.",
|
|
314
|
+
)
|
|
315
|
+
parser.set_defaults(func=_rollout)
|
|
316
|
+
|
|
317
|
+
|
|
265
318
|
def _set_rev(args):
|
|
266
319
|
client = get_client(args.host, args.team)
|
|
267
320
|
with client.connect() as connection:
|
|
@@ -348,14 +401,14 @@ def _add_runners_parser(subparsers, parents):
|
|
|
348
401
|
action=SinceAction,
|
|
349
402
|
limit="1 day",
|
|
350
403
|
help=(
|
|
351
|
-
"Show
|
|
404
|
+
"Show terminated runners since the given time. "
|
|
352
405
|
"Accepts 'now', relative like '30m', '1h', '1d', "
|
|
353
406
|
"or an ISO timestamp. Max 24 hours."
|
|
354
407
|
),
|
|
355
408
|
)
|
|
356
409
|
parser.add_argument(
|
|
357
410
|
"--state",
|
|
358
|
-
choices=["all", "running", "pending", "setup", "
|
|
411
|
+
choices=["all", "running", "pending", "setup", "terminated"],
|
|
359
412
|
nargs="+",
|
|
360
413
|
default=None,
|
|
361
414
|
help=("Filter by runner state(s). Choose one or more, or 'all'(default)."),
|
|
@@ -431,6 +484,7 @@ def add_parser(main_subparsers, parents):
|
|
|
431
484
|
_add_list_rev_parser(subparsers, parents)
|
|
432
485
|
_add_set_rev_parser(subparsers, parents)
|
|
433
486
|
_add_scale_parser(subparsers, parents)
|
|
487
|
+
_add_rollout_parser(subparsers, parents)
|
|
434
488
|
_add_runners_parser(subparsers, parents)
|
|
435
489
|
_add_delete_parser(subparsers, parents)
|
|
436
490
|
_add_delete_rev_parser(subparsers, parents)
|
fal/cli/deploy.py
CHANGED
|
@@ -7,9 +7,23 @@ from .parser import FalClientParser, RefAction, get_output_parser
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def _deploy(args):
|
|
10
|
-
|
|
10
|
+
from ._utils import get_app_data_from_toml, is_app_name
|
|
11
|
+
|
|
12
|
+
team = args.team
|
|
13
|
+
app_ref = args.app_ref
|
|
14
|
+
|
|
15
|
+
# If the app_ref is an app name, get team from pyproject.toml
|
|
16
|
+
if app_ref and is_app_name(app_ref):
|
|
17
|
+
try:
|
|
18
|
+
*_, toml_team = get_app_data_from_toml(app_ref[0])
|
|
19
|
+
team = team or toml_team
|
|
20
|
+
except (ValueError, FileNotFoundError):
|
|
21
|
+
# If we can't find the app in pyproject.toml, team remains None
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
client = SyncServerlessClient(host=args.host, team=team)
|
|
11
25
|
res = client.deploy(
|
|
12
|
-
|
|
26
|
+
app_ref,
|
|
13
27
|
app_name=args.app_name,
|
|
14
28
|
auth=args.auth,
|
|
15
29
|
strategy=args.strategy,
|
|
@@ -86,7 +100,7 @@ def add_parser(main_subparsers, parents):
|
|
|
86
100
|
"command will look for a pyproject.toml file with a [tool.fal.apps] "
|
|
87
101
|
"section and deploy the application specified with the provided app name.\n"
|
|
88
102
|
"File path example: path/to/myfile.py::MyApp\n"
|
|
89
|
-
"App name example: my-app\n"
|
|
103
|
+
"App name example: my-app (configure team in pyproject.toml)\n"
|
|
90
104
|
),
|
|
91
105
|
)
|
|
92
106
|
|
fal/cli/files.py
CHANGED
|
@@ -1,52 +1,44 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
1
3
|
from .parser import FalClientParser
|
|
2
4
|
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from fal.files import FalFileSystem
|
|
3
7
|
|
|
4
|
-
def _list(args):
|
|
5
|
-
import posixpath
|
|
6
8
|
|
|
9
|
+
def _get_fs(args) -> "FalFileSystem":
|
|
7
10
|
from fal.files import FalFileSystem
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
return FalFileSystem(host=args.host, team=args.team)
|
|
13
|
+
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
def _list(args):
|
|
16
|
+
import posixpath
|
|
17
|
+
|
|
18
|
+
for entry in _get_fs(args).ls(args.path, detail=True):
|
|
12
19
|
name = posixpath.basename(entry["name"])
|
|
13
20
|
color = "blue" if entry["type"] == "directory" else "default"
|
|
14
21
|
args.console.print(f"[{color}]{name}[/{color}]")
|
|
15
22
|
|
|
16
23
|
|
|
17
24
|
def _download(args):
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
fs = FalFileSystem()
|
|
21
|
-
fs.get(args.remote_path, args.local_path, recursive=True)
|
|
25
|
+
_get_fs(args).get(args.remote_path, args.local_path, recursive=True)
|
|
22
26
|
|
|
23
27
|
|
|
24
28
|
def _upload(args):
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
fs = FalFileSystem()
|
|
28
|
-
fs.put(args.local_path, args.remote_path, recursive=True)
|
|
29
|
+
_get_fs(args).put(args.local_path, args.remote_path, recursive=True)
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
def _upload_url(args):
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
fs = FalFileSystem()
|
|
35
|
-
fs.put_file_from_url(args.url, args.remote_path)
|
|
33
|
+
_get_fs(args).put_file_from_url(args.url, args.remote_path)
|
|
36
34
|
|
|
37
35
|
|
|
38
36
|
def _mv(args):
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
fs = FalFileSystem()
|
|
42
|
-
fs.mv(args.source, args.destination)
|
|
37
|
+
_get_fs(args).mv(args.source, args.destination)
|
|
43
38
|
|
|
44
39
|
|
|
45
40
|
def _rm(args):
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
fs = FalFileSystem()
|
|
49
|
-
fs.rm(args.path)
|
|
41
|
+
_get_fs(args).rm(args.path)
|
|
50
42
|
|
|
51
43
|
|
|
52
44
|
def add_parser(main_subparsers, parents):
|
fal/cli/keys.py
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
|
+
from fal.api.client import SyncServerlessClient
|
|
1
2
|
from fal.sdk import KeyScope
|
|
2
3
|
|
|
3
|
-
from ._utils import get_client
|
|
4
4
|
from .parser import FalClientParser
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
def _create(args):
|
|
8
|
-
client =
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
args.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
args.console.print(f"FAL_KEY='{result[1]}:{result[0]}'")
|
|
8
|
+
client = SyncServerlessClient(host=args.host, team=args.team)
|
|
9
|
+
parsed_scope = KeyScope(args.scope)
|
|
10
|
+
key_id, key_secret = client.keys.create(scope=parsed_scope, description=args.desc)
|
|
11
|
+
args.console.print(
|
|
12
|
+
f"Generated key id and key secret, with the scope `{args.scope}`.\n"
|
|
13
|
+
"This is the only time the secret will be visible.\n"
|
|
14
|
+
"You will need to generate a new key pair if you lose access to this "
|
|
15
|
+
"secret."
|
|
16
|
+
)
|
|
17
|
+
args.console.print(f"FAL_KEY='{key_id}:{key_secret}'")
|
|
19
18
|
|
|
20
19
|
|
|
21
20
|
def _add_create_parser(subparsers, parents):
|
|
@@ -42,41 +41,40 @@ def _add_create_parser(subparsers, parents):
|
|
|
42
41
|
def _list(args):
|
|
43
42
|
import json
|
|
44
43
|
|
|
45
|
-
client =
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
raise AssertionError(f"Invalid output format: {args.output}")
|
|
44
|
+
client = SyncServerlessClient(host=args.host, team=args.team)
|
|
45
|
+
keys = client.keys.list()
|
|
46
|
+
|
|
47
|
+
if args.output == "json":
|
|
48
|
+
json_keys = [
|
|
49
|
+
{
|
|
50
|
+
"key_id": key.key_id,
|
|
51
|
+
"created_at": str(key.created_at),
|
|
52
|
+
"scope": str(key.scope.value),
|
|
53
|
+
"description": key.alias,
|
|
54
|
+
}
|
|
55
|
+
for key in keys
|
|
56
|
+
]
|
|
57
|
+
args.console.print(json.dumps({"keys": json_keys}))
|
|
58
|
+
elif args.output == "pretty":
|
|
59
|
+
from rich.table import Table
|
|
60
|
+
|
|
61
|
+
table = Table()
|
|
62
|
+
table.add_column("Key ID")
|
|
63
|
+
table.add_column("Created At")
|
|
64
|
+
table.add_column("Scope")
|
|
65
|
+
table.add_column("Description")
|
|
66
|
+
|
|
67
|
+
for key in keys:
|
|
68
|
+
table.add_row(
|
|
69
|
+
key.key_id,
|
|
70
|
+
str(key.created_at),
|
|
71
|
+
str(key.scope.value),
|
|
72
|
+
key.alias,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
args.console.print(table)
|
|
76
|
+
else:
|
|
77
|
+
raise AssertionError(f"Invalid output format: {args.output}")
|
|
80
78
|
|
|
81
79
|
|
|
82
80
|
def _add_list_parser(subparsers, parents):
|
|
@@ -93,9 +91,8 @@ def _add_list_parser(subparsers, parents):
|
|
|
93
91
|
|
|
94
92
|
|
|
95
93
|
def _revoke(args):
|
|
96
|
-
client =
|
|
97
|
-
|
|
98
|
-
connection.revoke_user_key(args.key_id)
|
|
94
|
+
client = SyncServerlessClient(host=args.host, team=args.team)
|
|
95
|
+
client.keys.revoke(args.key_id)
|
|
99
96
|
|
|
100
97
|
|
|
101
98
|
def _add_revoke_parser(subparsers, parents):
|
fal/cli/queue.py
CHANGED
|
@@ -5,20 +5,20 @@ from http import HTTPStatus
|
|
|
5
5
|
|
|
6
6
|
import httpx
|
|
7
7
|
|
|
8
|
-
from fal.rest_client import REST_CLIENT
|
|
9
|
-
|
|
10
8
|
from .parser import FalClientParser, get_output_parser
|
|
11
9
|
|
|
12
10
|
|
|
13
11
|
def _queue_size(args):
|
|
12
|
+
from fal.api.client import SyncServerlessClient
|
|
14
13
|
from fal.api.deploy import _get_user
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
client = SyncServerlessClient(host=args.host, team=args.team)._create_rest_client()
|
|
16
|
+
user = _get_user(client)
|
|
17
17
|
|
|
18
|
-
url = f"{
|
|
19
|
-
headers =
|
|
18
|
+
url = f"{client.base_url}/applications/{user.username}/{args.app_name}/queue"
|
|
19
|
+
headers = client.get_headers()
|
|
20
20
|
|
|
21
|
-
with httpx.Client(base_url=
|
|
21
|
+
with httpx.Client(base_url=client.base_url, headers=headers, timeout=300) as c:
|
|
22
22
|
resp = c.get(url)
|
|
23
23
|
|
|
24
24
|
if resp.status_code != HTTPStatus.OK:
|
|
@@ -38,14 +38,16 @@ def _queue_size(args):
|
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
def _queue_flush(args):
|
|
41
|
+
from fal.api.client import SyncServerlessClient
|
|
41
42
|
from fal.api.deploy import _get_user
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
client = SyncServerlessClient(host=args.host, team=args.team)._create_rest_client()
|
|
45
|
+
user = _get_user(client)
|
|
44
46
|
|
|
45
|
-
url = f"{
|
|
46
|
-
headers =
|
|
47
|
+
url = f"{client.base_url}/applications/{user.username}/{args.app_name}/queue"
|
|
48
|
+
headers = client.get_headers()
|
|
47
49
|
|
|
48
|
-
with httpx.Client(base_url=
|
|
50
|
+
with httpx.Client(base_url=client.base_url, headers=headers, timeout=300) as c:
|
|
49
51
|
resp = c.delete(url)
|
|
50
52
|
|
|
51
53
|
if resp.status_code != HTTPStatus.OK:
|
fal/cli/run.py
CHANGED
|
@@ -8,16 +8,20 @@ def _run(args):
|
|
|
8
8
|
from fal.api.client import SyncServerlessClient
|
|
9
9
|
from fal.utils import load_function_from
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
team = args.team
|
|
12
|
+
func_ref = args.func_ref
|
|
13
|
+
|
|
14
|
+
if is_app_name(func_ref):
|
|
15
|
+
app_name = func_ref[0]
|
|
16
|
+
app_ref, *_, toml_team = get_app_data_from_toml(app_name)
|
|
17
|
+
team = team or toml_team
|
|
14
18
|
file_path, func_name = RefAction.split_ref(app_ref)
|
|
15
19
|
else:
|
|
16
|
-
file_path, func_name =
|
|
20
|
+
file_path, func_name = func_ref
|
|
17
21
|
# Turn relative path into absolute path for files
|
|
18
22
|
file_path = str(Path(file_path).absolute())
|
|
19
23
|
|
|
20
|
-
client = SyncServerlessClient(host=args.host, team=
|
|
24
|
+
client = SyncServerlessClient(host=args.host, team=team)
|
|
21
25
|
host = client._create_host(local_file_path=file_path)
|
|
22
26
|
|
|
23
27
|
loaded = load_function_from(host, file_path, func_name)
|
|
@@ -30,7 +34,7 @@ def _run(args):
|
|
|
30
34
|
|
|
31
35
|
def add_parser(main_subparsers, parents):
|
|
32
36
|
run_help = "Run fal function."
|
|
33
|
-
epilog = "Examples:\n" " fal run path/to/myfile.py::myfunc"
|
|
37
|
+
epilog = "Examples:\n" " fal run path/to/myfile.py::myfunc\n" " fal run my-app\n"
|
|
34
38
|
parser = main_subparsers.add_parser(
|
|
35
39
|
"run",
|
|
36
40
|
description=run_help,
|
|
@@ -41,6 +45,6 @@ def add_parser(main_subparsers, parents):
|
|
|
41
45
|
parser.add_argument(
|
|
42
46
|
"func_ref",
|
|
43
47
|
action=RefAction,
|
|
44
|
-
help="Function reference.",
|
|
48
|
+
help="Function reference. Configure team in pyproject.toml for app names.",
|
|
45
49
|
)
|
|
46
50
|
parser.set_defaults(func=_run)
|