fal 0.11.1__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 (58) hide show
  1. fal/api.py +51 -19
  2. fal/auth/__init__.py +1 -2
  3. fal/auth/auth0.py +2 -5
  4. fal/cli.py +92 -108
  5. fal/rest_client.py +1 -0
  6. fal/sdk.py +49 -129
  7. fal/sync.py +3 -2
  8. fal/toolkit/file/file.py +6 -5
  9. fal/toolkit/file/providers/gcp.py +4 -1
  10. fal/toolkit/file/providers/r2.py +83 -0
  11. fal/toolkit/file/types.py +1 -1
  12. fal/toolkit/image/image.py +2 -2
  13. fal/toolkit/utils/download_utils.py +1 -1
  14. {fal-0.11.1.dist-info → fal-0.11.3.dist-info}/METADATA +40 -3
  15. {fal-0.11.1.dist-info → fal-0.11.3.dist-info}/RECORD +58 -44
  16. openapi_fal_rest/api/admin/get_usage_per_user.py +199 -0
  17. openapi_fal_rest/api/admin/handle_user_lock.py +6 -2
  18. openapi_fal_rest/api/applications/get_status_applications_app_user_id_app_alias_or_id_status_get.py +6 -2
  19. openapi_fal_rest/api/billing/delete_payment_method.py +9 -3
  20. openapi_fal_rest/api/billing/get_setup_intent_key.py +6 -2
  21. openapi_fal_rest/api/billing/get_user_price.py +6 -2
  22. openapi_fal_rest/api/billing/get_user_spending.py +6 -2
  23. openapi_fal_rest/api/billing/handle_stripe_webhook.py +21 -7
  24. openapi_fal_rest/api/billing/upcoming_invoice.py +6 -2
  25. openapi_fal_rest/api/billing/update_customer_budget.py +6 -2
  26. openapi_fal_rest/api/files/check_dir_hash.py +9 -3
  27. openapi_fal_rest/api/files/delete.py +6 -2
  28. openapi_fal_rest/api/files/download.py +6 -2
  29. openapi_fal_rest/api/files/file_exists.py +6 -2
  30. openapi_fal_rest/api/files/upload_from_url.py +6 -2
  31. openapi_fal_rest/api/files/upload_local_file.py +9 -3
  32. openapi_fal_rest/api/keys/create_key.py +6 -2
  33. openapi_fal_rest/api/keys/delete_key.py +6 -2
  34. openapi_fal_rest/api/{usage/get_request_stats_by_time.py → requests/requests.py} +33 -18
  35. openapi_fal_rest/api/storage/get_file_link.py +200 -0
  36. openapi_fal_rest/api/storage/initiate_upload.py +172 -0
  37. openapi_fal_rest/api/tokens/__init__.py +0 -0
  38. openapi_fal_rest/api/{application/get_status_application_status_user_id_alias_get.py → tokens/create_token.py} +41 -48
  39. openapi_fal_rest/api/usage/get_gateway_request_stats.py +49 -1
  40. openapi_fal_rest/api/usage/get_gateway_request_stats_by_time.py +270 -0
  41. openapi_fal_rest/api/usage/per_machine_usage_details.py +3 -1
  42. openapi_fal_rest/models/__init__.py +18 -0
  43. openapi_fal_rest/models/body_create_token.py +68 -0
  44. openapi_fal_rest/models/body_upload_file.py +4 -1
  45. openapi_fal_rest/models/body_upload_local_file.py +4 -1
  46. openapi_fal_rest/models/gateway_stats_by_time.py +27 -27
  47. openapi_fal_rest/models/gateway_usage_stats.py +58 -31
  48. openapi_fal_rest/models/get_gateway_request_stats_by_time_response_get_gateway_request_stats_by_time.py +80 -0
  49. openapi_fal_rest/models/initiate_upload_info.py +64 -0
  50. openapi_fal_rest/models/presigned_upload_url.py +64 -0
  51. openapi_fal_rest/models/request_io.py +112 -0
  52. openapi_fal_rest/models/request_io_json_input.py +43 -0
  53. openapi_fal_rest/models/request_io_json_output.py +43 -0
  54. openapi_fal_rest/models/stats_timeframe.py +1 -0
  55. openapi_fal_rest/models/usage_per_user.py +71 -0
  56. {fal-0.11.1.dist-info → fal-0.11.3.dist-info}/WHEEL +0 -0
  57. {fal-0.11.1.dist-info → fal-0.11.3.dist-info}/entry_points.txt +0 -0
  58. /openapi_fal_rest/api/{application → requests}/__init__.py +0 -0
