fal 0.15.0__py3-none-any.whl → 1.0.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/cli.py DELETED
@@ -1,619 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- from dataclasses import dataclass, field
5
- from http import HTTPStatus
6
- from sys import argv
7
- from typing import Any, Callable, Literal
8
- from uuid import uuid4
9
-
10
- import click
11
- import openapi_fal_rest.api.billing.get_user_details as get_user_details
12
- from rich.table import Table
13
- from rich_click import RichCommand, RichGroup
14
-
15
- import fal
16
- import fal.auth as auth
17
- from fal import _serialization, api, sdk
18
- from fal.console import console
19
- from fal.exceptions import ApplicationExceptionHandler
20
- from fal.logging import get_logger, set_debug_logging
21
- from fal.logging.trace import get_tracer
22
- from fal.rest_client import REST_CLIENT
23
- from fal.sdk import AliasInfo, KeyScope
24
-
25
- DEFAULT_HOST = "api.alpha.fal.ai"
26
- HOST_ENVVAR = "FAL_HOST"
27
-
28
- DEFAULT_PORT = "443"
29
- PORT_ENVVAR = "FAL_PORT"
30
-
31
- DEBUG_ENABLED = False
32
-
33
-
34
- logger = get_logger(__name__)
35
-
36
-
37
- @dataclass
38
- class State:
39
- debug: bool = False
40
- invocation_id: str = field(default_factory=lambda: str(uuid4()))
41
-
42
-
43
- def debug_option(*param_decls: str, **kwargs: Any) -> Callable[[click.FC], click.FC]:
44
- def callback(ctx: click.Context, param: click.Parameter, value: bool) -> None:
45
- state = ctx.ensure_object(State)
46
- state.debug = value
47
- set_debug_logging(value)
48
-
49
- if not param_decls:
50
- param_decls = ("--debug",)
51
-
52
- kwargs.setdefault("is_flag", True)
53
- kwargs.setdefault("expose_value", False)
54
- kwargs.setdefault("callback", callback)
55
- kwargs.setdefault("help", "Enable detailed errors and verbose logging.")
56
- return click.option(*param_decls, **kwargs)
57
-
58
-
59
- class MainGroup(RichGroup):
60
- """A custom implementation of the top-level group
61
- (i.e. called on all commands and subcommands).
62
-
63
- This implementation allows for centralized behavior, including
64
- exception handling.
65
- """
66
-
67
- _exception_handler = ApplicationExceptionHandler()
68
-
69
- _tracer = get_tracer(__name__)
70
-
71
- def invoke(self, ctx):
72
- from click.exceptions import Abort, ClickException, Exit
73
-
74
- state = ctx.ensure_object(State)
75
- qualified_name = " ".join([ctx.info_name] + argv[1:])
76
-
77
- with self._tracer.start_as_current_span(
78
- qualified_name, attributes={"invocation_id": state.invocation_id}
79
- ):
80
- try:
81
- logger.debug(
82
- f"Executing command: {qualified_name}",
83
- command=qualified_name,
84
- )
85
- return super().invoke(ctx)
86
- except (EOFError, KeyboardInterrupt, ClickException, Exit, Abort):
87
- # let click's main handle these
88
- raise
89
- except Exception as exception:
90
- logger.error(exception)
91
- if state.debug:
92
- # Here we supress detailed errors on click lines because
93
- # they're mostly decorator calls, irrelevant to the dev's error tracing
94
- console.print_exception(suppress=[click])
95
- console.print()
96
- console.print(
97
- f"The [markdown.code]invocation_id[/] for this operation is: [white]{state.invocation_id}[/]"
98
- )
99
- else:
100
- self._exception_handler.handle(exception)
101
-
102
- def add_command(
103
- self,
104
- cmd: RichCommand,
105
- name: str | None = None,
106
- aliases: list[str] | None = None,
107
- ) -> None:
108
- name = name or cmd.name
109
- assert name, "Command must have a name"
110
-
111
- if not aliases:
112
- aliases = []
113
-
114
- if aliases:
115
- # Add aliases to the help text
116
- cmd.help = (cmd.help or "") + "\n\nAlias: " + ", ".join([name, *aliases])
117
- cmd.short_help = (
118
- (cmd.short_help or "") + "(Alias: " + ", ".join(aliases) + ")"
119
- )
120
-
121
- super().add_command(cmd, name)
122
- alias_cmd = AliasCommand(cmd)
123
-
124
- for alias in aliases:
125
- self.add_command(alias_cmd, alias)
126
-
127
-
128
- class AliasCommand(RichCommand):
129
- def __init__(self, wrapped):
130
- self._wrapped = wrapped
131
-
132
- def __getattribute__(self, __name: str):
133
- if __name == "_wrapped":
134
- # To be able to call `self._wrapped` below
135
- return super().__getattribute__(__name)
136
-
137
- if __name == "hidden":
138
- return True
139
-
140
- return self._wrapped.__getattribute__(__name)
141
-
142
-
143
- @click.group(cls=MainGroup)
144
- @click.version_option()
145
- @debug_option()
146
- def cli():
147
- pass
148
-
149
-
150
- ###### Auth group ######
151
- @click.group(cls=RichGroup)
152
- @debug_option()
153
- def auth_cli():
154
- pass
155
-
156
-
157
- @auth_cli.command(name="login")
158
- @debug_option()
159
- def auth_login():
160
- auth.login()
161
-
162
-
163
- @auth_cli.command(name="logout")
164
- @debug_option()
165
- def auth_logout():
166
- auth.logout()
167
-
168
-
169
- @auth_cli.command(name="hello", hidden=True)
170
- @debug_option()
171
- def auth_test():
172
- """
173
- To test auth.
174
- """
175
- print(f"Hello, {auth.USER.info['name']} - '{auth.USER.info['sub']}'")
176
-
177
-
178
- ###### Key group ######
179
- @click.group(cls=RichGroup)
180
- @click.option("--host", default=DEFAULT_HOST, envvar=HOST_ENVVAR)
181
- @click.option("--port", default=DEFAULT_PORT, envvar=PORT_ENVVAR, hidden=True)
182
- @debug_option()
183
- @click.pass_context
184
- def key_cli(ctx, host: str, port: str):
185
- ctx.obj = sdk.FalServerlessClient(f"{host}:{port}")
186
-
187
-
188
- @key_cli.command(name="generate", no_args_is_help=True)
189
- @click.option(
190
- "--scope",
191
- default=None,
192
- required=True,
193
- type=click.Choice([KeyScope.ADMIN.value, KeyScope.API.value]),
194
- help="The privilage scope of the key.",
195
- )
196
- @click.option(
197
- "--alias",
198
- default=None,
199
- help="An alias for the key.",
200
- )
201
- @debug_option()
202
- @click.pass_obj
203
- def key_generate(client: sdk.FalServerlessClient, scope: str, alias: str | None):
204
- with client.connect() as connection:
205
- parsed_scope = KeyScope(scope)
206
- result = connection.create_user_key(parsed_scope, alias)
207
- print(
208
- f"Generated key id and key secret, with the scope `{scope}`.\n"
209
- "This is the only time the secret will be visible.\n"
210
- "You will need to generate a new key pair if you lose access to this secret."
211
- )
212
- print(f"FAL_KEY='{result[1]}:{result[0]}'")
213
-
214
-
215
- @key_cli.command(name="list")
216
- @debug_option()
217
- @click.pass_obj
218
- def key_list(client: sdk.FalServerlessClient):
219
- table = Table(title="Keys")
220
- table.add_column("Key ID")
221
- table.add_column("Created At")
222
- table.add_column("Scope")
223
- table.add_column("Alias")
224
-
225
- with client.connect() as connection:
226
- keys = connection.list_user_keys()
227
- for key in keys:
228
- table.add_row(
229
- key.key_id, str(key.created_at), str(key.scope.value), key.alias
230
- )
231
-
232
- console.print(table)
233
-
234
-
235
- @key_cli.command(name="revoke")
236
- @click.argument("key_id", required=True)
237
- @debug_option()
238
- @click.pass_obj
239
- def key_revoke(client: sdk.FalServerlessClient, key_id: str):
240
- with client.connect() as connection:
241
- connection.revoke_user_key(key_id)
242
-
243
-
244
- ##### Function group #####
245
- ALIAS_AUTH_OPTIONS = ["public", "private", "shared"]
246
- ALIAS_AUTH_TYPE = Literal["public", "private", "shared"]
247
-
248
-
249
- @click.group(cls=RichGroup)
250
- @click.option("--host", default=DEFAULT_HOST, envvar=HOST_ENVVAR)
251
- @click.option("--port", default=DEFAULT_PORT, envvar=PORT_ENVVAR, hidden=True)
252
- @debug_option()
253
- @click.pass_context
254
- def function_cli(ctx, host: str, port: str):
255
- ctx.obj = api.FalServerlessHost(f"{host}:{port}")
256
-
257
-
258
- def load_function_from(
259
- host: api.FalServerlessHost,
260
- file_path: str,
261
- function_name: str,
262
- ) -> api.IsolatedFunction:
263
- import runpy
264
-
265
- module = runpy.run_path(file_path)
266
- if function_name not in module:
267
- raise api.FalServerlessError(f"Function '{function_name}' not found in module")
268
-
269
- # The module for the function is set to <run_path> when runpy is used, in which
270
- # case we want to manually include the package it is defined in.
271
- _serialization.include_package_from_path(file_path)
272
-
273
- target = module[function_name]
274
- if isinstance(target, type) and issubclass(target, fal.App):
275
- target = fal.wrap_app(target, host=host)
276
-
277
- if not isinstance(target, api.IsolatedFunction):
278
- raise api.FalServerlessError(
279
- f"Function '{function_name}' is not a fal.function or a fal.App"
280
- )
281
- return target
282
-
283
-
284
- @function_cli.command("serve")
285
- @click.option("--alias", default=None)
286
- @click.option(
287
- "--auth",
288
- "auth_mode",
289
- type=click.Choice(ALIAS_AUTH_OPTIONS),
290
- default="private",
291
- )
292
- @click.argument("file_path", required=True)
293
- @click.argument("function_name", required=True)
294
- @debug_option()
295
- @click.pass_obj
296
- def register_application(
297
- host: api.FalServerlessHost,
298
- file_path: str,
299
- function_name: str,
300
- alias: str | None,
301
- auth_mode: ALIAS_AUTH_TYPE,
302
- ):
303
- user_id = _get_user_id()
304
-
305
- isolated_function = load_function_from(host, file_path, function_name)
306
- gateway_options = isolated_function.options.gateway
307
- if "serve" not in gateway_options and "exposed_port" not in gateway_options:
308
- raise api.FalServerlessError(
309
- "One of `serve` or `exposed_port` options needs to be specified in the isolated annotation to register a function"
310
- )
311
- elif (
312
- "exposed_port" in gateway_options
313
- and str(gateway_options.get("exposed_port")) != "8080"
314
- ):
315
- raise api.FalServerlessError(
316
- "Must expose port 8080 for now. This will be configurable in the future."
317
- )
318
-
319
- id = host.register(
320
- func=isolated_function.func,
321
- options=isolated_function.options,
322
- application_name=alias,
323
- application_auth_mode=auth_mode,
324
- metadata=isolated_function.options.host.get("metadata", {}),
325
- )
326
-
327
- if id:
328
- gateway_host = remove_http_and_port_from_url(host.url)
329
- gateway_host = (
330
- gateway_host.replace("api.", "").replace("alpha.", "").replace("ai", "run")
331
- )
332
-
333
- if alias:
334
- console.print(
335
- f"Registered a new revision for function '{alias}' (revision='{id}')."
336
- )
337
- console.print(f"URL: https://{gateway_host}/{user_id}/{alias}")
338
- else:
339
- console.print(f"Registered anonymous function '{id}'.")
340
- console.print(f"URL: https://{gateway_host}/{user_id}/{id}")
341
-
342
-
343
- @function_cli.command("run")
344
- @click.argument("file_path", required=True)
345
- @click.argument("function_name", required=True)
346
- @debug_option()
347
- @click.pass_obj
348
- def run(host: api.FalServerlessHost, file_path: str, function_name: str):
349
- isolated_function = load_function_from(host, file_path, function_name)
350
- # let our exc handlers handle UserFunctionException
351
- isolated_function.reraise = False
352
- isolated_function()
353
-
354
-
355
- @function_cli.command("logs")
356
- @click.option("--lines", default=100)
357
- @click.option("--url", default=None)
358
- @debug_option()
359
- @click.pass_obj
360
- def get_logs(
361
- host: api.FalServerlessHost, lines: int | None = 100, url: str | None = None
362
- ):
363
- console.print(
364
- "logs command is deprecated. To see logs, got to fal web page: https://www.fal.ai/dashboard/logs"
365
- )
366
-
367
-
368
- ##### Alias group #####
369
- @click.group(cls=RichGroup)
370
- @click.option("--host", default=DEFAULT_HOST, envvar=HOST_ENVVAR)
371
- @click.option("--port", default=DEFAULT_PORT, envvar=PORT_ENVVAR, hidden=True)
372
- @debug_option()
373
- @click.pass_context
374
- def alias_cli(ctx, host: str, port: str):
375
- ctx.obj = api.FalServerlessClient(f"{host}:{port}")
376
-
377
-
378
- def _alias_table(aliases: list[AliasInfo]):
379
- table = Table(title="Function Aliases")
380
- table.add_column("Alias")
381
- table.add_column("Revision")
382
- table.add_column("Auth")
383
- table.add_column("Min Concurrency")
384
- table.add_column("Max Concurrency")
385
- table.add_column("Max Multiplexing")
386
- table.add_column("Keep Alive")
387
- table.add_column("Active Workers")
388
-
389
- for app_alias in aliases:
390
- table.add_row(
391
- app_alias.alias,
392
- app_alias.revision,
393
- app_alias.auth_mode,
394
- str(app_alias.min_concurrency),
395
- str(app_alias.max_concurrency),
396
- str(app_alias.max_multiplexing),
397
- str(app_alias.keep_alive),
398
- str(app_alias.active_runners),
399
- )
400
-
401
- return table
402
-
403
-
404
- @alias_cli.command("set")
405
- @click.argument("alias", required=True)
406
- @click.argument("revision", required=True)
407
- @click.option(
408
- "--auth",
409
- "auth_mode",
410
- type=click.Choice(ALIAS_AUTH_OPTIONS),
411
- default="private",
412
- )
413
- @debug_option()
414
- @click.pass_obj
415
- def alias_set(
416
- client: api.FalServerlessClient,
417
- alias: str,
418
- revision: str,
419
- auth_mode: ALIAS_AUTH_TYPE,
420
- ):
421
- with client.connect() as connection:
422
- connection.create_alias(alias, revision, auth_mode)
423
-
424
-
425
- @alias_cli.command("delete")
426
- @click.argument("alias", required=True)
427
- @debug_option()
428
- @click.pass_obj
429
- def alias_delete(client: api.FalServerlessClient, alias: str):
430
- with client.connect() as connection:
431
- application_id = connection.delete_alias(alias)
432
-
433
- console.print(f"Deleted alias '{alias}' for application '{application_id}'.")
434
-
435
-
436
- @alias_cli.command("list")
437
- @debug_option()
438
- @click.pass_obj
439
- def alias_list(client: api.FalServerlessClient):
440
- with client.connect() as connection:
441
- aliases = connection.list_aliases()
442
- table = _alias_table(aliases)
443
-
444
- console.print(table)
445
-
446
-
447
- @alias_cli.command("update")
448
- @click.argument("alias", required=True)
449
- @click.option("--keep-alive", "-k", type=int)
450
- @click.option("--max-multiplexing", "-m", type=int)
451
- @click.option("--max-concurrency", "-c", type=int)
452
- @click.option("--min-concurrency", type=int)
453
- # TODO: add auth_mode
454
- # @click.option(
455
- # "--auth",
456
- # "auth_mode",
457
- # type=click.Choice(ALIAS_AUTH_OPTIONS),
458
- # )
459
- @debug_option()
460
- @click.pass_obj
461
- def alias_update(
462
- client: api.FalServerlessClient,
463
- alias: str,
464
- keep_alive: int | None,
465
- max_multiplexing: int | None,
466
- max_concurrency: int | None,
467
- min_concurrency: int | None,
468
- ):
469
- with client.connect() as connection:
470
- if (
471
- keep_alive is None
472
- and max_multiplexing is None
473
- and max_concurrency is None
474
- and min_concurrency is None
475
- ):
476
- console.log("No parameters for update were provided, ignoring.")
477
- return
478
-
479
- alias_info = connection.update_application(
480
- application_name=alias,
481
- keep_alive=keep_alive,
482
- max_multiplexing=max_multiplexing,
483
- max_concurrency=max_concurrency,
484
- min_concurrency=min_concurrency,
485
- )
486
- table = _alias_table([alias_info])
487
-
488
- console.print(table)
489
-
490
-
491
- @alias_cli.command("runners")
492
- @click.argument("alias", required=True)
493
- @debug_option()
494
- @click.pass_obj
495
- def alias_list_runners(
496
- client: api.FalServerlessClient,
497
- alias: str,
498
- ):
499
- with client.connect() as connection:
500
- runners = connection.list_alias_runners(alias=alias)
501
-
502
- table = Table(title="Application Runners")
503
- table.add_column("Runner ID")
504
- table.add_column("In Flight Requests")
505
- table.add_column("Expires in")
506
- table.add_column("Uptime")
507
-
508
- for runner in runners:
509
- table.add_row(
510
- runner.runner_id,
511
- str(runner.in_flight_requests),
512
- (
513
- "N/A (active)"
514
- if not runner.expiration_countdown
515
- else f"{runner.expiration_countdown}s"
516
- ),
517
- f"{runner.uptime} ({runner.uptime.total_seconds()}s)",
518
- )
519
-
520
- console.print(table)
521
-
522
-
523
- ##### Secrets group #####
524
- @click.group(cls=RichGroup)
525
- @click.option("--host", default=DEFAULT_HOST, envvar=HOST_ENVVAR)
526
- @click.option("--port", default=DEFAULT_PORT, envvar=PORT_ENVVAR, hidden=True)
527
- @debug_option()
528
- @click.pass_context
529
- def secrets_cli(ctx, host: str, port: str):
530
- ctx.obj = sdk.FalServerlessClient(f"{host}:{port}")
531
-
532
-
533
- @secrets_cli.command("list")
534
- @debug_option()
535
- @click.pass_obj
536
- def list_secrets(client: api.FalServerlessClient):
537
- table = Table(title="Secrets")
538
- table.add_column("Secret Name")
539
- table.add_column("Created At")
540
-
541
- with client.connect() as connection:
542
- for secret in connection.list_secrets():
543
- table.add_row(secret.name, str(secret.created_at))
544
-
545
- console.print(table)
546
-
547
-
548
- @secrets_cli.command("set")
549
- @click.argument("secret_name", required=True)
550
- @click.argument("secret_value", required=True)
551
- @debug_option()
552
- @click.pass_obj
553
- def set_secret(client: api.FalServerlessClient, secret_name: str, secret_value: str):
554
- with client.connect() as connection:
555
- connection.set_secret(secret_name, secret_value)
556
- console.print(f"Secret '{secret_name}' has set")
557
-
558
-
559
- @secrets_cli.command("delete")
560
- @click.argument("secret_name", required=True)
561
- @debug_option()
562
- @click.pass_obj
563
- def delete_secret(client: api.FalServerlessClient, secret_name: str):
564
- with client.connect() as connection:
565
- connection.delete_secret(secret_name)
566
- console.print(f"Secret '{secret_name}' has deleted")
567
-
568
-
569
- # Setup of groups
570
- cli.add_command(auth_cli, name="auth")
571
- cli.add_command(key_cli, name="key", aliases=["keys"])
572
- cli.add_command(function_cli, name="function", aliases=["fn"])
573
- cli.add_command(alias_cli, name="alias", aliases=["aliases"])
574
- cli.add_command(secrets_cli, name="secret", aliases=["secrets"])
575
-
576
-
577
- def remove_http_and_port_from_url(url):
578
- # Remove http://
579
- if "http://" in url:
580
- url = url.replace("http://", "")
581
-
582
- # Remove https://
583
- if "https://" in url:
584
- url = url.replace("https://", "")
585
-
586
- # Remove port information
587
- url_parts = url.split(":")
588
- if len(url_parts) > 1:
589
- url = url_parts[0]
590
-
591
- return url
592
-
593
-
594
- def _get_user_id() -> str:
595
- try:
596
- user_details_response = get_user_details.sync_detailed(
597
- client=REST_CLIENT,
598
- )
599
- except Exception as e:
600
- raise api.FalServerlessError(f"Error fetching user details: {str(e)}")
601
-
602
- if user_details_response.status_code != HTTPStatus.OK:
603
- try:
604
- content = json.loads(user_details_response.content.decode("utf8"))
605
- except Exception:
606
- raise api.FalServerlessError(
607
- f"Error fetching user details: {user_details_response}"
608
- )
609
- else:
610
- raise api.FalServerlessError(content["detail"])
611
- try:
612
- full_user_id = user_details_response.parsed.user_id
613
- _provider, _, user_id = full_user_id.partition("|")
614
- if not user_id:
615
- user_id = full_user_id
616
-
617
- return user_id
618
- except Exception as e:
619
- raise api.FalServerlessError(f"Could not parse the user data: {e}")
@@ -1,58 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import TYPE_CHECKING, Generic, TypeVar
4
-
5
- from grpc import Call as RpcCall
6
-
7
- from fal.console import console
8
- from fal.console.icons import CROSS_ICON
9
-
10
- if TYPE_CHECKING:
11
- from fal.api import UserFunctionException
12
-
13
-
14
- ExceptionType = TypeVar("ExceptionType", bound=BaseException)
15
-
16
-
17
- class BaseExceptionHandler(Generic[ExceptionType]):
18
- """Base handler defaults to the string representation of the error"""
19
-
20
- def should_handle(self, _: Exception) -> bool:
21
- return True
22
-
23
- def handle(self, exception: ExceptionType):
24
- msg = f"{CROSS_ICON} {str(exception)}"
25
- cause = exception.__cause__
26
- if cause is not None:
27
- msg += f": {str(cause)}"
28
- console.print(msg)
29
-
30
-
31
- class GrpcExceptionHandler(BaseExceptionHandler[RpcCall]):
32
- """Handle GRPC errors. The user message is part of the `details()`"""
33
-
34
- def should_handle(self, exception: Exception) -> bool:
35
- return isinstance(exception, RpcCall)
36
-
37
- def handle(self, exception: RpcCall):
38
- console.print(exception.details())
39
-
40
-
41
- class UserFunctionExceptionHandler(BaseExceptionHandler["UserFunctionException"]):
42
- def should_handle(self, exception: Exception) -> bool:
43
- from fal.api import UserFunctionException
44
-
45
- return isinstance(exception, UserFunctionException)
46
-
47
- def handle(self, exception: UserFunctionException):
48
- import rich
49
-
50
- cause = exception.__cause__
51
- exc = cause or exception
52
- tb = rich.traceback.Traceback.from_exception(
53
- type(exc),
54
- exc,
55
- exc.__traceback__,
56
- )
57
- console.print(tb)
58
- super().handle(exception)
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- fal = fal.cli:cli
File without changes