fal 1.0.7__py3-none-any.whl → 1.1.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.

Files changed (50) hide show
  1. fal/_fal_version.py +2 -2
  2. fal/api.py +116 -29
  3. fal/app.py +63 -1
  4. fal/cli/deploy.py +18 -8
  5. fal/cli/doctor.py +37 -0
  6. fal/cli/main.py +2 -2
  7. fal/sdk.py +6 -2
  8. fal/toolkit/file/providers/fal.py +1 -0
  9. fal/workflows.py +1 -1
  10. {fal-1.0.7.dist-info → fal-1.1.0.dist-info}/METADATA +2 -1
  11. {fal-1.0.7.dist-info → fal-1.1.0.dist-info}/RECORD +49 -27
  12. {fal-1.0.7.dist-info → fal-1.1.0.dist-info}/WHEEL +1 -1
  13. openapi_fal_rest/api/comfy/__init__.py +0 -0
  14. openapi_fal_rest/api/comfy/create_workflow.py +172 -0
  15. openapi_fal_rest/api/comfy/delete_workflow.py +175 -0
  16. openapi_fal_rest/api/comfy/get_workflow.py +181 -0
  17. openapi_fal_rest/api/comfy/list_user_workflows.py +189 -0
  18. openapi_fal_rest/api/comfy/update_workflow.py +198 -0
  19. openapi_fal_rest/api/users/__init__.py +0 -0
  20. openapi_fal_rest/api/users/get_current_user.py +143 -0
  21. openapi_fal_rest/api/workflows/{create_or_update_workflow_workflows_post.py → create_workflow.py} +4 -4
  22. openapi_fal_rest/api/workflows/{get_workflows_workflows_get.py → list_user_workflows.py} +4 -4
  23. openapi_fal_rest/api/workflows/update_workflow.py +198 -0
  24. openapi_fal_rest/models/__init__.py +32 -10
  25. openapi_fal_rest/models/comfy_workflow_detail.py +109 -0
  26. openapi_fal_rest/models/comfy_workflow_item.py +88 -0
  27. openapi_fal_rest/models/comfy_workflow_schema.py +119 -0
  28. openapi_fal_rest/models/{execute_workflow_workflows_user_id_workflow_name_post_json_body_type_0.py → comfy_workflow_schema_extra_data.py} +5 -5
  29. openapi_fal_rest/models/{execute_workflow_workflows_user_id_workflow_name_post_response_200_type_0.py → comfy_workflow_schema_fal_inputs.py} +5 -5
  30. openapi_fal_rest/models/comfy_workflow_schema_fal_inputs_dev_info.py +44 -0
  31. openapi_fal_rest/models/{workflow_detail_contents_type_0.py → comfy_workflow_schema_prompt.py} +5 -5
  32. openapi_fal_rest/models/current_user.py +138 -0
  33. openapi_fal_rest/models/customer_details.py +8 -8
  34. openapi_fal_rest/models/lock_reason.py +3 -0
  35. openapi_fal_rest/models/page_comfy_workflow_item.py +107 -0
  36. openapi_fal_rest/models/team_role.py +10 -0
  37. openapi_fal_rest/models/typed_comfy_workflow.py +85 -0
  38. openapi_fal_rest/models/typed_comfy_workflow_update.py +95 -0
  39. openapi_fal_rest/models/typed_workflow_update.py +95 -0
  40. openapi_fal_rest/models/user_member.py +87 -0
  41. openapi_fal_rest/models/workflow_contents.py +20 -1
  42. openapi_fal_rest/models/workflow_contents_metadata.py +44 -0
  43. openapi_fal_rest/models/workflow_detail.py +18 -59
  44. openapi_fal_rest/models/workflow_detail_contents.py +44 -0
  45. openapi_fal_rest/models/workflow_item.py +19 -1
  46. openapi_fal_rest/api/workflows/execute_workflow_workflows_user_id_workflow_name_post.py +0 -268
  47. {fal-1.0.7.dist-info → fal-1.1.0.dist-info}/entry_points.txt +0 -0
  48. {fal-1.0.7.dist-info → fal-1.1.0.dist-info}/top_level.txt +0 -0
  49. /openapi_fal_rest/api/workflows/{delete_workflow_workflows_user_id_workflow_name_delete.py → delete_workflow.py} +0 -0
  50. /openapi_fal_rest/api/workflows/{get_workflow_workflows_user_id_workflow_name_get.py → get_workflow.py} +0 -0