fal/sdk.py CHANGED
@@ -26,6 +26,7 @@ UNSET = object()
26
26
 
27
27
  _DEFAULT_SERIALIZATION_METHOD = "dill"
28
28
  FAL_SERVERLESS_DEFAULT_KEEP_ALIVE = 10
29
+ FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING = 1
29
30
 
30
31
  log = get_logger(__name__)
31
32
 
@@ -183,23 +184,9 @@ class AliasInfo:
183
184
  alias: str
184
185
  revision: str
185
186
  auth_mode: str
187
+ keep_alive: int
186
188
  max_concurrency: int
187
-
188
-
189
- @dataclass(frozen=True)
190
- class Cron:
191
- cron_id: str
192
- cron_string: str
193
- next_run: datetime
194
- active: bool
195
-
196
-
197
- @dataclass(frozen=True)
198
- class ScheduledRunActivation:
199
- cron_id: str
200
- activation_id: str
201
- started_at: datetime
202
- finished_at: datetime
189
+ max_multiplexing: int
203
190
 
204
191
 
205
192
  @dataclass
@@ -216,16 +203,6 @@ class RegisterApplicationResult:
216
203
  logs: list[Log] = field(default_factory=list)
217
204
 
218
205
 
219
- @dataclass(frozen=True)
220
- class RegisterCronResultType:
221
- cron_id: str
222
-
223
-
224
- @dataclass(frozen=True)
225
- class RegisterCronResult:
226
- result: RegisterCronResultType
227
-
228
-
229
206
  @dataclass
230
207
  class RegisterApplicationResultType:
231
208
  application_id: str
@@ -268,27 +245,6 @@ class KeyScope(enum.Enum):
268
245
  raise ValueError(f"Unknown KeyScope: {proto}")
269
246
 
270
247
 
271
- @from_grpc.register(isolate_proto.RegisterCronResult)
272
- def _from_grpc_register_cron_result(
273
- message: isolate_proto.RegisterCronResult,
274
- ) -> RegisterCronResult:
275
- return RegisterCronResult(
276
- result=RegisterCronResultType(message.result.cron_id),
277
- )
278
-
279
-
280
- @from_grpc.register(isolate_proto.CronResultType)
281
- def _from_grpc_cron_result_type(
282
- message: isolate_proto.CronResultType,
283
- ) -> Cron:
284
- return Cron(
285
- cron_id=message.cron_id,
286
- cron_string=message.cron_string,
287
- next_run=message.next_run.ToDatetime(),
288
- active=message.is_active,
289
- )
290
-
291
-
292
248
  @from_grpc.register(isolate_proto.AliasInfo)
293
249
  def _from_grpc_alias_info(message: isolate_proto.AliasInfo) -> AliasInfo:
294
250
  if message.auth_mode is isolate_proto.ApplicationAuthMode.PUBLIC:
@@ -304,7 +260,9 @@ def _from_grpc_alias_info(message: isolate_proto.AliasInfo) -> AliasInfo:
304
260
  alias=message.alias,
305
261
  revision=message.revision,
306
262
  auth_mode=auth_mode,
263
+ keep_alive=message.keep_alive,
307
264
  max_concurrency=message.max_concurrency,
265
+ max_multiplexing=message.max_multiplexing,
308
266
  )
309
267
 
310
268
 
@@ -344,13 +302,6 @@ def _from_grpc_hosted_run_result(
344
302
  )
345
303
 
346
304
 
347
- def _get_cron_id(run: Cron | str) -> str:
348
- if isinstance(run, Cron):
349
- return run.cron_id
350
- else:
351
- return run
352
-
353
-
354
305
  @dataclass
355
306
  class MachineRequirements:
356
307
  machine_type: str
@@ -359,6 +310,8 @@ class MachineRequirements:
359
310
  exposed_port: int | None = None
360
311
  scheduler: str | None = None
361
312
  scheduler_options: dict[str, Any] | None = None
313
+ max_concurrency: int | None = None
314
+ max_multiplexing: int | None = None
362
315
 
363
316
 
364
317
  @dataclass
