fal 1.16.0__py3-none-any.whl → 1.17.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.16.0'
21
- __version_tuple__ = version_tuple = (1, 16, 0)
20
+ __version__ = version = '1.17.0'
21
+ __version_tuple__ = version_tuple = (1, 17, 0)
fal/_version.py CHANGED
@@ -1,6 +1,89 @@
1
+ import json
2
+ import os
3
+ import tempfile
4
+ from typing import Any, Dict, Optional
5
+
1
6
  try:
2
7
  from ._fal_version import version as __version__ # type: ignore[import]
3
8
  from ._fal_version import version_tuple # type: ignore[import]
4
9
  except ImportError:
5
10
  __version__ = "UNKNOWN"
6
11
  version_tuple = (0, 0, __version__) # type: ignore[assignment]
12
+
13
+
14
+ _PYPI_URL = "https://pypi.org/pypi/fal/json"
15
+ _PYPI_CACHE_TTL = 60 * 60 # 1 hour
16
+ _PYPI_CACHE_PATH = os.path.expanduser("~/.fal/cache/pypi.json")
17
+ _URLOPEN_TIMEOUT = 1
18
+
19
+
20
+ def _write_pypi_cache(data: Dict[str, Any]) -> None:
21
+ cache_dir = os.path.dirname(_PYPI_CACHE_PATH)
22
+ os.makedirs(cache_dir, exist_ok=True)
23
+ prefix = os.path.basename(_PYPI_CACHE_PATH) + ".tmp."
24
+ with tempfile.NamedTemporaryFile(
25
+ mode="w",
26
+ dir=cache_dir,
27
+ prefix=prefix,
28
+ delete=False,
29
+ ) as fobj:
30
+ fobj.write(json.dumps(data))
31
+ os.rename(fobj.name, _PYPI_CACHE_PATH)
32
+
33
+
34
+ def _get_pypi_cache() -> Optional[Dict[str, Any]]:
35
+ import time
36
+
37
+ try:
38
+ mtime = os.path.getmtime(_PYPI_CACHE_PATH)
39
+ except FileNotFoundError:
40
+ return None
41
+
42
+ if mtime + _PYPI_CACHE_TTL < time.time():
43
+ return None
44
+
45
+ with open(_PYPI_CACHE_PATH) as fobj:
46
+ try:
47
+ return json.load(fobj)
48
+ except ValueError:
49
+ return None
50
+
51
+
52
+ def _fetch_pypi_data() -> Dict[str, Any]:
53
+ from urllib.request import urlopen
54
+
55
+ response = urlopen(_PYPI_URL, timeout=_URLOPEN_TIMEOUT)
56
+ if response.status != 200:
57
+ raise Exception(f"Failed to fetch {_PYPI_URL}")
58
+
59
+ data = response.read()
60
+ return json.loads(data)
61
+
62
+
63
+ def get_latest_version() -> str:
64
+ from fal.logging import get_logger
65
+
66
+ logger = get_logger(__name__)
67
+
68
+ try:
69
+ data = _get_pypi_cache()
70
+ except Exception:
71
+ logger.warning("Failed to get pypi cache", exc_info=True)
72
+ data = None
73
+
74
+ if data is None:
75
+ try:
76
+ data = _fetch_pypi_data()
77
+ except Exception:
78
+ logger.warning("Failed to get latest fal version", exc_info=True)
79
+ data = {}
80
+
81
+ try:
82
+ _write_pypi_cache(data)
83
+ except Exception:
84
+ logger.warning("Failed to write pypi cache", exc_info=True)
85
+
86
+ try:
87
+ return data["info"]["version"]
88
+ except KeyError:
89
+ return "0.0.0"
fal/api.py CHANGED
@@ -501,12 +501,7 @@ class FalServerlessHost(Host):
501
501
  if isinstance(func, ServeWrapper):
502
502
  # Assigning in a separate property leaving a place for the user
503
503
  # to add more metadata in the future
