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.

Files changed (36) hide show
  1. fal/api.py +8 -12
  2. fal/cli.py +90 -31
  3. fal/rest_client.py +1 -0
  4. fal/sdk.py +41 -10
  5. fal/sync.py +3 -2
  6. fal/toolkit/file/file.py +6 -5
  7. fal/toolkit/file/providers/r2.py +83 -0
  8. fal/toolkit/file/types.py +1 -1
  9. fal/toolkit/image/image.py +2 -2
  10. {fal-0.11.2.dist-info → fal-0.11.3.dist-info}/METADATA +40 -3
  11. {fal-0.11.2.dist-info → fal-0.11.3.dist-info}/RECORD +36 -35
  12. openapi_fal_rest/api/admin/handle_user_lock.py +6 -2
  13. openapi_fal_rest/api/applications/get_status_applications_app_user_id_app_alias_or_id_status_get.py +6 -2
  14. openapi_fal_rest/api/billing/delete_payment_method.py +9 -3
  15. openapi_fal_rest/api/billing/get_setup_intent_key.py +6 -2
  16. openapi_fal_rest/api/billing/get_user_price.py +6 -2
  17. openapi_fal_rest/api/billing/get_user_spending.py +6 -2
  18. openapi_fal_rest/api/billing/handle_stripe_webhook.py +21 -7
  19. openapi_fal_rest/api/billing/upcoming_invoice.py +6 -2
  20. openapi_fal_rest/api/billing/update_customer_budget.py +6 -2
  21. openapi_fal_rest/api/files/check_dir_hash.py +9 -3
  22. openapi_fal_rest/api/files/delete.py +6 -2
  23. openapi_fal_rest/api/files/download.py +6 -2
  24. openapi_fal_rest/api/files/file_exists.py +6 -2
  25. openapi_fal_rest/api/files/upload_from_url.py +6 -2
  26. openapi_fal_rest/api/files/upload_local_file.py +9 -3
  27. openapi_fal_rest/api/keys/create_key.py +6 -2
  28. openapi_fal_rest/api/keys/delete_key.py +6 -2
  29. openapi_fal_rest/api/tokens/create_token.py +6 -2
  30. openapi_fal_rest/api/usage/get_gateway_request_stats_by_time.py +41 -7
  31. openapi_fal_rest/api/usage/per_machine_usage_details.py +3 -1
  32. openapi_fal_rest/models/body_upload_file.py +4 -1
  33. openapi_fal_rest/models/body_upload_local_file.py +4 -1
  34. openapi_fal_rest/models/get_gateway_request_stats_by_time_response_get_gateway_request_stats_by_time.py +15 -5
  35. {fal-0.11.2.dist-info → fal-0.11.3.dist-info}/WHEEL +0 -0
  36. {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
- 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
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(["public", "private", "shared"]),
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: Literal["public", "private", "shared"],
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
- @alias_cli.command("list")
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 alias_list(client: api.FalServerlessClient):
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
- 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")
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
- console.print(table)
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
- @alias_cli.command("scale")
366
- @click.argument("alias", required=True)
367
- @click.argument("max_concurrency", required=True, type=int)
398
+
399
+ @alias_cli.command("list")
368
400
  @click.pass_obj
369
- def alias_scale(client: api.FalServerlessClient, alias: str, max_concurrency: int):
401
+ def alias_list(client: api.FalServerlessClient):
370
402
  with client.connect() as connection:
371
- connection.scale(application_name=alias, max_concurrency=max_concurrency)
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 not (keep_alive or max_multiplexing):
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
@@ -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
 
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
- max_multiplexing: int = FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING
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
- request = isolate_proto.ScaleApplicationRequest(
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
- ) -> None:
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
- self.stub.UpdateApplication(request)
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, Literal
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[BuiltInRepositoryId, FileRepositoryFactory] = {
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: BuiltInRepositoryId) -> FileRepository:
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 | BuiltInRepositoryId = "fal"
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
@@ -31,7 +31,7 @@ class FileData:
31
31
  self.file_name = file_name
32
32
 
33
33
 
34
- RepositoryId = Literal["fal", "in_memory", "gcp_storage"]
34
+ RepositoryId = Literal["fal", "in_memory", "gcp_storage", "r2"]
35
35
 
36
36
 
37
37
  @mainify
@@ -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=4096
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=4096
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.2
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 (==0.2.0)
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
- Check out to the [docs](https://serverless.fal.ai/docs) for more details.
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