fal 0.11.2__py3-none-any.whl → 0.11.3__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/api.py +8 -12
- fal/cli.py +90 -31
- 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.3.dist-info}/METADATA +40 -3
- {fal-0.11.2.dist-info → fal-0.11.3.dist-info}/RECORD +36 -35
- 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/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
- {fal-0.11.2.dist-info → fal-0.11.3.dist-info}/WHEEL +0 -0
- {fal-0.11.2.dist-info → fal-0.11.3.dist-info}/entry_points.txt +0 -0
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
|
fal/cli.py
CHANGED
|
@@ -10,8 +10,6 @@ from uuid import uuid4
|
|
|
10
10
|
import click
|
|
11
11
|
import fal.auth as auth
|
|
12
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
|
|
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
|
|
|
@@ -231,6 +232,10 @@ def key_revoke(client: sdk.FalServerlessClient, key_id: str):
|
|
|
231
232
|
|
|
232
233
|
|
|
233
234
|
##### Function group #####
|
|
235
|
+
ALIAS_AUTH_OPTIONS = ["public", "private", "shared"]
|
|
236
|
+
ALIAS_AUTH_TYPE = Literal["public", "private", "shared"]
|
|
237
|
+
|
|
238
|
+
|
|
234
239
|
@click.group
|
|
235
240
|
@click.option("--host", default=DEFAULT_HOST, envvar=HOST_ENVVAR)
|
|
236
241
|
@click.option("--port", default=DEFAULT_PORT, envvar=PORT_ENVVAR, hidden=True)
|
|
@@ -244,7 +249,7 @@ def function_cli(ctx, host: str, port: str):
|
|
|
244
249
|
@click.option(
|
|
245
250
|
"--auth",
|
|
246
251
|
"auth_mode",
|
|
247
|
-
type=click.Choice(
|
|
252
|
+
type=click.Choice(ALIAS_AUTH_OPTIONS),
|
|
248
253
|
default="private",
|
|
249
254
|
)
|
|
250
255
|
@click.argument("file_path", required=True)
|
|
@@ -255,7 +260,7 @@ def register_application(
|
|
|
255
260
|
file_path: str,
|
|
256
261
|
function_name: str,
|
|
257
262
|
alias: str | None,
|
|
258
|
-
auth_mode:
|
|
263
|
+
auth_mode: ALIAS_AUTH_TYPE,
|
|
259
264
|
):
|
|
260
265
|
import runpy
|
|
261
266
|
|
|
@@ -279,13 +284,11 @@ def register_application(
|
|
|
279
284
|
"Must expose port 8080 for now. This will be configurable in the future."
|
|
280
285
|
)
|
|
281
286
|
|
|
282
|
-
max_concurrency = gateway_options.get("max_concurrency")
|
|
283
287
|
id = host.register(
|
|
284
288
|
func=isolated_function.func,
|
|
285
289
|
options=isolated_function.options,
|
|
286
290
|
application_name=alias,
|
|
287
291
|
application_auth_mode=auth_mode,
|
|
288
|
-
max_concurrency=max_concurrency,
|
|
289
292
|
metadata={},
|
|
290
293
|
)
|
|
291
294
|
|
|
@@ -341,57 +344,113 @@ def alias_cli(ctx, host: str, port: str):
|
|
|
341
344
|
ctx.obj = api.FalServerlessClient(f"{host}:{port}")
|
|
342
345
|
|
|
343
346
|
|
|
344
|
-
|
|
347
|
+
def _alias_table(aliases: list[AliasInfo]):
|
|
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")
|
|
353
|
+
table.add_column("Max Multiplexing")
|
|
354
|
+
table.add_column("Keep Alive")
|
|
355
|
+
|
|
356
|
+
for app_alias in aliases:
|
|
357
|
+
table.add_row(
|
|
358
|
+
app_alias.alias,
|
|
359
|
+
app_alias.revision,
|
|
360
|
+
app_alias.auth_mode,
|
|
361
|
+
str(app_alias.max_concurrency),
|
|
362
|
+
str(app_alias.max_multiplexing),
|
|
363
|
+
str(app_alias.keep_alive),
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
return table
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
@alias_cli.command("set")
|
|
370
|
+
@click.argument("alias", required=True)
|
|
371
|
+
@click.argument("revision", required=True)
|
|
372
|
+
@click.option(
|
|
373
|
+
"--auth",
|
|
374
|
+
"auth_mode",
|
|
375
|
+
type=click.Choice(ALIAS_AUTH_OPTIONS),
|
|
376
|
+
default="private",
|
|
377
|
+
)
|
|
345
378
|
@click.pass_obj
|
|
346
|
-
def
|
|
379
|
+
def alias_set(
|
|
380
|
+
client: api.FalServerlessClient,
|
|
381
|
+
alias: str,
|
|
382
|
+
revision: str,
|
|
383
|
+
auth_mode: ALIAS_AUTH_TYPE,
|
|
384
|
+
):
|
|
347
385
|
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")
|
|
386
|
+
connection.create_alias(alias, revision, auth_mode)
|
|
353
387
|
|
|
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
388
|
|
|
362
|
-
|
|
389
|
+
@alias_cli.command("delete")
|
|
390
|
+
@click.argument("alias", required=True)
|
|
391
|
+
@click.pass_obj
|
|
392
|
+
def alias_delete(client: api.FalServerlessClient, alias: str):
|
|
393
|
+
with client.connect() as connection:
|
|
394
|
+
application_id = connection.delete_alias(alias)
|
|
363
395
|
|
|
396
|
+
console.print(f"Deleted alias '{alias}' for application '{application_id}'.")
|
|
364
397
|
|
|
365
|
-
|
|
366
|
-
@
|
|
367
|
-
@click.argument("max_concurrency", required=True, type=int)
|
|
398
|
+
|
|
399
|
+
@alias_cli.command("list")
|
|
368
400
|
@click.pass_obj
|
|
369
|
-
def
|
|
401
|
+
def alias_list(client: api.FalServerlessClient):
|
|
370
402
|
with client.connect() as connection:
|
|
371
|
-
connection.
|
|
403
|
+
aliases = connection.list_aliases()
|
|
404
|
+
table = _alias_table(aliases)
|
|
405
|
+
|
|
406
|
+
console.print(table)
|
|
372
407
|
|
|
373
408
|
|
|
374
409
|
@alias_cli.command("update")
|
|
375
410
|
@click.argument("alias", required=True)
|
|
376
|
-
@click.option("--keep-alive", type=int)
|
|
377
|
-
@click.option("--max-multiplexing", type=int)
|
|
411
|
+
@click.option("--keep-alive", "-k", type=int)
|
|
412
|
+
@click.option("--max-multiplexing", "-m", type=int)
|
|
413
|
+
@click.option("--max-concurrency", "-c", type=int)
|
|
414
|
+
# TODO: add auth_mode
|
|
415
|
+
# @click.option(
|
|
416
|
+
# "--auth",
|
|
417
|
+
# "auth_mode",
|
|
418
|
+
# type=click.Choice(ALIAS_AUTH_OPTIONS),
|
|
419
|
+
# )
|
|
378
420
|
@click.pass_obj
|
|
379
421
|
def alias_update(
|
|
380
422
|
client: api.FalServerlessClient,
|
|
381
423
|
alias: str,
|
|
382
424
|
keep_alive: int | None,
|
|
383
425
|
max_multiplexing: int | None,
|
|
426
|
+
max_concurrency: int | None,
|
|
384
427
|
):
|
|
385
428
|
with client.connect() as connection:
|
|
386
|
-
if
|
|
429
|
+
if keep_alive is None and max_multiplexing is None and max_concurrency is None:
|
|
387
430
|
console.log("No parameters for update were provided, ignoring.")
|
|
388
431
|
return
|
|
389
432
|
|
|
390
|
-
connection.update_application(
|
|
433
|
+
alias_info = connection.update_application(
|
|
391
434
|
application_name=alias,
|
|
392
435
|
keep_alive=keep_alive,
|
|
393
436
|
max_multiplexing=max_multiplexing,
|
|
437
|
+
max_concurrency=max_concurrency,
|
|
394
438
|
)
|
|
439
|
+
table = _alias_table([alias_info])
|
|
440
|
+
|
|
441
|
+
console.print(table)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
@alias_cli.command("scale")
|
|
445
|
+
@click.argument("alias", required=True)
|
|
446
|
+
@click.argument("max_concurrency", required=True, type=int)
|
|
447
|
+
def alias_scale(alias: str, max_concurrency: int):
|
|
448
|
+
alias_update.callback(
|
|
449
|
+
alias=alias,
|
|
450
|
+
keep_alive=None,
|
|
451
|
+
max_multiplexing=None,
|
|
452
|
+
max_concurrency=max_concurrency,
|
|
453
|
+
) # type: ignore
|
|
395
454
|
|
|
396
455
|
|
|
397
456
|
##### Secrets group #####
|
fal/rest_client.py
CHANGED
fal/sdk.py
CHANGED
|
@@ -184,7 +184,9 @@ class AliasInfo:
|
|
|
184
184
|
alias: str
|
|
185
185
|
revision: str
|
|
186
186
|
auth_mode: str
|
|
187
|
+
keep_alive: int
|
|
187
188
|
max_concurrency: int
|
|
189
|
+
max_multiplexing: int
|
|
188
190
|
|
|
189
191
|
|
|
190
192
|
@dataclass
|
|
@@ -258,7 +260,9 @@ def _from_grpc_alias_info(message: isolate_proto.AliasInfo) -> AliasInfo:
|
|
|
258
260
|
alias=message.alias,
|
|
259
261
|
revision=message.revision,
|
|
260
262
|
auth_mode=auth_mode,
|
|
263
|
+
keep_alive=message.keep_alive,
|
|
261
264
|
max_concurrency=message.max_concurrency,
|
|
265
|
+
max_multiplexing=message.max_multiplexing,
|
|
262
266
|
)
|
|
263
267
|
|
|
264
268
|
|
|
@@ -306,7 +310,8 @@ class MachineRequirements:
|
|
|
306
310
|
exposed_port: int | None = None
|
|
307
311
|
scheduler: str | None = None
|
|
308
312
|
scheduler_options: dict[str, Any] | None = None
|
|
309
|
-
|
|
313
|
+
max_concurrency: int | None = None
|
|
314
|
+
max_multiplexing: int | None = None
|
|
310
315
|
|
|
311
316
|
|
|
312
317
|
@dataclass
|
|
@@ -386,7 +391,6 @@ class FalServerlessConnection:
|
|
|
386
391
|
application_name: str | None = None,
|
|
387
392
|
application_auth_mode: Literal["public", "private", "shared"] | None = None,
|
|
388
393
|
*,
|
|
389
|
-
max_concurrency: int | None = None,
|
|
390
394
|
serialization_method: str = _DEFAULT_SERIALIZATION_METHOD,
|
|
391
395
|
machine_requirements: MachineRequirements | None = None,
|
|
392
396
|
metadata: dict[str, Any] | None = None,
|
|
@@ -402,6 +406,7 @@ class FalServerlessConnection:
|
|
|
402
406
|
scheduler_options=to_struct(
|
|
403
407
|
machine_requirements.scheduler_options or {}
|
|
404
408
|
),
|
|
409
|
+
max_concurrency=machine_requirements.max_concurrency,
|
|
405
410
|
max_multiplexing=machine_requirements.max_multiplexing,
|
|
406
411
|
)
|
|
407
412
|
else:
|
|
@@ -423,7 +428,6 @@ class FalServerlessConnection:
|
|
|
423
428
|
function=wrapped_function,
|
|
424
429
|
environments=environments,
|
|
425
430
|
machine_requirements=wrapped_requirements,
|
|
426
|
-
max_concurrency=max_concurrency,
|
|
427
431
|
application_name=application_name,
|
|
428
432
|
auth_mode=auth_mode,
|
|
429
433
|
metadata=struct_metadata,
|
|
@@ -432,24 +436,25 @@ class FalServerlessConnection:
|
|
|
432
436
|
yield from_grpc(partial_result)
|
|
433
437
|
|
|
434
438
|
def scale(self, application_name: str, max_concurrency: int | None = None) -> None:
|
|
435
|
-
|
|
436
|
-
application_name=application_name,
|
|
437
|
-
max_concurrency=max_concurrency,
|
|
438
|
-
)
|
|
439
|
-
self.stub.ScaleApplication(request)
|
|
439
|
+
raise NotImplementedError
|
|
440
440
|
|
|
441
441
|
def update_application(
|
|
442
442
|
self,
|
|
443
443
|
application_name: str,
|
|
444
444
|
keep_alive: int | None = None,
|
|
445
445
|
max_multiplexing: int | None = None,
|
|
446
|
-
|
|
446
|
+
max_concurrency: int | None = None,
|
|
447
|
+
) -> AliasInfo:
|
|
447
448
|
request = isolate_proto.UpdateApplicationRequest(
|
|
448
449
|
application_name=application_name,
|
|
449
450
|
keep_alive=keep_alive,
|
|
450
451
|
max_multiplexing=max_multiplexing,
|
|
452
|
+
max_concurrency=max_concurrency,
|
|
453
|
+
)
|
|
454
|
+
res: isolate_proto.UpdateApplicationResult = self.stub.UpdateApplication(
|
|
455
|
+
request
|
|
451
456
|
)
|
|
452
|
-
|
|
457
|
+
return from_grpc(res.alias_info)
|
|
453
458
|
|
|
454
459
|
def run(
|
|
455
460
|
self,
|
|
@@ -471,6 +476,7 @@ class FalServerlessConnection:
|
|
|
471
476
|
scheduler_options=to_struct(
|
|
472
477
|
machine_requirements.scheduler_options or {}
|
|
473
478
|
),
|
|
479
|
+
max_concurrency=machine_requirements.max_concurrency,
|
|
474
480
|
max_multiplexing=machine_requirements.max_multiplexing,
|
|
475
481
|
)
|
|
476
482
|
else:
|
|
@@ -488,6 +494,31 @@ class FalServerlessConnection:
|
|
|
488
494
|
for partial_result in self.stub.Run(request):
|
|
489
495
|
yield from_grpc(partial_result)
|
|
490
496
|
|
|
497
|
+
def create_alias(
|
|
498
|
+
self,
|
|
499
|
+
alias: str,
|
|
500
|
+
revision: str,
|
|
501
|
+
auth_mode: Literal["public", "private", "shared"],
|
|
502
|
+
):
|
|
503
|
+
if auth_mode == "public":
|
|
504
|
+
auth = isolate_proto.ApplicationAuthMode.PUBLIC
|
|
505
|
+
elif auth_mode == "shared":
|
|
506
|
+
auth = isolate_proto.ApplicationAuthMode.SHARED
|
|
507
|
+
else:
|
|
508
|
+
auth = isolate_proto.ApplicationAuthMode.PRIVATE
|
|
509
|
+
|
|
510
|
+
request = isolate_proto.SetAliasRequest(
|
|
511
|
+
alias=alias,
|
|
512
|
+
revision=revision,
|
|
513
|
+
auth_mode=auth,
|
|
514
|
+
)
|
|
515
|
+
self.stub.SetAlias(request)
|
|
516
|
+
|
|
517
|
+
def delete_alias(self, alias: str) -> str:
|
|
518
|
+
request = isolate_proto.DeleteAliasRequest(alias=alias)
|
|
519
|
+
res: isolate_proto.DeleteAliasResult = self.stub.DeleteAlias(request)
|
|
520
|
+
return res.revision
|
|
521
|
+
|
|
491
522
|
def list_aliases(self) -> list[AliasInfo]:
|
|
492
523
|
request = isolate_proto.ListAliasesRequest()
|
|
493
524
|
response: isolate_proto.ListAliasesResult = self.stub.ListAliases(request)
|
fal/sync.py
CHANGED
|
@@ -5,13 +5,14 @@ import os
|
|
|
5
5
|
import zipfile
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
|
+
from fal.rest_client import REST_CLIENT
|
|
9
|
+
from pathspec import PathSpec
|
|
10
|
+
|
|
8
11
|
import openapi_fal_rest.api.files.check_dir_hash as check_dir_hash_api
|
|
9
12
|
import openapi_fal_rest.api.files.upload_local_file as upload_local_file_api
|
|
10
13
|
import openapi_fal_rest.models.body_upload_local_file as upload_file_model
|
|
11
14
|
import openapi_fal_rest.models.hash_check as hash_check_model
|
|
12
15
|
import openapi_fal_rest.types as rest_types
|
|
13
|
-
from fal.rest_client import REST_CLIENT
|
|
14
|
-
from pathspec import PathSpec
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
def _check_hash(target_path: str, hash_string: str) -> bool:
|
fal/toolkit/file/file.py
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import Callable
|
|
4
|
+
from typing import Callable
|
|
5
5
|
|
|
6
6
|
from fal.toolkit.file.providers.fal import FalFileRepository, InMemoryRepository
|
|
7
7
|
from fal.toolkit.file.providers.gcp import GoogleStorageRepository
|
|
8
|
+
from fal.toolkit.file.providers.r2 import R2Repository
|
|
8
9
|
from fal.toolkit.file.types import FileData, FileRepository, RepositoryId
|
|
9
10
|
from fal.toolkit.mainify import mainify
|
|
10
11
|
from pydantic import BaseModel, Field, PrivateAttr
|
|
11
12
|
|
|
12
|
-
BuiltInRepositoryId = Literal["fal", "in_memory", "gcp_storage"]
|
|
13
13
|
FileRepositoryFactory = Callable[[], FileRepository]
|
|
14
14
|
|
|
15
|
-
BUILT_IN_REPOSITORIES: dict[
|
|
15
|
+
BUILT_IN_REPOSITORIES: dict[RepositoryId, FileRepositoryFactory] = {
|
|
16
16
|
"fal": lambda: FalFileRepository(),
|
|
17
17
|
"in_memory": lambda: InMemoryRepository(),
|
|
18
18
|
"gcp_storage": lambda: GoogleStorageRepository(),
|
|
19
|
+
"r2": lambda: R2Repository(),
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
|
|
22
|
-
def get_builtin_repository(id:
|
|
23
|
+
def get_builtin_repository(id: RepositoryId) -> FileRepository:
|
|
23
24
|
if id not in BUILT_IN_REPOSITORIES.keys():
|
|
24
25
|
raise ValueError(f'"{id}" is not a valid built-in file repository')
|
|
25
26
|
return BUILT_IN_REPOSITORIES[id]()
|
|
@@ -27,7 +28,7 @@ def get_builtin_repository(id: BuiltInRepositoryId) -> FileRepository:
|
|
|
27
28
|
|
|
28
29
|
get_builtin_repository.__module__ = "__main__"
|
|
29
30
|
|
|
30
|
-
DEFAULT_REPOSITORY: FileRepository |
|
|
31
|
+
DEFAULT_REPOSITORY: FileRepository | RepositoryId = "fal"
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
@mainify
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from io import BytesIO
|
|
7
|
+
|
|
8
|
+
from fal.toolkit.file.types import FileData, FileRepository
|
|
9
|
+
from fal.toolkit.mainify import mainify
|
|
10
|
+
|
|
11
|
+
DEFAULT_URL_TIMEOUT = 60 * 15 # 15 minutes
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@mainify
|
|
15
|
+
@dataclass
|
|
16
|
+
class R2Repository(FileRepository):
|
|
17
|
+
bucket_name: str = "fal_file_storage"
|
|
18
|
+
url_expiration: int = DEFAULT_URL_TIMEOUT
|
|
19
|
+
r2_account_json: str | None = None
|
|
20
|
+
key: str = ""
|
|
21
|
+
|
|
22
|
+
_storage_client = None
|
|
23
|
+
_bucket = None
|
|
24
|
+
|
|
25
|
+
def __post_init__(self):
|
|
26
|
+
import boto3
|
|
27
|
+
from botocore.client import Config
|
|
28
|
+
|
|
29
|
+
r2_account_json = self.r2_account_json
|
|
30
|
+
if r2_account_json is None:
|
|
31
|
+
r2_account_json = os.environ.get("R2_CREDS_JSON")
|
|
32
|
+
if r2_account_json is None:
|
|
33
|
+
raise Exception("R2_CREDS_JSON environment secret is not set")
|
|
34
|
+
|
|
35
|
+
r2_account_info = json.loads(r2_account_json)
|
|
36
|
+
account_id = r2_account_info["ACCOUNT_ID"]
|
|
37
|
+
access_key_id = r2_account_info["ACCESS_KEY_ID"]
|
|
38
|
+
secret_access_key = r2_account_info["SECRET_ACCESS_KEY"]
|
|
39
|
+
|
|
40
|
+
self._s3_client = boto3.client(
|
|
41
|
+
"s3",
|
|
42
|
+
endpoint_url=f"https://{account_id}.r2.cloudflarestorage.com",
|
|
43
|
+
aws_access_key_id=access_key_id,
|
|
44
|
+
aws_secret_access_key=secret_access_key,
|
|
45
|
+
config=Config(signature_version="s3v4"),
|
|
46
|
+
)
|
|
47
|
+
self._s3_resource = boto3.resource(
|
|
48
|
+
"s3",
|
|
49
|
+
endpoint_url=f"https://{account_id}.r2.cloudflarestorage.com",
|
|
50
|
+
aws_access_key_id=access_key_id,
|
|
51
|
+
aws_secret_access_key=secret_access_key,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
self._bucket = self._s3_resource.Bucket(self.bucket_name)
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def storage_client(self):
|
|
58
|
+
if self._s3_resource is None:
|
|
59
|
+
raise Exception("S3 Resource is not initialized")
|
|
60
|
+
|
|
61
|
+
return self._s3_resource
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def bucket(self):
|
|
65
|
+
if self._bucket is None:
|
|
66
|
+
raise Exception("S3 bucket is not initialized")
|
|
67
|
+
|
|
68
|
+
return self._bucket
|
|
69
|
+
|
|
70
|
+
def save(self, data: FileData) -> str:
|
|
71
|
+
destination_path = os.path.join(self.key, data.file_name)
|
|
72
|
+
|
|
73
|
+
s3_object = self.bucket.Object(destination_path)
|
|
74
|
+
s3_object.upload_fileobj(
|
|
75
|
+
BytesIO(data.data), ExtraArgs={"ContentType": data.content_type}
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
public_url = self._s3_client.generate_presigned_url(
|
|
79
|
+
ClientMethod="get_object",
|
|
80
|
+
Params={"Bucket": self.bucket_name, "Key": destination_path},
|
|
81
|
+
ExpiresIn=self.url_expiration,
|
|
82
|
+
)
|
|
83
|
+
return public_url
|
fal/toolkit/file/types.py
CHANGED
fal/toolkit/image/image.py
CHANGED
|
@@ -25,10 +25,10 @@ ImageSizePreset = Literal[
|
|
|
25
25
|
@mainify
|
|
26
26
|
class ImageSize(BaseModel):
|
|
27
27
|
width: int = Field(
|
|
28
|
-
default=512, description="The width of the generated image.", gt=0, le=
|
|
28
|
+
default=512, description="The width of the generated image.", gt=0, le=14142
|
|
29
29
|
)
|
|
30
30
|
height: int = Field(
|
|
31
|
-
default=512, description="The height of the generated image.", gt=0, le=
|
|
31
|
+
default=512, description="The height of the generated image.", gt=0, le=14142
|
|
32
32
|
)
|
|
33
33
|
|
|
34
34
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: fal
|
|
3
|
-
Version: 0.11.
|
|
3
|
+
Version: 0.11.3
|
|
4
4
|
Summary: fal is an easy-to-use Serverless Python Framework
|
|
5
5
|
Author: Features & Labels
|
|
6
6
|
Author-email: hello@fal.ai
|
|
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
13
|
Requires-Dist: attrs (>=21.3.0)
|
|
14
14
|
Requires-Dist: auth0-python (>=4.1.0,<5.0.0)
|
|
15
|
+
Requires-Dist: boto3 (>=1.33.8,<2.0.0)
|
|
15
16
|
Requires-Dist: click (>=8.1.3,<9.0.0)
|
|
16
17
|
Requires-Dist: colorama (>=0.4.6,<0.5.0)
|
|
17
18
|
Requires-Dist: datadog-api-client (==2.12.0)
|
|
@@ -21,7 +22,7 @@ Requires-Dist: grpc-interceptor (>=0.15.0,<0.16.0)
|
|
|
21
22
|
Requires-Dist: grpcio (>=1.50.0,<2.0.0)
|
|
22
23
|
Requires-Dist: httpx (>=0.15.4,<0.25.0)
|
|
23
24
|
Requires-Dist: importlib-metadata (>=4.4) ; python_version < "3.10"
|
|
24
|
-
Requires-Dist: isolate-proto (
|
|
25
|
+
Requires-Dist: isolate-proto (>=0.2.1,<0.3.0)
|
|
25
26
|
Requires-Dist: isolate[build] (>=0.12.3,<1.0)
|
|
26
27
|
Requires-Dist: opentelemetry-api (>=1.15.0,<2.0.0)
|
|
27
28
|
Requires-Dist: opentelemetry-sdk (>=1.15.0,<2.0.0)
|
|
@@ -43,5 +44,41 @@ fal is a serverless Python runtime that lets you run and scale code in the cloud
|
|
|
43
44
|
|
|
44
45
|
With fal, you can build pipelines, serve ML models and scale them up to many users. You scale down to 0 when you don't use any resources.
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
## Quickstart
|
|
48
|
+
|
|
49
|
+
First, you need to install the `fal` package. You can do so using pip:
|
|
50
|
+
```shell
|
|
51
|
+
pip install fal
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Then you need to authenticate:
|
|
55
|
+
```shell
|
|
56
|
+
fal auth login
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
You can also use fal keys that you can get from [our dashboard](https://fal.ai/dashboard/keys).
|
|
60
|
+
|
|
61
|
+
Now can use the fal package in your Python scripts as follows:
|
|
62
|
+
|
|
63
|
+
```py
|
|
64
|
+
import fal
|
|
65
|
+
|
|
66
|
+
@fal.function(
|
|
67
|
+
"virtualenv",
|
|
68
|
+
requirements=["pyjokes"],
|
|
69
|
+
)
|
|
70
|
+
def tell_joke() -> str:
|
|
71
|
+
import pyjokes
|
|
72
|
+
|
|
73
|
+
joke = pyjokes.get_joke()
|
|
74
|
+
return joke
|
|
75
|
+
|
|
76
|
+
print("Joke from the clouds: ", tell_joke())
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
A new virtual environment will be created by fal in the cloud and the set of requirements that we passed will be installed as soon as this function is called. From that point on, our code will be executed as if it were running locally, and the joke prepared by the pyjokes library will be returned.
|
|
80
|
+
|
|
81
|
+
## Next steps
|
|
82
|
+
|
|
83
|
+
If you would like to find out more about the capabilities of fal, check out to the [docs](https://fal.ai/docs). You can learn more about persistent storage, function caches and deploying your functions as API endpoints.
|
|
47
84
|
|