504
- try:
505
- metadata["openapi"] = func.openapi()
506
- except Exception as e:
507
- print(
508
- f"[warning] Failed to generate OpenAPI metadata for function: {e}"
509
- )
504
+ metadata["openapi"] = func.openapi()
510
505
 
511
506
  for partial_result in self._connection.register(
512
507
  partial_func,
@@ -1169,7 +1164,12 @@ class BaseServable:
1169
1164
  Build the OpenAPI specification for the served function.
1170
1165
  Attach needed metadata for a better integration to fal.
1171
1166
  """
1172
- return self._build_app().openapi()
1167
+ try:
1168
+ return self._build_app().openapi()
1169
+ except Exception as e:
1170
+ raise FalServerlessException(
1171
+ "Failed to generate OpenAPI metadata for function"
1172
+ ) from e
1173
1173
 
1174
1174
  def serve(self) -> None:
1175
1175
  import asyncio
fal/app.py CHANGED
@@ -105,15 +105,12 @@ def wrap_app(cls: type[App], **kwargs) -> IsolatedFunction:
105
105
  app.serve()
106
106
 
107
107
  metadata = {}
108
- try:
109
- app = cls(_allow_init=True)
110
- metadata["openapi"] = app.openapi()
111
- except Exception:
112
- logger.warning("Failed to build OpenAPI specification for %s", cls.__name__)
113
- realtime_app = False
114
- else:
115
- routes = app.collect_routes()
116
- realtime_app = any(route.is_websocket for route in routes)
108
+ app = cls(_allow_init=True)
109
+
110
+ metadata["openapi"] = app.openapi()
111
+
112
+ routes = app.collect_routes()
113
+ realtime_app = any(route.is_websocket for route in routes)
117
114
 
118
115
  kind = cls.host_kwargs.pop("kind", "virtualenv")
119
116
  if kind == "container":
fal/auth/__init__.py CHANGED
@@ -63,8 +63,13 @@ def key_credentials() -> tuple[str, str] | None:
63
63
 
64
64
  key = os.environ.get("FAL_KEY") or config.get("key") or get_colab_token()
65
65
  if key:
66
- key_id, key_secret = key.split(":", 1)
67
- return (key_id, key_secret)
66
+ try:
67
+ key_id, key_secret = key.split(":", 1)
68
+ return (key_id, key_secret)
69
+ except ValueError:
70
+ print(f"Invalid key format: {key}")
71
+ return None
72
+
68
73
  elif "FAL_KEY_ID" in os.environ and "FAL_KEY_SECRET" in os.environ:
69
74
  return (os.environ["FAL_KEY_ID"], os.environ["FAL_KEY_SECRET"])
70
75
  else:
fal/cli/main.py CHANGED
@@ -77,11 +77,48 @@ def _print_error(msg):
77
77
  console.print(f"{CROSS_ICON} {msg}")
78
78
 
79
79
 
80
+ def _check_latest_version():
81
+ from packaging.version import parse
82
+ from rich.emoji import Emoji
83
+ from rich.panel import Panel
84
+ from rich.text import Text
85
+
86
+ from fal._version import get_latest_version, version_tuple
87
+
88
+ latest_version = get_latest_version()
89
+ parsed = parse(latest_version)
90
+ latest_version_tuple = (parsed.major, parsed.minor, parsed.micro)
91
+ if latest_version_tuple <= version_tuple:
92
+ return
93
+
94
+ if not console.is_terminal:
95
+ return
96
+
97
+ line1 = Text.assemble(
98
+ (Emoji.replace(":warning-emoji: "), "bold white"),
99
+ ("A new version of fal is available: ", "bold white"),
100
+ (latest_version, "bold green"),
101
+ )
102
+ line2 = Text.assemble(("pip install --upgrade fal", "bold cyan"))
103
+ line2.align("center", width=len(line1))
104
+
105
+ panel = Panel(
106
+ line1 + "\n\n" + line2,
107
+ border_style="yellow",
108
+ padding=(1, 2),
109
+ highlight=True,
110
+ expand=False,
111
+ )
112
+ console.print(panel)
113
+
114
+
80
115
  def main(argv=None) -> int:
81
116
  import grpc
82
117
 
83
118
  from fal.api import UserFunctionException
84
119
 
120
+ _check_latest_version()
121
+
85
122
  ret = 1
86
123
  try:
87
124
  args = parse_args(argv)
fal/cli/profile.py CHANGED
@@ -66,7 +66,7 @@ def _delete(args):
66
66
  if config.profile == args.PROFILE:
67
67
  config.set_internal("profile", None)
68
68
 
69
- config.delete(args.PROFILE)
69
+ config.delete_profile(args.PROFILE)
70
70
  args.console.print(f"Profile [cyan]{args.PROFILE}[/] deleted.")
71
71
 
72
72
 
fal/cli/runners.py CHANGED
@@ -21,17 +21,21 @@ def runners_table(runners: List[RunnerInfo]):
21
21
  table.add_column("Revision")
22
22
 
23
23
  for runner in runners:
24
+ external_metadata = runner.external_metadata
25
+ present = external_metadata.get("present_in_group", True)
26
+
24
27
  num_leases_with_request = len(
25
28
  [
26
29
  lease
27
- for lease in runner.external_metadata.get("leases", [])
30
+ for lease in external_metadata.get("leases", [])
28
31
  if lease.get("request_id") is not None
29
32
  ]
30
33
  )
31
34
 
32
35
  table.add_row(
33
36
  runner.alias,
34
- runner.runner_id,
37
+ # Mark lost runners in red
38
+ runner.runner_id if present else f"[red]{runner.runner_id}[/]",
35
39
  str(runner.in_flight_requests),
36
40
  str(runner.in_flight_requests - num_leases_with_request),
37
41
  (
fal/config.py CHANGED
@@ -99,7 +99,7 @@ class Config:
99
99
  def unset_internal(self, key: str) -> None:
100
100
  self._config.get(SETTINGS_SECTION, {}).pop(key, None)
101
101
 
102
- def delete(self, profile: str) -> None:
102
+ def delete_profile(self, profile: str) -> None:
103
103
  del self._config[profile]
104
104
 
105
105
  @contextmanager
fal/files.py CHANGED
@@ -28,26 +28,39 @@ class FalFileSystem(AbstractFileSystem):
28
28
  },
29
29
  )
30
30
 
31
+ def _ls(self, path):
32
+ response = self._client.get(f"/files/list/{path}")
33
+ response.raise_for_status()
34
+ files = response.json()
35
+ return sorted(
36
+ (
37
+ {
38
+ "name": entry["path"],
39
+ "size": entry["size"],
40
+ "type": "file" if entry["is_file"] else "directory",
41
+ "mtime": entry["updated_time"],
42
+ }
43
+ for entry in files
44
+ ),
45
+ key=lambda x: x["name"],
46
+ )
47
+
31
48
  def ls(self, path, detail=True, **kwargs):
32
- if path in self.dircache:
33
- entries = self.dircache[path]
49
+ abs_path = "/" + path.lstrip("/")
50
+ if abs_path in self.dircache:
51
+ entries = self.dircache[abs_path]
52
+ elif abs_path in ["/", "", "."]:
53
+ entries = [
54
+ {
55
+ "name": "/data",
56
+ "size": 0,
57
+ "type": "directory",
58
+ "mtime": 0,
59
+ }
60
+ ]
34
61
  else:
35
- response = self._client.get(f"/files/list/{path.lstrip('/')}")
36
- response.raise_for_status()
37
- files = response.json()
38
- entries = sorted(
39
- (
40
- {
41
- "name": entry["path"].lstrip("/data/"),
42
- "size": entry["size"],
43
- "type": "file" if entry["is_file"] else "directory",
44
- "mtime": entry["updated_time"],
45
- }
46
- for entry in files
47
- ),
48
- key=lambda x: x["name"],
49
- )
50
- self.dircache[path] = entries
62
+ entries = self._ls(abs_path)
63
+ self.dircache[abs_path] = entries
51
64
 
52
65
  if detail:
53
66
  return entries
@@ -68,7 +81,7 @@ class FalFileSystem(AbstractFileSystem):
68
81
  return
69
82
 
70
83
  with open(lpath, "wb") as fobj:
71
- response = self._client.get(f"/files/file/{rpath.lstrip('/')}")
84
+ response = self._client.get(f"/files/file/{rpath}")
72
85
  response.raise_for_status()
73
86
  fobj.write(response.content)
74
87
 
@@ -78,13 +91,13 @@ class FalFileSystem(AbstractFileSystem):
78
91
 
79
92
  with open(lpath, "rb") as fobj:
80
93
  response = self._client.post(
81
- f"/files/file/local/{rpath.lstrip('/')}",
94
+ f"/files/file/local/{rpath}",
82
95
  files={"file_upload": (posixpath.basename(lpath), fobj, "text/plain")},
83
96
  )
84
97
  response.raise_for_status()
85
98
  self.dircache.clear()
86
99
 
87
100
  def rm(self, path, **kwargs):
88
- response = self._client.delete(f"/files/file/{path.lstrip('/')}")
101
+ response = self._client.delete(f"/files/file/{path}")
89
102
  response.raise_for_status()
90
103
  self.dircache.clear()
@@ -0,0 +1,29 @@
1
+ from contextlib import asynccontextmanager
2
+
3
+ from anyio import create_task_group
4
+ from fastapi import Request
5
+
6
+
7
+ @asynccontextmanager
8
+ async def cancel_on_disconnect(request: Request):
9
+ """
10
+ Async context manager for async code that needs to be cancelled if client
11
+ disconnects prematurely.
12
+ The client disconnect is monitored through the Request object.
13
+ """
14
+ async with create_task_group() as tg:
15
+
16
+ async def watch_disconnect():
17
+ while True:
18
+ message = await request.receive()
19
+
20
+ if message["type"] == "http.disconnect":
21
+ tg.cancel_scope.cancel()
22
+ break
23
+
24
+ tg.start_soon(watch_disconnect)
25
+
26
+ try:
27
+ yield
28
+ finally:
29
+ tg.cancel_scope.cancel()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fal
3
- Version: 1.16.0
3
+ Version: 1.17.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,14 +1,14 @@
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=82WJEz-zkeAg7wgq_Qa170slIBg2QEGzstYPwtLKAR8,513
3
+ fal/_fal_version.py,sha256=L-yA9qIW9CPsU6jdmxu58Kyq_FoQoD9IW8plBJ9XLvQ,513
4
4
  fal/_serialization.py,sha256=npXNsFJ5G7jzBeBIyVMH01Ww34mGY4XWhHpRbSrTtnQ,7598
5
- fal/_version.py,sha256=EBGqrknaf1WygENX-H4fBefLvHryvJBBGtVJetaB0NY,266
6
- fal/api.py,sha256=cQuE9tr8lBHiBZ08DJD8jKfqjM0-deP4k1M24HS6mrQ,46335
7
- fal/app.py,sha256=R-DYIpB2GE0t_9GMvcARXjSBwAKDKG_QUhkMzCRwrsE,24322
5
+ fal/_version.py,sha256=1BbTFnucNC_6ldKJ_ZoC722_UkW4S9aDBSW9L0fkKAw,2315
6
+ fal/api.py,sha256=moDNT8wt20uzsI-NTEsbVTpjFXFkSuuRXJx7Apux3SI,46329
7
+ fal/app.py,sha256=S5VHxDaj5J9YVC8ECenHCZJlTHalapHyOyHbCBNsDfs,24153
8
8
  fal/apps.py,sha256=pzCd2mrKl5J_4oVc40_pggvPtFahXBCdrZXWpnaEJVs,12130
9
- fal/config.py,sha256=19Q7fymEkfxCd9AIy8SxhaQaRvb_vKvYAG3AeZAI6uk,3116
9
+ fal/config.py,sha256=BEMH10B2bfWJ9yNawnLG6v3kBLnLmkhMe201EAODzs4,3124
10
10
  fal/container.py,sha256=OvR-Zq-NPbYFHTnw0SBUUFxr890Fgbe68J2kSJEpLOk,1905
11
- fal/files.py,sha256=LHJxT4fs2jDs1hH26YoXdq77hUQp4IiaNJ0TE2-RFjo,2773
11
+ fal/files.py,sha256=gBQi1KJ91KURCFNMoGn8GfxCx0n5k0i-TXHWtOQp-ro,3022
12
12
  fal/flags.py,sha256=48pgtc9xb4LMpR9RE5KG2A2sH7zQRk_VjrgpND-H4Tc,942
13
13
  fal/project.py,sha256=QgfYfMKmNobMPufrAP_ga1FKcIAlSbw18Iar1-0qepo,2650
14
14
  fal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -17,7 +17,7 @@ fal/sdk.py,sha256=OvNgoV6ERnFup7ulylBDSohiXQpBa1ycqNuycPZb1-Q,25816
17
17
  fal/sync.py,sha256=ZuIJA2-hTPNANG9B_NNJZUsO68EIdTH0dc9MzeVE2VU,4340
18
18
  fal/utils.py,sha256=iQTBG3-i6JZgHkkwbY_I4210g0xoW-as51yrke608u0,2208
19
19
  fal/workflows.py,sha256=Zl4f6Bs085hY40zmqScxDUyCu7zXkukDbW02iYOLTTI,14805
20
- fal/auth/__init__.py,sha256=2tki_o_IaQbaZeCTDAS1wBtrvcAOPRTQMtPSXAqk_Ig,6157
20
+ fal/auth/__init__.py,sha256=2mEKdk6_1GclF3cPC3uWSRKFf0KHNIUNAi0xYRbdJ1A,6278
21
21
  fal/auth/auth0.py,sha256=g5OgEKe4rsbkLQp6l7EauOAVL6WsmKjuA1wmzmyvvhc,5354
22
22
  fal/auth/local.py,sha256=sndkM6vKpeVny6NHTacVlTbiIFqaksOmw0Viqs_RN1U,1790
23
23
  fal/cli/__init__.py,sha256=padK4o0BFqq61kxAA1qQ0jYr2SuhA2mf90B3AaRkmJA,37
@@ -32,11 +32,11 @@ fal/cli/deploy.py,sha256=CWf0Y56w-hNCrht-qrfgiOi9nuvve1Kl5NFZJpt_oRA,7770
32
32
  fal/cli/doctor.py,sha256=U4ne9LX5gQwNblsYQ27XdO8AYDgbYjTO39EtxhwexRM,983
33
33
  fal/cli/files.py,sha256=zOJeRy1W1CsNw0QMxt2vT8Q352phh3l4lZSOLiTQa2w,1968
34
34
  fal/cli/keys.py,sha256=7Sf4DT4le89G42eAOt0ltRjbZAtE70AVQ62hmjZhUy0,3059
35
- fal/cli/main.py,sha256=CNh-i1xL0G2pbYMsk0VUC6qsxBT9rrQuLCIeDSiRuQs,2260
35
+ fal/cli/main.py,sha256=ao8EEV_Fkd7AdN5En6k_dZWp158Et5DrqNRutl98MHY,3273
36
36
  fal/cli/parser.py,sha256=jYsGQ0BLQuKI7KtN1jnLVYKMbLtez7hPjwTNfG3UPSk,2964
37
- fal/cli/profile.py,sha256=SzAswTckfc5qhpVsFZIQedzGYHkH8wWaXwQxwe5mc8E,3937
37
+ fal/cli/profile.py,sha256=lYOz0S1kr5DW4_r5pB5cEaCHsUEbiPQFmm8HT6_bx9k,3945
38
38
  fal/cli/run.py,sha256=nAC12Qss4Fg1XmV0qOS9RdGNLYcdoHeRgQMvbTN4P9I,1202
39
- fal/cli/runners.py,sha256=z7WkZZC9rCW2mU5enowVQsxd1W18iBtLNOnPjrzhEf0,3491
39
+ fal/cli/runners.py,sha256=7efNX9vm6D1aBlg0M5-u5plw3HHC41Sj-N7eRNIHnqw,3689
40
40
  fal/cli/secrets.py,sha256=QKSmazu-wiNF6fOpGL9v2TDYxAjX9KTi7ot7vnv6f5E,2474
41
41
  fal/cli/teams.py,sha256=6fR2rKJtiUJPThP7QsO4NLo9UdhUxraGvQZk3_Di6Ow,1218
42
42
  fal/console/__init__.py,sha256=ernZ4bzvvliQh5SmrEqQ7lA5eVcbw6Ra2jalKtA7dxg,132
@@ -72,6 +72,7 @@ fal/toolkit/image/nsfw_filter/model.py,sha256=63mu8D15z_IosoRUagRLGHy6VbLqFmrG-y
72
72
  fal/toolkit/image/nsfw_filter/requirements.txt,sha256=3Pmrd0Ny6QAeBqUNHCgffRyfaCARAPJcfSCX5cRYpbM,37
73
73
  fal/toolkit/utils/__init__.py,sha256=CrmM9DyCz5-SmcTzRSm5RaLgxy3kf0ZsSEN9uhnX2Xo,97
74
74
  fal/toolkit/utils/download_utils.py,sha256=NgOMNs-bQGSg3gWnu123BgZitJgJrvtRexIefTMuylY,19739
75
+ fal/toolkit/utils/endpoint.py,sha256=5EXoshA2PD_brjEfhNWAWasjqLOCRrjBnfhj6QGuMt8,782
75
76
  fal/toolkit/utils/retry.py,sha256=mHcQvvNIpu-Hi29P1HXSZuyvolRd48dMaJToqzlG0NY,1353
76
77
  openapi_fal_rest/__init__.py,sha256=ziculmF_i6trw63LzZGFX-6W3Lwq9mCR8_UpkpvpaHI,152
77
78
  openapi_fal_rest/client.py,sha256=G6BpJg9j7-JsrAUGddYwkzeWRYickBjPdcVgXoPzxuE,2817
@@ -136,8 +137,8 @@ openapi_fal_rest/models/workflow_node_type.py,sha256=-FzyeY2bxcNmizKbJI8joG7byRi
136
137
  openapi_fal_rest/models/workflow_schema.py,sha256=4K5gsv9u9pxx2ItkffoyHeNjBBYf6ur5bN4m_zePZNY,2019
137
138
  openapi_fal_rest/models/workflow_schema_input.py,sha256=2OkOXWHTNsCXHWS6EGDFzcJKkW5FIap-2gfO233EvZQ,1191
138
139
  openapi_fal_rest/models/workflow_schema_output.py,sha256=EblwSPAGfWfYVWw_WSSaBzQVju296is9o28rMBAd0mc,1196
139
- fal-1.16.0.dist-info/METADATA,sha256=ofJflcV0SxzRjZCXOAOQTC3mYGknv4sjdhlbIBXcNe8,4084
140
- fal-1.16.0.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
141
- fal-1.16.0.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
142
- fal-1.16.0.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
143
- fal-1.16.0.dist-info/RECORD,,
140
+ fal-1.17.0.dist-info/METADATA,sha256=cxh2muMvhGGT7OCuoNXAw2Es7qlRdPkdrpAdX6TzYF8,4084
141
+ fal-1.17.0.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
142
+ fal-1.17.0.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
143
+ fal-1.17.0.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
144
+ fal-1.17.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.7.1)
2
+ Generator: setuptools (80.8.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5