fal 0.15.0__py3-none-any.whl → 1.0.0__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,13 +1,14 @@
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
9
8
  from fal.sync import sync_dir
10
9
 
10
+ from ._version import __version__, version_tuple # noqa: F401
11
+
11
12
  local = LocalHost()
12
13
  serverless = FalServerlessHost()
13
14
 
@@ -23,15 +24,6 @@ __all__ = [
23
24
  # "wrap_app",
24
25
  "FalServerlessKeyCredentials",
25
26
  "sync_dir",
27
+ "__version__",
28
+ "version_tuple",
26
29
  ]
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/__main__.py CHANGED
@@ -1,4 +1,4 @@
1
- from .cli import cli
1
+ from .cli import main
2
2
 
3
3
  if __name__ == "__main__":
4
- cli()
4
+ main()
fal/_fal_version.py ADDED
@@ -0,0 +1,16 @@
1
+ # file generated by setuptools_scm
2
+ # don't change, don't track in version control
3
+ TYPE_CHECKING = False
4
+ if TYPE_CHECKING:
5
+ from typing import Tuple, Union
6
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
7
+ else:
8
+ VERSION_TUPLE = object
9
+
10
+ version: str
11
+ __version__: str
12
+ __version_tuple__: VERSION_TUPLE
13
+ version_tuple: VERSION_TUPLE
14
+
15
+ __version__ = version = '1.0.0'
16
+ __version_tuple__ = version_tuple = (1, 0, 0)
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/_version.py ADDED
@@ -0,0 +1,6 @@
1
+ try:
2
+ from ._fal_version import version as __version__ # type: ignore[import]
3
+ from ._fal_version import version_tuple # type: ignore[import]
4
+ except ImportError:
5
+ __version__ = "UNKNOWN"
6
+ version_tuple = (0, 0, __version__) # type: ignore[assignment]
fal/api.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import inspect
4
+ import os
4
5
  import sys
5
6
  import threading
6
7
  from collections import defaultdict
@@ -40,8 +41,8 @@ from pydantic import __version__ as pydantic_version
40
41
  from typing_extensions import Concatenate, ParamSpec
41
42
 
42
43
  import fal.flags as flags
43
- from fal.exceptions import FalServerlessException
44
44
  from fal._serialization import include_modules_from, patch_pickle
45
+ from fal.exceptions import FalServerlessException
45
46
  from fal.logging.isolate import IsolateLogPrinter
46
47
  from fal.sdk import (
47
48
  FAL_SERVERLESS_DEFAULT_KEEP_ALIVE,
@@ -57,7 +58,7 @@ from fal.sdk import (
57
58
  )
58
59
 
59
60
  ArgsT = ParamSpec("ArgsT")
60
- ReturnT = TypeVar("ReturnT", covariant=True)
61
+ ReturnT = TypeVar("ReturnT", covariant=True) # noqa: PLC0105
61
62
 
62
63
  BasicConfig = Dict[str, Any]
63
64
  _UNSET = object()
@@ -113,8 +114,8 @@ class Host(Generic[ArgsT, ReturnT]):
113
114
  environment options."""
114
115
 
115
116
  options = Options()
116
- for key, value in config.items():
117
- key, value = cls.parse_key(key, value)
117
+ for item in config.items():
118
+ key, value = cls.parse_key(*item)
118
119
  if key in cls._SUPPORTED_KEYS:
119
120
  options.host[key] = value
120
121
  elif key in cls._GATEWAY_KEYS:
@@ -545,7 +546,8 @@ _DEFAULT_HOST = FalServerlessHost()
545
546
  _SERVE_PORT = 8080
546
547
 
547
548
  # 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.
549
+ # NOTE: This is both in sync with host options and with environment configs from
550
+ # `isolate` package.
549
551
 
550
552
 
551
553
  ## virtualenv
@@ -785,7 +787,8 @@ class FalFastAPI(FastAPI):
785
787
  """
786
788
  Add x-fal-order-* keys to the OpenAPI specification to help the rendering of UI.
787
789
 
788
- NOTE: We rely on the fact that fastapi and Python dicts keep the order of properties.
790
+ NOTE: We rely on the fact that fastapi and Python dicts keep the order of
791
+ properties.
789
792
  """
790
793
 
791
794
  def mark_order(obj: dict[str, Any], key: str):
@@ -830,6 +833,9 @@ class BaseServable:
830
833
  """
831
834
  pass
832
835
 
836
+ def _add_extra_routes(self, app: FastAPI):
837
+ pass
838
+
833
839
  @asynccontextmanager
834
840
  async def lifespan(self, app: FastAPI):
835
841
  yield
@@ -840,7 +846,10 @@ class BaseServable:
840
846
  from fastapi.responses import JSONResponse
841
847
  from starlette_exporter import PrometheusMiddleware
842
848
 
843
- _app = FalFastAPI(lifespan=self.lifespan)
849
+ _app = FalFastAPI(
850
+ lifespan=self.lifespan,
851
+ root_path=os.getenv("FAL_APP_ROOT_PATH") or "",
852
+ )
844
853
 
845
854
  _app.add_middleware(
846
855
  CORSMiddleware,
@@ -891,6 +900,8 @@ class BaseServable:
891
900
  methods=["POST"],
892
901
  )
893
902
 
903
+ self._add_extra_routes(_app)
904
+
894
905
  return _app
895
906
 
896
907
  def openapi(self) -> dict[str, Any]:
@@ -918,7 +929,9 @@ class BaseServable:
918
929
  asyncio.create_task(metrics_server.serve()): metrics_server,
919
930
  }
