fal 1.6.2__py3-none-any.whl → 1.7.1__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
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '1.6.2'
16
- __version_tuple__ = version_tuple = (1, 6, 2)
15
+ __version__ = version = '1.7.1'
16
+ __version_tuple__ = version_tuple = (1, 7, 1)
fal/app.py CHANGED
@@ -249,9 +249,9 @@ def _to_fal_app_name(name: str) -> str:
249
249
 
250
250
 
251
251
  def _print_python_packages() -> None:
252
- from pkg_resources import working_set
252
+ from importlib.metadata import distributions
253
253
 
254
- packages = [f"{package.key}=={package.version}" for package in working_set]
254
+ packages = [f"{dist.metadata['Name']}=={dist.version}" for dist in distributions()]
255
255
 
256
256
  print("[debug] Python packages installed:", ", ".join(packages))
257
257
 
fal/auth/__init__.py CHANGED
@@ -2,22 +2,70 @@ from __future__ import annotations
2
2
 
3
3
  import os
4
4
  from dataclasses import dataclass, field
5
+ from threading import Lock
6
+ from typing import Optional
5
7
 
6
8
  import click
7
9
 
8
10
  from fal.auth import auth0, local
11
+ from fal.config import Config
9
12
  from fal.console import console
10
13
  from fal.console.icons import CHECK_ICON
11
14
  from fal.exceptions.auth import UnauthenticatedException
12
15
 
13
16
 
17
+ class GoogleColabState:
18
+ def __init__(self):
19
+ self.is_checked = False
20
+ self.lock = Lock()
21
+ self.secret: Optional[str] = None
22
+
23
+
24
+ _colab_state = GoogleColabState()
25
+
26
+
27
+ def is_google_colab() -> bool:
28
+ try:
29
+ from IPython import get_ipython
30
+
31
+ return "google.colab" in str(get_ipython())
32
+ except ModuleNotFoundError:
33
+ return False
34
+ except NameError:
35
+ return False
36
+
37
+
38
+ def get_colab_token() -> Optional[str]:
39
+ if not is_google_colab():
40
+ return None
41
+ with _colab_state.lock:
42
+ if _colab_state.is_checked: # request access only once
43
+ return _colab_state.secret
44
+
45
+ try:
46
+ from google.colab import userdata # noqa: I001
47
+ except ImportError:
48
+ return None
49
+
50
+ try:
51
+ token = userdata.get("FAL_KEY")
52
+ _colab_state.secret = token.strip()
53
+ except Exception:
54
+ _colab_state.secret = None
55
+
56
+ _colab_state.is_checked = True
57
+ return _colab_state.secret
58
+
59
+
14
60
  def key_credentials() -> tuple[str, str] | None:
15
61
  # Ignore key credentials when the user forces auth by user.
16
62
  if os.environ.get("FAL_FORCE_AUTH_BY_USER") == "1":
17
63
  return None
18
64
 
19
- if "FAL_KEY" in os.environ:
20
- key = os.environ["FAL_KEY"]
65
+ config = Config()
66
+
67
+ key = os.environ.get("FAL_KEY") or config.get("key") or get_colab_token()
68
+ if key:
21
69
  key_id, key_secret = key.split(":", 1)
22
70
  return (key_id, key_secret)
23
71
  elif "FAL_KEY_ID" in os.environ and "FAL_KEY_SECRET" in os.environ:
fal/config.py ADDED
@@ -0,0 +1,23 @@
1
+ import os
2
+
3
+ import tomli
4
+
5
+
6
+ class Config:
7
+ DEFAULT_CONFIG_PATH = "~/.fal/config.toml"
8
+ DEFAULT_PROFILE = "default"
9
+
10
+ def __init__(self):
11
+ self.config_path = os.path.expanduser(
12
+ os.getenv("FAL_CONFIG_PATH", self.DEFAULT_CONFIG_PATH)
13
+ )
14
+ self.profile = os.getenv("FAL_PROFILE", self.DEFAULT_PROFILE)
15
+
16
+ try:
17
+ with open(self.config_path, "rb") as file:
18
+ self.config = tomli.load(file)
19
+ except FileNotFoundError:
20
+ self.config = {}
21
+
22
+ def get(self, key):
23
+ return self.config.get(self.profile, {}).get(key)
fal/container.py CHANGED
@@ -3,7 +3,7 @@ class ContainerImage:
3
3
  from a Dockerfile.
