fal 1.11.0__py3-none-any.whl → 1.11.2__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.11.0'
21
- __version_tuple__ = version_tuple = (1, 11, 0)
20
+ __version__ = version = '1.11.2'
21
+ __version_tuple__ = version_tuple = (1, 11, 2)
fal/auth/__init__.py CHANGED
@@ -128,16 +128,40 @@ def _fetch_access_token() -> str:
128
128
  return token_data["access_token"]
129
129
 
130
130
 
131
+ def _fetch_teams(bearer_token: str) -> list[dict]:
132
+ import json
133
+ from urllib.error import HTTPError
134
+ from urllib.request import Request, urlopen
135
+
136
+ from fal.exceptions import FalServerlessException
137
+ from fal.flags import REST_URL
138
+
139
+ request = Request(
140
+ method="GET",
141
+ url=f"{REST_URL}/users/teams",
142
+ headers={"Authorization": bearer_token},
143
+ )
144
+ try:
145
+ with urlopen(request) as response:
146
+ teams = json.load(response)
147
+ except HTTPError as exc:
148
+ raise FalServerlessException("Failed to fetch teams") from exc
149
+
150
+ return [team for team in teams if not team["is_personal"]]
151
+
152
+
131
153
  @dataclass
132
154
  class UserAccess:
133
155
  _access_token: str | None = field(repr=False, default=None)
134
156
  _user_info: dict | None = field(repr=False, default=None)
135
157
  _exc: Exception | None = field(repr=False, default=None)
158
+ _teams: list[dict] | None = field(repr=False, default=None)
136
159
 
137
160
  def invalidate(self) -> None:
138
161
  self._access_token = None
139
162
  self._user_info = None
140
163
  self._exc = None
164
+ self._teams = None
141
165
 
142
166
  @property
143
167
  def info(self) -> dict:
@@ -167,5 +191,17 @@ class UserAccess:
167
191
  def bearer_token(self) -> str:
168
192
  return "Bearer " + self.access_token
169
193
 
194
+ @property
195
+ def teams(self) -> list[dict]:
196
+ if self._teams is None:
197
+ self._teams = _fetch_teams(self.bearer_token)
198
+ return self._teams
199
+
200
+ def get_team(self, team: str) -> dict:
201
+ for t in self.teams:
202
+ if t["nickname"].lower() == team.lower():
203
+ return t
204
+ raise ValueError(f"Team {team} not found")
205
+
170
206
 
171
207
  USER = UserAccess()
fal/cli/_utils.py CHANGED
@@ -3,6 +3,13 @@ from __future__ import annotations
3
3
  from fal.files import find_project_root, find_pyproject_toml, parse_pyproject_toml
4
4
 
5
5
 
6
+ def get_client(host: str, team: str | None = None):
7
+ from fal.sdk import FalServerlessClient, get_default_credentials
8
+
9
+ credentials = get_default_credentials(team=team)
10
+ return FalServerlessClient(host, credentials)
11
+
12
+
6
13
  def is_app_name(app_ref: tuple[str, str | None]) -> bool:
7
14
  is_single_file = app_ref[1] is None
8
15
  is_python_file = app_ref[0].endswith(".py")
@@ -25,7 +32,7 @@ def get_app_data_from_toml(app_name):
25
32
  raise ValueError(f"App {app_name} not found in pyproject.toml")
26
33
 
27
34
  try:
28
- app_ref = app_data["ref"]
35
+ app_ref = app_data.pop("ref")
29
36
  except KeyError:
30
37
  raise ValueError(f"App {app_name} does not have a ref key in pyproject.toml")
31
38
 
@@ -33,8 +40,11 @@ def get_app_data_from_toml(app_name):
33
40
  project_root, _ = find_project_root(None)
34
41
  app_ref = str(project_root / app_ref)
35
42
 
36
- app_auth = app_data.get("auth", "private")
37
- app_deployment_strategy = app_data.get("deployment_strategy", "recreate")
38
- app_no_scale = app_data.get("no_scale", False)
43
+ app_auth = app_data.pop("auth", "private")
44
+ app_deployment_strategy = app_data.pop("deployment_strategy", "recreate")
45
+ app_no_scale = app_data.pop("no_scale", False)
46
+
47
+ if len(app_data) > 0:
48
+ raise ValueError(f"Found unexpected keys in pyproject.toml: {app_data}")
39
49
 
40
50
  return app_ref, app_auth, app_deployment_strategy, app_no_scale
fal/cli/apps.py CHANGED
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING
4
4
 
5
+ from ._utils import get_client
5
6
  from .parser import FalClientParser
6
7
 
7
8
  if TYPE_CHECKING:
@@ -47,9 +48,7 @@ def _apps_table(apps: list[AliasInfo]):
47
48
 
48
49
 
49
50
  def _list(args):
50
- from fal.sdk import FalServerlessClient
51
-
52
- client = FalServerlessClient(args.host)
51
+ client = get_client(args.host, args.team)
53
52
  with client.connect() as connection:
