fal 0.15.0__py3-none-any.whl → 0.15.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/__init__.py CHANGED
@@ -1,8 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from fal import apps # noqa: F401
4
- from fal.api import FalServerlessHost, LocalHost, cached
5
- from fal.api import function
4
+ from fal.api import FalServerlessHost, LocalHost, cached, function
6
5
  from fal.api import function as isolated # noqa: F401
7
6
  from fal.app import App, endpoint, realtime, wrap_app # noqa: F401
8
7
  from fal.sdk import FalServerlessKeyCredentials
@@ -24,14 +23,3 @@ __all__ = [
24
23
  "FalServerlessKeyCredentials",
25
24
  "sync_dir",
26
25
  ]
27
-
28
-
29
- # NOTE: This makes `import fal.dbt` import the `dbt-fal` module and `import fal` import the `fal` module
30
- # NOTE: taken from dbt-core: https://github.com/dbt-labs/dbt-core/blob/ac539fd5cf325cfb5315339077d03399d575f570/core/dbt/adapters/__init__.py#L1-L7
31
- # N.B.
32
- # This will add to the package’s __path__ all subdirectories of directories on sys.path named after the package which effectively combines both modules into a single namespace (dbt.adapters)
33
- # The matching statement is in plugins/postgres/dbt/adapters/__init__.py
34
-
35
- from pkgutil import extend_path # noqa: E402
36
-
37
- __path__ = extend_path(__path__, __name__)
fal/_serialization.py CHANGED
@@ -1,8 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import pickle
3
4
  from typing import Any, Callable
4
5
 
5
- import pickle
6
6
  import cloudpickle
7
7
 
8
8
 
@@ -98,11 +98,12 @@ def _patch_pydantic_field_serialization() -> None:
98
98
 
99
99
 
100
100
  def _patch_pydantic_model_serialization() -> None:
101
- # If user has created new pydantic models in his namespace, we will try to pickle those
102
- # by value, which means recreating class skeleton, which will stumble upon
103
- # __pydantic_parent_namespace__ in its __dict__ and it may contain modules that happened
104
- # to be imported in the namespace but are not actually used, resulting in pickling errors.
105
- # Unfortunately this also means that `model_rebuid()` might not work.
101
+ # If user has created new pydantic models in his namespace, we will try to pickle
102
+ # those by value, which means recreating class skeleton, which will stumble upon
103
+ # __pydantic_parent_namespace__ in its __dict__ and it may contain modules that
104
+ # happened to be imported in the namespace but are not actually used, resulting
105
+ # in pickling errors. Unfortunately this also means that `model_rebuid()` might
106
+ # not work.
106
107
  try:
107
108
  import pydantic
108
109
  except ImportError:
@@ -133,7 +134,8 @@ def _patch_lru_cache() -> None:
133
134
  # https://github.com/cloudpipe/cloudpickle/issues/178
134
135
  # https://github.com/uqfoundation/dill/blob/70f569b0dd268d2b1e85c0f300951b11f53c5d53/dill/_dill.py#L1429
135
136
 
136
- from functools import lru_cache, _lru_cache_wrapper as LRUCacheType
137
+ from functools import _lru_cache_wrapper as LRUCacheType
138
+ from functools import lru_cache
137
139
 
138
140
  def create_lru_cache(func: Callable, kwargs: dict) -> LRUCacheType:
139
141
  return lru_cache(**kwargs)(func)
@@ -155,8 +157,8 @@ def _patch_lru_cache() -> None:
155
157
 
156
158
  def _patch_lock() -> None:
157
159
  # https://github.com/uqfoundation/dill/blob/70f569b0dd268d2b1e85c0f300951b11f53c5d53/dill/_dill.py#L1310
158
- from threading import Lock
159
160
  from _thread import LockType
161
+ from threading import Lock
160
162
 
161
163
  def create_lock(locked: bool) -> Lock:
162
164
  lock = Lock()
@@ -199,7 +201,11 @@ def _patch_console_thread_locals() -> None:
199
201
  return ConsoleThreadLocals(**kwargs)
200
202
 
201
203
  def pickle_locals(obj: ConsoleThreadLocals) -> tuple[Callable, tuple]:
202
- kwargs = {"theme_stack": obj.theme_stack, "buffer": obj.buffer, "buffer_index": obj.buffer_index}
204
+ kwargs = {
205
+ "theme_stack": obj.theme_stack,
206
+ "buffer": obj.buffer,
207
+ "buffer_index": obj.buffer_index,
208
+ }
203
209
  return create_locals, (kwargs, )
204
210
 
205
211
  _register(ConsoleThreadLocals, pickle_locals)
fal/api.py CHANGED
@@ -40,8 +40,8 @@ from pydantic import __version__ as pydantic_version
40
40
  from typing_extensions import Concatenate, ParamSpec
41
41
 
42
42
  import fal.flags as flags
43
- from fal.exceptions import FalServerlessException
44
43
  from fal._serialization import include_modules_from, patch_pickle
44
+ from fal.exceptions import FalServerlessException
45
45
  from fal.logging.isolate import IsolateLogPrinter