fal/_fal_version.py CHANGED
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '1.0.7'
16
- __version_tuple__ = version_tuple = (1, 0, 7)
15
+ __version__ = version = '1.1.0'
16
+ __version_tuple__ = version_tuple = (1, 1, 0)
fal/api.py CHANGED
@@ -5,11 +5,12 @@ import os
5
5
  import sys
6
6
  import threading
7
7
  from collections import defaultdict
8
- from concurrent.futures import ThreadPoolExecutor
8
+ from concurrent.futures import Future, ThreadPoolExecutor
9
9
  from contextlib import asynccontextmanager, suppress
10
10
  from dataclasses import dataclass, field, replace
11
11
  from functools import wraps
12
12
  from os import PathLike
13
+ from queue import Queue
13
14
  from typing import (
14
15
  Any,
15
16
  Callable,
@@ -72,6 +73,9 @@ SERVE_REQUIREMENTS = [
72
73
  ]
73
74
 
74
75
 
76
+ THREAD_POOL = ThreadPoolExecutor()
77
+
78
+
75
79
  @dataclass
76
80
  class FalServerlessError(FalServerlessException):
77
81
  message: str
@@ -83,8 +87,32 @@ class InternalFalServerlessError(FalServerlessException):
83
87
 
84
88
 
85
89
  @dataclass
86
- class FalMissingDependencyError(FalServerlessError):
87
- ...
90
+ class FalMissingDependencyError(FalServerlessError): ...
91
+
92
+
93
+ @dataclass
94
+ class SpawnInfo:
95
+ future: Future | None = None
96
+ logs: Queue = field(default_factory=Queue)
97
+ _url_ready: threading.Event = field(default_factory=threading.Event)
98
+ _url: str | None = None
99
+ stream: Any = None
100
+
101
+ @property
102
+ def return_value(self):
103
+ if self.future is None:
104
+ raise ValueError
105
+ return self.future.result()
106
+
107
+ @property
108
+ def url(self):
109
+ self._url_ready.wait()
110
+ return self._url
111
+
112
+ @url.setter
113
+ def url(self, value):
114
+ self._url_ready.set()
115
+ self._url = value
88
116
 
89
117
 
90
118
  @dataclass
@@ -150,6 +178,15 @@ class Host(Generic[ArgsT, ReturnT]):
150
178
  """Run the given function in the isolated environment."""
151
179
  raise NotImplementedError
152
180
 
181
+ def spawn(
182
+ self,
183
+ func: Callable[ArgsT, ReturnT],
184
+ options: Options,
185
+ args: tuple[Any, ...],
186
+ kwargs: dict[str, Any],
187
+ ) -> SpawnInfo:
188
+ raise NotImplementedError
189
+
153
190
 
154
191
  def cached(func: Callable[ArgsT, ReturnT]) -> Callable[ArgsT, ReturnT]:
155
192
  """Cache the result of the given function in-memory."""
@@ -444,12 +481,13 @@ class FalServerlessHost(Host):
444
481
  return None
445
482
 
446
483
  @_handle_grpc_error()
447
- def run(
484
+ def _run(
448
485
  self,
449
486
  func: Callable[..., ReturnT],
450
487
  options: Options,
451
488
  args: tuple[Any, ...],
452
489
  kwargs: dict[str, Any],
490
+ result_handler: Callable[..., None],
453
491
  ) -> ReturnT:
454
492
  environment_options = options.environment.copy()
455
493
  environment_options.setdefault("python_version", active_python())
@@ -490,8 +528,7 @@ class FalServerlessHost(Host):
490
528
  machine_requirements=machine_requirements,
491
529
  setup_function=setup_function,
492
530
  ):
493
- for log in partial_result.logs:
494
- self._log_printer.print(log)
531
+ result_handler(partial_result)
495
532
 
496
533
  if partial_result.status.state is not HostedRunState.IN_PROGRESS:
497
534
  state = partial_result.status.state
@@ -511,6 +548,46 @@ class FalServerlessHost(Host):
511
548
 
512
549
  return cast(ReturnT, return_value)
513
550
 
551
+ def run(
552
+ self,
553
+ func: Callable[..., ReturnT],
554
+ options: Options,
555
+ args: tuple[Any, ...],
556
+ kwargs: dict[str, Any],
557
+ ) -> ReturnT:
558
+ def result_handler(partial_result):
559
+ for log in partial_result.logs:
560
+ self._log_printer.print(log)
561
+
562
+ return self._run(func, options, args, kwargs, result_handler=result_handler)
563
+
564
+ def spawn(
565
+ self,
566
+ func: Callable[..., ReturnT],
567
+ options: Options,
568
+ args: tuple[Any, ...],
569
+ kwargs: dict[str, Any],
570
+ ) -> SpawnInfo:
571
+ ret = SpawnInfo()
572
+
573
+ def result_handler(partial_result):
574
+ ret.stream = partial_result.stream
575
+ for log in partial_result.logs:
576
+ if "Access your exposed service at" in log.message:
577
+ ret.url = log.message.rsplit()[-1]
578
+ ret.logs.put(log)
579
+
580
+ THREAD_POOL.submit(
581
+ self._run,
582
+ func,
583
+ options,
584
+ args,
585
+ kwargs,
586
+ result_handler=result_handler,
587
+ )
588
+
589
+ return ret
590
+
514
591
 
515
592
  @dataclass
516
593
  class Options:
@@ -566,8 +643,7 @@ def function(
566
643
  max_concurrency: int | None = None,
567
644
  ) -> Callable[
568
645
  [Callable[Concatenate[ArgsT], ReturnT]], IsolatedFunction[ArgsT, ReturnT]
569
- ]:
570
- ...
646
+ ]: ...
571
647
 