@@ -420,31 +373,17 @@ class FalServerlessConnection:
420
373
  request = isolate_proto.RevokeUserKeyRequest(key_id=key_id)
421
374
  self.stub.RevokeUserKey(request)
422
375
 
423
- # TODO: get rid of this in favor of define_environment
424
- def create_environment(
425
- self,
426
- kind: str,
427
- configuration_options: dict[str, Any],
376
+ def define_environment(
377
+ self, kind: str, **options: Any
428
378
  ) -> isolate_proto.EnvironmentDefinition:
429
- assert isinstance(
430
- configuration_options, dict
431
- ), "configuration_options must be a dict"
432
379
  struct = isolate_proto.Struct()
433
- struct.update(configuration_options)
380
+ struct.update(options)
434
381
 
435
382
  return isolate_proto.EnvironmentDefinition(
436
383
  kind=kind,
437
384
  configuration=struct,
438
385
  )
439
386
 
440
- def define_environment(
441
- self, kind: str, **options: Any
442
- ) -> isolate_proto.EnvironmentDefinition:
443
- return self.create_environment(
444
- kind=kind,
445
- configuration_options=options,
446
- )
447
-
448
387
  def register(
449
388
  self,
450
389
  function: Callable[..., ResultT],
@@ -452,7 +391,6 @@ class FalServerlessConnection:
452
391
  application_name: str | None = None,
453
392
  application_auth_mode: Literal["public", "private", "shared"] | None = None,
454
393
  *,
455
- max_concurrency: int | None = None,
456
394
  serialization_method: str = _DEFAULT_SERIALIZATION_METHOD,
457
395
  machine_requirements: MachineRequirements | None = None,
458
396
  metadata: dict[str, Any] | None = None,
@@ -468,6 +406,8 @@ class FalServerlessConnection:
468
406
  scheduler_options=to_struct(
469
407
  machine_requirements.scheduler_options or {}
470
408
  ),
409
+ max_concurrency=machine_requirements.max_concurrency,
410
+ max_multiplexing=machine_requirements.max_multiplexing,
471
411
  )
472
412
  else:
473
413
  wrapped_requirements = None
@@ -488,7 +428,6 @@ class FalServerlessConnection:
488
428
  function=wrapped_function,
489
429
  environments=environments,
490
430
  machine_requirements=wrapped_requirements,
491
- max_concurrency=max_concurrency,
492
431
  application_name=application_name,
493
432
  auth_mode=auth_mode,
494
433
  metadata=struct_metadata,
@@ -497,20 +436,25 @@ class FalServerlessConnection:
497
436
  yield from_grpc(partial_result)
498
437
 
499
438
  def scale(self, application_name: str, max_concurrency: int | None = None) -> None:
500
- request = isolate_proto.ScaleApplicationRequest(
501
- application_name=application_name,
502
- max_concurrency=max_concurrency,
503
- )
504
- self.stub.ScaleApplication(request)
439
+ raise NotImplementedError
505
440
 
506
441
  def update_application(
507
- self, application_name: str, keep_alive: int | None = None
508
- ) -> None:
442
+ self,
443
+ application_name: str,
444
+ keep_alive: int | None = None,
445
+ max_multiplexing: int | None = None,
446
+ max_concurrency: int | None = None,
447
+ ) -> AliasInfo:
509
448
  request = isolate_proto.UpdateApplicationRequest(
510
449
  application_name=application_name,
511
450
  keep_alive=keep_alive,
451
+ max_multiplexing=max_multiplexing,
452
+ max_concurrency=max_concurrency,
512
453
  )
513
- self.stub.UpdateApplication(request)
454
+ res: isolate_proto.UpdateApplicationResult = self.stub.UpdateApplication(
455
+ request
456
+ )
457
+ return from_grpc(res.alias_info)
514
458
 
515
459
  def run(
516
460
  self,
@@ -532,6 +476,8 @@ class FalServerlessConnection:
532
476
  scheduler_options=to_struct(
533
477
  machine_requirements.scheduler_options or {}
534
478
  ),
479
+ max_concurrency=machine_requirements.max_concurrency,
480
+ max_multiplexing=machine_requirements.max_multiplexing,
535
481
  )
536
482
  else:
537
483
  wrapped_requirements = None
@@ -548,62 +494,36 @@ class FalServerlessConnection:
548
494
  for partial_result in self.stub.Run(request):
