fal 0.11.2__py3-none-any.whl → 0.11.4__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/__init__.py +2 -28
- fal/api.py +13 -13
- fal/app.py +162 -0
- fal/cli.py +124 -73
- fal/exceptions/_base.py +1 -1
- fal/exceptions/auth.py +1 -1
- fal/rest_client.py +1 -0
- fal/sdk.py +41 -10
- fal/sync.py +3 -2
- fal/toolkit/file/file.py +6 -5
- fal/toolkit/file/providers/r2.py +83 -0
- fal/toolkit/file/types.py +1 -1
- fal/toolkit/image/image.py +2 -2
- {fal-0.11.2.dist-info → fal-0.11.4.dist-info}/METADATA +40 -3
- {fal-0.11.2.dist-info → fal-0.11.4.dist-info}/RECORD +42 -40
- openapi_fal_rest/api/admin/handle_user_lock.py +6 -2
- openapi_fal_rest/api/applications/get_status_applications_app_user_id_app_alias_or_id_status_get.py +6 -2
- openapi_fal_rest/api/billing/delete_payment_method.py +9 -3
- openapi_fal_rest/api/billing/get_setup_intent_key.py +6 -2
- openapi_fal_rest/api/billing/get_user_price.py +6 -2
- openapi_fal_rest/api/billing/get_user_spending.py +6 -2
- openapi_fal_rest/api/billing/handle_stripe_webhook.py +21 -7
- openapi_fal_rest/api/billing/upcoming_invoice.py +6 -2
- openapi_fal_rest/api/billing/update_customer_budget.py +6 -2
- openapi_fal_rest/api/files/check_dir_hash.py +9 -3
- openapi_fal_rest/api/files/delete.py +6 -2
- openapi_fal_rest/api/files/download.py +6 -2
- openapi_fal_rest/api/files/file_exists.py +6 -2
- openapi_fal_rest/api/files/upload_from_url.py +6 -2
- openapi_fal_rest/api/files/upload_local_file.py +9 -3
- openapi_fal_rest/api/keys/create_key.py +6 -2
- openapi_fal_rest/api/keys/delete_key.py +6 -2
- openapi_fal_rest/api/tokens/create_token.py +6 -2
- openapi_fal_rest/api/usage/get_gateway_request_stats_by_time.py +41 -7
- openapi_fal_rest/api/usage/per_machine_usage_details.py +3 -1
- openapi_fal_rest/models/__init__.py +3 -1
- openapi_fal_rest/models/body_upload_file.py +4 -1
- openapi_fal_rest/models/body_upload_local_file.py +4 -1
- openapi_fal_rest/models/get_gateway_request_stats_by_time_response_get_gateway_request_stats_by_time.py +15 -5
- openapi_fal_rest/models/grouped_usage_detail.py +6 -6
- {fal-0.11.2.dist-info → fal-0.11.4.dist-info}/WHEEL +0 -0
- {fal-0.11.2.dist-info → fal-0.11.4.dist-info}/entry_points.txt +0 -0
fal/__init__.py
CHANGED
|
@@ -6,6 +6,7 @@ from fal import apps
|
|
|
6
6
|
from fal.api import FalServerlessHost, LocalHost, cached
|
|
7
7
|
from fal.api import function
|
|
8
8
|
from fal.api import function as isolated
|
|
9
|
+
from fal.app import App, endpoint, wrap_app
|
|
9
10
|
from fal.sdk import FalServerlessKeyCredentials
|
|
10
11
|
from fal.sync import sync_dir
|
|
11
12
|
|
|
@@ -15,35 +16,8 @@ serverless = FalServerlessHost()
|
|
|
15
16
|
# DEPRECATED - use serverless instead
|
|
16
17
|
cloud = FalServerlessHost()
|
|
17
18
|
|
|
18
|
-
DBT_FAL_IMPORT_NOTICE = """
|
|
19
|
-
The dbt tool `fal` and `dbt-fal` adapter have been merged into a single tool.
|
|
20
|
-
Please import from the `fal.dbt` module instead.
|
|
21
|
-
Running `pip install dbt-fal` will install the new tool and the adapter alongside.
|
|
22
|
-
Then import from the `fal.dbt` module like
|
|
23
|
-
|
|
24
|
-
from fal.dbt import {name}
|
|
25
|
-
|
|
26
|
-
"""
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
# Avoid printing on non-direct imports
|
|
30
|
-
def __getattr__(name: str):
|
|
31
|
-
if name in (
|
|
32
|
-
"NodeStatus",
|
|
33
|
-
"FalDbt",
|
|
34
|
-
"DbtModel",
|
|
35
|
-
"DbtSource",
|
|
36
|
-
"DbtTest",
|
|
37
|
-
"DbtGenericTest",
|
|
38
|
-
"DbtSingularTest",
|
|
39
|
-
"Context",
|
|
40
|
-
"CurrentModel",
|
|
41
|
-
):
|
|
42
|
-
raise ImportError(DBT_FAL_IMPORT_NOTICE.format(name=name))
|
|
43
|
-
|
|
44
|
-
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
45
|
-
|
|
46
19
|
|
|
20
|
+
# NOTE: This makes `import fal.dbt` import the `dbt-fal` module and `import fal` import the `fal` module
|
|
47
21
|
# NOTE: taken from dbt-core: https://github.com/dbt-labs/dbt-core/blob/ac539fd5cf325cfb5315339077d03399d575f570/core/dbt/adapters/__init__.py#L1-L7
|
|
48
22
|
# N.B.
|
|
49
23
|
# This will add to the package’s __path__ all subdirectories of directories on sys.path named after the package which effectively combines both modules into a single namespace (dbt.adapters)
|
fal/api.py
CHANGED
|
@@ -76,9 +76,7 @@ class Host(Generic[ArgsT, ReturnT]):
|
|
|
76
76
|
is executed."""
|
|
77
77
|
|
|
78
78
|
_SUPPORTED_KEYS: ClassVar[frozenset[str]] = frozenset()
|
|
79
|
-
_GATEWAY_KEYS: ClassVar[frozenset[str]] = frozenset(
|
|
80
|
-
{"serve", "exposed_port", "max_concurrency"}
|
|
81
|
-
)
|
|
79
|
+
_GATEWAY_KEYS: ClassVar[frozenset[str]] = frozenset({"serve", "exposed_port"})
|
|
82
80
|
|
|
83
81
|
def __post_init__(self):
|
|
84
82
|
assert not self._SUPPORTED_KEYS.intersection(
|
|
@@ -118,7 +116,6 @@ class Host(Generic[ArgsT, ReturnT]):
|
|
|
118
116
|
self,
|
|
119
117
|
func: Callable[ArgsT, ReturnT],
|
|
120
118
|
options: Options,
|
|
121
|
-
max_concurrency: int | None = None,
|
|
122
119
|
application_name: str | None = None,
|
|
123
120
|
application_auth_mode: Literal["public", "shared", "private"] | None = None,
|
|
124
121
|
metadata: dict[str, Any] | None = None,
|
|
@@ -311,6 +308,7 @@ class FalServerlessHost(Host):
|
|
|
311
308
|
{
|
|
312
309
|
"machine_type",
|
|
313
310
|
"keep_alive",
|
|
311
|
+
"max_concurrency",
|
|
314
312
|
"max_multiplexing",
|
|
315
313
|
"setup_function",
|
|
316
314
|
"metadata",
|
|
@@ -341,7 +339,6 @@ class FalServerlessHost(Host):
|
|
|
341
339
|
self,
|
|
342
340
|
func: Callable[ArgsT, ReturnT],
|
|
343
341
|
options: Options,
|
|
344
|
-
max_concurrency: int | None = None,
|
|
345
342
|
application_name: str | None = None,
|
|
346
343
|
application_auth_mode: Literal["public", "shared", "private"] | None = None,
|
|
347
344
|
metadata: dict[str, Any] | None = None,
|
|
@@ -354,9 +351,8 @@ class FalServerlessHost(Host):
|
|
|
354
351
|
"machine_type", FAL_SERVERLESS_DEFAULT_MACHINE_TYPE
|
|
355
352
|
)
|
|
356
353
|
keep_alive = options.host.get("keep_alive", FAL_SERVERLESS_DEFAULT_KEEP_ALIVE)
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
)
|
|
354
|
+
max_concurrency = options.host.get("max_concurrency")
|
|
355
|
+
max_multiplexing = options.host.get("max_multiplexing")
|
|
360
356
|
base_image = options.host.get("_base_image", None)
|
|
361
357
|
scheduler = options.host.get("_scheduler", None)
|
|
362
358
|
scheduler_options = options.host.get("_scheduler_options", None)
|
|
@@ -370,6 +366,7 @@ class FalServerlessHost(Host):
|
|
|
370
366
|
scheduler=scheduler,
|
|
371
367
|
scheduler_options=scheduler_options,
|
|
372
368
|
max_multiplexing=max_multiplexing,
|
|
369
|
+
max_concurrency=max_concurrency,
|
|
373
370
|
)
|
|
374
371
|
|
|
375
372
|
partial_func = _prepare_partial_func(func)
|
|
@@ -394,7 +391,6 @@ class FalServerlessHost(Host):
|
|
|
394
391
|
application_name=application_name,
|
|
395
392
|
application_auth_mode=application_auth_mode,
|
|
396
393
|
machine_requirements=machine_requirements,
|
|
397
|
-
max_concurrency=max_concurrency,
|
|
398
394
|
metadata=metadata,
|
|
399
395
|
):
|
|
400
396
|
for log in partial_result.logs:
|
|
@@ -419,9 +415,8 @@ class FalServerlessHost(Host):
|
|
|
419
415
|
"machine_type", FAL_SERVERLESS_DEFAULT_MACHINE_TYPE
|
|
420
416
|
)
|
|
421
417
|
keep_alive = options.host.get("keep_alive", FAL_SERVERLESS_DEFAULT_KEEP_ALIVE)
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
)
|
|
418
|
+
max_concurrency = options.host.get("max_concurrency")
|
|
419
|
+
max_multiplexing = options.host.get("max_multiplexing")
|
|
425
420
|
base_image = options.host.get("_base_image", None)
|
|
426
421
|
scheduler = options.host.get("_scheduler", None)
|
|
427
422
|
scheduler_options = options.host.get("_scheduler_options", None)
|
|
@@ -436,6 +431,7 @@ class FalServerlessHost(Host):
|
|
|
436
431
|
scheduler=scheduler,
|
|
437
432
|
scheduler_options=scheduler_options,
|
|
438
433
|
max_multiplexing=max_multiplexing,
|
|
434
|
+
max_concurrency=max_concurrency,
|
|
439
435
|
)
|
|
440
436
|
|
|
441
437
|
return_value = _UNSET
|
|
@@ -556,6 +552,7 @@ def function(
|
|
|
556
552
|
exposed_port: int | None = None,
|
|
557
553
|
max_concurrency: int | None = None,
|
|
558
554
|
# FalServerlessHost options
|
|
555
|
+
metadata: dict[str, Any] | None = None,
|
|
559
556
|
machine_type: str = FAL_SERVERLESS_DEFAULT_MACHINE_TYPE,
|
|
560
557
|
keep_alive: int = FAL_SERVERLESS_DEFAULT_KEEP_ALIVE,
|
|
561
558
|
max_multiplexing: int = FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING,
|
|
@@ -580,6 +577,7 @@ def function(
|
|
|
580
577
|
exposed_port: int | None = None,
|
|
581
578
|
max_concurrency: int | None = None,
|
|
582
579
|
# FalServerlessHost options
|
|
580
|
+
metadata: dict[str, Any] | None = None,
|
|
583
581
|
machine_type: str = FAL_SERVERLESS_DEFAULT_MACHINE_TYPE,
|
|
584
582
|
keep_alive: int = FAL_SERVERLESS_DEFAULT_KEEP_ALIVE,
|
|
585
583
|
max_multiplexing: int = FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING,
|
|
@@ -656,6 +654,7 @@ def function(
|
|
|
656
654
|
exposed_port: int | None = None,
|
|
657
655
|
max_concurrency: int | None = None,
|
|
658
656
|
# FalServerlessHost options
|
|
657
|
+
metadata: dict[str, Any] | None = None,
|
|
659
658
|
machine_type: str = FAL_SERVERLESS_DEFAULT_MACHINE_TYPE,
|
|
660
659
|
keep_alive: int = FAL_SERVERLESS_DEFAULT_KEEP_ALIVE,
|
|
661
660
|
max_multiplexing: int = FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING,
|
|
@@ -685,6 +684,7 @@ def function(
|
|
|
685
684
|
exposed_port: int | None = None,
|
|
686
685
|
max_concurrency: int | None = None,
|
|
687
686
|
# FalServerlessHost options
|
|
687
|
+
metadata: dict[str, Any] | None = None,
|
|
688
688
|
machine_type: str = FAL_SERVERLESS_DEFAULT_MACHINE_TYPE,
|
|
689
689
|
keep_alive: int = FAL_SERVERLESS_DEFAULT_KEEP_ALIVE,
|
|
690
690
|
max_multiplexing: int = FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING,
|
|
@@ -792,7 +792,7 @@ class ServeWrapper:
|
|
|
792
792
|
if "properties" in schema:
|
|
793
793
|
mark_order(schema, "properties")
|
|
794
794
|
|
|
795
|
-
for key in spec
|
|
795
|
+
for key in spec.get("components", {}).get("schemas") or {}:
|
|
796
796
|
order_schema_object(spec["components"]["schemas"][key])
|
|
797
797
|
|
|
798
798
|
return spec
|
fal/app.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import os
|
|
5
|
+
import fal.api
|
|
6
|
+
from fal.toolkit import mainify
|
|
7
|
+
from fastapi import FastAPI
|
|
8
|
+
from typing import Any, NamedTuple, Callable, TypeVar, ClassVar
|
|
9
|
+
from fal.logging import get_logger
|
|
10
|
+
|
|
11
|
+
EndpointT = TypeVar("EndpointT", bound=Callable[..., Any])
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def wrap_app(cls: type[App], **kwargs) -> fal.api.IsolatedFunction:
|
|
16
|
+
def initialize_and_serve():
|
|
17
|
+
app = cls()
|
|
18
|
+
app.serve()
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
app = cls(_allow_init=True)
|
|
22
|
+
metadata = app.openapi()
|
|
23
|
+
except Exception as exc:
|
|
24
|
+
logger.warning("Failed to build OpenAPI specification for %s", cls.__name__)
|
|
25
|
+
metadata = {}
|
|
26
|
+
|
|
27
|
+
wrapper = fal.api.function(
|
|
28
|
+
"virtualenv",
|
|
29
|
+
requirements=cls.requirements,
|
|
30
|
+
machine_type=cls.machine_type,
|
|
31
|
+
**cls.host_kwargs,
|
|
32
|
+
**kwargs,
|
|
33
|
+
metadata=metadata,
|
|
34
|
+
serve=True,
|
|
35
|
+
)
|
|
36
|
+
return wrapper(initialize_and_serve).on(
|
|
37
|
+
serve=False,
|
|
38
|
+
exposed_port=8080,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@mainify
|
|
43
|
+
class RouteSignature(NamedTuple):
|
|
44
|
+
path: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@mainify
|
|
48
|
+
class App:
|
|
49
|
+
requirements: ClassVar[list[str]] = []
|
|
50
|
+
machine_type: ClassVar[str] = "S"
|
|
51
|
+
host_kwargs: ClassVar[dict[str, Any]] = {}
|
|
52
|
+
|
|
53
|
+
def __init_subclass__(cls, **kwargs):
|
|
54
|
+
cls.host_kwargs = kwargs
|
|
55
|
+
|
|
56
|
+
if cls.__init__ is not App.__init__:
|
|
57
|
+
raise ValueError(
|
|
58
|
+
"App classes should not override __init__ directly. "
|
|
59
|
+
"Use setup() instead."
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def __init__(self, *, _allow_init: bool = False):
|
|
63
|
+
if not _allow_init and not os.getenv("IS_ISOLATE_AGENT"):
|
|
64
|
+
raise NotImplementedError(
|
|
65
|
+
"Running apps through SDK is not implemented yet."
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def setup(self):
|
|
69
|
+
"""Setup the application before serving."""
|
|
70
|
+
|
|
71
|
+
def serve(self) -> None:
|
|
72
|
+
import uvicorn
|
|
73
|
+
|
|
74
|
+
app = self._build_app()
|
|
75
|
+
self.setup()
|
|
76
|
+
uvicorn.run(app, host="0.0.0.0", port=8080)
|
|
77
|
+
|
|
78
|
+
def _build_app(self) -> FastAPI:
|
|
79
|
+
from fastapi import FastAPI
|
|
80
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
81
|
+
|
|
82
|
+
_app = FastAPI()
|
|
83
|
+
|
|
84
|
+
_app.add_middleware(
|
|
85
|
+
CORSMiddleware,
|
|
86
|
+
allow_credentials=True,
|
|
87
|
+
allow_headers=("*"),
|
|
88
|
+
allow_methods=("*"),
|
|
89
|
+
allow_origins=("*"),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
routes: dict[RouteSignature, Callable[..., Any]] = {
|
|
93
|
+
signature: endpoint
|
|
94
|
+
for _, endpoint in inspect.getmembers(self, inspect.ismethod)
|
|
95
|
+
if (signature := getattr(endpoint, "route_signature", None))
|
|
96
|
+
}
|
|
97
|
+
if not routes:
|
|
98
|
+
raise ValueError("An application must have at least one route!")
|
|
99
|
+
|
|
100
|
+
for signature, endpoint in routes.items():
|
|
101
|
+
_app.add_api_route(
|
|
102
|
+
signature.path,
|
|
103
|
+
endpoint,
|
|
104
|
+
name=endpoint.__name__,
|
|
105
|
+
methods=["POST"],
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return _app
|
|
109
|
+
|
|
110
|
+
def openapi(self) -> dict[str, Any]:
|
|
111
|
+
"""
|
|
112
|
+
Build the OpenAPI specification for the served function.
|
|
113
|
+
Attach needed metadata for a better integration to fal.
|
|
114
|
+
"""
|
|
115
|
+
app = self._build_app()
|
|
116
|
+
spec = app.openapi()
|
|
117
|
+
self._mark_order_openapi(spec)
|
|
118
|
+
return spec
|
|
119
|
+
|
|
120
|
+
def _mark_order_openapi(self, spec: dict[str, Any]):
|
|
121
|
+
"""
|
|
122
|
+
Add x-fal-order-* keys to the OpenAPI specification to help the rendering of UI.
|
|
123
|
+
|
|
124
|
+
NOTE: We rely on the fact that fastapi and Python dicts keep the order of properties.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
def mark_order(obj: dict[str, Any], key: str):
|
|
128
|
+
obj[f"x-fal-order-{key}"] = list(obj[key].keys())
|
|
129
|
+
|
|
130
|
+
mark_order(spec, "paths")
|
|
131
|
+
|
|
132
|
+
def order_schema_object(schema: dict[str, Any]):
|
|
133
|
+
"""
|
|
134
|
+
Mark the order of properties in the schema object.
|
|
135
|
+
They can have 'allOf', 'properties' or '$ref' key.
|
|
136
|
+
"""
|
|
137
|
+
if "allOf" in schema:
|
|
138
|
+
for sub_schema in schema["allOf"]:
|
|
139
|
+
order_schema_object(sub_schema)
|
|
140
|
+
if "properties" in schema:
|
|
141
|
+
mark_order(schema, "properties")
|
|
142
|
+
|
|
143
|
+
for key in spec["components"].get("schemas") or {}:
|
|
144
|
+
order_schema_object(spec["components"]["schemas"][key])
|
|
145
|
+
|
|
146
|
+
return spec
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@mainify
|
|
150
|
+
def endpoint(path: str) -> Callable[[EndpointT], EndpointT]:
|
|
151
|
+
"""Designate the decorated function as an application endpoint."""
|
|
152
|
+
|
|
153
|
+
def marker_fn(callable: EndpointT) -> EndpointT:
|
|
154
|
+
if hasattr(callable, "route_signature"):
|
|
155
|
+
raise ValueError(
|
|
156
|
+
f"Can't set multiple routes for the same function: {callable.__name__}"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
callable.route_signature = RouteSignature(path=path) # type: ignore
|
|
160
|
+
return callable
|
|
161
|
+
|
|
162
|
+
return marker_fn
|
fal/cli.py
CHANGED
|
@@ -9,9 +9,7 @@ from uuid import uuid4
|
|
|
9
9
|
|
|
10
10
|
import click
|
|
11
11
|
import fal.auth as auth
|
|
12
|
-
import
|
|
13
|
-
import openapi_fal_rest.api.billing.get_user_details as get_user_details
|
|
14
|
-
import openapi_fal_rest.api.logs.list_since as list_logs
|
|
12
|
+
import fal
|
|
15
13
|
from fal import api, sdk
|
|
16
14
|
from fal.console import console
|
|
17
15
|
from fal.exceptions import ApplicationExceptionHandler
|
|
@@ -19,10 +17,13 @@ from fal.logging import get_logger, set_debug_logging
|
|
|
19
17
|
from fal.logging.isolate import IsolateLogPrinter
|
|
20
18
|
from fal.logging.trace import get_tracer
|
|
21
19
|
from fal.rest_client import REST_CLIENT
|
|
22
|
-
from fal.sdk import KeyScope
|
|
20
|
+
from fal.sdk import AliasInfo, KeyScope
|
|
23
21
|
from isolate.logs import Log, LogLevel, LogSource
|
|
24
22
|
from rich.table import Table
|
|
25
23
|
|
|
24
|
+
import openapi_fal_rest.api.billing.get_user_details as get_user_details
|
|
25
|
+
import openapi_fal_rest.api.logs.list_since as list_logs
|
|
26
|
+
|
|
26
27
|
DEFAULT_HOST = "api.alpha.fal.ai"
|
|
27
28
|
HOST_ENVVAR = "FAL_HOST"
|
|
28
29
|
|
|
@@ -144,15 +145,6 @@ def auth_cli():
|
|
|
144
145
|
@auth_cli.command(name="login")
|
|
145
146
|
def auth_login():
|
|
146
147
|
auth.login()
|
|
147
|
-
try:
|
|
148
|
-
client = sdk.FalServerlessClient(f"{DEFAULT_HOST}:{DEFAULT_PORT}")
|
|
149
|
-
with client.connect() as connection:
|
|
150
|
-
connection.list_aliases()
|
|
151
|
-
except grpc.RpcError as e:
|
|
152
|
-
if "Insufficient permissions" in e.details():
|
|
153
|
-
console.print(e.details())
|
|
154
|
-
else:
|
|
155
|
-
raise e
|
|
156
148
|
|
|
157
149
|
|
|
158
150
|
@auth_cli.command(name="logout")
|
|
@@ -231,6 +223,10 @@ def key_revoke(client: sdk.FalServerlessClient, key_id: str):
|
|
|
231
223
|
|
|
232
224
|
|
|
233
225
|
##### Function group #####
|
|
226
|
+
ALIAS_AUTH_OPTIONS = ["public", "private", "shared"]
|
|
227
|
+
ALIAS_AUTH_TYPE = Literal["public", "private", "shared"]
|
|
228
|
+
|
|
229
|
+
|
|
234
230
|
@click.group
|
|
235
231
|
@click.option("--host", default=DEFAULT_HOST, envvar=HOST_ENVVAR)
|
|
236
232
|
@click.option("--port", default=DEFAULT_PORT, envvar=PORT_ENVVAR, hidden=True)
|
|
@@ -239,12 +235,34 @@ def function_cli(ctx, host: str, port: str):
|
|
|
239
235
|
ctx.obj = api.FalServerlessHost(f"{host}:{port}")
|
|
240
236
|
|
|
241
237
|
|
|
238
|
+
def load_function_from(
|
|
239
|
+
host: api.FalServerlessHost,
|
|
240
|
+
file_path: str,
|
|
241
|
+
function_name: str,
|
|
242
|
+
) -> api.IsolatedFunction:
|
|
243
|
+
import runpy
|
|
244
|
+
|
|
245
|
+
module = runpy.run_path(file_path)
|
|
246
|
+
if function_name not in module:
|
|
247
|
+
raise api.FalServerlessError(f"Function '{function_name}' not found in module")
|
|
248
|
+
|
|
249
|
+
target = module[function_name]
|
|
250
|
+
if isinstance(target, type) and issubclass(target, fal.App):
|
|
251
|
+
target = fal.wrap_app(target, host=host)
|
|
252
|
+
|
|
253
|
+
if not isinstance(target, api.IsolatedFunction):
|
|
254
|
+
raise api.FalServerlessError(
|
|
255
|
+
f"Function '{function_name}' is not a fal.function or a fal.App"
|
|
256
|
+
)
|
|
257
|
+
return target
|
|
258
|
+
|
|
259
|
+
|
|
242
260
|
@function_cli.command("serve")
|
|
243
261
|
@click.option("--alias", default=None)
|
|
244
262
|
@click.option(
|
|
245
263
|
"--auth",
|
|
246
264
|
"auth_mode",
|
|
247
|
-
type=click.Choice(
|
|
265
|
+
type=click.Choice(ALIAS_AUTH_OPTIONS),
|
|
248
266
|
default="private",
|
|
249
267
|
)
|
|
250
268
|
@click.argument("file_path", required=True)
|
|
@@ -255,17 +273,11 @@ def register_application(
|
|
|
255
273
|
file_path: str,
|
|
256
274
|
function_name: str,
|
|
257
275
|
alias: str | None,
|
|
258
|
-
auth_mode:
|
|
276
|
+
auth_mode: ALIAS_AUTH_TYPE,
|
|
259
277
|
):
|
|
260
|
-
import runpy
|
|
261
|
-
|
|
262
278
|
user_id = _get_user_id()
|
|
263
279
|
|
|
264
|
-
|
|
265
|
-
if function_name not in module:
|
|
266
|
-
raise api.FalServerlessError(f"Function '{function_name}' not found in module")
|
|
267
|
-
|
|
268
|
-
isolated_function: api.IsolatedFunction = module[function_name]
|
|
280
|
+
isolated_function = load_function_from(host, file_path, function_name)
|
|
269
281
|
gateway_options = isolated_function.options.gateway
|
|
270
282
|
if "serve" not in gateway_options and "exposed_port" not in gateway_options:
|
|
271
283
|
raise api.FalServerlessError(
|
|
@@ -279,14 +291,12 @@ def register_application(
|
|
|
279
291
|
"Must expose port 8080 for now. This will be configurable in the future."
|
|
280
292
|
)
|
|
281
293
|
|
|
282
|
-
max_concurrency = gateway_options.get("max_concurrency")
|
|
283
294
|
id = host.register(
|
|
284
295
|
func=isolated_function.func,
|
|
285
296
|
options=isolated_function.options,
|
|
286
297
|
application_name=alias,
|
|
287
298
|
application_auth_mode=auth_mode,
|
|
288
|
-
|
|
289
|
-
metadata={},
|
|
299
|
+
metadata=isolated_function.options.host.get("metadata", {}),
|
|
290
300
|
)
|
|
291
301
|
|
|
292
302
|
if id:
|
|
@@ -304,6 +314,15 @@ def register_application(
|
|
|
304
314
|
console.print(f"URL: https://{user_id}-{id}.{gateway_host}")
|
|
305
315
|
|
|
306
316
|
|
|
317
|
+
@function_cli.command("run")
|
|
318
|
+
@click.argument("file_path", required=True)
|
|
319
|
+
@click.argument("function_name", required=True)
|
|
320
|
+
@click.pass_obj
|
|
321
|
+
def run(host: api.FalServerlessHost, file_path: str, function_name: str):
|
|
322
|
+
isolated_function = load_function_from(host, file_path, function_name)
|
|
323
|
+
isolated_function()
|
|
324
|
+
|
|
325
|
+
|
|
307
326
|
@function_cli.command("logs")
|
|
308
327
|
@click.option("--lines", default=100)
|
|
309
328
|
@click.option("--url", default=None)
|
|
@@ -341,57 +360,113 @@ def alias_cli(ctx, host: str, port: str):
|
|
|
341
360
|
ctx.obj = api.FalServerlessClient(f"{host}:{port}")
|
|
342
361
|
|
|
343
362
|
|
|
344
|
-
|
|
363
|
+
def _alias_table(aliases: list[AliasInfo]):
|
|
364
|
+
table = Table(title="Function Aliases")
|
|
365
|
+
table.add_column("Alias")
|
|
366
|
+
table.add_column("Revision")
|
|
367
|
+
table.add_column("Auth")
|
|
368
|
+
table.add_column("Max Concurrency")
|
|
369
|
+
table.add_column("Max Multiplexing")
|
|
370
|
+
table.add_column("Keep Alive")
|
|
371
|
+
|
|
372
|
+
for app_alias in aliases:
|
|
373
|
+
table.add_row(
|
|
374
|
+
app_alias.alias,
|
|
375
|
+
app_alias.revision,
|
|
376
|
+
app_alias.auth_mode,
|
|
377
|
+
str(app_alias.max_concurrency),
|
|
378
|
+
str(app_alias.max_multiplexing),
|
|
379
|
+
str(app_alias.keep_alive),
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
return table
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
@alias_cli.command("set")
|
|
386
|
+
@click.argument("alias", required=True)
|
|
387
|
+
@click.argument("revision", required=True)
|
|
388
|
+
@click.option(
|
|
389
|
+
"--auth",
|
|
390
|
+
"auth_mode",
|
|
391
|
+
type=click.Choice(ALIAS_AUTH_OPTIONS),
|
|
392
|
+
default="private",
|
|
393
|
+
)
|
|
345
394
|
@click.pass_obj
|
|
346
|
-
def
|
|
395
|
+
def alias_set(
|
|
396
|
+
client: api.FalServerlessClient,
|
|
397
|
+
alias: str,
|
|
398
|
+
revision: str,
|
|
399
|
+
auth_mode: ALIAS_AUTH_TYPE,
|
|
400
|
+
):
|
|
347
401
|
with client.connect() as connection:
|
|
348
|
-
|
|
349
|
-
table.add_column("Alias")
|
|
350
|
-
table.add_column("Revision")
|
|
351
|
-
table.add_column("Auth")
|
|
352
|
-
table.add_column("Max Concurrency")
|
|
402
|
+
connection.create_alias(alias, revision, auth_mode)
|
|
353
403
|
|
|
354
|
-
for app_alias in connection.list_aliases():
|
|
355
|
-
table.add_row(
|
|
356
|
-
app_alias.alias,
|
|
357
|
-
app_alias.revision,
|
|
358
|
-
app_alias.auth_mode,
|
|
359
|
-
str(app_alias.max_concurrency),
|
|
360
|
-
)
|
|
361
404
|
|
|
362
|
-
|
|
405
|
+
@alias_cli.command("delete")
|
|
406
|
+
@click.argument("alias", required=True)
|
|
407
|
+
@click.pass_obj
|
|
408
|
+
def alias_delete(client: api.FalServerlessClient, alias: str):
|
|
409
|
+
with client.connect() as connection:
|
|
410
|
+
application_id = connection.delete_alias(alias)
|
|
363
411
|
|
|
412
|
+
console.print(f"Deleted alias '{alias}' for application '{application_id}'.")
|
|
364
413
|
|
|
365
|
-
|
|
366
|
-
@
|
|
367
|
-
@click.argument("max_concurrency", required=True, type=int)
|
|
414
|
+
|
|
415
|
+
@alias_cli.command("list")
|
|
368
416
|
@click.pass_obj
|
|
369
|
-
def
|
|
417
|
+
def alias_list(client: api.FalServerlessClient):
|
|
370
418
|
with client.connect() as connection:
|
|
371
|
-
connection.
|
|
419
|
+
aliases = connection.list_aliases()
|
|
420
|
+
table = _alias_table(aliases)
|
|
421
|
+
|
|
422
|
+
console.print(table)
|
|
372
423
|
|
|
373
424
|
|
|
374
425
|
@alias_cli.command("update")
|
|
375
426
|
@click.argument("alias", required=True)
|
|
376
|
-
@click.option("--keep-alive", type=int)
|
|
377
|
-
@click.option("--max-multiplexing", type=int)
|
|
427
|
+
@click.option("--keep-alive", "-k", type=int)
|
|
428
|
+
@click.option("--max-multiplexing", "-m", type=int)
|
|
429
|
+
@click.option("--max-concurrency", "-c", type=int)
|
|
430
|
+
# TODO: add auth_mode
|
|
431
|
+
# @click.option(
|
|
432
|
+
# "--auth",
|
|
433
|
+
# "auth_mode",
|
|
434
|
+
# type=click.Choice(ALIAS_AUTH_OPTIONS),
|
|
435
|
+
# )
|
|
378
436
|
@click.pass_obj
|
|
379
437
|
def alias_update(
|
|
380
438
|
client: api.FalServerlessClient,
|
|
381
439
|
alias: str,
|
|
382
440
|
keep_alive: int | None,
|
|
383
441
|
max_multiplexing: int | None,
|
|
442
|
+
max_concurrency: int | None,
|
|
384
443
|
):
|
|
385
444
|
with client.connect() as connection:
|
|
386
|
-
if
|
|
445
|
+
if keep_alive is None and max_multiplexing is None and max_concurrency is None:
|
|
387
446
|
console.log("No parameters for update were provided, ignoring.")
|
|
388
447
|
return
|
|
389
448
|
|
|
390
|
-
connection.update_application(
|
|
449
|
+
alias_info = connection.update_application(
|
|
391
450
|
application_name=alias,
|
|
392
451
|
keep_alive=keep_alive,
|
|
393
452
|
max_multiplexing=max_multiplexing,
|
|
453
|
+
max_concurrency=max_concurrency,
|
|
394
454
|
)
|
|
455
|
+
table = _alias_table([alias_info])
|
|
456
|
+
|
|
457
|
+
console.print(table)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
@alias_cli.command("scale")
|
|
461
|
+
@click.argument("alias", required=True)
|
|
462
|
+
@click.argument("max_concurrency", required=True, type=int)
|
|
463
|
+
def alias_scale(alias: str, max_concurrency: int):
|
|
464
|
+
alias_update.callback(
|
|
465
|
+
alias=alias,
|
|
466
|
+
keep_alive=None,
|
|
467
|
+
max_multiplexing=None,
|
|
468
|
+
max_concurrency=max_concurrency,
|
|
469
|
+
) # type: ignore
|
|
395
470
|
|
|
396
471
|
|
|
397
472
|
##### Secrets group #####
|
|
@@ -461,30 +536,6 @@ def remove_http_and_port_from_url(url):
|
|
|
461
536
|
return url
|
|
462
537
|
|
|
463
538
|
|
|
464
|
-
# dbt-fal commands to be errored out
|
|
465
|
-
DBT_FAL_COMMAND_NOTICE = """
|
|
466
|
-
The dbt tool `fal` and `dbt-fal` adapter have been merged into a single tool.
|
|
467
|
-
Please use the new `dbt-fal` command line tool instead.
|
|
468
|
-
Running `pip install dbt-fal` will install the new tool and the adapter alongside.
|
|
469
|
-
Then run your command like
|
|
470
|
-
|
|
471
|
-
dbt-fal <command>
|
|
472
|
-
|
|
473
|
-
"""
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
@cli.command("run", context_settings={"ignore_unknown_options": True})
|
|
477
|
-
@click.argument("any", nargs=-1, type=click.UNPROCESSED)
|
|
478
|
-
def dbt_run(any):
|
|
479
|
-
raise click.BadArgumentUsage(DBT_FAL_COMMAND_NOTICE)
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
@cli.command("flow", context_settings={"ignore_unknown_options": True})
|
|
483
|
-
@click.argument("any", nargs=-1, type=click.UNPROCESSED)
|
|
484
|
-
def dbt_flow(any):
|
|
485
|
-
raise click.BadArgumentUsage(DBT_FAL_COMMAND_NOTICE)
|
|
486
|
-
|
|
487
|
-
|
|
488
539
|
def _get_user_id() -> str:
|
|
489
540
|
try:
|
|
490
541
|
user_details_response = get_user_details.sync_detailed(
|
fal/exceptions/_base.py
CHANGED
fal/exceptions/auth.py
CHANGED
|
@@ -9,5 +9,5 @@ class UnauthenticatedException(FalServerlessException):
|
|
|
9
9
|
def __init__(self) -> None:
|
|
10
10
|
super().__init__(
|
|
11
11
|
message="You must be authenticated.",
|
|
12
|
-
hint="Login via `fal auth login`",
|
|
12
|
+
hint="Login via `fal auth login` or make sure to setup fal keys correctly.",
|
|
13
13
|
)
|