572
648
 
573
649
  @overload
@@ -583,8 +659,7 @@ def function(
583
659
  max_concurrency: int | None = None,
584
660
  ) -> Callable[
585
661
  [Callable[Concatenate[ArgsT], ReturnT]], ServedIsolatedFunction[ArgsT, ReturnT]
586
- ]:
587
- ...
662
+ ]: ...
588
663
 
589
664
 
590
665
  ### FalServerlessHost
@@ -610,8 +685,7 @@ def function(
610
685
  _scheduler: str | None = None,
611
686
  ) -> Callable[
612
687
  [Callable[Concatenate[ArgsT], ReturnT]], IsolatedFunction[ArgsT, ReturnT]
613
- ]:
614
- ...
688
+ ]: ...
615
689
 
616
690
 
617
691
  @overload
@@ -636,8 +710,7 @@ def function(
636
710
  _scheduler: str | None = None,
637
711
  ) -> Callable[
638
712
  [Callable[Concatenate[ArgsT], ReturnT]], ServedIsolatedFunction[ArgsT, ReturnT]
639
- ]:
640
- ...
713
+ ]: ...
641
714
 
642
715
 
643
716
  ## conda
@@ -660,8 +733,7 @@ def function(
660
733
  max_concurrency: int | None = None,
661
734
  ) -> Callable[
662
735
  [Callable[Concatenate[ArgsT], ReturnT]], IsolatedFunction[ArgsT, ReturnT]
663
- ]:
664
- ...
736
+ ]: ...
665
737
 
666
738
 
667
739
  @overload