54
53
  apps = connection.list_aliases()
55
54
 
@@ -120,9 +119,7 @@ def _app_rev_table(revs: list[ApplicationInfo]):
120
119
 
121
120
 
122
121
  def _list_rev(args):
123
- from fal.sdk import FalServerlessClient
124
-
125
- client = FalServerlessClient(args.host)
122
+ client = get_client(args.host, args.team)
126
123
  with client.connect() as connection:
127
124
  revs = connection.list_applications()
128
125
  table = _app_rev_table(revs)
@@ -142,9 +139,7 @@ def _add_list_rev_parser(subparsers, parents):
142
139
 
143
140
 
144
141
  def _scale(args):
145
- from fal.sdk import FalServerlessClient
146
-
147
- client = FalServerlessClient(args.host)
142
+ client = get_client(args.host, args.team)
148
143
  with client.connect() as connection:
149
144
  if (
150
145
  args.keep_alive is None
@@ -240,9 +235,7 @@ def _add_scale_parser(subparsers, parents):
240
235
 
241
236
 
242
237
  def _set_rev(args):
243
- from fal.sdk import FalServerlessClient
244
-
245
- client = FalServerlessClient(args.host)
238
+ client = get_client(args.host, args.team)
246
239
  with client.connect() as connection:
247
240
  connection.create_alias(args.app_name, args.app_rev, args.auth)
248
241
 
@@ -277,9 +270,7 @@ def _add_set_rev_parser(subparsers, parents):
277
270
  def _runners(args):
278
271
  from rich.table import Table
279
272
 
280
- from fal.sdk import FalServerlessClient
281
-
282
- client = FalServerlessClient(args.host)
273
+ client = get_client(args.host, args.team)
283
274
  with client.connect() as connection:
284
275
  runners = connection.list_alias_runners(alias=args.app_name)
285
276
 
@@ -350,9 +341,7 @@ def _add_runners_parser(subparsers, parents):
350
341
 
351
342
 
352
343
  def _delete(args):
353
- from fal.sdk import FalServerlessClient
354
-
355
- client = FalServerlessClient(args.host)
344
+ client = get_client(args.host, args.team)
356
345
  with client.connect() as connection:
357
346
  connection.delete_alias(args.app_name)
358
347
 
@@ -373,9 +362,7 @@ def _add_delete_parser(subparsers, parents):
373
362
 
374
363
 
375
364
  def _delete_rev(args):
376
- from fal.sdk import FalServerlessClient
377
-
378
- client = FalServerlessClient(args.host)
365
+ client = get_client(args.host, args.team)
379
366
  with client.connect() as connection:
380
367
  connection.delete_application(args.app_rev)
381
368
 
fal/cli/auth.py CHANGED
@@ -2,11 +2,38 @@ from fal.auth import USER, login, logout
2
2
 
3
3
 
4
4
  def _login(args):
5
+ from rich.prompt import Prompt
6
+
7
+ from fal.config import Config
8
+
5
9
  login()
10
+ teams = [team["nickname"].lower() for team in USER.teams]
11
+ if not teams:
12
+ return
13
+
14
+ team = Prompt.ask(
15
+ "\nPlease choose a team account to use or leave blank to "
16
+ "use your personal account:",
17
+ choices=teams,
18
+ default=None,
19
+ )
20
+ with Config().edit() as config:
21
+ if team:
22
+ args.console.print(
23
+ f"Setting team to [cyan]{team}[/]. "
24
+ "You can change this later with [bold]fal team set[/]."
25
+ )
26
+ config.set("team", team)
27
+ else:
28
+ config.unset("team")
6
29
 
7
30
 
8
31
  def _logout(args):
32
+ from fal.config import Config
33
+
9
34
  logout()
35
+ with Config().edit() as config:
36
+ config.unset("team")
10
37
 
11
38
 
12
39
  def _whoami(args):
fal/cli/keys.py CHANGED
@@ -1,12 +1,11 @@
1
1
  from fal.sdk import KeyScope
2
2
 
3
+ from ._utils import get_client
3
4
  from .parser import FalClientParser
4
5
 
5
6
 
6
7
  def _create(args):
7
- from fal.sdk import FalServerlessClient
8
-
9
- client = FalServerlessClient(args.host)
8
+ client = get_client(args.host, args.team)
10
9
  with client.connect() as connection:
11
10
  parsed_scope = KeyScope(args.scope)
12
11
  result = connection.create_user_key(parsed_scope, args.desc)
@@ -43,15 +42,13 @@ def _add_create_parser(subparsers, parents):
43
42
  def _list(args):
44
43
  from rich.table import Table
45
44
 
46
- from fal.sdk import FalServerlessClient
47
-
48
- client = FalServerlessClient(args.host)
49
45
  table = Table()
50
46
  table.add_column("Key ID")
51
47
  table.add_column("Created At")
52
48
  table.add_column("Scope")
53
49
  table.add_column("Description")
54
50
 
51
+ client = get_client(args.host, args.team)
55
52
  with client.connect() as connection:
56
53
  keys = connection.list_user_keys()
57
54
  for key in keys:
@@ -77,9 +74,7 @@ def _add_list_parser(subparsers, parents):
77
74
 
78
75
 
79
76
  def _revoke(args):
80
- from fal.sdk import FalServerlessClient
81
-
82
- client = FalServerlessClient(args.host)
77
+ client = get_client(args.host, args.team)
83
78
  with client.connect() as connection:
84
79
  connection.revoke_user_key(args.key_id)
85
80
 
fal/cli/main.py CHANGED
@@ -18,6 +18,7 @@ from . import (
18
18
  run,
19
19
  runners,
20
20
  secrets,
21
+ teams,
21
22
  )
22
23
  from .debug import debugtools, get_debug_parser
23
24
  from .parser import FalParser, FalParserExit
@@ -55,6 +56,7 @@ def _get_main_parser() -> argparse.ArgumentParser:
55
56
  doctor,
56
57
  create,
57
58
  runners,
59
+ teams,
58
60
  ]:
59
61
  cmd.add_parser(subparsers, parents)
60
62
 
fal/cli/parser.py CHANGED
@@ -99,3 +99,7 @@ class FalClientParser(FalParser):
99
99
  default=GRPC_HOST,
100
100
  help=argparse.SUPPRESS,
101
101
  )
102
+ self.add_argument(
103
+ "--team",
104
+ help="The team to use.",
105
+ )
fal/cli/profile.py CHANGED
@@ -4,13 +4,12 @@ from fal.config import Config
4
4
 
5
5
 
6
6
  def _list(args):
7
- config = Config()
8
-
9
7
  table = Table()
10
8
  table.add_column("Default")
11
9
  table.add_column("Profile")
12
10
  table.add_column("Settings")
13
11
 
12
+ config = Config()
14
13
  for profile in config.profiles():
15
14
  table.add_row(
16
15
  "*" if profile == config._profile else "",
@@ -22,28 +21,24 @@ def _list(args):
22
21
 
23
22
 
24
23
  def _set(args):
25
- config = Config()
26
- config.set_internal("profile", args.PROFILE)
27
- args.console.print(f"Default profile set to [cyan]{args.PROFILE}[/].")
28
- config.profile = args.PROFILE
29
- if not config.get("key"):
30
- args.console.print(
31
- "No key set for profile. Use [bold]fal profile key[/] to set a key."
32
- )
33
- config.save()
24
+ with Config().edit() as config:
25
+ config.set_internal("profile", args.PROFILE)
26
+ args.console.print(f"Default profile set to [cyan]{args.PROFILE}[/].")
27
+ config.profile = args.PROFILE
28
+ if not config.get("key"):
29
+ args.console.print(
30
+ "No key set for profile. Use [bold]fal profile key[/] to set a key."
31
+ )
34
32
 
35
33
 
36
34
  def _unset(args):
37
- config = Config()
38
- config.set_internal("profile", None)
39
- args.console.print("Default profile unset.")
40
- config.profile = None
41
- config.save()
35
+ with Config().edit() as config:
36
+ config.set_internal("profile", None)
37
+ args.console.print("Default profile unset.")
38
+ config.profile = None
42
39
 
43
40
 
44
41
  def _key_set(args):
45
- config = Config()
46
-
47
42
  while True:
48
43
  key = input("Enter the key: ")
49
44
  if ":" in key:
@@ -52,25 +47,25 @@ def _key_set(args):
52
47
  "[red]Invalid key. The key must be in the format [bold]key:value[/].[/]"
53
48
  )
54
49
 
55
- config.set("key", key)
56
- args.console.print(f"Key set for profile [cyan]{config.profile}[/].")
57
- config.save()
50
+ with Config().edit() as config:
51
+ config.set("key", key)
52
+ args.console.print(f"Key set for profile [cyan]{config.profile}[/].")
58
53
 
59
54
 
60
55
  def _delete(args):
61
- config = Config()
62
- if config.profile == args.PROFILE:
63
- config.set_internal("profile", None)
56
+ with Config().edit() as config:
57
+ if config.profile == args.PROFILE:
58
+ config.set_internal("profile", None)
64
59
 
65
- config.delete(args.PROFILE)
66
- args.console.print(f"Profile [cyan]{args.PROFILE}[/] deleted.")
67
- config.save()
60
+ config.delete(args.PROFILE)
61
+ args.console.print(f"Profile [cyan]{args.PROFILE}[/] deleted.")
68
62
 
69
63
 
70
64
  def add_parser(main_subparsers, parents):
71
65
  auth_help = "Profile management."
