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.

Files changed (42) hide show
  1. fal/__init__.py +2 -28
  2. fal/api.py +13 -13
  3. fal/app.py +162 -0
  4. fal/cli.py +124 -73
  5. fal/exceptions/_base.py +1 -1
  6. fal/exceptions/auth.py +1 -1
  7. fal/rest_client.py +1 -0
  8. fal/sdk.py +41 -10
  9. fal/sync.py +3 -2
  10. fal/toolkit/file/file.py +6 -5
  11. fal/toolkit/file/providers/r2.py +83 -0
  12. fal/toolkit/file/types.py +1 -1
  13. fal/toolkit/image/image.py +2 -2
  14. {fal-0.11.2.dist-info → fal-0.11.4.dist-info}/METADATA +40 -3
  15. {fal-0.11.2.dist-info → fal-0.11.4.dist-info}/RECORD +42 -40
  16. openapi_fal_rest/api/admin/handle_user_lock.py +6 -2
  17. openapi_fal_rest/api/applications/get_status_applications_app_user_id_app_alias_or_id_status_get.py +6 -2
  18. openapi_fal_rest/api/billing/delete_payment_method.py +9 -3
  19. openapi_fal_rest/api/billing/get_setup_intent_key.py +6 -2
  20. openapi_fal_rest/api/billing/get_user_price.py +6 -2
  21. openapi_fal_rest/api/billing/get_user_spending.py +6 -2
  22. openapi_fal_rest/api/billing/handle_stripe_webhook.py +21 -7
  23. openapi_fal_rest/api/billing/upcoming_invoice.py +6 -2
  24. openapi_fal_rest/api/billing/update_customer_budget.py +6 -2
  25. openapi_fal_rest/api/files/check_dir_hash.py +9 -3
  26. openapi_fal_rest/api/files/delete.py +6 -2
  27. openapi_fal_rest/api/files/download.py +6 -2
  28. openapi_fal_rest/api/files/file_exists.py +6 -2
  29. openapi_fal_rest/api/files/upload_from_url.py +6 -2
  30. openapi_fal_rest/api/files/upload_local_file.py +9 -3
  31. openapi_fal_rest/api/keys/create_key.py +6 -2
  32. openapi_fal_rest/api/keys/delete_key.py +6 -2
  33. openapi_fal_rest/api/tokens/create_token.py +6 -2
  34. openapi_fal_rest/api/usage/get_gateway_request_stats_by_time.py +41 -7
  35. openapi_fal_rest/api/usage/per_machine_usage_details.py +3 -1
  36. openapi_fal_rest/models/__init__.py +3 -1
  37. openapi_fal_rest/models/body_upload_file.py +4 -1
  38. openapi_fal_rest/models/body_upload_local_file.py +4 -1
  39. openapi_fal_rest/models/get_gateway_request_stats_by_time_response_get_gateway_request_stats_by_time.py +15 -5
  40. openapi_fal_rest/models/grouped_usage_detail.py +6 -6
  41. {fal-0.11.2.dist-info → fal-0.11.4.dist-info}/WHEEL +0 -0
  42. {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
- max_multiplexing = options.host.get(
358
- "max_multiplexing", FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING
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
- max_multiplexing = options.host.get(
423
- "max_multiplexing", FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING
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["components"].get("schemas") or {}:
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 grpc
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(["public", "private", "shared"]),
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: Literal["public", "private", "shared"],
276
+ auth_mode: ALIAS_AUTH_TYPE,
259
277
  ):
260
- import runpy
261
-
262
278
  user_id = _get_user_id()
263
279
 
264
- module = runpy.run_path(file_path)
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
- max_concurrency=max_concurrency,
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
- @alias_cli.command("list")
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 alias_list(client: api.FalServerlessClient):
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
- table = Table(title="Function Aliases")
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
- console.print(table)
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
- @alias_cli.command("scale")
366
- @click.argument("alias", required=True)
367
- @click.argument("max_concurrency", required=True, type=int)
414
+
415
+ @alias_cli.command("list")
368
416
  @click.pass_obj
369
- def alias_scale(client: api.FalServerlessClient, alias: str, max_concurrency: int):
417
+ def alias_list(client: api.FalServerlessClient):
370
418
  with client.connect() as connection:
371
- connection.scale(application_name=alias, max_concurrency=max_concurrency)
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 not (keep_alive or max_multiplexing):
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
@@ -14,4 +14,4 @@ class FalServerlessException(Exception):
14
14
  super().__init__(message)
15
15
 
16
16
  def __str__(self) -> str:
17
- return self.message
17
+ return self.message + (f"\nHint: {self.hint}" if self.hint else "")
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
  )
fal/rest_client.py CHANGED
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import fal.flags as flags
4
4
  from fal.sdk import get_default_credentials
5
+
5
6
  from openapi_fal_rest.client import Client
6
7
 
7
8