@@ -682,8 +754,7 @@ def function(
682
754
  max_concurrency: int | None = None,
683
755
  ) -> Callable[
684
756
  [Callable[Concatenate[ArgsT], ReturnT]], ServedIsolatedFunction[ArgsT, ReturnT]
685
- ]:
686
- ...
757
+ ]: ...
687
758
 
688
759
 
689
760
  ### FalServerlessHost
@@ -714,8 +785,7 @@ def function(
714
785
  _scheduler: str | None = None,
715
786
  ) -> Callable[
716
787
  [Callable[Concatenate[ArgsT], ReturnT]], IsolatedFunction[ArgsT, ReturnT]
717
- ]:
718
- ...
788
+ ]: ...
719
789
 
720
790
 
721
791
  @overload
@@ -889,6 +959,9 @@ class BaseServable:
889
959
  yield
890
960
 
891
961
  def _build_app(self) -> FastAPI:
962
+ import json
963
+ import traceback
964
+
892
965
  from fastapi import HTTPException, Request
893
966
  from fastapi.middleware.cors import CORSMiddleware
894
967
  from fastapi.responses import JSONResponse
@@ -929,6 +1002,15 @@ class BaseServable:
929
1002
  # If it's not a generic 404, just return the original message.
930
1003
  return JSONResponse({"detail": exc.detail}, 404)
931
1004
 
1005
+ @_app.exception_handler(Exception)
1006
+ async def traceback_logging_exception_handler(request: Request, exc: Exception):
1007
+ print(
1008
+ json.dumps(
1009
+ {"traceback": "".join(traceback.format_exception(exc)[::-1])} # type: ignore
1010
+ )
1011
+ )
1012
+ return JSONResponse({"detail": "Internal Server Error"}, 500)
1013
+
932
1014
  routes = self.collect_routes()
933
1015
  if not routes:
934
1016
  raise ValueError("An application must have at least one route!")
@@ -978,7 +1060,8 @@ class BaseServable:
978
1060
  }
979
1061
 
980
1062
  _, pending = await asyncio.wait(
981
- tasks.keys(), return_when=asyncio.FIRST_COMPLETED,
1063
+ tasks.keys(),
1064
+ return_when=asyncio.FIRST_COMPLETED,
982
1065
  )
983
1066
  if not pending:
984
1067
  return
@@ -1052,6 +1135,14 @@ class IsolatedFunction(Generic[ArgsT, ReturnT]):
1052
1135
  )
1053
1136
  return future
1054
1137
 
1138
+ def spawn(self, *args: ArgsT.args, **kwargs: ArgsT.kwargs):
1139
+ return self.host.spawn(
1140
+ self.func,
1141
+ self.options,
1142
+ args,
1143
+ kwargs,
1144
+ )
1145
+
1055
1146
  def __call__(self, *args: ArgsT.args, **kwargs: ArgsT.kwargs) -> ReturnT:
1056
1147
  try:
1057
1148
  return self.host.run(
@@ -1089,14 +1180,12 @@ class IsolatedFunction(Generic[ArgsT, ReturnT]):
1089
1180
  @overload
1090
1181
  def on(
1091
1182
  self, host: Host | None = None, *, serve: Literal[False] = False, **config: Any
1092
- ) -> IsolatedFunction[ArgsT, ReturnT]:
1093
- ...
1183
+ ) -> IsolatedFunction[ArgsT, ReturnT]: ...
1094
1184
 
1095
1185
  @overload
1096
1186
  def on(
1097
1187
  self, host: Host | None = None, *, serve: Literal[True], **config: Any
1098
- ) -> ServedIsolatedFunction[ArgsT, ReturnT]:
1099
- ...
1188
+ ) -> ServedIsolatedFunction[ArgsT, ReturnT]: ...
1100
1189
 
1101
1190
  def on(self, host: Host | None = None, **config: Any): # type: ignore
1102
1191
  host = host or self.host
