fal 1.26.2__py3-none-any.whl → 1.26.4__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/_fal_version.py CHANGED
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '1.26.2'
21
- __version_tuple__ = version_tuple = (1, 26, 2)
20
+ __version__ = version = '1.26.4'
21
+ __version_tuple__ = version_tuple = (1, 26, 4)
fal/cli/profile.py CHANGED
@@ -23,14 +23,38 @@ def _list(args):
23
23
 
24
24
 
25
25
  def _set(args):
26
- with Config().edit() as config:
27
- config.set_internal("profile", args.PROFILE)
28
- args.console.print(f"Default profile set to [cyan]{args.PROFILE}[/].")
29
- config.profile = args.PROFILE
30
- if not config.get("key"):
26
+ config = Config()
27
+
28
+ # Check if the profile exists
29
+ if args.PROFILE not in config._config:
30
+ # Profile doesn't exist, offer to create it
31
+ args.console.print(f"Profile [cyan]{args.PROFILE}[/] does not exist.")
32
+ create_profile = input("Would you like to create it? (y/N): ").strip().lower()
33
+
34
+ if create_profile in ["y", "yes"]:
35
+ # Create the profile by setting it
36
+ with config.edit() as config:
37
+ config.set_internal("profile", args.PROFILE)
38
+ config._config[args.PROFILE] = {}
39
+ config._profile = args.PROFILE
31
40
  args.console.print(
32
- "No key set for profile. Use [bold]fal profile key[/] to set a key."
41
+ f"Profile [cyan]{args.PROFILE}[/] created and set as default."
33
42
  )
43
+ else:
44
+ args.console.print("Profile creation cancelled.")
45
+ return
46
+ else:
47
+ # Profile exists, just set it as default
48
+ with config.edit() as config:
49
+ config.set_internal("profile", args.PROFILE)
50
+ config._profile = args.PROFILE
51
+ args.console.print(f"Default profile set to [cyan]{args.PROFILE}[/].")
52
+
53
+ # Check if key is set for the profile
54
+ if not config.get("key"):
55
+ args.console.print(
56
+ "No key set for profile. Use [bold]fal profile key[/] to set a key."
57
+ )
34
58
 
35
59
 
36
60
  def _unset(args, config: Config | None = None):
@@ -42,6 +66,8 @@ def _unset(args, config: Config | None = None):
42
66
 
43
67
 
44
68
  def _key_set(args):
69
+ config = Config(validate_profile=True)
70
+
45
71
  while True:
46
72
  key = input("Enter the key: ")
47
73
  if ":" in key:
@@ -50,7 +76,7 @@ def _key_set(args):
50
76
  "[red]Invalid key. The key must be in the format [bold]key:value[/].[/]"
51
77
  )
52
78
 
53
- with Config().edit() as config:
79
+ with config.edit():
54
80
  config.set("key", key)
55
81
  args.console.print(f"Key set for profile [cyan]{config.profile}[/].")
56
82
 
@@ -61,6 +87,24 @@ def _host_set(args):
61
87
  args.console.print(f"Fal host set to [cyan]{args.HOST}[/].")
62
88
 
63
89
 
90
+ def _create(args):
91
+ config = Config()
92
+
93
+ # Check if the profile already exists
94
+ if args.PROFILE in config._config:
95
+ args.console.print(f"Profile [cyan]{args.PROFILE}[/] already exists.")
96
+ return
97
+
98
+ # Create the profile
99
+ with config.edit() as config:
100
+ config._config[args.PROFILE] = {}
101
+
102
+ args.console.print(f"Profile [cyan]{args.PROFILE}[/] created.")
103
+ args.console.print(
104
+ f"Use [bold]fal profile set {args.PROFILE}[/] to set it as default."
105
+ )
106
+
107
+
64
108
  def _delete(args):
65
109
  with Config().edit() as config:
66
110
  if config.profile == args.PROFILE:
@@ -96,7 +140,7 @@ def add_parser(main_subparsers, parents):
96
140
  )
97
141
  list_parser.set_defaults(func=_list)
98
142
 
99
- set_help = "Set default profile."
143
+ set_help = "Set default profile. If the profile doesn't exist, you'll be prompted to create it." # noqa: E501
100
144
  set_parser = subparsers.add_parser(
101
145
  "set",
102
146
  description=set_help,
@@ -140,6 +184,19 @@ def add_parser(main_subparsers, parents):
140
184
  )