549
495
  yield from_grpc(partial_result)
550
496
 
551
- def schedule_cronjob(
497
+ def create_alias(
552
498
  self,
553
- application_id: str,
554
- cron: str,
555
- ) -> str:
556
- request = isolate_proto.RegisterCronRequest(
557
- application_id=application_id, cron=cron
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,
558
514
  )
559
- response: isolate_proto.RegisterCronResult = self.stub.RegisterCron(request)
560
- return response.result.cron_id
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
561
521
 
562
522
  def list_aliases(self) -> list[AliasInfo]:
563
523
  request = isolate_proto.ListAliasesRequest()
564
524
  response: isolate_proto.ListAliasesResult = self.stub.ListAliases(request)
565
525
  return [from_grpc(alias) for alias in response.aliases]
566
526
 
567
- def list_scheduled_runs(self) -> list[Cron]:
568
- request = isolate_proto.ListCronsRequest()
569
- response: isolate_proto.ListCronsResult = self.stub.ListCrons(request)
570
- return [from_grpc(cron) for cron in response.crons]
571
-
572
- def list_run_activations(self, run: str | Cron) -> list[ScheduledRunActivation]:
573
- request = isolate_proto.ListActivationsRequest(cron_id=_get_cron_id(run))
574
- response: isolate_proto.ListActivationsResult = self.stub.ListActivations(
575
- request
576
- )
577
- return [
578
- ScheduledRunActivation(
579
- cron_id=_get_cron_id(run),
580
- activation_id=activation.activation_id,
581
- started_at=activation.started_at.ToDatetime(),
582
- finished_at=activation.finished_at.ToDatetime(),
583
- )
584
- for activation in response.activations
585
- ]
586
-
587
- def cancel_scheduled_run(self, cron_id: str) -> None:
588
- request = isolate_proto.CancelCronRequest(cron_id=cron_id)
589
- response: isolate_proto.CancelCronResult = self.stub.CancelCron(request)
590
- return
591
-
592
- def get_activation_logs(self, cron_id: str, activation_id: str) -> list[Log]:
593
- request = isolate_proto.GetActivationLogsRequest(
594
- cron_id=cron_id, activation_id=activation_id
595
- )
596
- response = self.stub.GetActivationLogs(request)
597
- return [from_grpc(log) for log in response.logs]
598
-
599
- def get_logs(
600
- self, lines: int | None = None, url: str | None = None
601
- ) -> Iterator[Log]:
602
- filter = isolate_proto.LogsFilter(lines=lines, url=url)
603
- request = isolate_proto.GetLogsRequest(filter=filter)
604
- for partial_result in self.stub.GetLogs(request):
605
- yield from_grpc(partial_result.log_entry)
606
-
607
527
  def set_secret(self, name: str, value: str) -> None:
608
528
  request = isolate_proto.SetSecretRequest(name=name, value=value)
609
529
  self.stub.SetSecret(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
@@ -17,6 +17,7 @@ class GoogleStorageRepository(FileRepository):
17
17
  bucket_name: str = "fal_file_storage"
18
18
  url_expiration: int | None = DEFAULT_URL_TIMEOUT
19
19
  gcp_account_json: str | None = None
20
+ folder: str = ""
20
21
 
21
22
  _storage_client = None
22
23
  _bucket = None
@@ -50,7 +51,9 @@ class GoogleStorageRepository(FileRepository):
50
51
  return self._bucket
51
52
 
52
53
  def save(self, data: FileData) -> str:
53
- gcp_blob = self.bucket.blob(data.file_name)
54
+ destination_path = os.path.join(self.folder, data.file_name)
55
+
56
+ gcp_blob = self.bucket.blob(destination_path)
54
57
  gcp_blob.upload_from_string(data.data, content_type=data.content_type)
55
58
 
56
59
  if self.url_expiration is None:
@@ -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
 
@@ -148,7 +148,7 @@ def download_file(
148
148
 
149
149
  # If target_dir is not an absolute path, use "/data" as the relative directory
150
150
  if not target_dir_path.is_absolute():
151
- target_dir_path = Path("/data") / target_dir_path
151
+ target_dir_path = FAL_PERSISTENT_DIR / target_dir_path
152
152
 
153
153
  target_path = target_dir_path.resolve() / file_name
154
154
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fal
3
- Version: 0.11.1
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.1.4)
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