@@ -1150,14 +1239,12 @@ class ServedIsolatedFunction(
1150
1239
  @overload # type: ignore[override,no-overload-impl]
1151
1240
  def on( # type: ignore[no-overload-impl]
1152
1241
  self, host: Host | None = None, *, serve: Literal[True] = True, **config: Any
1153
- ) -> ServedIsolatedFunction[ArgsT, ReturnT]:
1154
- ...
1242
+ ) -> ServedIsolatedFunction[ArgsT, ReturnT]: ...
1155
1243
 
1156
1244
  @overload
1157
1245
  def on(
1158
1246
  self, host: Host | None = None, *, serve: Literal[False], **config: Any
1159
- ) -> IsolatedFunction[ArgsT, ReturnT]:
1160
- ...
1247
+ ) -> IsolatedFunction[ArgsT, ReturnT]: ...
1161
1248
 
1162
1249
 
1163
1250
  class Server(uvicorn.Server):
fal/app.py CHANGED
@@ -4,10 +4,12 @@ import inspect
4
4
  import json
5
5
  import os
6
6
  import re
7
+ import time
7
8
  import typing
8
- from contextlib import asynccontextmanager
9
+ from contextlib import asynccontextmanager, contextmanager
9
10
  from typing import Any, Callable, ClassVar, TypeVar
10
11
 
12
+ import httpx
11
13
  from fastapi import FastAPI
12
14
 
13
15
  import fal.api
@@ -69,6 +71,66 @@ def wrap_app(cls: type[App], **kwargs) -> fal.api.IsolatedFunction:
69
71
  return fn
70
72
 
71
73
 
74
+ class EndpointClient:
75
+ def __init__(self, url, endpoint, signature):
76
+ self.url = url
77
+ self.endpoint = endpoint
78
+ self.signature = signature
79
+
80
+ annotations = endpoint.__annotations__ or {}
81
+ self.return_type = annotations.get("return") or None
82
+
83
+ def __call__(self, data):
84
+ with httpx.Client() as client:
85
+ resp = client.post(self.url + self.signature.path, json=dict(data))
86
+ resp.raise_for_status()
87
+ resp_dict = resp.json()
88
+
89
+ if not self.return_type:
90
+ return resp_dict
91
+
92
+ return self.return_type(**resp_dict)
93
+
94
+
95
+ class AppClient:
96
+ def __init__(self, cls, url):
97
+ self.url = url
98
+ self.cls = cls
99
+
100
+ for name, endpoint in inspect.getmembers(cls, inspect.isfunction):
101
+ signature = getattr(endpoint, "route_signature", None)
102
+ if signature is None:
103
+ continue
104
+
105
+ setattr(self, name, EndpointClient(self.url, endpoint, signature))
106
+
107
+ @classmethod
108
+ @contextmanager
109
+ def connect(cls, app_cls):
110
+ app = wrap_app(app_cls)
111
+ info = app.spawn()
112
+ try:
113
+ with httpx.Client() as client:
114
+ retries = 100
115
+ while retries:
116
+ resp = client.get(info.url + "/health")
117
+ if resp.is_success:
118
+ break
119
+ elif resp.status_code != 500:
120
+ resp.raise_for_status()
121
+ time.sleep(0.1)
122
+ retries -= 1
123
+
124
+ yield cls(app_cls, info.url)
125
+ finally:
126
+ info.stream.cancel()
127
+
128
+ def health(self):
129
+ with httpx.Client() as client:
130
+ resp = client.get(self.url + "/health")
131
+ resp.raise_for_status()
132
+ return resp.json()
133
+
72
134
 
73
135
  PART_FINDER_RE = re.compile(r"[A-Z][a-z]*")
74
136
 
fal/cli/deploy.py CHANGED
@@ -1,7 +1,11 @@
1
+ import argparse
2
+ from collections import namedtuple
1
3
  from pathlib import Path
2
4
 
3
5
  from .parser import FalClientParser, RefAction
4
6
 
7
+ User = namedtuple("User", ["user_id", "username"])
8
+
5
9
 
6
10
  def _remove_http_and_port_from_url(url):
