fal 0.12.2__py3-none-any.whl → 0.12.4__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 +11 -2
- fal/api.py +130 -50
- fal/app.py +81 -134
- fal/apps.py +24 -6
- fal/auth/__init__.py +14 -2
- fal/auth/auth0.py +34 -25
- fal/cli.py +9 -4
- fal/env.py +0 -4
- fal/flags.py +1 -0
- fal/logging/__init__.py +0 -2
- fal/logging/trace.py +8 -1
- fal/sdk.py +33 -6
- fal/toolkit/__init__.py +16 -0
- fal/workflows.py +481 -0
- {fal-0.12.2.dist-info → fal-0.12.4.dist-info}/METADATA +4 -7
- fal-0.12.4.dist-info/RECORD +88 -0
- openapi_fal_rest/__init__.py +1 -0
- openapi_fal_rest/api/workflows/__init__.py +0 -0
- openapi_fal_rest/api/workflows/create_or_update_workflow_workflows_post.py +172 -0
- openapi_fal_rest/api/workflows/delete_workflow_workflows_user_id_workflow_name_delete.py +175 -0
- openapi_fal_rest/api/workflows/execute_workflow_workflows_user_id_workflow_name_post.py +268 -0
- openapi_fal_rest/api/workflows/get_workflow_workflows_user_id_workflow_name_get.py +181 -0
- openapi_fal_rest/api/workflows/get_workflows_workflows_get.py +189 -0
- openapi_fal_rest/models/__init__.py +34 -0
- openapi_fal_rest/models/app_metadata_response_app_metadata.py +1 -0
- openapi_fal_rest/models/customer_details.py +15 -14
- openapi_fal_rest/models/execute_workflow_workflows_user_id_workflow_name_post_json_body_type_0.py +44 -0
- openapi_fal_rest/models/execute_workflow_workflows_user_id_workflow_name_post_response_200_type_0.py +44 -0
- openapi_fal_rest/models/page_workflow_item.py +107 -0
- openapi_fal_rest/models/typed_workflow.py +85 -0
- openapi_fal_rest/models/workflow_contents.py +98 -0
- openapi_fal_rest/models/workflow_contents_nodes.py +59 -0
- openapi_fal_rest/models/workflow_contents_output.py +44 -0
- openapi_fal_rest/models/workflow_detail.py +149 -0
- openapi_fal_rest/models/workflow_detail_contents_type_0.py +44 -0
- openapi_fal_rest/models/workflow_item.py +80 -0
- openapi_fal_rest/models/workflow_node.py +74 -0
- openapi_fal_rest/models/workflow_node_type.py +9 -0
- openapi_fal_rest/models/workflow_schema.py +73 -0
- openapi_fal_rest/models/workflow_schema_input.py +44 -0
- openapi_fal_rest/models/workflow_schema_output.py +44 -0
- openapi_fal_rest/types.py +1 -0
- fal/logging/datadog.py +0 -78
- fal-0.12.2.dist-info/RECORD +0 -67
- {fal-0.12.2.dist-info → fal-0.12.4.dist-info}/WHEEL +0 -0
- {fal-0.12.2.dist-info → fal-0.12.4.dist-info}/entry_points.txt +0 -0
fal/workflows.py
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import graphlib
|
|
4
|
+
import json
|
|
5
|
+
import webbrowser
|
|
6
|
+
from argparse import ArgumentParser
|
|
7
|
+
from collections import Counter
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any, Iterator, Union, cast
|
|
10
|
+
|
|
11
|
+
import rich
|
|
12
|
+
from openapi_fal_rest.api.workflows import (
|
|
13
|
+
create_or_update_workflow_workflows_post as publish_workflow,
|
|
14
|
+
)
|
|
15
|
+
from pydantic import BaseModel
|
|
16
|
+
from rich.syntax import Syntax
|
|
17
|
+
|
|
18
|
+
import fal
|
|
19
|
+
from fal import flags
|
|
20
|
+
from fal.rest_client import REST_CLIENT
|
|
21
|
+
|
|
22
|
+
JSONType = Union[dict[str, Any], list[Any], str, int, float, bool, None, "Leaf"]
|
|
23
|
+
SchemaType = dict[str, Any]
|
|
24
|
+
|
|
25
|
+
VARIABLE_PREFIX = "$"
|
|
26
|
+
INPUT_VARIABLE_NAME = "input"
|
|
27
|
+
|
|
28
|
+
# Will be 1.0 once the server is finalized and anything <1.0
|
|
29
|
+
# is going to be rejected.
|
|
30
|
+
WORKFLOW_EXPORT_VERSION = "0.1"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class WorkflowSyntaxError(Exception):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class MisconfiguredGraphError(WorkflowSyntaxError):
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def parse_leaf(raw_leaf: str) -> Leaf:
|
|
42
|
+
"""Parses a leaf (which is in the form of $variable.field.field_2[index] etc.)
|
|
43
|
+
into a tree of Leaf objects."""
|
|
44
|
+
raw_parts = raw_leaf.split(".")
|
|
45
|
+
reference, *raw_parts = raw_parts
|
|
46
|
+
if not reference.startswith(VARIABLE_PREFIX):
|
|
47
|
+
raise WorkflowSyntaxError(
|
|
48
|
+
f"Invalid leaf: {raw_leaf} (must start with a reference)"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
leaf: Leaf = ReferenceLeaf(reference.removeprefix(VARIABLE_PREFIX))
|
|
52
|
+
for raw_part in raw_parts:
|
|
53
|
+
if raw_part.isdigit():
|
|
54
|
+
leaf = IndexLeaf(leaf, int(raw_part))
|
|
55
|
+
elif raw_part.isidentifier():
|
|
56
|
+
leaf = AttributeLeaf(leaf, raw_part)
|
|
57
|
+
else:
|
|
58
|
+
raise WorkflowSyntaxError(
|
|
59
|
+
f"Invalid leaf: {raw_leaf} (unexpected {raw_part})"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
return leaf
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def export_workflow_json(data: JSONType) -> JSONType:
|
|
66
|
+
if isinstance(data, dict):
|
|
67
|
+
return {k: export_workflow_json(v) for k, v in data.items()}
|
|
68
|
+
elif isinstance(data, list):
|
|
69
|
+
return [export_workflow_json(v) for v in data]
|
|
70
|
+
elif isinstance(data, Leaf):
|
|
71
|
+
return repr(data)
|
|
72
|
+
else:
|
|
73
|
+
return data
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def import_workflow_json(data: JSONType) -> JSONType:
|
|
77
|
+
if isinstance(data, dict):
|
|
78
|
+
return {k: import_workflow_json(v) for k, v in data.items()}
|
|
79
|
+
elif isinstance(data, list):
|
|
80
|
+
return [import_workflow_json(v) for v in data]
|
|
81
|
+
elif isinstance(data, str) and data.startswith(VARIABLE_PREFIX):
|
|
82
|
+
return parse_leaf(data)
|
|
83
|
+
else:
|
|
84
|
+
return data
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def iter_leaves(data: JSONType) -> Iterator[JSONType]:
|
|
88
|
+
if isinstance(data, dict):
|
|
89
|
+
for value in data.values():
|
|
90
|
+
yield from iter_leaves(value)
|
|
91
|
+
elif isinstance(data, list):
|
|
92
|
+
for item in data:
|
|
93
|
+
yield from iter_leaves(item)
|
|
94
|
+
else:
|
|
95
|
+
yield data
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def depends(data: JSONType) -> set[str]:
|
|
99
|
+
return {
|
|
100
|
+
leaf.referee.id # type: ignore
|
|
101
|
+
for leaf in iter_leaves(data)
|
|
102
|
+
if isinstance(leaf, Leaf)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class Context:
|
|
108
|
+
vars: dict[str, JSONType]
|
|
109
|
+
|
|
110
|
+
def hydrate(self, input: JSONType) -> JSONType:
|
|
111
|
+
if isinstance(input, dict):
|
|
112
|
+
return {k: self.hydrate(v) for k, v in input.items()}
|
|
113
|
+
elif isinstance(input, list):
|
|
114
|
+
return [self.hydrate(v) for v in input]
|
|
115
|
+
elif isinstance(input, Leaf):
|
|
116
|
+
return input.execute(self)
|
|
117
|
+
else:
|
|
118
|
+
return input
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass
|
|
122
|
+
class Leaf:
|
|
123
|
+
def execute(self, context: Context) -> JSONType:
|
|
124
|
+
raise NotImplementedError
|
|
125
|
+
|
|
126
|
+
def __getattr__(self, name: str) -> AttributeLeaf:
|
|
127
|
+
return AttributeLeaf(self, name)
|
|
128
|
+
|
|
129
|
+
def __getitem__(self, index: int) -> IndexLeaf:
|
|
130
|
+
return IndexLeaf(self, index)
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def referee(self) -> ReferenceLeaf:
|
|
134
|
+
raise NotImplementedError
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass
|
|
138
|
+
class AttributeLeaf(Leaf):
|
|
139
|
+
leaf: Leaf
|
|
140
|
+
attribute: str
|
|
141
|
+
|
|
142
|
+
def execute(self, context: Context) -> JSONType:
|
|
143
|
+
output = self.leaf.execute(context)
|
|
144
|
+
assert isinstance(output, dict), f"{self.leaf!r} is not a dict"
|
|
145
|
+
return output[self.attribute]
|
|
146
|
+
|
|
147
|
+
def __repr__(self) -> str:
|
|
148
|
+
return f"{self.leaf!r}.{self.attribute}"
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def referee(self) -> ReferenceLeaf:
|
|
152
|
+
return self.leaf.referee
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@dataclass
|
|
156
|
+
class IndexLeaf(Leaf):
|
|
157
|
+
leaf: Leaf
|
|
158
|
+
index: int
|
|
159
|
+
|
|
160
|
+
def execute(self, context: Context) -> JSONType:
|
|
161
|
+
output = self.leaf.execute(context)
|
|
162
|
+
assert isinstance(output, list), f"{self.leaf!r} is not an array"
|
|
163
|
+
return output[self.index]
|
|
164
|
+
|
|
165
|
+
def __repr__(self) -> str:
|
|
166
|
+
return f"{self.leaf!r}.{self.index}"
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def referee(self) -> ReferenceLeaf:
|
|
170
|
+
return self.leaf.referee
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@dataclass
|
|
174
|
+
class ReferenceLeaf(Leaf):
|
|
175
|
+
id: str
|
|
176
|
+
|
|
177
|
+
def execute(self, context: Context) -> JSONType:
|
|
178
|
+
try:
|
|
179
|
+
return context.vars[self.id]
|
|
180
|
+
except KeyError:
|
|
181
|
+
raise MisconfiguredGraphError(f"Variable {self.id!r} is not defined")
|
|
182
|
+
|
|
183
|
+
def __repr__(self) -> str:
|
|
184
|
+
return VARIABLE_PREFIX + self.id
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def referee(self) -> ReferenceLeaf:
|
|
188
|
+
return self
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@dataclass
|
|
192
|
+
class Node:
|
|
193
|
+
id: str
|
|
194
|
+
depends: set[str]
|
|
195
|
+
|
|
196
|
+
@classmethod
|
|
197
|
+
def from_json(cls, data: dict[str, Any]) -> Node:
|
|
198
|
+
type = data.pop("type")
|
|
199
|
+
if type == "display":
|
|
200
|
+
return Display.from_json(data)
|
|
201
|
+
elif type == "run":
|
|
202
|
+
return Run.from_json(data)
|
|
203
|
+
else:
|
|
204
|
+
raise WorkflowSyntaxError(f"Invalid node type: {type}")
|
|
205
|
+
|
|
206
|
+
def to_json(self) -> dict[str, Any]:
|
|
207
|
+
raise NotImplementedError
|
|
208
|
+
|
|
209
|
+
def execute(self, context: Context) -> JSONType:
|
|
210
|
+
raise NotImplementedError
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@dataclass
|
|
214
|
+
class Display(Node):
|
|
215
|
+
fields: list[Leaf]
|
|
216
|
+
|
|
217
|
+
@classmethod
|
|
218
|
+
def from_json(cls, data: dict[str, Any]) -> Display:
|
|
219
|
+
return cls(
|
|
220
|
+
id=data["id"],
|
|
221
|
+
depends=set(data["depends"]),
|
|
222
|
+
fields=import_workflow_json(data["fields"]), # type: ignore
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def to_json(self) -> dict[str, Any]:
|
|
226
|
+
return {
|
|
227
|
+
"type": "display",
|
|
228
|
+
"id": self.id,
|
|
229
|
+
"depends": list(self.depends),
|
|
230
|
+
"fields": export_workflow_json(self.fields),
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
def execute(self, context: Context) -> JSONType:
|
|
234
|
+
for url in context.hydrate(self.fields): # type: ignore
|
|
235
|
+
if flags.DONT_OPEN_LINKS:
|
|
236
|
+
print("Link:", url)
|
|
237
|
+
else:
|
|
238
|
+
webbrowser.open(url)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@dataclass
|
|
242
|
+
class Run(Node):
|
|
243
|
+
app: str
|
|
244
|
+
input: JSONType
|
|
245
|
+
|
|
246
|
+
@classmethod
|
|
247
|
+
def from_json(cls, data: dict[str, Any]) -> Run:
|
|
248
|
+
return cls(
|
|
249
|
+
id=data["id"],
|
|
250
|
+
depends=set(data["depends"]),
|
|
251
|
+
app=data["app"],
|
|
252
|
+
input=import_workflow_json(data["input"]),
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
def execute(self, context: Context) -> JSONType:
|
|
256
|
+
input = context.hydrate(self.input)
|
|
257
|
+
assert isinstance(input, dict)
|
|
258
|
+
return cast(JSONType, fal.apps.run(self.app, input))
|
|
259
|
+
|
|
260
|
+
def to_json(self) -> dict[str, Any]:
|
|
261
|
+
return {
|
|
262
|
+
"type": "run",
|
|
263
|
+
"id": self.id,
|
|
264
|
+
"app": self.app,
|
|
265
|
+
"depends": list(self.depends),
|
|
266
|
+
"input": export_workflow_json(self.input), # type: ignore
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@dataclass
|
|
271
|
+
class Workflow:
|
|
272
|
+
name: str
|
|
273
|
+
input_schema: SchemaType
|
|
274
|
+
output_schema: SchemaType
|
|
275
|
+
nodes: dict[str, Node] = field(default_factory=dict)
|
|
276
|
+
output: dict[str, Any] | None = None
|
|
277
|
+
_app_counter: Counter = field(default_factory=Counter)
|
|
278
|
+
|
|
279
|
+
@classmethod
|
|
280
|
+
def from_json(cls, data: dict[str, Any]) -> Workflow:
|
|
281
|
+
data = import_workflow_json(data) # type: ignore
|
|
282
|
+
return cls(
|
|
283
|
+
name=data["name"],
|
|
284
|
+
input_schema=data["schema"]["input"],
|
|
285
|
+
output_schema=data["schema"]["output"],
|
|
286
|
+
nodes={
|
|
287
|
+
node_id: Node.from_json(node_data)
|
|
288
|
+
for node_id, node_data in data["nodes"].items()
|
|
289
|
+
},
|
|
290
|
+
output=data["output"],
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
def __post_init__(self) -> None:
|
|
294
|
+
for node in self.nodes.values():
|
|
295
|
+
if isinstance(node, Run):
|
|
296
|
+
self._app_counter[node.app] += 1
|
|
297
|
+
|
|
298
|
+
def _generate_node_id(self, app: str) -> str:
|
|
299
|
+
self._app_counter[app] += 1
|
|
300
|
+
return f"{app.replace('/', '_').replace('-', '_')}_{self._app_counter[app]}"
|
|
301
|
+
|
|
302
|
+
def run(self, app: str, input: JSONType) -> ReferenceLeaf:
|
|
303
|
+
node_id = self._generate_node_id(app)
|
|
304
|
+
node = self.nodes[node_id] = Run(
|
|
305
|
+
id=node_id,
|
|
306
|
+
depends=depends(input),
|
|
307
|
+
app=app,
|
|
308
|
+
input=input,
|
|
309
|
+
)
|
|
310
|
+
return ReferenceLeaf(node.id)
|
|
311
|
+
|
|
312
|
+
def display(self, *fields: Leaf) -> None:
|
|
313
|
+
node_id = self._generate_node_id("display")
|
|
314
|
+
self.nodes[node_id] = Display(
|
|
315
|
+
node_id,
|
|
316
|
+
depends=depends(list(fields)),
|
|
317
|
+
fields=list(fields),
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
def set_output(self, output: JSONType) -> None:
|
|
321
|
+
self.output = output # type: ignore
|
|
322
|
+
|
|
323
|
+
def execute(self, input: JSONType) -> JSONType:
|
|
324
|
+
if not self.output:
|
|
325
|
+
raise WorkflowSyntaxError(
|
|
326
|
+
"Can't execute the workflow before the output is set."
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
context = Context({INPUT_VARIABLE_NAME: input})
|
|
330
|
+
|
|
331
|
+
sorter = graphlib.TopologicalSorter(
|
|
332
|
+
graph={
|
|
333
|
+
node.id: node.depends - {INPUT_VARIABLE_NAME}
|
|
334
|
+
for node in self.nodes.values()
|
|
335
|
+
}
|
|
336
|
+
)
|
|
337
|
+
for node_id in sorter.static_order():
|
|
338
|
+
node = self.nodes[node_id]
|
|
339
|
+
context.vars[node_id] = node.execute(context)
|
|
340
|
+
|
|
341
|
+
return context.hydrate(self.output)
|
|
342
|
+
|
|
343
|
+
@property
|
|
344
|
+
def input(self) -> ReferenceLeaf:
|
|
345
|
+
return ReferenceLeaf(INPUT_VARIABLE_NAME)
|
|
346
|
+
|
|
347
|
+
def to_json(self) -> dict[str, JSONType]:
|
|
348
|
+
if not self.output:
|
|
349
|
+
raise WorkflowSyntaxError(
|
|
350
|
+
"Can't serialize the workflow before the output is set."
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
"name": self.name,
|
|
355
|
+
"schema": {
|
|
356
|
+
"input": self.input_schema,
|
|
357
|
+
"output": self.output_schema,
|
|
358
|
+
},
|
|
359
|
+
"nodes": {node.id: node.to_json() for node in self.nodes.values()},
|
|
360
|
+
"output": export_workflow_json(self.output),
|
|
361
|
+
"version": WORKFLOW_EXPORT_VERSION,
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
to_dict = to_json
|
|
365
|
+
|
|
366
|
+
def publish(self, title: str, *, is_public: bool = True) -> None:
|
|
367
|
+
workflow_contents = publish_workflow.TypedWorkflow(
|
|
368
|
+
name=self.name,
|
|
369
|
+
title=title,
|
|
370
|
+
contents=self, # type: ignore
|
|
371
|
+
is_public=is_public,
|
|
372
|
+
)
|
|
373
|
+
published_workflow = publish_workflow.sync(
|
|
374
|
+
client=REST_CLIENT,
|
|
375
|
+
json_body=workflow_contents,
|
|
376
|
+
)
|
|
377
|
+
if isinstance(published_workflow, Exception):
|
|
378
|
+
raise published_workflow
|
|
379
|
+
|
|
380
|
+
return (
|
|
381
|
+
REST_CLIENT.base_url
|
|
382
|
+
+ "/workflows/"
|
|
383
|
+
+ published_workflow.user_id
|
|
384
|
+
+ "/"
|
|
385
|
+
+ published_workflow.name
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def create_workflow(
|
|
390
|
+
name: str,
|
|
391
|
+
input: type[BaseModel],
|
|
392
|
+
output: type[BaseModel],
|
|
393
|
+
) -> Workflow:
|
|
394
|
+
return Workflow(
|
|
395
|
+
name=name,
|
|
396
|
+
input_schema=input.schema(),
|
|
397
|
+
output_schema=output.schema(),
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def main() -> None:
|
|
402
|
+
import cli_nested_json
|
|
403
|
+
|
|
404
|
+
parser = ArgumentParser()
|
|
405
|
+
parser.add_argument("workflow_file", type=str)
|
|
406
|
+
args, input_params = parser.parse_known_args()
|
|
407
|
+
|
|
408
|
+
with open(args.workflow_file) as stream:
|
|
409
|
+
workflow = Workflow.from_json(json.load(stream))
|
|
410
|
+
|
|
411
|
+
payload = cli_nested_json.interpret_nested_json(
|
|
412
|
+
[part.split("=") for part in input_params]
|
|
413
|
+
)
|
|
414
|
+
console = rich.get_console()
|
|
415
|
+
console.print(
|
|
416
|
+
f"🤧 Loaded {workflow.name!r} with {len(workflow.nodes)} nodes!",
|
|
417
|
+
style="bold magenta",
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
context = Context({INPUT_VARIABLE_NAME: payload})
|
|
421
|
+
|
|
422
|
+
sorter = graphlib.TopologicalSorter(
|
|
423
|
+
graph={
|
|
424
|
+
node.id: node.depends - {INPUT_VARIABLE_NAME}
|
|
425
|
+
for node in workflow.nodes.values()
|
|
426
|
+
}
|
|
427
|
+
)
|
|
428
|
+
with console.status("Starting the execution", spinner="bouncingBall") as status:
|
|
429
|
+
for n, node_id in enumerate(sorter.static_order()):
|
|
430
|
+
node = workflow.nodes[node_id]
|
|
431
|
+
status.update(
|
|
432
|
+
status=f"Executing {node_id!r} ({n}/{len(workflow.nodes)})",
|
|
433
|
+
spinner="runner",
|
|
434
|
+
)
|
|
435
|
+
if isinstance(node, Run):
|
|
436
|
+
input = context.hydrate(node.input)
|
|
437
|
+
assert isinstance(input, dict)
|
|
438
|
+
|
|
439
|
+
owner, _, app = node.app.partition("/")
|
|
440
|
+
app, sep, path = app.partition("/")
|
|
441
|
+
|
|
442
|
+
handle = fal.apps.submit(
|
|
443
|
+
f"{owner}/{app}",
|
|
444
|
+
path=f"{sep}{path}",
|
|
445
|
+
arguments=input,
|
|
446
|
+
)
|
|
447
|
+
log_count = 0
|
|
448
|
+
for event in handle.iter_events(logs=True):
|
|
449
|
+
if isinstance(event, fal.apps.Queued):
|
|
450
|
+
status.update(
|
|
451
|
+
status=f"Queued for {node_id!r} (position={event.position}) ({n}/{len(workflow.nodes)})",
|
|
452
|
+
spinner="dots",
|
|
453
|
+
)
|
|
454
|
+
elif isinstance(event, fal.apps.InProgress):
|
|
455
|
+
status.update(
|
|
456
|
+
status=f"Executing {node_id!r} ({n}/{len(workflow.nodes)})",
|
|
457
|
+
spinner="runner",
|
|
458
|
+
)
|
|
459
|
+
for log in event.logs[log_count:]: # type: ignore
|
|
460
|
+
console.log(log["message"], style="dim")
|
|
461
|
+
log_count += 1
|
|
462
|
+
|
|
463
|
+
handle_status = handle.status(logs=True)
|
|
464
|
+
assert isinstance(handle_status, fal.apps.Completed)
|
|
465
|
+
for log in handle_status.logs[log_count:]: # type: ignore
|
|
466
|
+
console.log(log["message"], style="dim")
|
|
467
|
+
|
|
468
|
+
context.vars[node_id] = handle.get()
|
|
469
|
+
else:
|
|
470
|
+
context.vars[node_id] = node.execute(context)
|
|
471
|
+
|
|
472
|
+
console.print(
|
|
473
|
+
f"🎉 Execution complete!",
|
|
474
|
+
style="bold green",
|
|
475
|
+
)
|
|
476
|
+
output = context.hydrate(workflow.output)
|
|
477
|
+
console.print(Syntax(json.dumps(output, indent=2), "json"))
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
if __name__ == "__main__":
|
|
481
|
+
main()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: fal
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.4
|
|
4
4
|
Summary: fal is an easy-to-use Serverless Python Framework
|
|
5
5
|
Author: Features & Labels
|
|
6
6
|
Author-email: hello@fal.ai
|
|
@@ -11,18 +11,15 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.10
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
13
|
Requires-Dist: attrs (>=21.3.0)
|
|
14
|
-
Requires-Dist: auth0-python (>=4.1.0,<5.0.0)
|
|
15
|
-
Requires-Dist: boto3 (>=1.33.8,<2.0.0)
|
|
16
14
|
Requires-Dist: click (>=8.1.3,<9.0.0)
|
|
17
15
|
Requires-Dist: colorama (>=0.4.6,<0.5.0)
|
|
18
|
-
Requires-Dist: datadog-api-client (==2.12.0)
|
|
19
16
|
Requires-Dist: dill (==0.3.7)
|
|
20
17
|
Requires-Dist: fastapi (==0.99.1)
|
|
21
18
|
Requires-Dist: grpc-interceptor (>=0.15.0,<0.16.0)
|
|
22
19
|
Requires-Dist: grpcio (>=1.50.0,<2.0.0)
|
|
23
|
-
Requires-Dist: httpx (>=0.15.4
|
|
20
|
+
Requires-Dist: httpx (>=0.15.4)
|
|
24
21
|
Requires-Dist: importlib-metadata (>=4.4) ; python_version < "3.10"
|
|
25
|
-
Requires-Dist: isolate-proto (>=0.3.
|
|
22
|
+
Requires-Dist: isolate-proto (>=0.3.4,<0.4.0)
|
|
26
23
|
Requires-Dist: isolate[build] (>=0.12.3,<1.0)
|
|
27
24
|
Requires-Dist: msgpack (>=1.0.7,<2.0.0)
|
|
28
25
|
Requires-Dist: opentelemetry-api (>=1.15.0,<2.0.0)
|
|
@@ -32,8 +29,8 @@ Requires-Dist: pathspec (>=0.11.1,<0.12.0)
|
|
|
32
29
|
Requires-Dist: pillow (>=10.2.0,<11.0.0)
|
|
33
30
|
Requires-Dist: portalocker (>=2.7.0,<3.0.0)
|
|
34
31
|
Requires-Dist: pydantic (<2.0)
|
|
32
|
+
Requires-Dist: pyjwt (>=2.8.0,<3.0.0)
|
|
35
33
|
Requires-Dist: python-dateutil (>=2.8.0,<3.0.0)
|
|
36
|
-
Requires-Dist: requests (>=2.28.1,<3.0.0)
|
|
37
34
|
Requires-Dist: rich (>=13.3.2,<14.0.0)
|
|
38
35
|
Requires-Dist: structlog (>=22.3.0,<23.0.0)
|
|
39
36
|
Requires-Dist: types-python-dateutil (>=2.8.0,<3.0.0)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
openapi_fal_rest/__init__.py,sha256=ziculmF_i6trw63LzZGFX-6W3Lwq9mCR8_UpkpvpaHI,152
|
|
2
|
+
openapi_fal_rest/api/__init__.py,sha256=87ApBzKyGb5zsgTMOkQXDqsLZCmaSFoJMwbGzCDQZMw,47
|
|
3
|
+
openapi_fal_rest/api/applications/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
openapi_fal_rest/api/applications/app_metadata.py,sha256=GqG6Q7jt8Jcyhb3ms_6i0M1B3cy205y3_A8W-AGEapY,5120
|
|
5
|
+
openapi_fal_rest/api/billing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
openapi_fal_rest/api/billing/get_user_details.py,sha256=2HQHRUQj8QwqSKgiV_USBdXCxGlfaVTBbLiPaDsMBUM,4013
|
|
7
|
+
openapi_fal_rest/api/files/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
openapi_fal_rest/api/files/check_dir_hash.py,sha256=zPNlOwG4YVvnhgfrleQtYLhI1lG0t8YQ1CU3TyvXvfk,4747
|
|
9
|
+
openapi_fal_rest/api/files/upload_local_file.py,sha256=p2lM7hswGbs8KNLg1Pp6vwV7x-1PKtWX-aYmaHUHSDU,5649
|
|
10
|
+
openapi_fal_rest/api/workflows/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
openapi_fal_rest/api/workflows/create_or_update_workflow_workflows_post.py,sha256=bS-CUo3CMsBxkXBEVOoa_GqL-EDgVMW9FHUMyYBneug,4611
|
|
12
|
+
openapi_fal_rest/api/workflows/delete_workflow_workflows_user_id_workflow_name_delete.py,sha256=svJcV5q2e8caxuLJKIld3M7-raQFqSIxPNq58e3AgaA,4590
|
|
13
|
+
openapi_fal_rest/api/workflows/execute_workflow_workflows_user_id_workflow_name_post.py,sha256=kofDDU3TEVQ8Fmua_K2-lk5xh16g6_FNFHQ0KzoFMcM,8743
|
|
14
|
+
openapi_fal_rest/api/workflows/get_workflow_workflows_user_id_workflow_name_get.py,sha256=-E-TELi9Q_-yqobdHAcTHVm-8HithJRUGk7wc1mLA18,4763
|
|
15
|
+
openapi_fal_rest/api/workflows/get_workflows_workflows_get.py,sha256=iJF3lNYp22p8-JbbBMDoHO9iXQB8779lSnH2fNimYP4,5242
|
|
16
|
+
openapi_fal_rest/client.py,sha256=G6BpJg9j7-JsrAUGddYwkzeWRYickBjPdcVgXoPzxuE,2817
|
|
17
|
+
openapi_fal_rest/errors.py,sha256=8mXSxdfSGzxT82srdhYbR0fHfgenxJXaUtMkaGgb6iU,470
|
|
18
|
+
openapi_fal_rest/models/__init__.py,sha256=pGB91qXG3VDxob4DPAh3OMa2fyJFYBxc-jQ-9A4y3Gs,2037
|
|
19
|
+
openapi_fal_rest/models/app_metadata_response_app_metadata.py,sha256=1vx_5cp8V0jyE8iBRIe8TfngaeXMojfEpMCpT6i3qvs,1252
|
|
20
|
+
openapi_fal_rest/models/body_upload_local_file.py,sha256=rOTEbYBXfwZk8TsywZWSPPQQEfJgvsLIufT6A40RJZs,1980
|
|
21
|
+
openapi_fal_rest/models/customer_details.py,sha256=_zvJ_Y2uLowpXqhUnkcpoZpyIB0s8kc5pWSdlBA2_7o,3913
|
|
22
|
+
openapi_fal_rest/models/execute_workflow_workflows_user_id_workflow_name_post_json_body_type_0.py,sha256=84MkDJeOgxwPozZZuP8Fb2dqlF57fmXuRGlrvMKxUew,1418
|
|
23
|
+
openapi_fal_rest/models/execute_workflow_workflows_user_id_workflow_name_post_response_200_type_0.py,sha256=SAGhH0grz3tYISHqsTr-MeiKWqTQsi8gFwSK2ksBoqY,1433
|
|
24
|
+
openapi_fal_rest/models/hash_check.py,sha256=T9R7n4EdadCxbFUZvresZZFPYwDfyJMZVNxY6wIJEE8,1352
|
|
25
|
+
openapi_fal_rest/models/http_validation_error.py,sha256=2nhqlv8RX2qp6VR7hb8-SKtzJWXSZ0J95ThW9J4agJo,2131
|
|
26
|
+
openapi_fal_rest/models/lock_reason.py,sha256=3b_foCV6bZKvsbyic3hM1_qzvJk_9ZD_5mS1GzSawdw,703
|
|
27
|
+
openapi_fal_rest/models/page_workflow_item.py,sha256=5DCUMQ2b8LAH8So6nQJW8mhlxZbedM4ixAJqbf-KZNE,2812
|
|
28
|
+
openapi_fal_rest/models/typed_workflow.py,sha256=haE4Sa16s4iea_VNYtVR7cP3A8Z2ja2KNywYJhc6GmQ,2119
|
|
29
|
+
openapi_fal_rest/models/validation_error.py,sha256=I6tB-HbEOmE0ua27erDX5PX5YUynENv_dgPN3SrwTrQ,2091
|
|
30
|
+
openapi_fal_rest/models/workflow_contents.py,sha256=TIockMwkpjeUjbWtZmtzcC327akwD4XgnTskgdsHQFQ,2703
|
|
31
|
+
openapi_fal_rest/models/workflow_contents_nodes.py,sha256=mMeQO_DlQZQPSwyGKTNC9eBovFVCVQNFbrB60k7tTtU,1695
|
|
32
|
+
openapi_fal_rest/models/workflow_contents_output.py,sha256=2m4ITxXcQxTt8iiY5bos0QQW_uMYOGAR1xAcm1rzrcI,1206
|
|
33
|
+
openapi_fal_rest/models/workflow_detail.py,sha256=lxlPkiOgc9_Dz6c0v3mDJNFXa4CWDkMWw9on-9McojE,4597
|
|
34
|
+
openapi_fal_rest/models/workflow_detail_contents_type_0.py,sha256=DUQg0mDQKczL5g76rJMYvFK_RFcQaQPC1BoTr43eheA,1237
|
|
35
|
+
openapi_fal_rest/models/workflow_item.py,sha256=M_8ojGsBpnKUe3l9yDnciPPgEjmfq1v-Bn9kVHPxT-0,1988
|
|
36
|
+
openapi_fal_rest/models/workflow_node.py,sha256=DZ3i-auxvm2cWFTBE52YSoLOEIVFvLPW9MyzyR91e78,1797
|
|
37
|
+
openapi_fal_rest/models/workflow_node_type.py,sha256=-FzyeY2bxcNmizKbJI8joG7byRiPyAthdBLlioLH8Zw,161
|
|
38
|
+
openapi_fal_rest/models/workflow_schema.py,sha256=4K5gsv9u9pxx2ItkffoyHeNjBBYf6ur5bN4m_zePZNY,2019
|
|
39
|
+
openapi_fal_rest/models/workflow_schema_input.py,sha256=2OkOXWHTNsCXHWS6EGDFzcJKkW5FIap-2gfO233EvZQ,1191
|
|
40
|
+
openapi_fal_rest/models/workflow_schema_output.py,sha256=EblwSPAGfWfYVWw_WSSaBzQVju296is9o28rMBAd0mc,1196
|
|
41
|
+
openapi_fal_rest/py.typed,sha256=8ZJUsxZiuOy1oJeVhsTWQhTG_6pTVHVXk5hJL79ebTk,25
|
|
42
|
+
openapi_fal_rest/types.py,sha256=GLwJwOotUOdfqryo_r0naw55-dh6Ilm4IvxePekSACk,994
|
|
43
|
+
fal/__init__.py,sha256=6SvCuotCb0tuqSWDZSFDjtySktJ5m1QpVIlefumJpvM,1199
|
|
44
|
+
fal/_serialization.py,sha256=l_dZuSX5BT7SogXw1CalYLfT2H3zy3tfq4y6jHuxZqQ,4201
|
|
45
|
+
fal/api.py,sha256=Qack_oYNkvF4qown3P_oKvyvRfTJkhOG7PL1xpa8FUQ,32872
|
|
46
|
+
fal/app.py,sha256=KAIgvBBpvzp6oY8BpH5hFOLDUpG4bjtwlV5jPGj2IE0,12487
|
|
47
|
+
fal/apps.py,sha256=T387WJDtKpKEytu27b2AVqqo0uijKrRT9ymk6FcRiEw,6705
|
|
48
|
+
fal/auth/__init__.py,sha256=ZnR1fxonzDk0UhS3-i33Kq2xOrN-leYXvJ-Ddnj94xc,3594
|
|
49
|
+
fal/auth/auth0.py,sha256=Lb63u99fjZVIpbkAicVL1B5V6iPb2mE04bwHmorXbu4,5542
|
|
50
|
+
fal/auth/local.py,sha256=lZqp4j32l2xFpY8zYvLoIHHyJrNAJDcm5MxgsLpY_pw,1786
|
|
51
|
+
fal/cli.py,sha256=-3P9O3QORA0NolZR9juR8fv59JdJo37_puwUcinzDlY,17299
|
|
52
|
+
fal/console/__init__.py,sha256=ernZ4bzvvliQh5SmrEqQ7lA5eVcbw6Ra2jalKtA7dxg,132
|
|
53
|
+
fal/console/icons.py,sha256=De9MfFaSkO2Lqfne13n3PrYfTXJVIzYZVqYn5BWsdrA,108
|
|
54
|
+
fal/console/ux.py,sha256=4vj1aGA3grRn-ebeMuDLR6u3YjMwUGpqtNgdTG9su5s,485
|
|
55
|
+
fal/env.py,sha256=-fA8x62BbOX3MOuO0maupa-_QJ9PNwr8ogfeG11QUyQ,53
|
|
56
|
+
fal/exceptions/__init__.py,sha256=Q4LCSqIrJ8GFQZWH5BvWL5mDPR0HwYQuIhNvsdiOkEU,938
|
|
57
|
+
fal/exceptions/_base.py,sha256=LeQmx-soL_-s1742WKN18VwTVjUuYP0L0BdQHPJBpM4,460
|
|
58
|
+
fal/exceptions/auth.py,sha256=01Ro7SyGJpwchubdHe14Cl6-Al1jUj16Sy4BvakNWf4,384
|
|
59
|
+
fal/exceptions/handlers.py,sha256=b21a8S13euECArjpgm2N69HsShqLYVqAboIeMoWlWA4,1414
|
|
60
|
+
fal/flags.py,sha256=AATQO65M4C87dGp0j7o6cSQWcr62xE-8DnJYsUjFFbw,942
|
|
61
|
+
fal/logging/__init__.py,sha256=snqprf7-sKw6oAATS_Yxklf-a3XhLg0vIHICPwLp6TM,1583
|
|
62
|
+
fal/logging/isolate.py,sha256=yDW_P4aR-t53IRmvD2Iprufv1Wn-xQXoBbMB2Ufr59s,2122
|
|
63
|
+
fal/logging/style.py,sha256=ckIgHzvF4DShM5kQh8F133X53z_vF46snuDHVmo_h9g,386
|
|
64
|
+
fal/logging/trace.py,sha256=OhzB6d4rQZimBc18WFLqH_9BGfqFFumKKTAGSsmWRMg,1904
|
|
65
|
+
fal/logging/user.py,sha256=A8vbZX9z13TPZEDzvlbvCDDdD0EL1KrCP3qHdrT58-A,632
|
|
66
|
+
fal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
67
|
+
fal/rest_client.py,sha256=kGBGmuyHfX1lR910EoKCYPjsyU8MdXawT_cW2q8Sajc,568
|
|
68
|
+
fal/sdk.py,sha256=Z3MQsD8MMQZq_GEC2VjaYChdNafFJtsgdk77-VK6N44,18782
|
|
69
|
+
fal/sync.py,sha256=Ljet584PVFz4r888-0bwV1Kio-tTneF_85TnHvBPvJw,4277
|
|
70
|
+
fal/toolkit/__init__.py,sha256=JDNBT_duflp93geeAzw2kFmGzG5odWnPJEXFLXE2nF4,713
|
|
71
|
+
fal/toolkit/exceptions.py,sha256=--WKKYxUop6WFy_vqAPXK6uH8C-JR98gnNXwhHNCb7E,258
|
|
72
|
+
fal/toolkit/file/__init__.py,sha256=YpUU6YziZV1AMuq12L0EDWToS0sgpHSGWsARbiOEHWk,56
|
|
73
|
+
fal/toolkit/file/file.py,sha256=ku4agJiGXU2gdfZmFrU5mDlVsag834zoeskbo-6ErEU,5926
|
|
74
|
+
fal/toolkit/file/providers/fal.py,sha256=hO59loXzGP4Vg-Q1FFR56nWbbI6BccJRnFsEI6z6EQE,3404
|
|
75
|
+
fal/toolkit/file/providers/gcp.py,sha256=Bq5SJSghXF8YfFnbZ83_mPdrWs2dFhi8ytODp92USgk,1962
|
|
76
|
+
fal/toolkit/file/providers/r2.py,sha256=xJtZfX3cfzJgLXS3F8mHArbrHi0_QBpIMy5M4-tS8H8,2586
|
|
77
|
+
fal/toolkit/file/types.py,sha256=MTIj6Y_ioL4CiMZXMiqx74vlmUifc3SNvcrWAXQfULE,1109
|
|
78
|
+
fal/toolkit/image/__init__.py,sha256=liEq0CqkRqUQ1udnnyGVFBwCXUhR2f6o5ffbtbAlP8o,57
|
|
79
|
+
fal/toolkit/image/image.py,sha256=bF1PzO4cJoFGJFpQYeG0sNaGuw3cC1zmobmbZrxbPFY,4339
|
|
80
|
+
fal/toolkit/mainify.py,sha256=E7gE45nZQZoaJdSlIq0mqajcH-IjcuPBWFmKm5hvhAU,406
|
|
81
|
+
fal/toolkit/optimize.py,sha256=OIhX0T-efRMgUJDpvL0bujdun5SovZgTdKxNOv01b_Y,1394
|
|
82
|
+
fal/toolkit/utils/__init__.py,sha256=b3zVpm50Upx1saXU7RiV9r9in6-Chs4OU9KRjBv7MYI,83
|
|
83
|
+
fal/toolkit/utils/download_utils.py,sha256=bigcLJjLK1OBAGxpYisJ0-5vcQCh0HAPuCykPrcCNd0,15596
|
|
84
|
+
fal/workflows.py,sha256=2xMC4dybiAv05eEua_YpKRAs395YR2UVKvhGS0HZdm8,14155
|
|
85
|
+
fal-0.12.4.dist-info/entry_points.txt,sha256=nE9GBVV3PdBosudFwbIzZQUe_9lfPR6EH8K_FdDASnM,62
|
|
86
|
+
fal-0.12.4.dist-info/METADATA,sha256=lcJ7E_Q3-0l1rtEIb-5tc2MLY4rpzIVSE7UXFh1D780,2937
|
|
87
|
+
fal-0.12.4.dist-info/WHEEL,sha256=vVCvjcmxuUltf8cYhJ0sJMRDLr1XsPuxEId8YDzbyCY,88
|
|
88
|
+
fal-0.12.4.dist-info/RECORD,,
|
openapi_fal_rest/__init__.py
CHANGED
|
File without changes
|