72
66
  parser = main_subparsers.add_parser(
73
67
  "profile",
68
+ aliases=["profiles"],
74
69
  description=auth_help,
75
70
  help=auth_help,
76
71
  parents=parents,
fal/cli/runners.py CHANGED
@@ -1,10 +1,9 @@
1
+ from ._utils import get_client
1
2
  from .parser import FalClientParser
2
3
 
3
4
 
4
5
  def _kill(args):
5
- from fal.sdk import FalServerlessClient
6
-
7
- client = FalServerlessClient(args.host)
6
+ client = get_client(args.host, args.team)
8
7
  with client.connect() as connection:
9
8
  connection.kill_runner(args.id)
10
9
 
fal/cli/secrets.py CHANGED
@@ -1,10 +1,9 @@
1
+ from ._utils import get_client
1
2
  from .parser import DictAction, FalClientParser
2
3
 
3
4
 
4
5
  def _set(args):
5
- from fal.sdk import FalServerlessClient
6
-
7
- client = FalServerlessClient(args.host)
6
+ client = get_client(args.host, args.team)
8
7
  with client.connect() as connection:
9
8
  for name, value in args.secrets.items():
10
9
  connection.set_secret(name, value)
@@ -34,13 +33,11 @@ def _add_set_parser(subparsers, parents):
34
33
  def _list(args):
35
34
  from rich.table import Table
36
35
 
37
- from fal.sdk import FalServerlessClient
38
-
39
36
  table = Table()
40
37
  table.add_column("Secret Name")
41
38
  table.add_column("Created At")
42
39
 
43
- client = FalServerlessClient(args.host)
40
+ client = get_client(args.host, args.team)
44
41
  with client.connect() as connection:
45
42
  for secret in connection.list_secrets():
46
43
  table.add_row(secret.name, str(secret.created_at))
@@ -60,9 +57,7 @@ def _add_list_parser(subparsers, parents):
60
57
 
61
58
 
62
59
  def _unset(args):
63
- from fal.sdk import FalServerlessClient
64
-
65
- client = FalServerlessClient(args.host)
60
+ client = get_client(args.host, args.team)
66
61
  with client.connect() as connection:
67
62
  connection.delete_secret(args.secret)
68
63
 
fal/cli/teams.py ADDED
@@ -0,0 +1,89 @@
1
+ def _list(args):
2
+ from rich.table import Table
3
+
4
+ from fal.auth import USER
5
+ from fal.config import Config
6
+
7
+ table = Table()
8
+ table.add_column("Default")
9
+ table.add_column("Team")
10
+ table.add_column("Full Name")
11
+ table.add_column("ID")
12
+
13
+ default_team = Config().get("team")
14
+
15
+ for team in USER.teams:
16
+ default = default_team and default_team.lower() == team["nickname"].lower()
17
+ table.add_row(
18
+ "*" if default else "", team["nickname"], team["full_name"], team["user_id"]
19
+ )
20
+
21
+ args.console.print(table)
22
+
23
+
24
+ def _set(args):
25
+ from fal.config import Config
26
+ from fal.sdk import USER
27
+
28
+ team = args.team.lower()
29
+ for team_info in USER.teams:
30
+ if team_info["nickname"].lower() == team:
31
+ break
32
+ else:
33
+ raise ValueError(f"Team {args.team} not found")
34
+
35
+ with Config().edit() as config:
36
+ config.set("team", team)
37
+
38
+
39
+ def _unset(args):
40
+ from fal.config import Config
41
+
42
+ with Config().edit() as config:
43
+ config.unset("team")
44
+
45
+
46
+ def add_parser(main_subparsers, parents):
47
+ teams_help = "Manage teams."
48
+ parser = main_subparsers.add_parser(
49
+ "teams",
50
+ aliases=["team"],
51
+ description=teams_help,
52
+ help=teams_help,
53
+ parents=parents,
54
+ )
55
+
56
+ subparsers = parser.add_subparsers(
57
+ title="Commands",
58
+ metavar="command",
59
+ dest="cmd",
60
+ required=True,
61
+ )
62
+
63
+ list_help = "List teams."
64
+ list_parser = subparsers.add_parser(
65
+ "list",
66
+ description=list_help,
67
+ help=list_help,
68
+ parents=parents,
69
+ )
70
+ list_parser.set_defaults(func=_list)
71
+
72
+ set_help = "Set the current team."
73
+ set_parser = subparsers.add_parser(
74
+ "set",
75
+ description=set_help,
76
+ help=set_help,
77
+ parents=parents,
78
+ )
79
+ set_parser.add_argument("team", help="The team to set.")
80
+ set_parser.set_defaults(func=_set)
81
+
82
+ unset_help = "Unset the current team."
83
+ unset_parser = subparsers.add_parser(
84
+ "unset",
85
+ description=unset_help,
86
+ help=unset_help,
87
+ parents=parents,
88
+ )
89
+ unset_parser.set_defaults(func=_unset)
fal/config.py CHANGED
@@ -1,7 +1,9 @@
1
1
  import os
2
+ from contextlib import contextmanager
2
3
  from typing import Dict, List, Optional
3
4
 
4
- SETTINGS_SECTION = "__internal__"
5
+ SETTINGS_SECTION = "__internal__" # legacy
6
+ DEFAULT_PROFILE = "default"
5
7
 
6
8
 
7
9
  class Config:
@@ -23,9 +25,9 @@ class Config:
23
25
  except FileNotFoundError:
24
26
  self._config = {}
25
27
 
26
- profile = os.getenv("FAL_PROFILE")
27
- if not profile:
28
- profile = self.get_internal("profile")
28
+ profile = (
29
+ os.getenv("FAL_PROFILE") or self.get_internal("profile") or DEFAULT_PROFILE
30
+ )
29
31
 
30
32
  self.profile = profile
31
33
 
@@ -66,6 +68,12 @@ class Config:
66
68
 
67
69
  self._config[self.profile][key] = value
68
70
 
71
+ def unset(self, key: str) -> None:
72
+ if not self.profile:
73
+ raise ValueError("No profile set.")
74
+
75
+ del self._config[self.profile][key]
76
+
69
77
  def get_internal(self, key: str) -> Optional[str]:
70
78
  if SETTINGS_SECTION not in self._config:
71
79
  self._config[SETTINGS_SECTION] = {}
@@ -83,3 +91,8 @@ class Config:
83
91
 
84
92
  def delete(self, profile: str) -> None:
85
93
  del self._config[profile]
94
+
95
+ @contextmanager
96
+ def edit(self):
97
+ yield self
98
+ self.save()
fal/sdk.py CHANGED
@@ -129,12 +129,22 @@ class FalServerlessKeyCredentials(Credentials):
129
129
  @dataclass
130
130
  class AuthenticatedCredentials(Credentials):
131
131
  user = USER
132
+ team_id: str | None = None
132
133
 
133
134
  def to_grpc(self) -> grpc.ChannelCredentials:
134
- return grpc.composite_channel_credentials(
135
+ creds = [
135
136
  self.server_credentials.to_grpc(),
136
137
  grpc.access_token_call_credentials(USER.access_token),
137
- )
138
+ ]
139
+
140
+ if self.team_id:
141
+ creds.append(
142
+ grpc.metadata_call_credentials(
143
+ _GRPCMetadata("fal-user-id", self.team_id)
144
+ )
145
+ )
146
+
147
+ return grpc.composite_channel_credentials(*creds)
138
148
 
139
149
  def to_headers(self) -> dict[str, str]:
140
150
  token = USER.bearer_token
@@ -158,16 +168,23 @@ def get_agent_credentials(original_credentials: Credentials) -> Credentials:
158
168
  return original_credentials
159
169
 
160
170
 
161
- def get_default_credentials() -> Credentials:
171
+ def get_default_credentials(team: str | None = None) -> Credentials:
172
+ from fal.config import Config
173
+
162
174
  if flags.AUTH_DISABLED:
163
175
  return Credentials()
164
176
 
165
177
  key_creds = key_credentials()
166
178
  if key_creds:
167
179
  logger.debug("Using key credentials")
180
+ if team:
181
+ raise ValueError("Using explicit team with key credentials is not allowed")
168
182
  return FalServerlessKeyCredentials(key_creds[0], key_creds[1])
169
183
  else:
170
- return AuthenticatedCredentials()
184
+ config = Config()
185
+ team = team or config.get("team")
186
+ team_id = USER.get_team(team)["user_id"] if team else None
187
+ return AuthenticatedCredentials(team_id=team_id)
171
188
 
172
189
 
173
190
  @dataclass
@@ -126,11 +126,12 @@ def download_file(
126
126
  *,
127
127
  force: bool = False,
128
128
  request_headers: dict[str, str] | None = None,
129
+ filesize_limit: int | None = None,
129
130
  ) -> Path:
130
131
  """Downloads a file from the specified URL to the target directory.
131
132
 
132
133
  The function downloads the file from the given URL and saves it in the specified
133
- target directory.
134
+ target directory, provided it is below the given filesize limit.
134
135
 
135
136
  It also checks whether the local file already exists and whether its content length
136
137
  matches the expected content length from the remote file. If the local file already
@@ -151,6 +152,8 @@ def download_file(
151
152
  Defaults to `False`.
152
153
  request_headers: A dictionary containing additional headers to be included in
153
154
  the HTTP request. Defaults to `None`.
155
+ filesize_limit: An integer specifying the maximum downloadable size,
156
+ in megabytes. Defaults to `None`.
154
157
 
155
158
 
156
159
  Returns:
@@ -160,12 +163,22 @@ def download_file(
160
163
  ValueError: If the provided `file_name` contains a forward slash ('/').
161
164
  DownloadError: If an error occurs during the download process.
162
165
  """
166
+ ONE_MB = 1024**2
167
+
163
168
  try:
164
- file_name = _get_remote_file_properties(url, request_headers)[0]
169
+ file_name, expected_filesize = _get_remote_file_properties(url, request_headers)
165
170
  except Exception as e:
166
- print(f"GOt error: {e}")
171
+ print(f"Got error: {e}")
167
172
  raise DownloadError(f"Failed to get remote file properties for {url}") from e
168
173
 
174
+ expected_filesize_mb = expected_filesize / ONE_MB
175
+
176
+ if filesize_limit is not None and expected_filesize_mb > filesize_limit:
177
+ raise DownloadError(
178
+ f"""File to be downloaded is of size {expected_filesize_mb},
179
+ which is over the limit of {filesize_limit}"""
180
+ )
181
+
169
182
  if "/" in file_name:
170
183
  raise ValueError(f"File name '{file_name}' cannot contain a slash.")
171
184
 
@@ -194,7 +207,10 @@ def download_file(
194
207
 
195
208
  try:
196
209
  _download_file_python(
197
- url=url, target_path=target_path, request_headers=request_headers
210
+ url=url,
211
+ target_path=target_path,
212
+ request_headers=request_headers,
213
+ filesize_limit=filesize_limit,
198
214
  )
199
215
  except Exception as e:
200
216
  msg = f"Failed to download {url} to {target_path}"
@@ -207,7 +223,10 @@ def download_file(
207
223
 
208
224
 
209
225
  def _download_file_python(
210
- url: str, target_path: Path | str, request_headers: dict[str, str] | None = None
226
+ url: str,
227
+ target_path: Path | str,
228
+ request_headers: dict[str, str] | None = None,
229
+ filesize_limit: int | None = None,
211
230
  ) -> Path:
212
231
  """Download a file from a given URL and save it to a specified path using a
213
232
  Python interface.
@@ -217,6 +236,8 @@ def _download_file_python(
217
236
  target_path: The path where the downloaded file will be saved.
218
237
  request_headers: A dictionary containing additional headers to be included in
219
238
  the HTTP request. Defaults to `None`.
239
+ filesize_limit: A integer value specifying how many megabytes can be
240
+ downloaded at maximum. Defaults to `None`.
220
241
 
221
242
  Returns:
222
243
  The path where the downloaded file has been saved.
@@ -233,7 +254,10 @@ def _download_file_python(
233
254
  file_path = temp_file.name
234
255
 
235
256
  for progress, total_size in _stream_url_data_to_file(
236
- url, temp_file.name, request_headers=request_headers
257
+ url,
258
+ temp_file.name,
259
+ request_headers=request_headers,
260
+ filesize_limit=filesize_limit,
237
261
  ):
238
262
  if total_size:
239
263
  progress_msg = f"Downloading {url} ... {progress:.2%}"
@@ -261,6 +285,7 @@ def _stream_url_data_to_file(
261
285
  file_path: str,
262
286
  chunk_size_in_mb: int = 64,
263
287
  request_headers: dict[str, str] | None = None,
288
+ filesize_limit: int | None = None,
264
289
  ):
265
290
  """Download data from a URL and stream it to a file.
266
291
 
@@ -277,6 +302,8 @@ def _stream_url_data_to_file(
277
302
  Defaults to 64.
278
303
  request_headers: A dictionary containing additional headers to be included in
279
304
  the HTTP request. Defaults to `None`.
305
+ filesize_limit: An integer specifying how many megabytes can be
306
+ downloaded at maximum. Defaults to `None`.
280
307
 
281
308
  Yields:
282
309
  A tuple containing two elements:
@@ -300,6 +327,11 @@ def _stream_url_data_to_file(
300
327
  f_stream.write(data)
301
328
 
302
329
  received_size = f_stream.tell()
330
+ if filesize_limit is not None and received_size > filesize_limit:
331
+ raise DownloadError(
332
+ f"""Attempted to download more data {received_size}
333
+ than the set limit of {filesize_limit}"""
334
+ )
303
335
 
304
336
  if total_size:
305
337
  progress = received_size / total_size
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fal
3
- Version: 1.11.0
3
+ Version: 1.11.2
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,41 +1,42 @@
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=NYxfUyIQwdONRcmiATXsc3iD0kuKImoMYD3H1Vdv9OM,513
3
+ fal/_fal_version.py,sha256=U5-HoUE2zTuerZ_jeB56PyGuex0oNVid4wSBeoHXcJA,513
4
4
  fal/_serialization.py,sha256=rD2YiSa8iuzCaZohZwN_MPEB-PpSKbWRDeaIDpTEjyY,7653
5
5
  fal/_version.py,sha256=EBGqrknaf1WygENX-H4fBefLvHryvJBBGtVJetaB0NY,266
6
6
  fal/api.py,sha256=jqmQfhRvZMYpWWvbTfARxjywP_72c314pnLPTb5dGwA,44755
7
7
  fal/app.py,sha256=3WhjRgJdJ2ajAeZ3IeFb20_Zm6EH19a_WIuDtanaMHE,23308
8
8
  fal/apps.py,sha256=RpmElElJnDYjsTRQOdNYiJwd74GEOGYA38L5O5GzNEg,11068
9
- fal/config.py,sha256=aVv0k2fxMZurlra4c7ZIKQQCNPI-Dm_Mns6PsYWdh-c,2264
9
+ fal/config.py,sha256=sERAcM-YBk1aF9-z-ZSwQqwgOc4AXJSkWCrDP5n7ktM,2582
10
10
  fal/container.py,sha256=PM7e1RloTCexZ64uAv7sa2RSZxPI-X8KcxkdaZqEfjw,1914
11
11
  fal/files.py,sha256=QgfYfMKmNobMPufrAP_ga1FKcIAlSbw18Iar1-0qepo,2650
12
12
  fal/flags.py,sha256=oWN_eidSUOcE9wdPK_77si3A1fpgOC0UEERPsvNLIMc,842
13
13
  fal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
14
  fal/rest_client.py,sha256=kGBGmuyHfX1lR910EoKCYPjsyU8MdXawT_cW2q8Sajc,568
15
- fal/sdk.py,sha256=mMUIPB3F91TDssG_J5BFen2hWW6GFYWLh6dedpvZ9mU,24480
15
+ fal/sdk.py,sha256=WuYeZREYHElNZJLXyOh-sVj_9nX1E9QQ8lQPG6PIlsA,25045
16
16
  fal/sync.py,sha256=ZuIJA2-hTPNANG9B_NNJZUsO68EIdTH0dc9MzeVE2VU,4340
17
17
  fal/utils.py,sha256=9q_QrQBlQN3nZYA1kEGRfhJWi4RjnO4H1uQswfaei9w,2146
18
18
  fal/workflows.py,sha256=Zl4f6Bs085hY40zmqScxDUyCu7zXkukDbW02iYOLTTI,14805
19
- fal/auth/__init__.py,sha256=MXwS5zyY1SYJWEkc6s39et73Dkg3cDJg1ZwxRhXNj4c,4704
19
+ fal/auth/__init__.py,sha256=i2zA9xiUQP3dxvL-6NFHodCHEktJ-1ApRYunLJamdtU,5807
20
20
  fal/auth/auth0.py,sha256=rSG1mgH-QGyKfzd7XyAaj1AYsWt-ho8Y_LZ-FUVWzh4,5421
21
21
  fal/auth/local.py,sha256=sndkM6vKpeVny6NHTacVlTbiIFqaksOmw0Viqs_RN1U,1790
22
22
  fal/cli/__init__.py,sha256=padK4o0BFqq61kxAA1qQ0jYr2SuhA2mf90B3AaRkmJA,37
23
- fal/cli/_utils.py,sha256=45G0LEz2bW-69MUQKPdatVE_CBC2644gC-V0qdNEsco,1252
23
+ fal/cli/_utils.py,sha256=pHmKzpUWc2n4yPv4R0Y6DuZC5j-rVKU8oqdQDVW_-Lo,1591
24
24
  fal/cli/api.py,sha256=-rl50A00CxqVZtDh0iZmpCHMFY0jZySaumbPCe3MSoQ,2090
25
- fal/cli/apps.py,sha256=KhweXdLdd1wZquhAMVwgl38dZAyFns-j3ZdIZLOLGWg,11504
26
- fal/cli/auth.py,sha256=--MhfHGwxmtHbRkGioyn1prKn_U-pBzbz0G_QeZou-U,1352
25
+ fal/cli/apps.py,sha256=bc1QyWVsEOsabNEAB0zZ6IEjkpkkjdoplLFWgoruFGc,11234
26
+ fal/cli/auth.py,sha256=LLyYByasUJdiUGGrnZiKMO8TRt4LVYT7ocAUi_xSg2w,2106
27
27
  fal/cli/cli_nested_json.py,sha256=veSZU8_bYV3Iu1PAoxt-4BMBraNIqgH5nughbs2UKvE,13539
28
28
  fal/cli/create.py,sha256=a8WDq-nJLFTeoIXqpb5cr7GR7YR9ZZrQCawNm34KXXE,627
29
29
  fal/cli/debug.py,sha256=u_urnyFzSlNnrq93zz_GXE9FX4VyVxDoamJJyrZpFI0,1312
30
30
  fal/cli/deploy.py,sha256=JX1jwyAFE0-u-PGGqU-pphKMCUpaPFbYMjRMhK0VmoQ,7783
31
31
  fal/cli/doctor.py,sha256=U4ne9LX5gQwNblsYQ27XdO8AYDgbYjTO39EtxhwexRM,983
32
- fal/cli/keys.py,sha256=trDpA3LJu9S27qE_K8Hr6fKLK4vwVzbxUHq8TFrV4pw,3157
33
- fal/cli/main.py,sha256=oUY1bUcfEC8LIsNxUBBjT3O6HxWCd1cKEMrJBue47iw,2199
34
- fal/cli/parser.py,sha256=edCqFWYAQSOhrxeEK9BtFRlTEUAlG2JUDjS_vhZ_nHE,2868
35
- fal/cli/profile.py,sha256=OplQgs8UGQzBH7_BnG0GBMYNQ8jtPnzzX8Q1FM3Y-5s,3320
32
+ fal/cli/keys.py,sha256=7Sf4DT4le89G42eAOt0ltRjbZAtE70AVQ62hmjZhUy0,3059
33
+ fal/cli/main.py,sha256=AgT6fpcmSA3NkG_fv4MXOM_fAQ9kmhZHeljyNa3p27U,2225
34
+ fal/cli/parser.py,sha256=jYsGQ0BLQuKI7KtN1jnLVYKMbLtez7hPjwTNfG3UPSk,2964
35
+ fal/cli/profile.py,sha256=vWngqkX7UizQIUQOpXauFz1UGJwDeh38Si6wXcIj3Eo,3396
36
36
  fal/cli/run.py,sha256=nAC12Qss4Fg1XmV0qOS9RdGNLYcdoHeRgQMvbTN4P9I,1202
37
- fal/cli/runners.py,sha256=5pXuKq7nSkf0VpnppNnvxwP8XDq0SWkc6mkfizDwWMQ,1046
38
- fal/cli/secrets.py,sha256=740msFm7d41HruudlcfqUXlFl53N-WmChsQP9B9M9Po,2572
37
+ fal/cli/runners.py,sha256=f5KdIUqNiqjodlKGr-RQq7DKR3hIfVYZmlrFRBkzC-A,1034
38
+ fal/cli/secrets.py,sha256=QKSmazu-wiNF6fOpGL9v2TDYxAjX9KTi7ot7vnv6f5E,2474
39
+ fal/cli/teams.py,sha256=ZrWdMvLRqPt1EuxoOKAfbP6sxJeOCaMeZ2LgZqw2S_w,2153
39
40
  fal/console/__init__.py,sha256=ernZ4bzvvliQh5SmrEqQ7lA5eVcbw6Ra2jalKtA7dxg,132
40
41
  fal/console/icons.py,sha256=De9MfFaSkO2Lqfne13n3PrYfTXJVIzYZVqYn5BWsdrA,108
41
42
  fal/console/ux.py,sha256=KMQs3UHQvVHDxDQQqlot-WskVKoMQXOE3jiVkkfmIMY,356
@@ -68,7 +69,7 @@ fal/toolkit/image/nsfw_filter/inference.py,sha256=BhIPF_zxRLetThQYxDDF0sdx9VRwvu
68
69
  fal/toolkit/image/nsfw_filter/model.py,sha256=63mu8D15z_IosoRUagRLGHy6VbLqFmrG-yZqnu2vVm4,457
69
70
  fal/toolkit/image/nsfw_filter/requirements.txt,sha256=3Pmrd0Ny6QAeBqUNHCgffRyfaCARAPJcfSCX5cRYpbM,37
70
71
  fal/toolkit/utils/__init__.py,sha256=CrmM9DyCz5-SmcTzRSm5RaLgxy3kf0ZsSEN9uhnX2Xo,97
71
- fal/toolkit/utils/download_utils.py,sha256=Ju-rvQ0oqFkARONRyrfIGVaqthjlxcFIb-qg5SE7yBw,18419
72
+ fal/toolkit/utils/download_utils.py,sha256=NgOMNs-bQGSg3gWnu123BgZitJgJrvtRexIefTMuylY,19739
72
73
  fal/toolkit/utils/retry.py,sha256=mHcQvvNIpu-Hi29P1HXSZuyvolRd48dMaJToqzlG0NY,1353
73
74
  openapi_fal_rest/__init__.py,sha256=ziculmF_i6trw63LzZGFX-6W3Lwq9mCR8_UpkpvpaHI,152
74
75
  openapi_fal_rest/client.py,sha256=G6BpJg9j7-JsrAUGddYwkzeWRYickBjPdcVgXoPzxuE,2817
@@ -133,8 +134,8 @@ openapi_fal_rest/models/workflow_node_type.py,sha256=-FzyeY2bxcNmizKbJI8joG7byRi
133
134
  openapi_fal_rest/models/workflow_schema.py,sha256=4K5gsv9u9pxx2ItkffoyHeNjBBYf6ur5bN4m_zePZNY,2019
134
135
  openapi_fal_rest/models/workflow_schema_input.py,sha256=2OkOXWHTNsCXHWS6EGDFzcJKkW5FIap-2gfO233EvZQ,1191
135
136
  openapi_fal_rest/models/workflow_schema_output.py,sha256=EblwSPAGfWfYVWw_WSSaBzQVju296is9o28rMBAd0mc,1196
136
- fal-1.11.0.dist-info/METADATA,sha256=gyk3rZlftiD1qXNBRHk5Z_g-CC65Qi9OTS_zuKfQGHE,4043
137
- fal-1.11.0.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
138
- fal-1.11.0.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
139
- fal-1.11.0.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
140
- fal-1.11.0.dist-info/RECORD,,
137
+ fal-1.11.2.dist-info/METADATA,sha256=BmLK6V5qidVEJ_qCf3egT714K26-dcQZ4AlsZx48-wM,4043
138
+ fal-1.11.2.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
139
+ fal-1.11.2.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
140
+ fal-1.11.2.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
141
+ fal-1.11.2.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (77.0.3)
2
+ Generator: setuptools (78.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5