7
11
  # Remove http://
@@ -20,17 +24,17 @@ def _remove_http_and_port_from_url(url):
20
24
  return url
21
25
 
22
26
 
23
- def _get_user_id() -> str:
27
+ def _get_user() -> User:
24
28
  import json
25
29
  from http import HTTPStatus
26
30
 
27
- import openapi_fal_rest.api.billing.get_user_details as get_user_details
31
+ import openapi_fal_rest.api.users.get_current_user as get_current_user
28
32
 
29
33
  from fal.api import FalServerlessError
30
34
  from fal.rest_client import REST_CLIENT
31
35
 
32
36
  try:
33
- user_details_response = get_user_details.sync_detailed(
37
+ user_details_response = get_current_user.sync_detailed(
34
38
  client=REST_CLIENT,
35
39
  )
36
40
  except Exception as e:
@@ -51,7 +55,7 @@ def _get_user_id() -> str:
51
55
  if not user_id:
52
56
  user_id = full_user_id
53
57
 
54
- return user_id
58
+ return User(user_id=user_id, username=user_details_response.parsed.nickname)
55
59
  except Exception as e:
56
60
  raise FalServerlessError(f"Could not parse the user data: {e}")
57
61
 
@@ -77,7 +81,7 @@ def _deploy(args):
77
81
  [file_path] = options
78
82
  file_path = str(file_path)
79
83
 
80
- user_id = _get_user_id()
84
+ user = _get_user()
81
85
  host = FalServerlessHost(args.host)
82
86
  isolated_function, app_name = load_function_from(
83
87
  host,
@@ -103,12 +107,18 @@ def _deploy(args):
103
107
  "Registered a new revision for function "
104
108
  f"'{app_name}' (revision='{app_id}')."
105
109
  )
106
- args.console.print(f"URL: https://{gateway_host}/{user_id}/{app_name}")
110
+ args.console.print(f"Playground: https://fal.ai/models/{user.username}/{app_name}")
111
+ args.console.print(f"Endpoint: https://{gateway_host}/{user.username}/{app_name}")
107
112
 
108
113
 
109
114
  def add_parser(main_subparsers, parents):
110
115
  from fal.sdk import ALIAS_AUTH_MODES
111
116
 
117
+ def valid_auth_option(option):
118
+ if option not in ALIAS_AUTH_MODES:
119
+ raise argparse.ArgumentTypeError(f"{option} is not a auth option")
120
+ return option
121
+
112
122
  deploy_help = "Deploy a fal application."
113
123
  epilog = (
114
124
  "Examples:\n"
@@ -139,8 +149,8 @@ def add_parser(main_subparsers, parents):
139
149
  )
140
150
  parser.add_argument(
141
151
  "--auth",
142
- choices=ALIAS_AUTH_MODES,
152
+ type=valid_auth_option,
143
153
  default="private",
144
- help="Application authentication mode.",
154
+ help="Application authentication mode (private, public).",
145
155
  )
146
156
  parser.set_defaults(func=_deploy)
fal/cli/doctor.py ADDED
@@ -0,0 +1,37 @@
1
+ import os
2
+ import platform
3
+
4
+
5
+ def _doctor(args):
6
+ import isolate
7
+ from rich.table import Table
8
+
9
+ import fal
10
+
11
+ table = Table(show_header=False, show_lines=False, box=None)
12
+ table.add_column("name", no_wrap=True, style="bold")
13
+ table.add_column("value", no_wrap=True)
14
+
15
+ table.add_row("fal", fal.__version__)
16
+ table.add_row("isolate", isolate.__version__)
17
+
18
+ table.add_row("", "")
19
+ table.add_row("python", platform.python_version())
20
+ table.add_row("platform", platform.platform())
21
+
22
+ table.add_row("", "")
23
+ table.add_row("FAL_HOST", fal.flags.GRPC_HOST)
24
+ table.add_row("FAL_KEY", os.getenv("FAL_KEY", "").split(":")[0])
25
+
26
+ args.console.print(table)
27
+
28
+
29
+ def add_parser(main_subparsers, parents):
30
+ doctor_help = "fal version and misc environment information."
31
+ parser = main_subparsers.add_parser(
32
+ "doctor",
33
+ description=doctor_help,
34
+ help=doctor_help,
35
+ parents=parents,
36
+ )
37
+ parser.set_defaults(func=_doctor)
fal/cli/main.py CHANGED
@@ -6,7 +6,7 @@ from fal import __version__
6
6
  from fal.console import console
7
7
  from fal.console.icons import CROSS_ICON
8
8
 
9
- from . import apps, auth, deploy, keys, run, secrets
9
+ from . import apps, auth, deploy, doctor, keys, run, secrets
10
10
  from .debug import debugtools, get_debug_parser
11
11
  from .parser import FalParser, FalParserExit
12
12
 
@@ -31,7 +31,7 @@ def _get_main_parser() -> argparse.ArgumentParser:
31
31
  required=True,
32
32
  )
