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.
- fal/api.py +51 -19
- fal/auth/__init__.py +1 -2
- fal/auth/auth0.py +2 -5
- fal/cli.py +92 -108
- fal/rest_client.py +1 -0
- fal/sdk.py +49 -129
- fal/sync.py +3 -2
- fal/toolkit/file/file.py +6 -5
- fal/toolkit/file/providers/gcp.py +4 -1
- fal/toolkit/file/providers/r2.py +83 -0
- fal/toolkit/file/types.py +1 -1
- fal/toolkit/image/image.py +2 -2
- fal/toolkit/utils/download_utils.py +1 -1
- {fal-0.11.1.dist-info → fal-0.11.3.dist-info}/METADATA +40 -3
- {fal-0.11.1.dist-info → fal-0.11.3.dist-info}/RECORD +58 -44
- openapi_fal_rest/api/admin/get_usage_per_user.py +199 -0
- 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/{usage/get_request_stats_by_time.py → requests/requests.py} +33 -18
- openapi_fal_rest/api/storage/get_file_link.py +200 -0
- openapi_fal_rest/api/storage/initiate_upload.py +172 -0
- openapi_fal_rest/api/tokens/__init__.py +0 -0
- openapi_fal_rest/api/{application/get_status_application_status_user_id_alias_get.py → tokens/create_token.py} +41 -48
- openapi_fal_rest/api/usage/get_gateway_request_stats.py +49 -1
- openapi_fal_rest/api/usage/get_gateway_request_stats_by_time.py +270 -0
- openapi_fal_rest/api/usage/per_machine_usage_details.py +3 -1
- openapi_fal_rest/models/__init__.py +18 -0
- openapi_fal_rest/models/body_create_token.py +68 -0
- 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/gateway_stats_by_time.py +27 -27
- openapi_fal_rest/models/gateway_usage_stats.py +58 -31
- openapi_fal_rest/models/get_gateway_request_stats_by_time_response_get_gateway_request_stats_by_time.py +80 -0
- openapi_fal_rest/models/initiate_upload_info.py +64 -0
- openapi_fal_rest/models/presigned_upload_url.py +64 -0
- openapi_fal_rest/models/request_io.py +112 -0
- openapi_fal_rest/models/request_io_json_input.py +43 -0
- openapi_fal_rest/models/request_io_json_output.py +43 -0
- openapi_fal_rest/models/stats_timeframe.py +1 -0
- openapi_fal_rest/models/usage_per_user.py +71 -0
- {fal-0.11.1.dist-info → fal-0.11.3.dist-info}/WHEEL +0 -0
- {fal-0.11.1.dist-info → fal-0.11.3.dist-info}/entry_points.txt +0 -0
- /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
|
-
|
|
424
|
-
|
|
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(
|
|
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
|
-
|
|
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,
|
|
508
|
-
|
|
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(
|
|
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
|
|
497
|
+
def create_alias(
|
|
552
498
|
self,
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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
|
-
|
|
560
|
-
|
|
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
|
|
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
|
|
@@ -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
|
-
|
|
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
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
|
|
|
@@ -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 =
|
|
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.
|
|
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
|
|