46
46
  from fal.sdk import (
47
47
  FAL_SERVERLESS_DEFAULT_KEEP_ALIVE,
@@ -57,7 +57,7 @@ from fal.sdk import (
57
57
  )
58
58
 
59
59
  ArgsT = ParamSpec("ArgsT")
60
- ReturnT = TypeVar("ReturnT", covariant=True)
60
+ ReturnT = TypeVar("ReturnT", covariant=True) # noqa: PLC0105
61
61
 
62
62
  BasicConfig = Dict[str, Any]
63
63
  _UNSET = object()
@@ -113,8 +113,8 @@ class Host(Generic[ArgsT, ReturnT]):
113
113
  environment options."""
114
114
 
115
115
  options = Options()
116
- for key, value in config.items():
117
- key, value = cls.parse_key(key, value)
116
+ for item in config.items():
117
+ key, value = cls.parse_key(*item)
118
118
  if key in cls._SUPPORTED_KEYS:
119
119
  options.host[key] = value
120
120
  elif key in cls._GATEWAY_KEYS:
@@ -545,7 +545,8 @@ _DEFAULT_HOST = FalServerlessHost()
545
545
  _SERVE_PORT = 8080
546
546
 
547
547
  # Overload @function to help users identify the correct signature.
548
- # NOTE: This is both in sync with host options and with environment configs from `isolate` package.
548
+ # NOTE: This is both in sync with host options and with environment configs from
549
+ # `isolate` package.
549
550
 
550
551
 
551
552
  ## virtualenv
@@ -785,7 +786,8 @@ class FalFastAPI(FastAPI):
785
786
  """
786
787
  Add x-fal-order-* keys to the OpenAPI specification to help the rendering of UI.
787
788
 
788
- NOTE: We rely on the fact that fastapi and Python dicts keep the order of properties.
789
+ NOTE: We rely on the fact that fastapi and Python dicts keep the order of
790
+ properties.
789
791
  """
790
792
 
791
793
  def mark_order(obj: dict[str, Any], key: str):
@@ -918,7 +920,9 @@ class BaseServable:
918
920
  asyncio.create_task(metrics_server.serve()): metrics_server,
919
921
  }
920
922
 
921
- _, pending = await asyncio.wait(tasks.keys(), return_when=asyncio.FIRST_COMPLETED)
923
+ _, pending = await asyncio.wait(
924
+ tasks.keys(), return_when=asyncio.FIRST_COMPLETED,
925
+ )
922
926
  if not pending:
923
927
  return
924
928
 
@@ -1007,13 +1011,15 @@ class IsolatedFunction(Generic[ArgsT, ReturnT]):
1007
1011
  lines = []
1008
1012
  for used_modules, references in pairs:
1009
1013
  lines.append(
1010
- f"\t- {used_modules!r} (accessed through {', '.join(map(repr, references))})"
1014
+ f"\t- {used_modules!r} "
1015
+ f"(accessed through {', '.join(map(repr, references))})"
1011
1016
  )
1012
1017
 
1013
1018
  function_name = self.func.__name__
1014
1019
  raise FalServerlessError(
1015
- f"Couldn't deserialize your function on the remote server. \n\n[Hint] {function_name!r} "
1016
- f"function uses the following modules which weren't present in the environment definition:\n"
1020
+ f"Couldn't deserialize your function on the remote server. \n\n"
1021
+ f"[Hint] {function_name!r} function uses the following modules "
1022
+ "which weren't present in the environment definition:\n"
1017
1023
  + "\n".join(lines)
1018
1024
  ) from None
1019
1025
  except Exception as exc:
@@ -1065,7 +1071,8 @@ class IsolatedFunction(Generic[ArgsT, ReturnT]):
1065
1071
  def func(self) -> Callable[ArgsT, ReturnT]:
1066
1072
  serve_mode = self.options.gateway.get("serve")
1067
1073
  if serve_mode:
1068
- # This type can be safely ignored because this case only happens when it is a ServedIsolatedFunction
1074
+ # This type can be safely ignored because this case only happens when it
1075
+ # is a ServedIsolatedFunction
1069
1076
  serve_func: Callable[[], None] = ServeWrapper(self.raw_func)
1070
1077
  return serve_func # type: ignore
1071
1078
  else:
@@ -1098,8 +1105,10 @@ class ServedIsolatedFunction(
1098
1105
 
1099
1106
  class Server(uvicorn.Server):
1100
1107
  """Server is a uvicorn.Server that actually plays nicely with signals.
1101
- By default, uvicorn's Server class overwrites the signal handler for SIGINT, swallowing the signal and preventing other tasks from cancelling.
1102
- This class allows the task to be gracefully cancelled using asyncio's built-in task cancellation or with an event, like aiohttp.
1108
+ By default, uvicorn's Server class overwrites the signal handler for SIGINT,
1109
+ swallowing the signal and preventing other tasks from cancelling.
1110
+ This class allows the task to be gracefully cancelled using asyncio's built-in task
1111
+ cancellation or with an event, like aiohttp.
1103
1112
  """
1104
1113
 
1105
1114
  def install_signal_handlers(self) -> None:
fal/app.py CHANGED
@@ -10,9 +10,11 @@ from typing import Any, Callable, ClassVar, TypeVar
10
10
  from fastapi import FastAPI
11
11
 
12
12
  import fal.api
13
+ from fal._serialization import include_modules_from
13
14
  from fal.api import RouteSignature
14
15
  from fal.logging import get_logger
15
- from fal._serialization import include_modules_from
16
+ from fal.toolkit.file.providers import fal as fal_provider_module
17
+
16
18
  REALTIME_APP_REQUIREMENTS = ["websockets", "msgpack"]
17
19
 
18
20
  EndpointT = TypeVar("EndpointT", bound=Callable[..., Any])
@@ -123,6 +125,22 @@ class App(fal.api.BaseServable):
123
125
  )
124
126
  return response
125
127
 
128
+ @app.middleware("http")
129
+ async def set_global_object_preference(request, call_next):
130
+ response = await call_next(request)
131
+ try:
132
+ fal_provider_module.GLOBAL_LIFECYCLE_PREFERENCE = request.headers.get(
133
+ "X-Fal-Object-Lifecycle-Preference"
134
+ )
135
+ except Exception:
136
+ from fastapi.logger import logger
137
+
138
+ logger.exception(
139
+ "Failed set a global lifecycle preference %s",
140
+ self.__class__.__name__,
141
+ )
142
+ return response
143
+
126
144
  def provide_hints(self) -> list[str]:
127
145
  """Provide hints for routing the application."""
128
146
  raise NotImplementedError
@@ -237,14 +255,16 @@ def _fal_websocket_template(
237
255
  output = output.dict()
238
256
  else:
239
257
  raise TypeError(
240
- f"Expected a dict or pydantic model as output, got {type(output)}"
258
+ "Expected a dict or pydantic model as output, got "
259
+ f"{type(output)}"
241
260
  )
242
261
 
243
262
  messages = [
244
263
  msgpack.packb(output, use_bin_type=True),
245
264
  ]
246
265
  if route_signature.emit_timings:
247
- # We emit x-fal messages in JSON, no matter what the input/output format is.
266
+ # We emit x-fal messages in JSON, no matter what the
267
+ # input/output format is.
248
268
  timings = {
249
269
  "type": "x-fal-message",
250
270
  "action": "timings",
@@ -354,7 +374,8 @@ def realtime(
354
374
 
355
375
  if hasattr(original_func, "route_signature"):
356
376
  raise ValueError(
357
- f"Can't set multiple routes for the same function: {original_func.__name__}"
377
+ "Can't set multiple routes for the same function: "
378
+ f"{original_func.__name__}"
358
379
  )
359
380
 
360
381
  if input_modal is _SENTINEL:
fal/auth/__init__.py CHANGED
@@ -51,7 +51,8 @@ def _fetch_access_token() -> str:
51
51
  Load the refresh token, request a new access_token (refreshing the refresh token)
52
52
  and return the access_token.
53
53
  """
54
- # We need to lock both read and write access because we could be reading a soon invalid refresh_token
54
+ # We need to lock both read and write access because we could be reading a soon
55
+ # invalid refresh_token
55
56
  with local.lock_token():
56
57
  refresh_token, access_token = local.load_token()
57
58
 
fal/auth/auth0.py CHANGED
@@ -30,7 +30,8 @@ def _open_browser(url: str, code: str | None) -> None:
30
30
  maybe_open_browser_tab(url)
31
31
 
32
32
  console.print(
33
- "If browser didn't open automatically, on your computer or mobile device navigate to"
33
+ "If browser didn't open automatically, "
34
+ "on your computer or mobile device navigate to"
34
35
  )
35
36
  console.print(url)
36
37
 
@@ -155,7 +156,8 @@ def build_jwk_client():
155
156
 
156
157
  def validate_id_token(token: str):
157
158
  """
158
- id_token is intended for the client (this sdk) only. Never send one to another service.
159
+ id_token is intended for the client (this sdk) only.
160
+ Never send one to another service.
159
161
  """
160
162
  from jwt import decode
161
163
 
fal/auth/local.py CHANGED
@@ -62,7 +62,8 @@ def delete_token() -> None:
62
62
  @contextmanager
63
63
  def lock_token():
64
64
  """
65
- Lock the access to the token file to avoid race conditions when running multiple scripts at the same time.
65
+ Lock the access to the token file to avoid race conditions when running multiple
66
+ scripts at the same time.
66
67
  """
67
68
  lock_file = _check_dir_exist() / _LOCK_FILE
68
69
  with portalocker.utils.TemporaryFileLock(
fal/cli.py CHANGED
@@ -89,12 +89,13 @@ class MainGroup(RichGroup):
89
89
  except Exception as exception:
90
90
  logger.error(exception)
91
91
  if state.debug:
92
- # Here we supress detailed errors on click lines because
93
- # they're mostly decorator calls, irrelevant to the dev's error tracing
92
+ # Here we supress detailed errors on click lines because they're
93
+ # mostly decorator calls, irrelevant to the dev's error tracing
94
94
  console.print_exception(suppress=[click])
95
95
  console.print()
96
96
  console.print(
97
- f"The [markdown.code]invocation_id[/] for this operation is: [white]{state.invocation_id}[/]"
97
+ "The [markdown.code]invocation_id[/] for this operation is: "
98
+ f"[white]{state.invocation_id}[/]"
98
99
  )
99
100
  else:
100
101
  self._exception_handler.handle(exception)
@@ -207,7 +208,8 @@ def key_generate(client: sdk.FalServerlessClient, scope: str, alias: str | None)
207
208
  print(
208
209
  f"Generated key id and key secret, with the scope `{scope}`.\n"
209
210
  "This is the only time the secret will be visible.\n"
210
- "You will need to generate a new key pair if you lose access to this secret."
211
+ "You will need to generate a new key pair if you lose access to this "
212
+ "secret."
211
213
  )
212
214
  print(f"FAL_KEY='{result[1]}:{result[0]}'")
213
215
 
@@ -306,7 +308,8 @@ def register_application(
306
308
  gateway_options = isolated_function.options.gateway
307
309
  if "serve" not in gateway_options and "exposed_port" not in gateway_options:
308
310
  raise api.FalServerlessError(
309
- "One of `serve` or `exposed_port` options needs to be specified in the isolated annotation to register a function"
311
+ "One of `serve` or `exposed_port` options needs to be specified "
312
+ "in the isolated annotation to register a function"
310
313
  )
311
314
  elif (
312
315
  "exposed_port" in gateway_options
@@ -13,7 +13,8 @@ class ApplicationExceptionHandler:
13
13
 
14
14
  This exception handler is capable of handling, i.e. customize the output
15
15
  and add behavior, of any type of exception. Click handles all `ClickException`
16
- types by default, but prints the stack for other exception not wrapped in ClickException.
16
+ types by default, but prints the stack for other exception not wrapped in
17
+ ClickException.
17
18
 
18
19
  The handler also allows for central metrics and logging collection.
19
20
  """
fal/flags.py CHANGED
@@ -18,8 +18,6 @@ GRPC_HOST = os.getenv("FAL_HOST", "api.alpha.fal.ai")
18
18
  if not TEST_MODE:
19
19
  assert GRPC_HOST.startswith("api"), "FAL_HOST must start with 'api'"
20
20
 
21
- GATEWAY_HOST = GRPC_HOST.replace("api", "gateway", 1)
22
-
23
21
  REST_HOST = GRPC_HOST.replace("api", "rest", 1)
24
22
  REST_SCHEME = "http" if TEST_MODE or AUTH_DISABLED else "https"
25
23
  REST_URL = f"{REST_SCHEME}://{REST_HOST}"
fal/logging/isolate.py CHANGED
@@ -34,10 +34,10 @@ class IsolateLogPrinter:
34
34
  timestamp = log.timestamp
35
35
  else:
36
36
  # Default value for timestamp if user has old `isolate` version.
37
- # Even if the controller version is controller by us, which means that the timestamp
38
- # is being sent in the gRPC message.
39
- # The `isolate` version users interpret that message with is out of our control.
40
- # So we need to handle this case.
37
+ # Even if the controller version is controller by us, which means that
38
+ # the timestamp is being sent in the gRPC message.
39
+ # The `isolate` version users interpret that message with is out of our
40
+ # control. So we need to handle this case.
41
41
  timestamp = datetime.now(timezone.utc)
42
42
 
43
43
  event: EventDict = {
fal/sdk.py CHANGED
@@ -8,17 +8,17 @@ from enum import Enum
8
8
  from typing import Any, Callable, Generic, Iterator, Literal, TypeVar
9
9
 
10
10
  import grpc
11
+ import isolate_proto
11
12
  from isolate.connections.common import is_agent
12
13
  from isolate.logs import Log
13
14
  from isolate.server.interface import from_grpc, to_serialized_object, to_struct
15
+ from isolate_proto.configuration import GRPC_OPTIONS
14
16
 
15
- import isolate_proto
16
17
  from fal import flags
17
18
  from fal._serialization import patch_pickle
18
19
  from fal.auth import USER, key_credentials
19
20
  from fal.logging import get_logger
20
21
  from fal.logging.trace import TraceContextInterceptor
21
- from isolate_proto.configuration import GRPC_OPTIONS
22
22
 
23
23
  ResultT = TypeVar("ResultT")
24
24
  InputT = TypeVar("InputT")
@@ -187,6 +187,16 @@ class HostedRunStatus:
187
187
  state: HostedRunState
188
188
 
189
189
 
190
+ @dataclass
191
+ class ApplicationInfo:
192
+ application_id: str
193
+ keep_alive: int
194
+ max_concurrency: int
195
+ max_multiplexing: int
196
+ active_runners: int
197
+ min_concurrency: int
198
+
199
+
190
200
  @dataclass
191
201
  class AliasInfo:
192
202
  alias: str
@@ -263,6 +273,20 @@ class KeyScope(enum.Enum):
263
273
  raise ValueError(f"Unknown KeyScope: {proto}")
264
274
 
265
275
 
276
+ @from_grpc.register(isolate_proto.ApplicationInfo)
277
+ def _from_grpc_application_info(
278
+ message: isolate_proto.ApplicationInfo
279
+ ) -> ApplicationInfo:
280
+ return ApplicationInfo(
281
+ application_id=message.application_id,
282
+ keep_alive=message.keep_alive,
283
+ max_concurrency=message.max_concurrency,
284
+ max_multiplexing=message.max_multiplexing,
285
+ active_runners=message.active_runners,
286
+ min_concurrency=message.min_concurrency,
287
+ )
288
+
289
+
266
290
  @from_grpc.register(isolate_proto.AliasInfo)
267
291
  def _from_grpc_alias_info(message: isolate_proto.AliasInfo) -> AliasInfo:
268
292
  if message.auth_mode is isolate_proto.ApplicationAuthMode.PUBLIC:
@@ -496,6 +520,18 @@ class FalServerlessConnection:
496
520
  )
497
521
  return from_grpc(res.alias_info)
498
522
 
523
+ def list_applications(self) -> list[ApplicationInfo]:
524
+ request = isolate_proto.ListApplicationsRequest()
525
+ res: isolate_proto.ListApplicationsResult = self.stub.ListApplications(request)
526
+ return [from_grpc(app) for app in res.applications]
527
+
528
+ def delete_application(
529
+ self,
530
+ application_id: str,
531
+ ) -> None:
532
+ request = isolate_proto.DeleteApplicationRequest(application_id=application_id)
533
+ self.stub.DeleteApplication(request)
534
+
499
535
  def run(
500
536
  self,
501
537
  function: Callable[..., ResultT],
fal/sync.py CHANGED
@@ -31,7 +31,8 @@ def _upload_file(source_path: str, target_path: str, unzip: bool = False):
31
31
  body = upload_file_model.BodyUploadLocalFile(
32
32
  rest_types.File(
33
33
  payload=file_to_upload,
34
- # We need to set a file_name, otherwise the server errors processing the file
34
+ # We need to set a file_name, otherwise the server errors
35
+ # processing the file
35
36
  file_name=os.path.basename(source_path),
36
37
  )
37
38
  )
@@ -45,7 +46,9 @@ def _upload_file(source_path: str, target_path: str, unzip: bool = False):
45
46
 
46
47
  if response.status_code != 200:
47
48
  raise Exception(
48
- f"Failed to upload file. Server returned status code {response.status_code} and message {response.parsed}"
49
+ "Failed to upload file. "
50
+ "Server returned status code "
51
+ f"{response.status_code} and message {response.parsed}"
49
52
  )
50
53
 
51
54
 
@@ -94,7 +97,8 @@ def sync_dir(local_dir: str | Path, remote_dir: str, force_upload=False) -> str:
94
97
  local_dir_abs = os.path.expanduser(local_dir)
95
98
  if not os.path.isabs(remote_dir) or not remote_dir.startswith("/data"):
96
99
  raise ValueError(
97
- "'remote_dir' must be an absolute path starting with `/data`, e.g. '/data/sync/my_dir'"
100
+ "'remote_dir' must be an absolute path starting with `/data`, "
101
+ "e.g. '/data/sync/my_dir'"
98
102
  )
99
103
 
100
104
  remote_dir = remote_dir.replace("/data/", "", 1)
fal/toolkit/file/file.py CHANGED
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import shutil
4
4
  from pathlib import Path
5
5
  from tempfile import NamedTemporaryFile, mkdtemp
6
- from typing import Callable, Optional, Any
6
+ from typing import Any, Callable, Optional
7
7
  from urllib.parse import urlparse
8
8
  from zipfile import ZipFile
9
9
 
@@ -13,8 +13,8 @@ import pydantic
13
13
  if not hasattr(pydantic, "__version__") or pydantic.__version__.startswith("1."):
14
14
  IS_PYDANTIC_V2 = False
15
15
  else:
16
- from pydantic_core import core_schema, CoreSchema
17
16
  from pydantic import GetCoreSchemaHandler
17
+ from pydantic_core import CoreSchema, core_schema
18
18
  IS_PYDANTIC_V2 = True
19
19
 
20
20
  from pydantic import BaseModel, Field
@@ -57,22 +57,28 @@ class File(BaseModel):
57
57
  description="The URL where the file can be downloaded from.",
58
58
  )
59
59
  content_type: Optional[str] = Field(
60
- None, description="The mime type of the file.",
60
+ None,
61
+ description="The mime type of the file.",
61
62
  examples=["image/png"],
62
63
  )
63
64
  file_name: Optional[str] = Field(
64
- None, description="The name of the file. It will be auto-generated if not provided.",
65
+ None,
66
+ description="The name of the file. It will be auto-generated if not provided.",
65
67
  examples=["z9RV14K95DvU.png"],
66
68
  )
67
69
  file_size: Optional[int] = Field(
68
70
  None, description="The size of the file in bytes.", examples=[4404019]
69
71
  )
70
72
  file_data: Optional[bytes] = Field(
71
- None, description="File data", exclude=True, repr=False,
73
+ None,
74
+ description="File data",
75
+ exclude=True,
76
+ repr=False,
72
77
  )
73
78
 
74
79
  # Pydantic custom validator for input type conversion
75
80
  if IS_PYDANTIC_V2:
81
+
76
82
  @classmethod
77
83
  def __get_pydantic_core_schema__(
78
84
  cls, source_type: Any, handler: GetCoreSchemaHandler
@@ -81,10 +87,12 @@ class File(BaseModel):
81
87
  cls.__convert_from_str,
82
88
  handler(source_type),
83
89
  )
90
+
84
91
  else:
92
+
85
93
  @classmethod
86
94
  def __get_validators__(cls):
87
- yield cls.__convert_from_str
95
+ yield cls.__convert_from_str
88
96
 
89
97
  @classmethod
90
98
  def __convert_from_str(cls, value: Any):
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import dataclasses
3
4
  import json
4
5
  import os
5
6
  from base64 import b64encode
@@ -14,6 +15,14 @@ from fal.toolkit.file.types import FileData, FileRepository
14
15
  _FAL_CDN = "https://fal.media"
15
16
 
16
17
 
18
+ @dataclass
19
+ class ObjectLifecyclePreference:
20
+ expriation_duration_seconds: int
21
+
22
+
23
+ GLOBAL_LIFECYCLE_PREFERENCE = ObjectLifecyclePreference(expriation_duration_seconds=2)
24
+
25
+
17
26
  @dataclass
18
27
  class FalFileRepository(FileRepository):
19
28
  def save(self, file: FileData) -> str:
@@ -70,19 +79,27 @@ class FalFileRepository(FileRepository):
70
79
 
71
80
  @dataclass
72
81
  class InMemoryRepository(FileRepository):
73
- def save(self, file: FileData) -> str:
82
+ def save(
83
+ self,
84
+ file: FileData,
85
+ ) -> str:
74
86
  return f'data:{file.content_type};base64,{b64encode(file.data).decode("utf-8")}'
75
87
 
76
88
 
77
89
  @dataclass
78
90
  class FalCDNFileRepository(FileRepository):
79
- def save(self, file: FileData) -> str:
91
+ def save(
92
+ self,
93
+ file: FileData,
94
+ ) -> str:
80
95
  headers = {
81
96
  **self.auth_headers,
82
97
  "Accept": "application/json",
83
98
  "Content-Type": file.content_type,
99
+ "X-Fal-Object-Lifecycle-Preference": json.dumps(
100
+ dataclasses.asdict(GLOBAL_LIFECYCLE_PREFERENCE)
101
+ ),
84
102
  }
85
-
86
103
  url = os.getenv("FAL_CDN_HOST", _FAL_CDN) + "/files/upload"
87
104
  request = Request(url, headers=headers, method="POST", data=file.data)
88
105
  try:
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import io
4
4
  from tempfile import NamedTemporaryFile
5
- from typing import TYPE_CHECKING, Literal, Union, Optional
5
+ from typing import TYPE_CHECKING, Literal, Optional, Union
6
6
 
7
7
  from pydantic import BaseModel, Field
8
8
 
fal/toolkit/optimize.py CHANGED
@@ -4,7 +4,6 @@ import os
4
4
  import traceback
5
5
  from typing import TYPE_CHECKING, Any
6
6
 
7
-
8
7
  if TYPE_CHECKING:
9
8
  import torch
10
9
 
@@ -17,7 +17,10 @@ FAL_MODEL_WEIGHTS_DIR = FAL_PERSISTENT_DIR / ".fal" / "model_weights"
17
17
 
18
18
  # TODO: how can we randomize the user agent to avoid being blocked?
19
19
  TEMP_HEADERS = {
20
- "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) Gecko/20100101 Firefox/21.0"
20
+ "User-Agent": (
21
+ "Mozilla/5.0 (Macintosh; "
22
+ "Intel Mac OS X 10.8; rv:21.0) Gecko/20100101 Firefox/21.0"
23
+ ),
21
24
  }
22
25
 
23
26
 
@@ -94,8 +97,8 @@ def _file_content_length_matches(url: str, file_path: Path) -> bool:
94
97
  file_path: The local path to the file being compared.
95
98
 
96
99
  Returns:
97
- bool:`True` if the local file's content length matches the remote file's content length,
98
- `False` otherwise.
100
+ bool: `True` if the local file's content length matches the remote file's
101
+ content length, `False` otherwise.
99
102
  """
100
103
  local_file_content_length = file_path.stat().st_size
101
104
  remote_file_content_length = _get_remote_file_properties(url)[1]
fal/workflows.py CHANGED
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import graphlib
4
3
  import json
5
4
  import webbrowser
6
5
  from argparse import ArgumentParser
@@ -8,6 +7,7 @@ from collections import Counter
8
7
  from dataclasses import dataclass, field
9
8
  from typing import Any, Iterator, Union, cast
10
9
 
10
+ import graphlib
11
11
  import rich
12
12
  from openapi_fal_rest.api.workflows import (
13
13
  create_or_update_workflow_workflows_post as publish_workflow,
@@ -450,7 +450,12 @@ def main() -> None:
450
450
  for event in handle.iter_events(logs=True):
451
451
  if isinstance(event, fal.apps.Queued):
452
452
  status.update(
453
- status=f"Queued for {node_id!r} (position={event.position}) ({n}/{len(workflow.nodes)})",
453
+ status=(
454
+ "Queued for "
455
+ f"{node_id!r} "
456
+ f"(position={event.position}) "
457
+ f"({n}/{len(workflow.nodes)})",
458
+ ),
454
459
  spinner="dots",
455
460
  )
456
461
  elif isinstance(event, fal.apps.InProgress):
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fal
3
- Version: 0.15.0
3
+ Version: 0.15.2
4
4
  Summary: fal is an easy-to-use Serverless Python Framework
5
5
  Author: Features & Labels <hello@fal.ai>
6
6
  Requires-Python: >=3.8
7
7
  Description-Content-Type: text/markdown
8
8
  Requires-Dist: isolate[build] <1.0,>=0.12.3
9
- Requires-Dist: isolate-proto ==0.3.4
9
+ Requires-Dist: isolate-proto ==0.4.0
10
10
  Requires-Dist: grpcio <2,>=1.50.0
11
11
  Requires-Dist: dill ==0.3.7
12
12
  Requires-Dist: cloudpickle ==3.0.0
@@ -39,9 +39,10 @@ Provides-Extra: dev
39
39
  Requires-Dist: fal[test] ; extra == 'dev'
40
40
  Requires-Dist: openapi-python-client <1,>=0.14.1 ; extra == 'dev'
41
41
  Provides-Extra: test
42
- Requires-Dist: pytest ; extra == 'test'
43
- Requires-Dist: pytest-xdist ; extra == 'test'
42
+ Requires-Dist: pytest <8 ; extra == 'test'
44
43
  Requires-Dist: pytest-asyncio ; extra == 'test'
44
+ Requires-Dist: pytest-xdist ; extra == 'test'
45
+ Requires-Dist: flaky ; extra == 'test'
45
46
 
46
47
  [![PyPI](https://img.shields.io/pypi/v/fal.svg?logo=PyPI)](https://pypi.org/project/fal)
47
48
  [![Tests](https://img.shields.io/github/actions/workflow/status/fal-ai/fal/integration_tests.yaml?label=Tests)](https://github.com/fal-ai/fal/actions)
@@ -89,3 +90,30 @@ A new virtual environment will be created by fal in the cloud and the set of req
89
90
  ## Next steps
90
91
 
91
92
  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.
93
+
94
+ ## Contributing
95
+
96
+ ### Installing in editable mode with dev dependencies
97
+
98
+ ```py
99
+ pip install -e 'projects/fal[dev]'
100
+ pip install -e 'projects/fal_client[dev]'
101
+ pip install -e 'projects/isolate_proto[dev]'
102
+ ```
103
+
104
+ ### Running tests
105
+
106
+ ```py
107
+ pytest
108
+ ```
109
+
110
+ ### Pre-commit
111
+
112
+ ```
113
+ cd projects/fal
114
+ pre-commit install
115
+ ```
116
+
117
+ ### Commit format
118
+
119
+ Please follow [conventional commits specification](https://www.conventionalcommits.org/) for descriptions/messages.
@@ -1,44 +1,44 @@
1
- fal/__init__.py,sha256=XOobhu3D94O9K9ka_AJh6sw3K7lV-s_NuSto_fx9TXg,1255
1
+ fal/__init__.py,sha256=unz75H_W1OpXsdkjHqjJs_GrA_BzwHaXImRH-pekaL0,617
2
2
  fal/__main__.py,sha256=8hDtWlaFZK24KhfNq_ZKgtXqYHsDQDetukOCMlsbW0Q,59
3
- fal/_serialization.py,sha256=d7cyacqQwwmSJJOh7WMGdv4jBzpLQJRsXxJc-GfhWN4,7517
4
- fal/api.py,sha256=7LChw4fl1ClJa7qDcL2V7wwb_Eiw8c3QwPZhtt9PBqg,35975
5
- fal/app.py,sha256=GCHes5uFQIH4cpK3YiiRSrsWSSkzWxlYh2T-nKra7Z8,12398
3
+ fal/_serialization.py,sha256=Tx_c_mpJ8dYAVmPwdLkwgozSqfdvdFyWRYx3lH3-koQ,7595
4
+ fal/api.py,sha256=Nqs8qhYHRpCit1DiXqfFCacSnpUnI3__bssqcx5eVPc,36095
5
+ fal/app.py,sha256=bo8NbJTCjbIoeMVyW5YBxGljzDAKAqOjOJwP5erT2GM,13129
6
6
  fal/apps.py,sha256=UhR6mq8jBiTAp-QvUnvbnMNcuJ5wHIKSqdlfyx8aBQ8,6829
7
- fal/cli.py,sha256=STeefdtKG9RSeVXOKWtWtxNZTgvZX7Z3s80br8HZyK8,18374
8
- fal/flags.py,sha256=AATQO65M4C87dGp0j7o6cSQWcr62xE-8DnJYsUjFFbw,942
7
+ fal/cli.py,sha256=2sPCP4G_y3aUX0SKVSOTkTaLdYJpcm9B6FYDqIVhx-8,18431
8
+ fal/flags.py,sha256=aWzOn3Ynl1s-eACerj1ZnRwlj3CvaGu0wIFcp7YXqX4,887
9
9
  fal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  fal/rest_client.py,sha256=kGBGmuyHfX1lR910EoKCYPjsyU8MdXawT_cW2q8Sajc,568
11
- fal/sdk.py,sha256=TNmxxte-oca8WnaiXwc7MIMrUKn_OeYaXtg1xC8t7eM,18793
12
- fal/sync.py,sha256=Ljet584PVFz4r888-0bwV1Kio-tTneF_85TnHvBPvJw,4277
13
- fal/workflows.py,sha256=oBOdCCnZ0qMtmuO5JVpXD4FyUWoQgLXAJRe8RtrsGDw,14510
14
- fal/auth/__init__.py,sha256=VTZfex_kV9WNozmpBDa7joNntOlz-btuu-wHqaGM9Bk,3555
15
- fal/auth/auth0.py,sha256=5y4-9udOSX2-N_zvinLCpFwl10MdaPydZX2v9GQMZEE,5406
16
- fal/auth/local.py,sha256=lZqp4j32l2xFpY8zYvLoIHHyJrNAJDcm5MxgsLpY_pw,1786
11
+ fal/sdk.py,sha256=4aQikS2xH90xZpphKljml6cG38aahK2gcVSLTBQQCBY,19925
12
+ fal/sync.py,sha256=ZuIJA2-hTPNANG9B_NNJZUsO68EIdTH0dc9MzeVE2VU,4340
13
+ fal/workflows.py,sha256=4rjqL4xB6GHLJsqTplJmAvpd6uHZJ28sc8su33BFXEo,14682
14
+ fal/auth/__init__.py,sha256=r8iA2-5ih7-Fik3gEC4HEWNFbGoxpYnXpZu1icPIoS0,3561
15
+ fal/auth/auth0.py,sha256=rSG1mgH-QGyKfzd7XyAaj1AYsWt-ho8Y_LZ-FUVWzh4,5421
16
+ fal/auth/local.py,sha256=sndkM6vKpeVny6NHTacVlTbiIFqaksOmw0Viqs_RN1U,1790
17
17
  fal/console/__init__.py,sha256=ernZ4bzvvliQh5SmrEqQ7lA5eVcbw6Ra2jalKtA7dxg,132
18
18
  fal/console/icons.py,sha256=De9MfFaSkO2Lqfne13n3PrYfTXJVIzYZVqYn5BWsdrA,108
19
19
  fal/console/ux.py,sha256=KMQs3UHQvVHDxDQQqlot-WskVKoMQXOE3jiVkkfmIMY,356
20
- fal/exceptions/__init__.py,sha256=MM9N4uG3_6UkAGw4SLDo-Cp35YawNc1kAIGMl1BFTeI,992
20
+ fal/exceptions/__init__.py,sha256=yAPFUv-RZCFi8joRVOE1hV79bb8WFHnj1zslF_zhYxw,996
21
21
  fal/exceptions/_base.py,sha256=U3n_4OtUx5MvfT2eol_a-N0dV9_eYFMvdbrhP-b_NXg,160
22
22
  fal/exceptions/auth.py,sha256=gxRago5coI__vSIcdcsqhhq1lRPkvCnwPAueIaXTAdw,329
23
23
  fal/exceptions/handlers.py,sha256=nZCmmWU47k4P9NBISNqn0b6-L53KMoNuuGBW4G9Bweo,1674
24
24
  fal/logging/__init__.py,sha256=snqprf7-sKw6oAATS_Yxklf-a3XhLg0vIHICPwLp6TM,1583
25
- fal/logging/isolate.py,sha256=rQbM-wwsCO3ddIFyEo34GBWxDWkDzw3x0iSK79D0yKk,1742
25
+ fal/logging/isolate.py,sha256=Gj_xylXc0ulGIyozLwTWisIclP7-du4tvhJWyPilrgo,1742
26
26
  fal/logging/style.py,sha256=ckIgHzvF4DShM5kQh8F133X53z_vF46snuDHVmo_h9g,386
27
27
  fal/logging/trace.py,sha256=OhzB6d4rQZimBc18WFLqH_9BGfqFFumKKTAGSsmWRMg,1904
28
28
  fal/logging/user.py,sha256=0Xvb8n6tSb9l_V51VDzv6SOdYEFNouV_6nF_W9e7uNQ,642
29
29
  fal/toolkit/__init__.py,sha256=sV95wiUzKoiDqF9vDgq4q-BLa2sD6IpuKSqp5kdTQNE,658
30
30
  fal/toolkit/exceptions.py,sha256=elHZ7dHCJG5zlHGSBbz-ilkZe9QUvQMomJFi8Pt91LA,198
31
- fal/toolkit/optimize.py,sha256=I-scpgmRODkzBCLkcsBwxkVm9L4bGVkSSZKiKMFYU0M,1340
31
+ fal/toolkit/optimize.py,sha256=p75sovF0SmRP6zxzpIaaOmqlxvXB_xEz3XPNf59EF7w,1339
32
32
  fal/toolkit/file/__init__.py,sha256=FbNl6wD-P0aSSTUwzHt4HujBXrbC3ABmaigPQA4hRfg,70
33
- fal/toolkit/file/file.py,sha256=_Kt-jqFoUdJ_UxSC7Ll6oeVWtkvjYZBJWONp31UvK7w,6025
33
+ fal/toolkit/file/file.py,sha256=r8PzNCgv8Gkj6s1zM0yW-pcMKIouyaiEH06iBue8MwM,6066
34
34
  fal/toolkit/file/types.py,sha256=9CqDh8SmNJNzfsrvtj468uo2SprJH9rOk8KMhhfU73c,1050
35
- fal/toolkit/file/providers/fal.py,sha256=7oqzkmeNoXjRZKsSIInC1ZdX8-lmYamIZmgWtNrYpJo,3314
35
+ fal/toolkit/file/providers/fal.py,sha256=AfNwdnvt_IIFrzvNZPFjGwfVOQ2OzPfG1ySz6-2tnvc,3690
36
36
  fal/toolkit/file/providers/gcp.py,sha256=7Lg7BXoHKkFu0jkGv3_vKh2Ks6eRfDMbw31N3mvDUtk,1913
37
37
  fal/toolkit/file/providers/r2.py,sha256=YW5aJBOX41MQxfx1rA_f-IiJhAPMZ5md0cxBcg3y08I,2537
38
38
  fal/toolkit/image/__init__.py,sha256=qNLyXsBWysionUjbeWbohLqWlw3G_UpzunamkZd_JLQ,71
39
- fal/toolkit/image/image.py,sha256=7Hy0Mxb_QAJI1o-5ycYGES4ReB3iLSmxVkCIX2Y6MEs,4252
39
+ fal/toolkit/image/image.py,sha256=2q1ZCBSSdmDx9q1S4ahoCOniNaRcfSFnCS31f3b8ZZ0,4252
40
40
  fal/toolkit/utils/__init__.py,sha256=CrmM9DyCz5-SmcTzRSm5RaLgxy3kf0ZsSEN9uhnX2Xo,97
41
- fal/toolkit/utils/download_utils.py,sha256=9rYLDOztOoib7TqCXuGj05nPmcgd3Soj0AT16O_sczA,15460
41
+ fal/toolkit/utils/download_utils.py,sha256=M-xUAV8kX6o1zcojozSTaArGIC_LgQriagCC8AoG0mM,15487
42
42
  openapi_fal_rest/__init__.py,sha256=ziculmF_i6trw63LzZGFX-6W3Lwq9mCR8_UpkpvpaHI,152
43
43
  openapi_fal_rest/client.py,sha256=G6BpJg9j7-JsrAUGddYwkzeWRYickBjPdcVgXoPzxuE,2817
44
44
  openapi_fal_rest/errors.py,sha256=8mXSxdfSGzxT82srdhYbR0fHfgenxJXaUtMkaGgb6iU,470
@@ -81,8 +81,8 @@ openapi_fal_rest/models/workflow_node_type.py,sha256=-FzyeY2bxcNmizKbJI8joG7byRi
81
81
  openapi_fal_rest/models/workflow_schema.py,sha256=4K5gsv9u9pxx2ItkffoyHeNjBBYf6ur5bN4m_zePZNY,2019
82
82
  openapi_fal_rest/models/workflow_schema_input.py,sha256=2OkOXWHTNsCXHWS6EGDFzcJKkW5FIap-2gfO233EvZQ,1191
83
83
  openapi_fal_rest/models/workflow_schema_output.py,sha256=EblwSPAGfWfYVWw_WSSaBzQVju296is9o28rMBAd0mc,1196
84
- fal-0.15.0.dist-info/METADATA,sha256=GyZiaXubOMkUTFuwCBujAq0mHZPBnExycm_DTNqndzA,3258
85
- fal-0.15.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
86
- fal-0.15.0.dist-info/entry_points.txt,sha256=1GadEh1IgXO5Bb42Xo9lwNsJnm9Xjfo3qIdqwbfdV8Q,36
87
- fal-0.15.0.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
88
- fal-0.15.0.dist-info/RECORD,,
84
+ fal-0.15.2.dist-info/METADATA,sha256=SfYdtZgpSWTSE3GKU-rqZc5fAi7XGwSM1AB1ZJdqoHM,3738
85
+ fal-0.15.2.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
86
+ fal-0.15.2.dist-info/entry_points.txt,sha256=1GadEh1IgXO5Bb42Xo9lwNsJnm9Xjfo3qIdqwbfdV8Q,36
87
+ fal-0.15.2.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
88
+ fal-0.15.2.dist-info/RECORD,,
File without changes