33
33
 
34
- for cmd in [auth, apps, deploy, run, keys, secrets]:
34
+ for cmd in [auth, apps, deploy, run, keys, secrets, doctor]:
35
35
  cmd.add_parser(subparsers, parents)
36
36
 
37
37
  return parser
fal/sdk.py CHANGED
@@ -224,6 +224,7 @@ class HostedRunResult(Generic[ResultT]):
224
224
  status: HostedRunStatus
225
225
  logs: list[Log] = field(default_factory=list)
226
226
  result: ResultT | None = None
227
+ stream: Any = None
227
228
 
228
229
 
229
230
  @dataclass
@@ -569,8 +570,11 @@ class FalServerlessConnection:
569
570
  request.setup_func.MergeFrom(
570
571
  to_serialized_object(setup_function, serialization_method)
571
572
  )
572
- for partial_result in self.stub.Run(request):
573
- yield from_grpc(partial_result)
573
+ stream = self.stub.Run(request)
574
+ for partial_result in stream:
575
+ res = from_grpc(partial_result)
576
+ res.stream = stream
577
+ yield res
574
578
 
575
579
  def create_alias(
576
580
  self,
@@ -98,6 +98,7 @@ class FalCDNFileRepository(FileRepository):
98
98
  **self.auth_headers,
99
99
  "Accept": "application/json",
100
100
  "Content-Type": file.content_type,
101
+ "X-Fal-File-Name": file.file_name,
101
102
  "X-Fal-Object-Lifecycle-Preference": json.dumps(
102
103
  dataclasses.asdict(GLOBAL_LIFECYCLE_PREFERENCE)
103
104
  ),
fal/workflows.py CHANGED
@@ -10,7 +10,7 @@ from typing import Any, Iterator, Union, cast
10
10
  import graphlib
11
11
  import rich
12
12
  from openapi_fal_rest.api.workflows import (
13
- create_or_update_workflow_workflows_post as publish_workflow,
13
+ create_workflow as publish_workflow,
14
14
  )
15
15
  from openapi_fal_rest.models.http_validation_error import HTTPValidationError
16
16
  from pydantic import BaseModel
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fal
3
- Version: 1.0.7
3
+ Version: 1.1.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
@@ -43,6 +43,7 @@ Requires-Dist: pytest <8 ; extra == 'test'
43
43
  Requires-Dist: pytest-asyncio ; extra == 'test'
44
44
  Requires-Dist: pytest-xdist ; extra == 'test'
45
45
  Requires-Dist: flaky ; extra == 'test'
46
+ Requires-Dist: boto3 ; extra == 'test'
46
47
 
47
48
  [![PyPI](https://img.shields.io/pypi/v/fal.svg?logo=PyPI)](https://pypi.org/project/fal)
48
49
  [![Tests](https://img.shields.io/github/actions/workflow/status/fal-ai/fal/integration_tests.yaml?label=Tests)](https://github.com/fal-ai/fal/actions)