fal 1.12.1__py3-none-any.whl → 1.13.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/_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.12.1'
21
- __version_tuple__ = version_tuple = (1, 12, 1)
20
+ __version__ = version = '1.13.0'
21
+ __version_tuple__ = version_tuple = (1, 13, 0)
fal/api.py CHANGED
@@ -12,6 +12,7 @@ from functools import wraps
12
12
  from os import PathLike
13
13
  from queue import Queue
14
14
  from typing import (
15
+ TYPE_CHECKING,
15
16
  Any,
16
17
  Callable,
17
18
  ClassVar,
@@ -27,15 +28,11 @@ from typing import (
27
28
 
28
29
  import cloudpickle
29
30
  import grpc
30
- import isolate
31
31
  import tblib
32
32
  import uvicorn
33
33
  import yaml
34
34
  from fastapi import FastAPI
35
35
  from fastapi import __version__ as fastapi_version
36
- from isolate.backends.common import active_python
37
- from isolate.backends.settings import DEFAULT_SETTINGS
38
- from isolate.connections import PythonIPC
39
36
  from packaging.requirements import Requirement
40
37
  from packaging.utils import canonicalize_name
41
38
  from pydantic import __version__ as pydantic_version
@@ -66,6 +63,9 @@ from fal.sdk import (
66
63
  get_default_credentials,
67
64
  )
68
65
 
66
+ if TYPE_CHECKING:
67
+ from isolate.backends import BaseEnvironment
68
+
69
69
  ArgsT = ParamSpec("ArgsT")
70
70
  ReturnT = TypeVar("ReturnT", covariant=True) # noqa: PLC0105
71
71
 
@@ -83,9 +83,6 @@ SERVE_REQUIREMENTS = [
83
83
  ]
84
84
 
85
85
 
86
- THREAD_POOL = ThreadPoolExecutor()
87
-
88
-
89
86
  @dataclass
90
87
  class FalServerlessError(FalServerlessException):
91
88
  message: str
@@ -222,9 +219,10 @@ def cached(func: Callable[ArgsT, ReturnT]) -> Callable[ArgsT, ReturnT]:
222
219
  ) -> ReturnT:
223
220
  from functools import lru_cache
224
221
 
225
- # HACK: Using the isolate module as a global cache.
226
222
  import isolate
227
223
 
224
+ # HACK: Using the isolate module as a global cache.
225
+
228
226
  if not hasattr(isolate, "__cached_functions__"):
229
227
  isolate.__cached_functions__ = {}
230
228
 
@@ -269,17 +267,23 @@ def _prepare_partial_func(
269
267
  return wrapper
270
268
 
271
269
 
272
- @dataclass
273
- class LocalHost(Host):
274
- # The environment which provides the default set of
275
- # packages for isolate agent to run.
276
- _AGENT_ENVIRONMENT = isolate.prepare_environment(
270
+ def _prepare_environment() -> BaseEnvironment:
271
+ import isolate
272
+
273
+ return isolate.prepare_environment(
277
274
  "virtualenv",
278
275
  requirements=[
279
276
  f"cloudpickle=={cloudpickle.__version__}",
280
277
  f"tblib=={tblib.__version__}",
281
278
  ],
282
279
  )
280
+
281
+
282
+ @dataclass
283
+ class LocalHost(Host):
284
+ # The environment which provides the default set of
285
+ # packages for isolate agent to run.
286
+ _AGENT_ENVIRONMENT: BaseEnvironment = field(default_factory=_prepare_environment)
283
287
  _log_printer = IsolateLogPrinter(debug=flags.DEBUG)
284
288
 
285
289
  def run(
@@ -289,6 +293,10 @@ class LocalHost(Host):
289
293
  args: tuple[Any, ...],
290
294
  kwargs: dict[str, Any],
291
295
  ) -> ReturnT:
296
+ import isolate
297
+ from isolate.backends.settings import DEFAULT_SETTINGS
298
+ from isolate.connections import PythonIPC
299
+
292
300
  settings = replace(
293
301
  DEFAULT_SETTINGS,
294
302
  serialization_method="cloudpickle",
@@ -419,8 +427,16 @@ class FalServerlessHost(Host):
419
427
 
420
428
  _log_printer = IsolateLogPrinter(debug=flags.DEBUG)
421
429
 
430
+ _thread_pool: ThreadPoolExecutor = field(default_factory=ThreadPoolExecutor)
431
+
432
+ def __getstate__(self) -> dict[str, Any]:
433
+ state = self.__dict__.copy()
434
+ state["_thread_pool"] = None
435
+ return state
436
+
422
437
  def __setstate__(self, state: dict[str, Any]) -> None:
423
438
  self.__dict__.update(state)
439
+ self._thread_pool = ThreadPoolExecutor()
424
440
  self.credentials = get_agent_credentials(self.credentials)
425
441
 
426
442
  @property
@@ -440,6 +456,8 @@ class FalServerlessHost(Host):
440
456
  deployment_strategy: Literal["recreate", "rolling"] = "recreate",
441
457
  scale: bool = True,
442
458
  ) -> str | None:
459
+ from isolate.backends.common import active_python
460
+
443
461
  environment_options = options.environment.copy()
444
462
  environment_options.setdefault("python_version", active_python())
445
463
  environments = [self._connection.define_environment(**environment_options)]
@@ -517,6 +535,8 @@ class FalServerlessHost(Host):
517
535
  kwargs: dict[str, Any],
518
536
  result_handler: Callable[..., None],
519
537
  ) -> ReturnT:
538
+ from isolate.backends.common import active_python
539
+
520
540
  environment_options = options.environment.copy()
521
541
  environment_options.setdefault("python_version", active_python())
522
542
  environments = [self._connection.define_environment(**environment_options)]
@@ -611,7 +631,7 @@ class FalServerlessHost(Host):
611
631
  ret.url = log.message.rsplit()[-1]
612
632
  ret.logs.put(log)
613
633
 
614
- THREAD_POOL.submit(
634
+ self._thread_pool.submit(
615
635
  self._run,
616
636
  func,
617
637
  options,
@@ -654,7 +674,6 @@ class Options:
654
674
  return self.gateway.get("exposed_port")
655
675
 
656
676
 
657
- _DEFAULT_HOST = FalServerlessHost()
658
677
  _SERVE_PORT = 8080
659
678
 
660
679
  # Overload @function to help users identify the correct signature.
@@ -704,7 +723,7 @@ def function(
704
723
  python_version: str | None = None,
705
724
  requirements: list[str] | None = None,
706
725
  # Common options
707
- host: FalServerlessHost = _DEFAULT_HOST,
726
+ host: FalServerlessHost | None = None,
708
727
  serve: Literal[False] = False,
709
728
  exposed_port: int | None = None,
710
729
  max_concurrency: int | None = None,
@@ -733,7 +752,7 @@ def function(
733
752
  python_version: str | None = None,
734
753
  requirements: list[str] | None = None,
735
754
  # Common options
736
- host: FalServerlessHost = _DEFAULT_HOST,
755
+ host: FalServerlessHost | None = None,
737
756
  serve: Literal[True],
738
757
  exposed_port: int | None = None,
739
758
  max_concurrency: int | None = None,
@@ -812,7 +831,7 @@ def function(
812
831
  pip: list[str] | None = None,
813
832
  channels: list[str] | None = None,
814
833
  # Common options
815
- host: FalServerlessHost = _DEFAULT_HOST,
834
+ host: FalServerlessHost | None = None,
816
835
  serve: Literal[False] = False,
817
836
  exposed_port: int | None = None,
818
837
  max_concurrency: int | None = None,
@@ -846,7 +865,7 @@ def function(
846
865
  pip: list[str] | None = None,
847
866
  channels: list[str] | None = None,
848
867
  # Common options
849
- host: FalServerlessHost = _DEFAULT_HOST,
868
+ host: FalServerlessHost | None = None,
850
869
  serve: Literal[True],
851
870
  exposed_port: int | None = None,
852
871
  max_concurrency: int | None = None,
@@ -874,7 +893,7 @@ def function(
874
893
  *,
875
894
  image: ContainerImage | None = None,
876
895
  # Common options
877
- host: FalServerlessHost = _DEFAULT_HOST,
896
+ host: FalServerlessHost | None = None,
878
897
  serve: Literal[False] = False,
879
898
  exposed_port: int | None = None,
880
899
  max_concurrency: int | None = None,
@@ -902,7 +921,7 @@ def function(
902
921
  *,
903
922
  image: ContainerImage | None = None,
904
923
  # Common options
905
- host: FalServerlessHost = _DEFAULT_HOST,
924
+ host: FalServerlessHost | None = None,
906
925
  serve: Literal[True],
907
926
  exposed_port: int | None = None,
908
927
  max_concurrency: int | None = None,
@@ -928,15 +947,17 @@ def function(
928
947
  def function( # type: ignore
929
948
  kind: str = "virtualenv",
930
949
  *,
931
- host: Host = _DEFAULT_HOST,
950
+ host: Host | None = None,
932
951
  **config: Any,
933
952
  ):
953
+ if host is None:
954
+ host = FalServerlessHost()
934
955
  options = host.parse_options(kind=kind, **config)
935
956
 
936
957
  def wrapper(func: Callable[ArgsT, ReturnT]):
937
958
  include_modules_from(func)
938
959
  proxy = IsolatedFunction(
939
- host=host,
960
+ host=host, # type: ignore
940
961
  raw_func=func, # type: ignore
941
962
  options=options,
942
963
  )
fal/app.py CHANGED
@@ -16,11 +16,17 @@ from typing import Any, Callable, ClassVar, Literal, TypeVar
16
16
  import fastapi
17
17
  import grpc.aio as async_grpc
18
18
  import httpx
19
- from isolate.server import definitions
20
19
 
21
- import fal.api
22
20
  from fal._serialization import include_modules_from
23
- from fal.api import RouteSignature
21
+ from fal.api import (
22
+ SERVE_REQUIREMENTS,
23
+ BaseServable,
24
+ IsolatedFunction,
25
+ RouteSignature,
26
+ )
27
+ from fal.api import (
28
+ function as fal_function,
29
+ )
24
30
  from fal.exceptions import FalServerlessException, RequestCancelledException
25
31
  from fal.logging import get_logger
26
32
  from fal.toolkit.file import request_lifecycle_preference
@@ -70,6 +76,8 @@ async def _set_logger_labels(
70
76
  try:
71
77
  import sys
72
78
 
79
+ from isolate.server import definitions
80
+
73
81
  # Flush any prints that were buffered before setting the logger labels
74
82
  sys.stderr.flush()
75
83
  sys.stdout.flush()
@@ -89,7 +97,7 @@ async def _set_logger_labels(
89
97
  pass
90
98
 
91
99
 
92
- def wrap_app(cls: type[App], **kwargs) -> fal.api.IsolatedFunction:
100
+ def wrap_app(cls: type[App], **kwargs) -> IsolatedFunction:
93
101
  include_modules_from(cls)
94
102
 
95
103
  def initialize_and_serve():
@@ -111,7 +119,7 @@ def wrap_app(cls: type[App], **kwargs) -> fal.api.IsolatedFunction:
111
119
  if kind == "container":
112
120
  cls.host_kwargs.pop("resolver", None)
113
121
 
114
- wrapper = fal.api.function(
122
+ wrapper = fal_function(
115
123
  kind,
116
124
  requirements=cls.requirements,
117
125
  machine_type=cls.machine_type,
@@ -123,7 +131,7 @@ def wrap_app(cls: type[App], **kwargs) -> fal.api.IsolatedFunction:
123
131
  serve=False,
124
132
  )
125
133
  fn = wrapper(initialize_and_serve)
126
- fn.options.add_requirements(fal.api.SERVE_REQUIREMENTS)
134
+ fn.options.add_requirements(SERVE_REQUIREMENTS)
127
135
  if realtime_app:
128
136
  fn.options.add_requirements(REALTIME_APP_REQUIREMENTS)
129
137
 
@@ -255,7 +263,7 @@ def _print_python_packages() -> None:
255
263
  print("[debug] Python packages installed:", ", ".join(packages))
256
264
 
257
265
 
258
- class App(fal.api.BaseServable):
266
+ class App(BaseServable):
259
267
  requirements: ClassVar[list[str]] = []
260
268
  machine_type: ClassVar[str] = "S"
261
269
  num_gpus: ClassVar[int | None] = None
fal/apps.py CHANGED
@@ -4,6 +4,7 @@ import json
4
4
  import time
5
5
  from contextlib import contextmanager
6
6
  from dataclasses import dataclass, field
7
+ from functools import lru_cache
7
8
  from typing import TYPE_CHECKING, Any, Iterator
8
9
 
9
10
  import httpx
@@ -14,6 +15,7 @@ from fal.sdk import Credentials, get_default_credentials
14
15
  if TYPE_CHECKING:
15
16
  from websockets.sync.connection import Connection
16
17
 
18
+ _STREAM_URL_FORMAT = f"https://{flags.FAL_RUN_HOST}/{{app_id}}"
17
19
  _QUEUE_URL_FORMAT = f"https://queue.{flags.FAL_RUN_HOST}/{{app_id}}"
18
20
  _REALTIME_URL_FORMAT = f"wss://{flags.FAL_RUN_HOST}/{{app_id}}"
19
21
  _WS_URL_FORMAT = f"wss://ws.{flags.FAL_RUN_HOST}/{{app_id}}"
@@ -55,6 +57,11 @@ class Completed(_Status):
55
57
  logs: list[dict[str, Any]] | None = field()
56
58
 
57
59
 
60
+ @lru_cache(maxsize=1)
61
+ def _get_http_client() -> httpx.Client:
62
+ return httpx.Client(headers={"User-Agent": "Fal/Python"})
63
+
64
+
58
65
  @dataclass
59
66
  class RequestHandle:
60
67
  """A handle to an async inference request."""
@@ -62,6 +69,8 @@ class RequestHandle:
62
69
  app_id: str
63
70
  request_id: str
64
71
 
72
+ _client: httpx.Client = field(default_factory=_get_http_client)
73
+
65
74
  # Use the credentials that were used to submit the request by default.
66
75
  _creds: Credentials = field(default_factory=get_default_credentials, repr=False)
67
76
 
@@ -82,7 +91,7 @@ class RequestHandle:
82
91
  _QUEUE_URL_FORMAT.format(app_id=self.app_id)
83
92
  + f"/requests/{self.request_id}/status/"
84
93
  )
85
- response = _HTTP_CLIENT.get(
94
+ response = self._client.get(
86
95
  url,
87
96
  headers=self._creds.to_headers(),
88
97
  params={"logs": int(logs)},
@@ -107,7 +116,7 @@ class RequestHandle:
107
116
  _QUEUE_URL_FORMAT.format(app_id=self.app_id)
108
117
  + f"/requests/{self.request_id}/cancel"
109
118
  )
110
- response = _HTTP_CLIENT.put(url, headers=self._creds.to_headers())
119
+ response = self._client.put(url, headers=self._creds.to_headers())
111
120
  response.raise_for_status()
112
121
 
113
122
  def iter_events(
@@ -134,7 +143,7 @@ class RequestHandle:
134
143
  _QUEUE_URL_FORMAT.format(app_id=self.app_id)
135
144
  + f"/requests/{self.request_id}/"
136
145
  )
137
- response = _HTTP_CLIENT.get(url, headers=self._creds.to_headers())
146
+ response = self._client.get(url, headers=self._creds.to_headers())
138
147
  try:
139
148
  response.raise_for_status()
140
149
  except httpx.HTTPStatusError as e:
@@ -159,7 +168,33 @@ class RequestHandle:
159
168
  return self.fetch_result()
160
169
 
161
170
 
162
- _HTTP_CLIENT = httpx.Client(headers={"User-Agent": "Fal/Python"})
171
+ def stream(
172
+ app_id: str, arguments: dict[str, Any], *, path: str = ""
173
+ ) -> Iterator[str | bytes]:
174
+ """Stream an inference task on a Fal app."""
175
+
176
+ app_id = _backwards_compatible_app_id(app_id)
177
+ url = _STREAM_URL_FORMAT.format(app_id=app_id)
178
+ if path:
179
+ _path = path[len("/") :] if path.startswith("/") else path
180
+ url += "/" + _path
181
+
182
+ creds = get_default_credentials()
183
+ client = _get_http_client()
184
+
185
+ response = client.post(
186
+ url,
187
+ json=arguments,
188
+ headers=creds.to_headers(),
189
+ )
190
+ response.raise_for_status()
191
+
192
+ if response.headers["Content-Type"].startswith("text/event-stream"):
193
+ for line in response.iter_lines():
194
+ if line:
195
+ yield line
196
+ else:
197
+ yield from response.iter_bytes()
163
198
 
164
199
 
165
200
  def run(app_id: str, arguments: dict[str, Any], *, path: str = "") -> dict[str, Any]:
@@ -181,8 +216,9 @@ def submit(app_id: str, arguments: dict[str, Any], *, path: str = "") -> Request
181
216
  url += "/" + _path
182
217
 
183
218
  creds = get_default_credentials()
219
+ client = _get_http_client()
184
220
 
185
- response = _HTTP_CLIENT.post(
221
+ response = client.post(
186
222
  url,
187
223
  json=arguments,
188
224
  headers=creds.to_headers(),
@@ -194,6 +230,7 @@ def submit(app_id: str, arguments: dict[str, Any], *, path: str = "") -> Request
194
230
  app_id=app_id,
195
231
  request_id=data["request_id"],
196
232
  _creds=creds,
233
+ _client=client,
197
234
  )
198
235
 
199
236
 
fal/auth/__init__.py CHANGED
@@ -7,8 +7,6 @@ from typing import Optional
7
7
 
8
8
  from fal.auth import auth0, local
9
9
  from fal.config import Config
10
- from fal.console import console
11
- from fal.console.icons import CHECK_ICON
12
10
  from fal.exceptions import FalServerlessException
13
11
  from fal.exceptions.auth import UnauthenticatedException
14
12
 
@@ -73,26 +71,6 @@ def key_credentials() -> tuple[str, str] | None:
73
71
  return None
74
72
 
75
73
 
76
- def login():
77
- token_data = auth0.login()
78
- with local.lock_token():
79
- local.save_token(token_data["refresh_token"])
80
-
81
- USER.invalidate()
82
-
83
-
84
- def logout():
85
- refresh_token, _ = local.load_token()
86
- if refresh_token is None:
87
- raise FalServerlessException("You're not logged in")
88
- auth0.revoke(refresh_token)
89
- with local.lock_token():
90
- local.delete_token()
91
-
92
- USER.invalidate()
93
- console.print(f"{CHECK_ICON} Logged out of [cyan bold]fal[/]. Bye!")
94
-
95
-
96
74
  def _fetch_access_token() -> str:
97
75
  """
98
76
  Load the refresh token, request a new access_token (refreshing the refresh token)
@@ -147,6 +125,21 @@ def _fetch_teams(bearer_token: str) -> list[dict]:
147
125
  raise FalServerlessException("Failed to fetch teams") from exc
148
126
 
149
127
 
128
+ def login(console):
129
+ token_data = auth0.login(console)
130
+ with local.lock_token():
131
+ local.save_token(token_data["refresh_token"])
132
+
133
+
134
+ def logout(console):
135
+ refresh_token, _ = local.load_token()
136
+ if refresh_token is None:
137
+ raise FalServerlessException("You're not logged in")
138
+ auth0.revoke(refresh_token, console)
139
+ with local.lock_token():
140
+ local.delete_token()
141
+
142
+
150
143
  @dataclass
151
144
  class UserAccess:
152
145
  _access_token: str | None = field(repr=False, default=None)
@@ -154,12 +147,6 @@ class UserAccess:
154
147
  _exc: Exception | None = field(repr=False, default=None)
155
148
  _accounts: list[dict] | None = field(repr=False, default=None)
156
149
 
157
- def invalidate(self) -> None:
158
- self._access_token = None
159
- self._user_info = None
160
- self._exc = None
161
- self._accounts = None
162
-
163
150
  @property
164
151
  def info(self) -> dict:
165
152
  if self._user_info is None:
@@ -203,6 +190,3 @@ class UserAccess:
203
190
  if t["nickname"].lower() == team.lower():
204
191
  return t
205
192
  raise ValueError(f"Team {team} not found")
206
-
207
-
208
- USER = UserAccess()
fal/auth/auth0.py CHANGED
@@ -6,8 +6,6 @@ import warnings
6
6
 
7
7
  import httpx
8
8
 
9
- from fal.console import console
10
- from fal.console.icons import CHECK_ICON
11
9
  from fal.console.ux import maybe_open_browser_tab
12
10
  from fal.exceptions import FalServerlessException
13
11
 
@@ -26,7 +24,7 @@ def logout_url(return_url: str):
26
24
  return f"https://{AUTH0_DOMAIN}/v2/logout?client_id={AUTH0_CLIENT_ID}&returnTo={return_url}"
27
25
 
28
26
 
29
- def _open_browser(url: str, code: str | None) -> None:
27
+ def _open_browser(url: str, code: str | None, console) -> None:
30
28
  maybe_open_browser_tab(url)
31
29
 
32
30
  console.print(
@@ -41,7 +39,7 @@ def _open_browser(url: str, code: str | None) -> None:
41
39
  )
42
40
 
43
41
 
44
- def login() -> dict:
42
+ def login(console) -> dict:
45
43
  """
46
44
  Runs the device authorization flow and stores the user object in memory
47
45
  """
@@ -63,7 +61,7 @@ def login() -> dict:
63
61
 
64
62
  url = logout_url(device_confirmation_url)
65
63
 
66
- _open_browser(url, device_user_code)
64
+ _open_browser(url, device_user_code, console)
67
65
 
68
66
  # This is needed to suppress the ResourceWarning emitted
69
67
  # when the process is waiting for user confirmation
@@ -84,7 +82,6 @@ def login() -> dict:
84
82
  token_data = token_response.json()
85
83
  if token_response.status_code == 200:
86
84
  status.update(spinner=None)
87
- console.print(f"{CHECK_ICON} Authenticated successfully, welcome!")
88
85
 
89
86
  validate_id_token(token_data["id_token"])
90
87
 
@@ -118,7 +115,7 @@ def refresh(token: str) -> dict:
118
115
  raise FalServerlessException(token_data["error_description"])
119
116
 
120
117
 
121
- def revoke(token: str):
118
+ def revoke(token: str, console):
122
119
  token_payload = {
123
120
  "client_id": AUTH0_CLIENT_ID,
124
121
  "token": token,
@@ -132,7 +129,7 @@ def revoke(token: str):
132
129
  token_data = token_response.json()
133
130
  raise FalServerlessException(token_data["error_description"])
134
131
 
135
- _open_browser(logout_url(WEBSITE_URL), None)
132
+ _open_browser(logout_url(WEBSITE_URL), None, console)
136
133
 
137
134
 
138
135
  def get_user_info(bearer_token: str) -> dict:
fal/cli/api.py CHANGED
@@ -10,19 +10,38 @@ KV_SPLIT_RE = re.compile(r"(=|:=)")
10
10
 
11
11
  def _api(args):
12
12
  """Handle the api command execution."""
13
- from rich.console import Group
14
- from rich.live import Live
15
- from rich.panel import Panel
16
- from rich.text import Text
17
-
18
13
  from . import cli_nested_json
19
14
 
20
- params = [KV_SPLIT_RE.split(param) for param in args.params]
15
+ params_split = [KV_SPLIT_RE.split(param) for param in args.params]
21
16
  params = cli_nested_json.interpret_nested_json( # type: ignore
22
- [(key, value) for key, _, value in params]
17
+ [(key, value) for key, _, value in params_split]
23
18
  )
24
19
 
25
- handle = fal.apps.submit(args.model_id, params) # type: ignore
20
+ if args.model_id.endswith("/stream"):
21
+ stream_run(args.model_id, params)
22
+ else:
23
+ queue_run(args.model_id, params)
24
+
25
+
26
+ def stream_run(model_id: str, params: dict):
27
+ res = fal.apps.stream(model_id, params) # type: ignore
28
+ for line in res:
29
+ if isinstance(line, str):
30
+ rich.print(line)
31
+ else:
32
+ if isinstance(line, memoryview):
33
+ rich.print(line.tobytes().decode())
34
+ else:
35
+ rich.print(line.decode())
36
+
37
+
38
+ def queue_run(model_id: str, params: dict):
39
+ from rich.console import Group
40
+ from rich.live import Live
41
+ from rich.panel import Panel
42
+ from rich.text import Text
43
+
44
+ handle = fal.apps.submit(model_id, params) # type: ignore
26
45
  logs = [] # type: ignore
27
46
 
28
47
  with Live(auto_refresh=False) as live:
fal/cli/auth.py CHANGED
@@ -1,10 +1,15 @@
1
- from fal.auth import USER, login, logout
2
-
3
-
4
1
  def _login(args):
2
+ from fal.auth import login
5
3
  from fal.config import Config
4
+ from fal.console.icons import CHECK_ICON, CROSS_ICON
5
+ from fal.exceptions import FalServerlessException
6
6
 
7
- login()
7
+ try:
8
+ login(args.console)
9
+ args.console.print(f"{CHECK_ICON} Authenticated successfully, welcome!")
10
+ except FalServerlessException as e:
11
+ args.console.print(f"{CROSS_ICON} {e}")
12
+ return
8
13
 
9
14
  with Config().edit() as config:
10
15
  config.unset("team")
@@ -13,9 +18,18 @@ def _login(args):
13
18
 
14
19
 
15
20
  def _logout(args):
21
+ from fal.auth import logout
16
22
  from fal.config import Config
23
+ from fal.console.icons import CHECK_ICON, CROSS_ICON
24
+ from fal.exceptions import FalServerlessException
25
+
26
+ try:
27
+ logout(args.console)
28
+ args.console.print(f"{CHECK_ICON} Logged out of [cyan bold]fal[/]. Bye!")
29
+ except FalServerlessException as e:
30
+ args.console.print(f"{CROSS_ICON} {e}")
31
+ return
17
32
 
18
- logout()
19
33
  with Config().edit() as config:
20
34
  config.unset("team")
21
35
 
@@ -24,17 +38,19 @@ def _list_accounts(args):
24
38
  from rich.style import Style
25
39
  from rich.table import Table
26
40
 
41
+ from fal.auth import UserAccess
27
42
  from fal.config import Config
28
43
 
44
+ user_access = UserAccess()
29
45
  config = Config()
30
- current_account = config.get("team") or USER.info["nickname"]
46
+ current_account = config.get("team") or user_access.info["nickname"]
31
47
 
32
48
  table = Table(border_style=Style(frame=False), show_header=False)
33
49
  table.add_column("#")
34
50
  table.add_column("Nickname")
35
51
  table.add_column("Type")
36
52
 
37
- for idx, account in enumerate(USER.accounts):
53
+ for idx, account in enumerate(user_access.accounts):
38
54
  selected = account["nickname"] == current_account
39
55
  color = "bold yellow" if selected else None
40
56
 
@@ -51,18 +67,21 @@ def _list_accounts(args):
51
67
  def _set_account(args):
52
68
  from rich.prompt import Prompt
53
69
 
70
+ from fal.auth import UserAccess
54
71
  from fal.config import Config
55
72
 
73
+ user_access = UserAccess()
74
+
56
75
  if hasattr(args, "account") and args.account:
57
76
  if args.account.isdigit():
58
77
  acc_index = int(args.account) - 1
59
- account = USER.accounts[acc_index]
78
+ account = user_access.accounts[acc_index]
60
79
  else:
61
- account = USER.get_account(args.account)
80
+ account = user_access.get_account(args.account)
62
81
  else:
63
82
  _list_accounts(args)
64
- indices = list(map(str, range(1, len(USER.accounts) + 1)))
65
- team_names = [account["nickname"] for account in USER.accounts]
83
+ indices = list(map(str, range(1, len(user_access.accounts) + 1)))
84
+ team_names = [account["nickname"] for account in user_access.accounts]
66
85
  acc_choice = Prompt.ask(
67
86
  "Select an account by number",
68
87
  choices=indices + team_names,
@@ -70,9 +89,9 @@ def _set_account(args):
70
89
  )
71
90
  if acc_choice in indices:
72
91
  acc_index = int(acc_choice) - 1
73
- account = USER.accounts[acc_index]
92
+ account = user_access.accounts[acc_index]
74
93
  else:
75
- account = USER.get_account(acc_choice)
94
+ account = user_access.get_account(acc_choice)
76
95
 
77
96
  if account["is_personal"]:
78
97
  args.console.print(
@@ -90,15 +109,17 @@ def _set_account(args):
90
109
 
91
110
 
92
111
  def _whoami(args):
112
+ from fal.auth import UserAccess
93
113
  from fal.config import Config
94
114
 
115
+ user_access = UserAccess()
95
116
  config = Config()
96
117
 
97
118
  team = config.get("team")
98
119
  if team:
99
- account = USER.get_account(team)
120
+ account = user_access.get_account(team)
100
121
  else:
101
- account = USER.get_account(USER.info["nickname"])
122
+ account = user_access.get_account(user_access.info["nickname"])
102
123
 
103
124
  nickname = account["nickname"]
104
125
  full_name = account["full_name"]
fal/cli/profile.py CHANGED
@@ -33,7 +33,7 @@ def _set(args):
33
33
 
34
34
  def _unset(args):
35
35
  with Config().edit() as config:
36
- config.set_internal("profile", None)
36
+ config.unset_internal("profile")
37
37
  args.console.print("Default profile unset.")
38
38
  config.profile = None
39
39
 
fal/config.py CHANGED
@@ -89,6 +89,9 @@ class Config:
89
89
  else:
90
90
  self._config[SETTINGS_SECTION][key] = value
91
91
 
92
+ def unset_internal(self, key: str) -> None:
93
+ self._config.get(SETTINGS_SECTION, {}).pop(key, None)
94
+
92
95
  def delete(self, profile: str) -> None:
93
96
  del self._config[profile]
94
97
 
fal/logging/__init__.py CHANGED
@@ -6,7 +6,7 @@ import structlog
6
6
  from structlog.typing import EventDict, WrappedLogger
7
7
 
8
8
  from .style import LEVEL_STYLES
9
- from .user import add_user_id
9
+ from .user import AddUserIdProcessor
10
10
 
11
11
  # Unfortunately structlog console processor does not support
12
12
  # more general theming as a public API. Consider a PR on the
@@ -43,7 +43,7 @@ structlog.configure(
43
43
  structlog.stdlib.PositionalArgumentsFormatter(),
44
44
  structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S"),
45
45
  structlog.processors.StackInfoRenderer(),
46
- add_user_id,
46
+ AddUserIdProcessor(),
47
47
  _console_log_output,
48
48
  ],
49
49
  wrapper_class=structlog.stdlib.BoundLogger,
fal/logging/isolate.py CHANGED
@@ -2,13 +2,16 @@ from __future__ import annotations
2
2
 
3
3
  import sys
4
4
  from datetime import datetime, timezone
5
+ from typing import TYPE_CHECKING
5
6
 
6
- from isolate.logs import Log, LogLevel, LogSource
7
7
  from structlog.dev import ConsoleRenderer
8
8
  from structlog.typing import EventDict
9
9
 
10
10
  from .style import LEVEL_STYLES
11
11
 
12
+ if TYPE_CHECKING:
13
+ from isolate.logs import Log, LogSource
14
+
12
15
  _renderer = ConsoleRenderer(level_styles=LEVEL_STYLES)
13
16
 
14
17
 
@@ -20,6 +23,8 @@ class IsolateLogPrinter:
20
23
  self._current_source: LogSource | None = None
21
24
 
22
25
  def _maybe_print_header(self, source: LogSource):
26
+ from isolate.logs import LogSource
27
+
23
28
  from fal.console import console
24
29
 
25
30
  if source == self._current_source:
@@ -37,6 +42,8 @@ class IsolateLogPrinter:
37
42
  self._current_source = source
38
43
 
39
44
  def print(self, log: Log):
45
+ from isolate.logs import LogLevel, LogSource
46
+
40
47
  if log.level < LogLevel.INFO and not self.debug:
41
48
  return
42
49
 
fal/logging/user.py CHANGED
@@ -2,20 +2,24 @@ from __future__ import annotations
2
2
 
3
3
  from structlog.typing import EventDict, WrappedLogger
4
4
 
5
- from fal.auth import USER
6
5
 
6
+ class AddUserIdProcessor:
7
+ def __init__(self):
8
+ from fal.auth import UserAccess
7
9
 
8
- def add_user_id(
9
- logger: WrappedLogger, method_name: str, event_dict: EventDict
10
- ) -> EventDict:
11
- """The structlog processor that sends the logged user id on every log"""
12
- user_id: str | None = None
13
- try:
14
- user_id = USER.info.get("sub")
15
- except Exception:
16
- # logs are fail-safe, so any exception is safe to ignore
17
- # this is expected to happen only when user is logged out
18
- # or there's no internet connection
19
- pass
20
- event_dict["usr.id"] = user_id
21
- return event_dict
10
+ self.user_access = UserAccess()
11
+
12
+ def __call__(
13
+ self, logger: WrappedLogger, method_name: str, event_dict: EventDict
14
+ ) -> EventDict:
15
+ """The structlog processor that sends the logged user id on every log"""
16
+ user_id: str | None = None
17
+ try:
18
+ user_id = self.user_access.info.get("sub")
19
+ except Exception:
20
+ # logs are fail-safe, so any exception is safe to ignore
21
+ # this is expected to happen only when user is logged out
22
+ # or there's no internet connection
23
+ pass
24
+ event_dict["usr.id"] = user_id
25
+ return event_dict
fal/sdk.py CHANGED
@@ -5,22 +5,30 @@ from contextlib import ExitStack
5
5
  from dataclasses import dataclass, field
6
6
  from datetime import datetime, timedelta
7
7
  from enum import Enum
8
- from typing import Any, Callable, Generic, Iterator, Literal, Optional, TypeVar
8
+ from typing import (
9
+ TYPE_CHECKING,
10
+ Any,
11
+ Callable,
12
+ Generic,
13
+ Iterator,
14
+ Literal,
15
+ Optional,
16
+ TypeVar,
17
+ )
9
18
 
10
19
  import grpc
11
20
  import isolate_proto
12
- from isolate.connections.common import is_agent
13
- from isolate.logs import Log
14
- from isolate.server import definitions as worker_definitions
15
21
  from isolate.server.interface import from_grpc, to_serialized_object, to_struct
16
- from isolate_proto.configuration import GRPC_OPTIONS
17
22
 
18
23
  from fal import flags
19
24
  from fal._serialization import patch_pickle
20
- from fal.auth import USER, key_credentials
25
+ from fal.auth import UserAccess, key_credentials
21
26
  from fal.logging import get_logger
22
27
  from fal.logging.trace import TraceContextInterceptor
23
28
 
29
+ if TYPE_CHECKING:
30
+ from isolate.logs import Log
31
+
24
32
  ResultT = TypeVar("ResultT")
25
33
  InputT = TypeVar("InputT")
26
34
  UNSET = object()
@@ -45,6 +53,8 @@ class ServerCredentials:
45
53
  def base_options(self) -> dict[str, str | int]:
46
54
  import json
47
55
 
56
+ from isolate_proto.configuration import GRPC_OPTIONS
57
+
48
58
  grpc_ops: dict[str, str | int] = dict(GRPC_OPTIONS)
49
59
  grpc_ops["grpc.enable_retries"] = 1
50
60
  grpc_ops["grpc.service_config"] = json.dumps(
@@ -128,26 +138,25 @@ class FalServerlessKeyCredentials(Credentials):
128
138
 
129
139
  @dataclass
130
140
  class AuthenticatedCredentials(Credentials):
131
- user = USER
132
- team_id: str | None = None
141
+ user: UserAccess = field(default_factory=UserAccess)
142
+ team: str | None = None
133
143
 
134
144
  def to_grpc(self) -> grpc.ChannelCredentials:
135
145
  creds = [
136
146
  self.server_credentials.to_grpc(),
137
- grpc.access_token_call_credentials(USER.access_token),
147
+ grpc.access_token_call_credentials(self.user.access_token),
138
148
  ]
139
149
 
140
- if self.team_id:
150
+ if self.team:
151
+ team_id = self.user.get_account(self.team)["user_id"]
141
152
  creds.append(
142
- grpc.metadata_call_credentials(
143
- _GRPCMetadata("fal-user-id", self.team_id)
144
- )
153
+ grpc.metadata_call_credentials(_GRPCMetadata("fal-user-id", team_id))
145
154
  )
146
155
 
147
156
  return grpc.composite_channel_credentials(*creds)
148
157
 
149
158
  def to_headers(self) -> dict[str, str]:
150
- token = USER.bearer_token
159
+ token = self.user.bearer_token
151
160
  return {"Authorization": token}
152
161
 
153
162
 
@@ -161,6 +170,8 @@ def get_agent_credentials(original_credentials: Credentials) -> Credentials:
161
170
  """If running inside a fal Serverless box, use the preconfigured credentials
162
171
  instead of the user provided ones."""
163
172
 
173
+ from isolate.connections.common import is_agent
174
+
164
175
  key_creds = key_credentials()
165
176
  if is_agent() and key_creds:
166
177
  return FalServerlessKeyCredentials(key_creds[0], key_creds[1])
@@ -183,8 +194,7 @@ def get_default_credentials(team: str | None = None) -> Credentials:
183
194
  else:
184
195
  config = Config()
185
196
  team = team or config.get("team")
186
- team_id = USER.get_account(team)["user_id"] if team else None
187
- return AuthenticatedCredentials(team_id=team_id)
197
+ return AuthenticatedCredentials(team=team)
188
198
 
189
199
 
190
200
  @dataclass
@@ -383,6 +393,8 @@ def _from_grpc_alias_info(message: isolate_proto.AliasInfo) -> AliasInfo:
383
393
 
384
394
  @from_grpc.register(isolate_proto.RunnerInfo)
385
395
  def _from_grpc_runner_info(message: isolate_proto.RunnerInfo) -> RunnerInfo:
396
+ from isolate.server import definitions as worker_definitions
397
+
386
398
  external_metadata = worker_definitions.struct_to_dict(message.external_metadata)
387
399
  return RunnerInfo(
388
400
  runner_id=message.runner_id,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fal
3
- Version: 1.12.1
3
+ Version: 1.13.0
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,29 +1,29 @@
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=cfUE-lwyb3aMjxkw1P4iXfZnHdFd9t86BAuv9RkmgYo,513
3
+ fal/_fal_version.py,sha256=tH8KJgTzCjiwedQXB_0jJIzqyRjqKXz40ci9OEj6wNA,513
4
4
  fal/_serialization.py,sha256=rD2YiSa8iuzCaZohZwN_MPEB-PpSKbWRDeaIDpTEjyY,7653
5
5
  fal/_version.py,sha256=EBGqrknaf1WygENX-H4fBefLvHryvJBBGtVJetaB0NY,266
6
- fal/api.py,sha256=aoA0-7JsO6dWhudzmDOidbPwxnJmIJaQWhGV1kqLCbw,44814
7
- fal/app.py,sha256=YZBmQ45QbWCO7OmbNhlzM0AMI6STog3fMjdEtpkAS5g,23309
8
- fal/apps.py,sha256=RpmElElJnDYjsTRQOdNYiJwd74GEOGYA38L5O5GzNEg,11068
9
- fal/config.py,sha256=26IrSU5LYOGVSULrolAWe6oa7NE8Vk1DLI0-RwS2NIs,2596
6
+ fal/api.py,sha256=dNl6c_CvoFqG5tGXcA6nH6zhuHUDQY3KKLZfNYAIGGo,45400
7
+ fal/app.py,sha256=kHBe5QT7eeptMZ_X70vjPaeBG7Qv2u0a-mxvcEABHgQ,23402
8
+ fal/apps.py,sha256=pzCd2mrKl5J_4oVc40_pggvPtFahXBCdrZXWpnaEJVs,12130
9
+ fal/config.py,sha256=mS38EIwjR6h2x5wdrTU5E2hubSZm6D35Qigjteg0RJk,2707
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=lS-nVp35qwlovQWhmkhWhBmTMCG-ntMekdrIIgzK-2A,25423
15
+ fal/sdk.py,sha256=d50umE2XpmNwfOJ17wGfeFciTzj9IZksskk_IXCD4yg,25515
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=WSDXxkrshGyOvfN6WomHGKrflbmJvPiDUSQRsp4mqsI,5932
20
- fal/auth/auth0.py,sha256=_OhfrqF41odNebFTr8SvUm-d9REVG6wfQBHhPIQxsZU,5468
19
+ fal/auth/__init__.py,sha256=PqWNK1OmaRA_bfKP5ySBW-LQL9PqQCoRRgYNNR1xRhU,5593
20
+ fal/auth/auth0.py,sha256=g5OgEKe4rsbkLQp6l7EauOAVL6WsmKjuA1wmzmyvvhc,5354
21
21
  fal/auth/local.py,sha256=sndkM6vKpeVny6NHTacVlTbiIFqaksOmw0Viqs_RN1U,1790
22
22
  fal/cli/__init__.py,sha256=padK4o0BFqq61kxAA1qQ0jYr2SuhA2mf90B3AaRkmJA,37
23
23
  fal/cli/_utils.py,sha256=pHmKzpUWc2n4yPv4R0Y6DuZC5j-rVKU8oqdQDVW_-Lo,1591
24
- fal/cli/api.py,sha256=-rl50A00CxqVZtDh0iZmpCHMFY0jZySaumbPCe3MSoQ,2090
24
+ fal/cli/api.py,sha256=ZuDE_PIC-czzneTAWMwvC7P7WnwIyluNZSuJqzCFhqI,2640
25
25
  fal/cli/apps.py,sha256=vKeTUw_uUxz5M9hlP0jcJ23qjR0GTz7ifeS4HBjKECo,10101
26
- fal/cli/auth.py,sha256=uxcV2jZwxXuFfOdXecK_BTSAm86ftdMOrSJy01PzViQ,3979
26
+ fal/cli/auth.py,sha256=CIxeuDmZGGK1B2zvUMIGCprLEKY4XfjFVDqYSEz3vxA,4920
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
@@ -32,7 +32,7 @@ fal/cli/doctor.py,sha256=U4ne9LX5gQwNblsYQ27XdO8AYDgbYjTO39EtxhwexRM,983
32
32
  fal/cli/keys.py,sha256=7Sf4DT4le89G42eAOt0ltRjbZAtE70AVQ62hmjZhUy0,3059
33
33
  fal/cli/main.py,sha256=4TnIno7fvFJbMPlpb8mnT7meKAR-UAOerxuo5qqPZRQ,2234
34
34
  fal/cli/parser.py,sha256=jYsGQ0BLQuKI7KtN1jnLVYKMbLtez7hPjwTNfG3UPSk,2964
35
- fal/cli/profile.py,sha256=vWngqkX7UizQIUQOpXauFz1UGJwDeh38Si6wXcIj3Eo,3396
35
+ fal/cli/profile.py,sha256=_freBFQ0M7gCpHcmbmQfkcwmVelSnywYeeIX9tL6yCY,3392
36
36
  fal/cli/run.py,sha256=nAC12Qss4Fg1XmV0qOS9RdGNLYcdoHeRgQMvbTN4P9I,1202
37
37
  fal/cli/runners.py,sha256=z7WkZZC9rCW2mU5enowVQsxd1W18iBtLNOnPjrzhEf0,3491
38
38
  fal/cli/secrets.py,sha256=QKSmazu-wiNF6fOpGL9v2TDYxAjX9KTi7ot7vnv6f5E,2474
@@ -44,11 +44,11 @@ fal/exceptions/__init__.py,sha256=m2okJEpax11mnwmoqO_pCGtbt-FvzKiiuMhKo2ok-_8,27
44
44
  fal/exceptions/_base.py,sha256=LwzpMaW_eYQEC5s26h2qGXbNA-S4bOqC8s-bMCX6HjE,1491
45
45
  fal/exceptions/_cuda.py,sha256=q5EPFYEb7Iyw03cHrQlRHnH5xOvjwTwQdM6a9N3GB8k,1494
46
46
  fal/exceptions/auth.py,sha256=gxRago5coI__vSIcdcsqhhq1lRPkvCnwPAueIaXTAdw,329
47
- fal/logging/__init__.py,sha256=snqprf7-sKw6oAATS_Yxklf-a3XhLg0vIHICPwLp6TM,1583
48
- fal/logging/isolate.py,sha256=6Ec0gI-dTuxFcQYyhRselGXKVT9ZrRm2cphWiUu6jx4,2305
47
+ fal/logging/__init__.py,sha256=avgKA2V8GJeUtZuWZJjSYgkkrXKpuDMS-YiIBmLda7w,1599
48
+ fal/logging/isolate.py,sha256=jIryi46ZVlJ1mfan4HLNQQ3jwMi8z-WwfqqLlttQVkc,2449
49
49
  fal/logging/style.py,sha256=ckIgHzvF4DShM5kQh8F133X53z_vF46snuDHVmo_h9g,386
50
50
  fal/logging/trace.py,sha256=OhzB6d4rQZimBc18WFLqH_9BGfqFFumKKTAGSsmWRMg,1904
51
- fal/logging/user.py,sha256=0Xvb8n6tSb9l_V51VDzv6SOdYEFNouV_6nF_W9e7uNQ,642
51
+ fal/logging/user.py,sha256=H7Pg-nqhpzsUb5f6uXyZUeLWAsr3oImQEaYSCIIAlqo,818
52
52
  fal/toolkit/__init__.py,sha256=sV95wiUzKoiDqF9vDgq4q-BLa2sD6IpuKSqp5kdTQNE,658
53
53
  fal/toolkit/exceptions.py,sha256=elHZ7dHCJG5zlHGSBbz-ilkZe9QUvQMomJFi8Pt91LA,198
54
54
  fal/toolkit/optimize.py,sha256=p75sovF0SmRP6zxzpIaaOmqlxvXB_xEz3XPNf59EF7w,1339
@@ -134,8 +134,8 @@ openapi_fal_rest/models/workflow_node_type.py,sha256=-FzyeY2bxcNmizKbJI8joG7byRi
134
134
  openapi_fal_rest/models/workflow_schema.py,sha256=4K5gsv9u9pxx2ItkffoyHeNjBBYf6ur5bN4m_zePZNY,2019
135
135
  openapi_fal_rest/models/workflow_schema_input.py,sha256=2OkOXWHTNsCXHWS6EGDFzcJKkW5FIap-2gfO233EvZQ,1191
136
136
  openapi_fal_rest/models/workflow_schema_output.py,sha256=EblwSPAGfWfYVWw_WSSaBzQVju296is9o28rMBAd0mc,1196
137
- fal-1.12.1.dist-info/METADATA,sha256=vzyI4j0xP0i07iKKLustjN3P037QQM6bGxkzVSgRaKI,4062
138
- fal-1.12.1.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
139
- fal-1.12.1.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
140
- fal-1.12.1.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
141
- fal-1.12.1.dist-info/RECORD,,
137
+ fal-1.13.0.dist-info/METADATA,sha256=g-mxfo9dF7Wkc-yfWft2lgN-VD7L1IqnQ6Tr5OwDHdQ,4062
138
+ fal-1.13.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
139
+ fal-1.13.0.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
140
+ fal-1.13.0.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
141
+ fal-1.13.0.dist-info/RECORD,,
File without changes