141
185
  host_set_parser.set_defaults(func=_host_set)
142
186
 
187
+ create_help = "Create a new profile. Use 'fal profile set <name>' to set it as default after creation." # noqa: E501
188
+ create_parser = subparsers.add_parser(
189
+ "create",
190
+ description=create_help,
191
+ help=create_help,
192
+ parents=parents,
193
+ )
194
+ create_parser.add_argument(
195
+ "PROFILE",
196
+ help="Profile name.",
197
+ )
198
+ create_parser.set_defaults(func=_create)
199
+
143
200
  delete_help = "Delete profile."
144
201
  delete_parser = subparsers.add_parser(
145
202
  "delete",
fal/config.py CHANGED
@@ -4,8 +4,9 @@ import os
4
4
  from contextlib import contextmanager
5
5
  from typing import Dict, Iterator, List, Optional
6
6
 
7
- SETTINGS_SECTION = "__internal__" # legacy
8
- DEFAULT_PROFILE = "default"
7
+ SETTINGS_SECTION = "__internal__"
8
+
9
+ NO_PROFILE_ERROR = ValueError("No profile set.")
9
10
 
10
11
 
11
12
  class Config:
@@ -15,7 +16,7 @@ class Config:
15
16
 
16
17
  DEFAULT_CONFIG_PATH = "~/.fal/config.toml"
17
18
 
18
- def __init__(self):
19
+ def __init__(self, *, validate_profile: bool = False):
19
20
  import tomli
20
21
 
21
22
  self.config_path = os.path.expanduser(
@@ -28,11 +29,17 @@ class Config:
28
29
  except FileNotFoundError:
29
30
  self._config = {}
30
31
 
31
- profile = (
32
- os.getenv("FAL_PROFILE") or self.get_internal("profile") or DEFAULT_PROFILE
33
- )
32
+ profile = os.getenv("FAL_PROFILE") or self.get_internal("profile")
33
+
34
+ # Try to set the profile, but don't fail if it doesn't exist
35
+ try:
36
+ self.profile = profile
37
+ except ValueError:
38
+ # Profile doesn't exist, set to None
39
+ self._profile = None
34
40
 
35
- self.profile = profile
41
+ if validate_profile and not self.profile:
42
+ raise NO_PROFILE_ERROR
36
43
 
37
44
  @property
38
45
  def profile(self) -> Optional[str]:
@@ -41,9 +48,10 @@ class Config:
41
48
  @profile.setter
42
49
  def profile(self, value: Optional[str]) -> None:
43
50
  if value and value not in self._config:
44
- # Make sure the section exists
45
- self._config[value] = {}
46
- self.set_internal("profile", value)
51
+ # Don't automatically create profiles - they should be created explicitly
52
+ raise ValueError(
53
+ f"Profile '{value}' does not exist. Create it first or use the profile set command." # noqa: E501
54
+ )
47
55
  elif not value:
48
56
  self.unset_internal("profile")
49
57
 
@@ -53,11 +61,7 @@ class Config:
53
61
  keys: List[str] = []
54
62
  for key in self._config:
55
63
  if key != SETTINGS_SECTION:
56
- if key == DEFAULT_PROFILE:
57
- # Add it at the beginning
58
- keys.insert(0, key)
59
- else:
60
- keys.append(key)
64
+ keys.append(key)
61
65
 
62
66
  return keys
63
67
 
@@ -75,13 +79,13 @@ class Config:
75
79
 
76
80
  def set(self, key: str, value: str) -> None:
77
81
  if not self.profile:
78
- raise ValueError("No profile set.")
82
+ raise NO_PROFILE_ERROR
79
83
 
80
84
  self._config[self.profile][key] = value
81
85
 
82
86
  def unset(self, key: str) -> None:
83
87
  if not self.profile:
84
- raise ValueError("No profile set.")
88
+ raise NO_PROFILE_ERROR
85
89
 
86
90
  self._config.get(self.profile, {}).pop(key, None)
87
91
 
@@ -9,7 +9,7 @@ from contextlib import contextmanager
9
9
  from dataclasses import dataclass
10
10
  from datetime import datetime, timezone
11
11
  from pathlib import Path
12
- from typing import Any, Generator, Generic, TypeVar
12
+ from typing import Any, Dict, Generator, Generic, TypeVar
13
13
  from urllib.error import HTTPError
14
14
  from urllib.parse import urlparse, urlunparse
15
15
  from urllib.request import Request, urlopen
@@ -45,6 +45,9 @@ def _should_retry(exc: Exception) -> bool:
45
45
  if isinstance(exc, HTTPError) and exc.code in RETRY_CODES:
46
46
  return True
47
47
 
48
+ if isinstance(exc, TimeoutError):
49
+ return True
50
+
48
51
  return False
49
52
 
50
53
 
@@ -66,6 +69,19 @@ def _maybe_retry_request(
66
69
  yield response
67
70
 
68
71
 
72
+ def _object_lifecycle_headers(
73
+ headers: dict[str, str],
74
+ object_lifecycle_preference: dict[str, str] | None,
75
+ ):
76
+ if object_lifecycle_preference:
77
+ # Used by V3 CDN
78
+ headers["X-Fal-Object-Lifecycle"] = json.dumps(object_lifecycle_preference)
79
+ # Used by V1 CDN
80
+ headers["X-Fal-Object-Lifecycle-Preference"] = json.dumps(
81
+ object_lifecycle_preference
82
+ )
83
+
84
+
69
85
  @dataclass
70
86
  class FalV2Token:
71
87
  token: str
@@ -448,14 +464,6 @@ class MultipartUploadGCS:
448
464
 
449
465
  @dataclass
450
466
  class FalFileRepository(FalFileRepositoryBase):
451
- def _object_lifecycle_headers(
452
- self,
453
- headers: dict[str, str],
454
- object_lifecycle_preference: dict[str, str] | None,
455
- ):
456
- if object_lifecycle_preference:
457
- headers["X-Fal-Object-Lifecycle"] = json.dumps(object_lifecycle_preference)
458
-
459
467
  def save(
460
468
  self,
461
469
  file: FileData,
@@ -476,9 +484,8 @@ class FalFileRepository(FalFileRepositoryBase):
476
484
  max_concurrency=multipart_max_concurrency,
477
485
  )
478
486
 
479
- headers = {}
480
- if object_lifecycle_preference:
481
- headers["X-Fal-Object-Lifecycle"] = json.dumps(object_lifecycle_preference)
487
+ headers: Dict[str, str] = {}
488
+ _object_lifecycle_headers(headers, object_lifecycle_preference)
482
489
 
483
490
  return self._save(file, "gcs", headers=headers)
484
491
 
@@ -1121,6 +1128,8 @@ class FalFileRepositoryV2(FalFileRepositoryBase):
1121
1128
  "Content-Type": file.content_type,
1122
1129
  }
1123
1130
 
1131
+ _object_lifecycle_headers(headers, object_lifecycle_preference)
1132
+
1124
1133
  storage_url = f"{token.base_upload_url}/upload"
1125
1134
 
1126
1135
  try:
@@ -1192,16 +1201,6 @@ class InMemoryRepository(FileRepository):
1192
1201
 
1193
1202
  @dataclass
1194
1203
  class FalCDNFileRepository(FileRepository):
1195
- def _object_lifecycle_headers(
1196
- self,
1197
- headers: dict[str, str],
1198
- object_lifecycle_preference: dict[str, str] | None,
1199
- ):
1200
- if object_lifecycle_preference:
1201
- headers["X-Fal-Object-Lifecycle-Preference"] = json.dumps(
1202
- object_lifecycle_preference
1203
- )
1204
-
1205
1204
  def save(
1206
1205
  self,
1207
1206
  file: FileData,
@@ -1218,7 +1217,7 @@ class FalCDNFileRepository(FileRepository):
1218
1217
  "X-Fal-File-Name": file.file_name,
1219
1218
  }
1220
1219
 
1221
- self._object_lifecycle_headers(headers, object_lifecycle_preference)
1220
+ _object_lifecycle_headers(headers, object_lifecycle_preference)
1222
1221
 
1223
1222
  url = os.getenv("FAL_CDN_HOST", _FAL_CDN) + "/files/upload"
1224
1223
  request = Request(url, headers=headers, method="POST", data=file.data)
@@ -1285,6 +1284,7 @@ class FalFileRepositoryV3(FileRepository):
1285
1284
  "Accept": "application/json",
1286
1285
  "Content-Type": "application/json",
1287
1286
  }
1287
+ _object_lifecycle_headers(headers, object_lifecycle_preference)
1288
1288
 
1289
1289
  grpc_host = os.environ.get("FAL_HOST", "api.alpha.fal.ai")
1290
1290
  rest_host = grpc_host.replace("api", "rest", 1)
@@ -1373,14 +1373,6 @@ class InternalFalFileRepositoryV3(FileRepository):
1373
1373
  That way it can avoid the need to refresh the token for every upload.
1374
1374
  """
1375
1375
 
1376
- def _object_lifecycle_headers(
1377
- self,
1378
- headers: dict[str, str],
1379
- object_lifecycle_preference: dict[str, str] | None,
1380
- ):
1381
- if object_lifecycle_preference:
1382
- headers["X-Fal-Object-Lifecycle"] = json.dumps(object_lifecycle_preference)
1383
-
1384
1376
  def save(
1385
1377
  self,
1386
1378
  file: FileData,
@@ -1410,7 +1402,7 @@ class InternalFalFileRepositoryV3(FileRepository):
1410
1402
  "X-Fal-File-Name": file.file_name,
1411
1403
  }
1412
1404
 
1413
- self._object_lifecycle_headers(headers, object_lifecycle_preference)
1405
+ _object_lifecycle_headers(headers, object_lifecycle_preference)
1414
1406
 
1415
1407
  url = os.getenv("FAL_CDN_V3_HOST", _FAL_CDN_V3) + "/files/upload"
1416
1408
  request = Request(url, headers=headers, method="POST", data=file.data)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fal
3
- Version: 1.26.2
3
+ Version: 1.26.4
4
4
  Summary: fal is an easy-to-use Serverless Python Framework
5
5
  Author: Features & Labels <support@fal.ai>
6
6
  Requires-Python: >=3.8
@@ -1,12 +1,12 @@
1
1
  fal/__init__.py,sha256=wXs1G0gSc7ZK60-bHe-B2m0l_sA6TrFk4BxY0tMoLe8,784
2
2
  fal/__main__.py,sha256=4JMK66Wj4uLZTKbF-sT3LAxOsr6buig77PmOkJCRRxw,83
3
- fal/_fal_version.py,sha256=C99N_Z6v_dImhQIs0wSP9hjugs8YW_m4cpJMf6e1I-4,513
3
+ fal/_fal_version.py,sha256=u70IaPMkznhV25wuaImHeCFFqIUwcsB7Z1rqTSGrz9g,513
4
4
  fal/_serialization.py,sha256=npXNsFJ5G7jzBeBIyVMH01Ww34mGY4XWhHpRbSrTtnQ,7598
5
5
  fal/_version.py,sha256=1BbTFnucNC_6ldKJ_ZoC722_UkW4S9aDBSW9L0fkKAw,2315
6
6
  fal/api.py,sha256=wIEt21P1C7U-dYQEcyHUxxuuTnvzFyTpWDoHoaxq7tg,47385
7
7
  fal/app.py,sha256=3RDFV6JUiUk8b3WJanKQYA-kW74t5bqaNy8OYuUH5-Q,25491
8
8
  fal/apps.py,sha256=pzCd2mrKl5J_4oVc40_pggvPtFahXBCdrZXWpnaEJVs,12130
9
- fal/config.py,sha256=19tR4zWr2yqsBRWNhFV1jKMiXAASRz2Et62k68-1r9Y,3290
9
+ fal/config.py,sha256=G3IOTLQuu_VAvzPFdx9v22NpJ9m4gU8qEHic7e7-WwU,3511
10
10
  fal/container.py,sha256=FTsa5hOW4ars-yV1lUoc0BNeIIvAZcpw7Ftyt3A4m_w,2000
11
11
  fal/files.py,sha256=iwR9jXWmAruP5tSS5-NkY-mZG53Pb80C40SqB0pYRrk,6819
12
12
  fal/flags.py,sha256=QonyDM7R2GqfAB1bJr46oriu-fHJCkpUwXuSdanePWg,987
@@ -34,7 +34,7 @@ fal/cli/files.py,sha256=pSgAnTm2eHdP-IPkMIVfnK_Ii7mkSSOVgvbsiFUVBC0,2936
34
34
  fal/cli/keys.py,sha256=7Sf4DT4le89G42eAOt0ltRjbZAtE70AVQ62hmjZhUy0,3059
35
35
  fal/cli/main.py,sha256=LnJCe83Fdr5-i3Fkpvrd9BZPjHtcX_H1EEmy6rVlCz8,3180
36
36
  fal/cli/parser.py,sha256=jYsGQ0BLQuKI7KtN1jnLVYKMbLtez7hPjwTNfG3UPSk,2964
37
- fal/cli/profile.py,sha256=9d6a8IEZhxKoJt4CmvRbqw1Js7HBNkJjng7I_3ggfTw,3941
37
+ fal/cli/profile.py,sha256=9Iz5Prk2mR2JmxBuNPcKAcQJVHlGGXIlKh_7MXDl7U4,5879
38
38
  fal/cli/run.py,sha256=nAC12Qss4Fg1XmV0qOS9RdGNLYcdoHeRgQMvbTN4P9I,1202
39
39
  fal/cli/runners.py,sha256=7efNX9vm6D1aBlg0M5-u5plw3HHC41Sj-N7eRNIHnqw,3689
40
40
  fal/cli/secrets.py,sha256=QKSmazu-wiNF6fOpGL9v2TDYxAjX9KTi7ot7vnv6f5E,2474
@@ -61,7 +61,7 @@ fal/toolkit/audio/audio.py,sha256=gt458h989iQ-EhQSH-mCuJuPBY4RneLJE05f_QWU1E0,57
61
61
  fal/toolkit/file/__init__.py,sha256=FbNl6wD-P0aSSTUwzHt4HujBXrbC3ABmaigPQA4hRfg,70
62
62
  fal/toolkit/file/file.py,sha256=4K28gr--5q0nmsm3P76SFoKQj3bPROVwxXrdoMjIiUE,10197
63
63
  fal/toolkit/file/types.py,sha256=MMAH_AyLOhowQPesOv1V25wB4qgbJ3vYNlnTPbdSv1M,2304
64
- fal/toolkit/file/providers/fal.py,sha256=vt4Mznbfca6blfk0psF1ix-zB6309kpIA0_5Qh7bmFw,47217
64
+ fal/toolkit/file/providers/fal.py,sha256=Ph8v3Cm_eFu1b1AXiPKZQ5r8AWUALD3Wk18uw3z8RDQ,46910
65
65
  fal/toolkit/file/providers/gcp.py,sha256=DKeZpm1MjwbvEsYvkdXUtuLIJDr_UNbqXj_Mfv3NTeo,2437
66
66
  fal/toolkit/file/providers/r2.py,sha256=YqnYkkAo_ZKIa-xoSuDnnidUFwJWHdziAR34PE6irdI,3061
67
67
  fal/toolkit/file/providers/s3.py,sha256=EI45T54Mox7lHZKROss_O8o0DIn3CHP9k1iaNYVrxvg,2714
@@ -142,8 +142,8 @@ openapi_fal_rest/models/workflow_node_type.py,sha256=-FzyeY2bxcNmizKbJI8joG7byRi
142
142
  openapi_fal_rest/models/workflow_schema.py,sha256=4K5gsv9u9pxx2ItkffoyHeNjBBYf6ur5bN4m_zePZNY,2019
143
143
  openapi_fal_rest/models/workflow_schema_input.py,sha256=2OkOXWHTNsCXHWS6EGDFzcJKkW5FIap-2gfO233EvZQ,1191
144
144
  openapi_fal_rest/models/workflow_schema_output.py,sha256=EblwSPAGfWfYVWw_WSSaBzQVju296is9o28rMBAd0mc,1196
145
- fal-1.26.2.dist-info/METADATA,sha256=kNAhtk3mo4CnZruMDUZOa47W3uh92DKvD0GdAT7YhOE,4089
146
- fal-1.26.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
147
- fal-1.26.2.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
148
- fal-1.26.2.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
149
- fal-1.26.2.dist-info/RECORD,,
145
+ fal-1.26.4.dist-info/METADATA,sha256=L40fhRUeSIvdiabKo8bge8--xUCldpk2heasbcQFUOg,4089
146
+ fal-1.26.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
147
+ fal-1.26.4.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
148
+ fal-1.26.4.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
149
+ fal-1.26.4.dist-info/RECORD,,
File without changes