4
4
  """
5
5
 
6
- _known_keys = {"dockerfile_str", "build_args", "registries"}
6
+ _known_keys = {"dockerfile_str", "build_args", "registries", "builder"}
7
7
 
8
8
  @classmethod
9
9
  def from_dockerfile_str(cls, text: str, **kwargs):
fal/toolkit/file/file.py CHANGED
@@ -47,7 +47,10 @@ BUILT_IN_REPOSITORIES: dict[RepositoryId, FileRepositoryFactory] = {
47
47
  }
48
48
 
49
49
 
50
- def get_builtin_repository(id: RepositoryId) -> FileRepository:
50
+ def get_builtin_repository(id: RepositoryId | FileRepository) -> FileRepository:
51
+ if isinstance(id, FileRepository):
52
+ return id
53
+
51
54
  if id not in BUILT_IN_REPOSITORIES.keys():
52
55
  raise ValueError(f'"{id}" is not a valid built-in file repository')
53
56
  return BUILT_IN_REPOSITORIES[id]()
@@ -122,7 +125,8 @@ class File(BaseModel):
122
125
  url=url,
123
126
  content_type=None,
124
127
  file_name=None,
125
- repository=DEFAULT_REPOSITORY,
128
+ file_size=None,
129
+ file_data=None,
126
130
  )
127
131
 
128
132
  @classmethod
@@ -139,11 +143,7 @@ class File(BaseModel):
139
143
  save_kwargs: Optional[dict] = None,
140
144
  fallback_save_kwargs: Optional[dict] = None,
141
145
  ) -> File:
142
- repo = (
143
- repository
144
- if isinstance(repository, FileRepository)
145
- else get_builtin_repository(repository)
146
- )
146
+ repo = get_builtin_repository(repository)
147
147
 
148
148
  save_kwargs = save_kwargs or {}
149
149
  fallback_save_kwargs = fallback_save_kwargs or {}
@@ -160,11 +160,7 @@ class File(BaseModel):
160
160
  if not fallback_repository:
161
161
  raise
162
162
 
163
- fallback_repo = (
164
- fallback_repository
165
- if isinstance(fallback_repository, FileRepository)
166
- else get_builtin_repository(fallback_repository)
167
- )
163
+ fallback_repo = get_builtin_repository(fallback_repository)
168
164
 
169
165
  url = fallback_repo.save(
170
166
  fdata, object_lifecycle_preference, **fallback_save_kwargs
@@ -196,11 +192,7 @@ class File(BaseModel):
196
192
  if not file_path.exists():
197
193
  raise FileNotFoundError(f"File {file_path} does not exist")
198
194
 
199
- repo = (
200
- repository
201
- if isinstance(repository, FileRepository)
202
- else get_builtin_repository(repository)
203
- )
195
+ repo = get_builtin_repository(repository)
204
196
 
205
197
  save_kwargs = save_kwargs or {}
206
198
  fallback_save_kwargs = fallback_save_kwargs or {}
@@ -222,11 +214,7 @@ class File(BaseModel):
222
214
  if not fallback_repository:
223
215
  raise
224
216
 
225
- fallback_repo = (
226
- fallback_repository
227
- if isinstance(fallback_repository, FileRepository)
228
- else get_builtin_repository(fallback_repository)
229
- )
217
+ fallback_repo = get_builtin_repository(fallback_repository)
230
218
 
231
219
  url, data = fallback_repo.save_file(
232
220
  file_path,
fal/toolkit/types.py ADDED
@@ -0,0 +1,140 @@
1
+ import re
2
+ import tempfile
3
+ from contextlib import contextmanager
4
+ from pathlib import Path
5
+ from typing import Any, Dict, Generator, Union
6
+
7
+ import pydantic
8
+ from pydantic.utils import update_not_none
9
+
10
+ from fal.toolkit.image import read_image_from_url
11
+ from fal.toolkit.utils.download_utils import download_file
12
+
13
+ # https://github.com/pydantic/pydantic/pull/2573
14
+ if not hasattr(pydantic, "__version__") or pydantic.__version__.startswith("1."):
15
+ IS_PYDANTIC_V2 = False
16
+ else:
17
+ IS_PYDANTIC_V2 = True
18
+
19
+ MAX_DATA_URI_LENGTH = 10 * 1024 * 1024
20
+ MAX_HTTPS_URL_LENGTH = 2048
21
+
22
+ HTTP_URL_REGEX = (
23
+ r"^https:\/\/(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(?::\d{1,5})?(?:\/[^\s]*)?$"
24
+ )
25
+
26
+
27
+ class DownloadFileMixin:
28
+ @contextmanager
29
+ def as_temp_file(self) -> Generator[Path, None, None]:
30
+ with tempfile.TemporaryDirectory() as temp_dir:
31
+ yield download_file(str(self), temp_dir)
32
+
33
+
34
+ class DownloadImageMixin:
35
+ def to_pil(self):
36
+ return read_image_from_url(str(self))
37
+
38
+
39
+ class DataUri(DownloadFileMixin, str):
40
+ if IS_PYDANTIC_V2:
41
+
42
+ @classmethod
43
+ def __get_pydantic_core_schema__(cls, source_type: Any, handler) -> Any:
44
+ return {
45
+ "type": "str",
46
+ "pattern": "^data:",
47
+ "max_length": MAX_DATA_URI_LENGTH,
48
+ "strip_whitespace": True,
49
+ }
50
+
51
+ def __get_pydantic_json_schema__(cls, core_schema, handler) -> Dict[str, Any]:
52
+ json_schema = handler(core_schema)
53
+ json_schema.update(format="data-uri")
54
+ return json_schema
55
+ else:
56
+
57
+ @classmethod
58
+ def __get_validators__(cls):
59
+ yield cls.validate
60
+
61
+ @classmethod
62
+ def validate(cls, value: Any) -> "DataUri":
63
+ from pydantic.validators import str_validator
64
+
65
+ value = str_validator(value)
66
+ value = value.strip()
67
+
68
+ if not value.startswith("data:"):
69
+ raise ValueError("Data URI must start with 'data:'")
70
+
71
+ if len(value) > MAX_DATA_URI_LENGTH:
72
+ raise ValueError(
73
+ f"Data URI is too long. Max length is {MAX_DATA_URI_LENGTH} bytes."
74
+ )
75
+
76
+ return cls(value)
77
+
78
+ @classmethod
79
+ def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
80
+ update_not_none(field_schema, format="data-uri")
81
+
82
+
83
+ class HttpsUrl(DownloadFileMixin, str):
84
+ if IS_PYDANTIC_V2:
85
+
86
+ @classmethod
87
+ def __get_pydantic_core_schema__(cls, source_type: Any, handler) -> Any:
88
+ return {
89
+ "type": "str",
90
+ "pattern": HTTP_URL_REGEX,
91
+ "max_length": MAX_HTTPS_URL_LENGTH,
92
+ "strip_whitespace": True,
93
+ }
94
+
95
+ def __get_pydantic_json_schema__(cls, core_schema, handler) -> Dict[str, Any]:
96
+ json_schema = handler(core_schema)
97
+ json_schema.update(format="https-url")
98
+ return json_schema
99
+
100
+ else:
101
+
102
+ @classmethod
103
+ def __get_validators__(cls):
104
+ yield cls.validate
105
+
106
+ @classmethod
107
+ def validate(cls, value: Any) -> "HttpsUrl":
108
+ from pydantic.validators import str_validator
109
+
110
+ value = str_validator(value)
111
+ value = value.strip()
112
+
113
+ if not re.match(HTTP_URL_REGEX, value):
114
+ raise ValueError(
115
+ "URL must start with 'https://' and follow the correct format."
116
+ )
117
+
118
+ if len(value) > MAX_HTTPS_URL_LENGTH:
119
+ raise ValueError(
120
+ f"HTTPS URL is too long. Max length is "
121
+ f"{MAX_HTTPS_URL_LENGTH} characters."
122
+ )
123
+
124
+ return cls(value)
125
+
126
+ @classmethod
127
+ def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
128
+ update_not_none(field_schema, format="https-url")
129
+
130
+
131
+ class ImageHttpsUrl(DownloadImageMixin, HttpsUrl):
132
+ pass
133
+
134
+
135
+ class ImageDataUri(DownloadImageMixin, DataUri):
136
+ pass
137
+
138
+
139
+ FileInput = Union[HttpsUrl, DataUri]
140
+ ImageInput = Union[ImageHttpsUrl, ImageDataUri]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: fal
3
- Version: 1.6.2
3
+ Version: 1.7.1
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,13 @@
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=ay9A4GSmtr3NioHirRgXvWfXtjwRjzXIO_WPuFobCoI,411
3
+ fal/_fal_version.py,sha256=kn-QYzzAhfbnfKK6EpE9gJz8TDZkEk52evaid1DHkG4,411
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=xTtPvDqaEHsq2lFsMwRZiHb4hzjVY3y6lV-xbzkSetI,43375
7
- fal/app.py,sha256=qJZcGGxCD3-kijbsXx3pocSyiiRKERuF5rXtx5hVt_Q,22902
7
+ fal/app.py,sha256=C1dTWjit90XdTKmrwd5Aqv3SD0MA1JDZoLLtmStn2Xc,22917
8
8
  fal/apps.py,sha256=RpmElElJnDYjsTRQOdNYiJwd74GEOGYA38L5O5GzNEg,11068
9
- fal/container.py,sha256=X1DO5Ypb7oIDdDg_yqt4GCR8NogSvqSqa5hNdAPUx8A,623
9
+ fal/config.py,sha256=hgI3kW4_2NoFsrYEiPss0mnDTr8_Td2z0pVgm93wi9o,600
10
+ fal/container.py,sha256=EjokKTULJ3fPUjDttjir-jmg0gqcUDe0iVzW2j5njec,634
10
11
  fal/files.py,sha256=QgfYfMKmNobMPufrAP_ga1FKcIAlSbw18Iar1-0qepo,2650
11
12
  fal/flags.py,sha256=oWN_eidSUOcE9wdPK_77si3A1fpgOC0UEERPsvNLIMc,842
12
13
  fal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -15,7 +16,7 @@ fal/sdk.py,sha256=HjlToPJkG0Z5h_D0D2FK43i3JFKeO4r2IhCGx4B82Z8,22564
15
16
  fal/sync.py,sha256=ZuIJA2-hTPNANG9B_NNJZUsO68EIdTH0dc9MzeVE2VU,4340
16
17
  fal/utils.py,sha256=9q_QrQBlQN3nZYA1kEGRfhJWi4RjnO4H1uQswfaei9w,2146
17
18
  fal/workflows.py,sha256=Zl4f6Bs085hY40zmqScxDUyCu7zXkukDbW02iYOLTTI,14805
18
- fal/auth/__init__.py,sha256=r8iA2-5ih7-Fik3gEC4HEWNFbGoxpYnXpZu1icPIoS0,3561
19
+ fal/auth/__init__.py,sha256=MXwS5zyY1SYJWEkc6s39et73Dkg3cDJg1ZwxRhXNj4c,4704
19
20
  fal/auth/auth0.py,sha256=rSG1mgH-QGyKfzd7XyAaj1AYsWt-ho8Y_LZ-FUVWzh4,5421
20
21
  fal/auth/local.py,sha256=sndkM6vKpeVny6NHTacVlTbiIFqaksOmw0Viqs_RN1U,1790
21
22
  fal/cli/__init__.py,sha256=padK4o0BFqq61kxAA1qQ0jYr2SuhA2mf90B3AaRkmJA,37
@@ -47,8 +48,9 @@ fal/logging/user.py,sha256=0Xvb8n6tSb9l_V51VDzv6SOdYEFNouV_6nF_W9e7uNQ,642
47
48
  fal/toolkit/__init__.py,sha256=sV95wiUzKoiDqF9vDgq4q-BLa2sD6IpuKSqp5kdTQNE,658
48
49
  fal/toolkit/exceptions.py,sha256=elHZ7dHCJG5zlHGSBbz-ilkZe9QUvQMomJFi8Pt91LA,198
49
50
  fal/toolkit/optimize.py,sha256=p75sovF0SmRP6zxzpIaaOmqlxvXB_xEz3XPNf59EF7w,1339
51
+ fal/toolkit/types.py,sha256=kkbOsDKj1qPGb1UARTBp7yuJ5JUuyy7XQurYUBCdti8,4064
50
52
  fal/toolkit/file/__init__.py,sha256=FbNl6wD-P0aSSTUwzHt4HujBXrbC3ABmaigPQA4hRfg,70
51
- fal/toolkit/file/file.py,sha256=fJpvydwefQ5CT_3q8YYfckH_6MdSFLF-se6jNOWGGxc,9475
53
+ fal/toolkit/file/file.py,sha256=-gccCKnarTu6Nfm_0yQ0sJM9aadB5tUNvKS1PTqxiFc,9071
52
54
  fal/toolkit/file/types.py,sha256=MjZ6xAhKPv4rowLo2Vcbho0sX7AQ3lm3KFyYDcw0dL4,1845
53
55
  fal/toolkit/file/providers/fal.py,sha256=V5CZz6EKmIs2-nm_mWeN9YxUOZCKIuPsZFjkZyazrgk,22375
54
56
  fal/toolkit/file/providers/gcp.py,sha256=iQtkoYUqbmKKpC5srVOYtrruZ3reGRm5lz4kM8bshgk,2247
@@ -128,8 +130,8 @@ openapi_fal_rest/models/workflow_node_type.py,sha256=-FzyeY2bxcNmizKbJI8joG7byRi
128
130
  openapi_fal_rest/models/workflow_schema.py,sha256=4K5gsv9u9pxx2ItkffoyHeNjBBYf6ur5bN4m_zePZNY,2019
129
131
  openapi_fal_rest/models/workflow_schema_input.py,sha256=2OkOXWHTNsCXHWS6EGDFzcJKkW5FIap-2gfO233EvZQ,1191
130
132
  openapi_fal_rest/models/workflow_schema_output.py,sha256=EblwSPAGfWfYVWw_WSSaBzQVju296is9o28rMBAd0mc,1196
131
- fal-1.6.2.dist-info/METADATA,sha256=ixrq8bQ0tK3u3xaRfraT_cR47A5h_2W1fDA7ZbBxNYU,3996
132
- fal-1.6.2.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
133
- fal-1.6.2.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
134
- fal-1.6.2.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
135
- fal-1.6.2.dist-info/RECORD,,
133
+ fal-1.7.1.dist-info/METADATA,sha256=kI7x5gDlwiymcO6n037_a22y8olxwqg7yrwLu4gt-kM,3996
134
+ fal-1.7.1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
135
+ fal-1.7.1.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
136
+ fal-1.7.1.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
137
+ fal-1.7.1.dist-info/RECORD,,
File without changes