920
931
 
921
- _, pending = await asyncio.wait(tasks.keys(), return_when=asyncio.FIRST_COMPLETED)
932
+ _, pending = await asyncio.wait(
933
+ tasks.keys(), return_when=asyncio.FIRST_COMPLETED,
934
+ )
922
935
  if not pending:
923
936
  return
924
937
 
@@ -1007,13 +1020,15 @@ class IsolatedFunction(Generic[ArgsT, ReturnT]):
1007
1020
  lines = []
1008
1021
  for used_modules, references in pairs:
1009
1022
  lines.append(
1010
- f"\t- {used_modules!r} (accessed through {', '.join(map(repr, references))})"
1023
+ f"\t- {used_modules!r} "
1024
+ f"(accessed through {', '.join(map(repr, references))})"
1011
1025
  )
1012
1026
 
1013
1027
  function_name = self.func.__name__
1014
1028
  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"
1029
+ f"Couldn't deserialize your function on the remote server. \n\n"
1030
+ f"[Hint] {function_name!r} function uses the following modules "
1031
+ "which weren't present in the environment definition:\n"
1017
1032
  + "\n".join(lines)
1018
1033
  ) from None
1019
1034
  except Exception as exc:
@@ -1065,7 +1080,8 @@ class IsolatedFunction(Generic[ArgsT, ReturnT]):
1065
1080
  def func(self) -> Callable[ArgsT, ReturnT]:
1066
1081
  serve_mode = self.options.gateway.get("serve")
1067
1082
  if serve_mode:
1068
- # This type can be safely ignored because this case only happens when it is a ServedIsolatedFunction
1083
+ # This type can be safely ignored because this case only happens when it
1084
+ # is a ServedIsolatedFunction
1069
1085
  serve_func: Callable[[], None] = ServeWrapper(self.raw_func)
1070
1086
  return serve_func # type: ignore
1071
1087
  else:
@@ -1098,8 +1114,10 @@ class ServedIsolatedFunction(
1098
1114
 
1099
1115
  class Server(uvicorn.Server):
1100
1116
  """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.
1117
+ By default, uvicorn's Server class overwrites the signal handler for SIGINT,
1118
+ swallowing the signal and preventing other tasks from cancelling.
1119
+ This class allows the task to be gracefully cancelled using asyncio's built-in task
1120
+ cancellation or with an event, like aiohttp.
1103
1121
  """
1104
1122
 
1105
1123
  def install_signal_handlers(self) -> None:
fal/app.py CHANGED
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import inspect
4
4
  import json
5
5
  import os
6
+ import re
6
7
  import typing
7
8
  from contextlib import asynccontextmanager
8
9
  from typing import Any, Callable, ClassVar, TypeVar
@@ -10,9 +11,11 @@ from typing import Any, Callable, ClassVar, TypeVar
10
11
  from fastapi import FastAPI
11
12
 
12
13
  import fal.api
14
+ from fal._serialization import include_modules_from
13
15
  from fal.api import RouteSignature
14
16
  from fal.logging import get_logger
15
- from fal._serialization import include_modules_from
17
+ from fal.toolkit.file.providers import fal as fal_provider_module
18
+
16
19
  REALTIME_APP_REQUIREMENTS = ["websockets", "msgpack"]
17
20
 
18
21
  EndpointT = TypeVar("EndpointT", bound=Callable[..., Any])
@@ -62,14 +65,33 @@ def wrap_app(cls: type[App], **kwargs) -> fal.api.IsolatedFunction:
62
65
  return fn
63
66
 
64
67
 
68
+
69
+ PART_FINDER_RE = re.compile(r"[A-Z][a-z]*")
70
+
71
+
72
+ def _to_fal_app_name(name: str) -> str:
73
+ # Convert MyGoodApp into my-good-app
74
+ return "-".join(part.lower() for part in PART_FINDER_RE.findall(name))
75
+
76
+
65
77
  class App(fal.api.BaseServable):
66
78
  requirements: ClassVar[list[str]] = []
67
79
  machine_type: ClassVar[str] = "S"
68
- host_kwargs: ClassVar[dict[str, Any]] = {}
80
+ host_kwargs: ClassVar[dict[str, Any]] = {
81
+ "_scheduler": "nomad",
82
+ "_scheduler_options": {
83
+ "storage_region": "us-east",
84
+ },
85
+ "resolver": "uv",
86
+ "keep_alive": 60,
87
+ }
88
+ app_name: ClassVar[str]
69
89
 
70
90
  def __init_subclass__(cls, **kwargs):
91
+ app_name = kwargs.pop("name", None) or _to_fal_app_name(cls.__name__)
71
92
  parent_settings = getattr(cls, "host_kwargs", {})
72
93
  cls.host_kwargs = {**parent_settings, **kwargs}
94
+ cls.app_name = app_name
73
95
 
74
96
  if cls.__init__ is not App.__init__:
75
97
  raise ValueError(
@@ -98,6 +120,9 @@ class App(fal.api.BaseServable):
98
120
  finally:
99
121
  await _call_any_fn(self.teardown)
100
122
 
123
+ def health(self):
124
+ return {}
125
+
101
126
  def setup(self):
102
127
  """Setup the application before serving."""
103
128
 
@@ -123,6 +148,27 @@ class App(fal.api.BaseServable):
123
148
  )
124
149
  return response
125
150
 
151
+ @app.middleware("http")
152
+ async def set_global_object_preference(request, call_next):
153
+ response = await call_next(request)
154
+ try:
155
+ fal_provider_module.GLOBAL_LIFECYCLE_PREFERENCE = request.headers.get(
156
+ "X-Fal-Object-Lifecycle-Preference"
157
+ )
158
+ except Exception:
159
+ from fastapi.logger import logger
160
+
161
+ logger.exception(
162
+ "Failed set a global lifecycle preference %s",
163
+ self.__class__.__name__,
164
+ )
165
+ return response
166
+
167
+ def _add_extra_routes(self, app: FastAPI):
168
+ @app.get("/health")
169
+ def health():
170
+ return self.health()
171
+
126
172
  def provide_hints(self) -> list[str]:
127
173
  """Provide hints for routing the application."""
128
174
  raise NotImplementedError
@@ -237,14 +283,16 @@ def _fal_websocket_template(
237
283
  output = output.dict()
238
284
  else:
239
285
  raise TypeError(
240
- f"Expected a dict or pydantic model as output, got {type(output)}"
286
+ "Expected a dict or pydantic model as output, got "
287
+ f"{type(output)}"
241
288
  )
242
289
 
243
290
  messages = [
244
291
  msgpack.packb(output, use_bin_type=True),
245
292
  ]
246
293
  if route_signature.emit_timings:
247
- # We emit x-fal messages in JSON, no matter what the input/output format is.
294
+ # We emit x-fal messages in JSON, no matter what the
295
+ # input/output format is.
248
296
  timings = {
249
297
  "type": "x-fal-message",
250
298
  "action": "timings",
@@ -354,7 +402,8 @@ def realtime(
354
402
 
355
403
  if hasattr(original_func, "route_signature"):
356
404
  raise ValueError(
357
- f"Can't set multiple routes for the same function: {original_func.__name__}"
405
+ "Can't set multiple routes for the same function: "
406
+ f"{original_func.__name__}"
358
407
  )
359
408
 
360
409
  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/__init__.py ADDED
@@ -0,0 +1 @@
1
+ from .main import main # noqa: F401