fal 0.12.2__py3-none-any.whl → 0.12.3__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 +1 -1
- fal/auth/auth0.py +31 -22
- fal/cli.py +5 -3
- fal/env.py +0 -4
- fal/logging/__init__.py +0 -2
- fal/logging/trace.py +8 -1
- fal/sdk.py +33 -6
- fal/toolkit/__init__.py +16 -0
- {fal-0.12.2.dist-info → fal-0.12.3.dist-info}/METADATA +4 -7
- {fal-0.12.2.dist-info → fal-0.12.3.dist-info}/RECORD +16 -17
- fal/logging/datadog.py +0 -78
- {fal-0.12.2.dist-info → fal-0.12.3.dist-info}/WHEEL +0 -0
- {fal-0.12.2.dist-info → fal-0.12.3.dist-info}/entry_points.txt +0 -0
fal/__init__.py
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from fal import apps
|
|
4
|
-
|
|
5
|
-
# TODO: DEPRECATED - use function instead
|
|
6
4
|
from fal.api import FalServerlessHost, LocalHost, cached
|
|
7
5
|
from fal.api import function
|
|
8
6
|
from fal.api import function as isolated
|
|
@@ -16,6 +14,17 @@ serverless = FalServerlessHost()
|
|
|
16
14
|
# DEPRECATED - use serverless instead
|
|
17
15
|
cloud = FalServerlessHost()
|
|
18
16
|
|
|
17
|
+
__all__ = [
|
|
18
|
+
"function",
|
|
19
|
+
"cached",
|
|
20
|
+
"App",
|
|
21
|
+
"endpoint",
|
|
22
|
+
"realtime",
|
|
23
|
+
# "wrap_app",
|
|
24
|
+
"FalServerlessKeyCredentials",
|
|
25
|
+
"sync_dir",
|
|
26
|
+
]
|
|
27
|
+
|
|
19
28
|
|
|
20
29
|
# NOTE: This makes `import fal.dbt` import the `dbt-fal` module and `import fal` import the `fal` module
|
|
21
30
|
# NOTE: taken from dbt-core: https://github.com/dbt-labs/dbt-core/blob/ac539fd5cf325cfb5315339077d03399d575f570/core/dbt/adapters/__init__.py#L1-L7
|
fal/api.py
CHANGED
|
@@ -4,7 +4,7 @@ import inspect
|
|
|
4
4
|
import sys
|
|
5
5
|
from collections import defaultdict
|
|
6
6
|
from concurrent.futures import ThreadPoolExecutor
|
|
7
|
-
from contextlib import suppress
|
|
7
|
+
from contextlib import asynccontextmanager, suppress
|
|
8
8
|
from dataclasses import dataclass, field, replace
|
|
9
9
|
from functools import partial, wraps
|
|
10
10
|
from os import PathLike
|
|
@@ -16,6 +16,7 @@ from typing import (
|
|
|
16
16
|
Generic,
|
|
17
17
|
Iterator,
|
|
18
18
|
Literal,
|
|
19
|
+
NamedTuple,
|
|
19
20
|
TypeVar,
|
|
20
21
|
cast,
|
|
21
22
|
overload,
|
|
@@ -26,6 +27,7 @@ import dill.detect
|
|
|
26
27
|
import grpc
|
|
27
28
|
import isolate
|
|
28
29
|
import yaml
|
|
30
|
+
from fastapi import FastAPI
|
|
29
31
|
from isolate.backends.common import active_python
|
|
30
32
|
from isolate.backends.settings import DEFAULT_SETTINGS
|
|
31
33
|
from isolate.connections import PythonIPC
|
|
@@ -56,6 +58,8 @@ ReturnT = TypeVar("ReturnT", covariant=True)
|
|
|
56
58
|
BasicConfig = Dict[str, Any]
|
|
57
59
|
_UNSET = object()
|
|
58
60
|
|
|
61
|
+
SERVE_REQUIREMENTS = ["fastapi==0.99.1", "uvicorn"]
|
|
62
|
+
|
|
59
63
|
|
|
60
64
|
@dataclass
|
|
61
65
|
class FalServerlessError(Exception):
|
|
@@ -110,7 +114,7 @@ class Host(Generic[ArgsT, ReturnT]):
|
|
|
110
114
|
options.environment[key] = value
|
|
111
115
|
|
|
112
116
|
if options.gateway.get("serve"):
|
|
113
|
-
options.add_requirements(
|
|
117
|
+
options.add_requirements(SERVE_REQUIREMENTS)
|
|
114
118
|
|
|
115
119
|
return options
|
|
116
120
|
|
|
@@ -730,53 +734,17 @@ def function( # type: ignore
|
|
|
730
734
|
|
|
731
735
|
|
|
732
736
|
@mainify
|
|
733
|
-
class
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
self._func = func
|
|
738
|
-
|
|
739
|
-
def build_app(self):
|
|
740
|
-
from fastapi import FastAPI
|
|
741
|
-
from fastapi.middleware.cors import CORSMiddleware
|
|
742
|
-
|
|
743
|
-
_app = FastAPI()
|
|
744
|
-
|
|
745
|
-
_app.add_middleware(
|
|
746
|
-
CORSMiddleware,
|
|
747
|
-
allow_credentials=True,
|
|
748
|
-
allow_headers=("*"),
|
|
749
|
-
allow_methods=("*"),
|
|
750
|
-
allow_origins=("*"),
|
|
751
|
-
)
|
|
752
|
-
|
|
753
|
-
_app.add_api_route(
|
|
754
|
-
"/",
|
|
755
|
-
self._func, # type: ignore
|
|
756
|
-
name=self._func.__name__,
|
|
757
|
-
methods=["POST"],
|
|
758
|
-
)
|
|
759
|
-
|
|
760
|
-
return _app
|
|
761
|
-
|
|
762
|
-
def __call__(self, *args, **kwargs) -> None:
|
|
763
|
-
if len(args) != 0 or len(kwargs) != 0:
|
|
764
|
-
print(
|
|
765
|
-
f"[warning] {self._func.__name__} function is served with no arguments."
|
|
766
|
-
)
|
|
767
|
-
|
|
768
|
-
from uvicorn import run
|
|
769
|
-
|
|
770
|
-
app = self.build_app()
|
|
771
|
-
run(app, host="0.0.0.0", port=8080)
|
|
737
|
+
class FalFastAPI(FastAPI):
|
|
738
|
+
"""
|
|
739
|
+
A subclass of FastAPI that adds some fal-specific functionality.
|
|
740
|
+
"""
|
|
772
741
|
|
|
773
742
|
def openapi(self) -> dict[str, Any]:
|
|
774
743
|
"""
|
|
775
744
|
Build the OpenAPI specification for the served function.
|
|
776
745
|
Attach needed metadata for a better integration to fal.
|
|
777
746
|
"""
|
|
778
|
-
|
|
779
|
-
spec = app.openapi()
|
|
747
|
+
spec = super().openapi()
|
|
780
748
|
self._mark_order_openapi(spec)
|
|
781
749
|
return spec
|
|
782
750
|
|
|
@@ -788,7 +756,8 @@ class ServeWrapper:
|
|
|
788
756
|
"""
|
|
789
757
|
|
|
790
758
|
def mark_order(obj: dict[str, Any], key: str):
|
|
791
|
-
|
|
759
|
+
if key in obj:
|
|
760
|
+
obj[f"x-fal-order-{key}"] = list(obj[key].keys())
|
|
792
761
|
|
|
793
762
|
mark_order(spec, "paths")
|
|
794
763
|
|
|
@@ -797,18 +766,129 @@ class ServeWrapper:
|
|
|
797
766
|
Mark the order of properties in the schema object.
|
|
798
767
|
They can have 'allOf', 'properties' or '$ref' key.
|
|
799
768
|
"""
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
mark_order(schema, "properties")
|
|
769
|
+
for sub_schema in schema.get("allOf", []):
|
|
770
|
+
order_schema_object(sub_schema)
|
|
771
|
+
|
|
772
|
+
mark_order(schema, "properties")
|
|
805
773
|
|
|
806
|
-
for key in spec.get("components", {}).get("schemas"
|
|
774
|
+
for key in spec.get("components", {}).get("schemas", {}):
|
|
807
775
|
order_schema_object(spec["components"]["schemas"][key])
|
|
808
776
|
|
|
809
777
|
return spec
|
|
810
778
|
|
|
811
779
|
|
|
780
|
+
@mainify
|
|
781
|
+
class RouteSignature(NamedTuple):
|
|
782
|
+
path: str
|
|
783
|
+
is_websocket: bool = False
|
|
784
|
+
input_modal: type | None = None
|
|
785
|
+
buffering: int | None = None
|
|
786
|
+
session_timeout: float | None = None
|
|
787
|
+
max_batch_size: int = 1
|
|
788
|
+
emit_timings: bool = False
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
@mainify
|
|
792
|
+
class BaseServable:
|
|
793
|
+
def collect_routes(self) -> dict[RouteSignature, Callable[..., Any]]:
|
|
794
|
+
raise NotImplementedError
|
|
795
|
+
|
|
796
|
+
def _add_extra_middlewares(self, app: FastAPI):
|
|
797
|
+
"""
|
|
798
|
+
For subclasses to add extra middlewares to the app.
|
|
799
|
+
"""
|
|
800
|
+
pass
|
|
801
|
+
|
|
802
|
+
@asynccontextmanager
|
|
803
|
+
async def lifespan(self, app: FastAPI):
|
|
804
|
+
yield
|
|
805
|
+
|
|
806
|
+
def _build_app(self) -> FastAPI:
|
|
807
|
+
from fastapi import HTTPException, Request
|
|
808
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
809
|
+
from fastapi.responses import JSONResponse
|
|
810
|
+
|
|
811
|
+
_app = FalFastAPI(lifespan=self.lifespan)
|
|
812
|
+
|
|
813
|
+
_app.add_middleware(
|
|
814
|
+
CORSMiddleware,
|
|
815
|
+
allow_credentials=True,
|
|
816
|
+
allow_headers=("*"),
|
|
817
|
+
allow_methods=("*"),
|
|
818
|
+
allow_origins=("*"),
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
self._add_extra_middlewares(_app)
|
|
822
|
+
|
|
823
|
+
@_app.exception_handler(404)
|
|
824
|
+
async def not_found_exception_handler(request: Request, exc: HTTPException):
|
|
825
|
+
# Rewrite the message to include the path that was not found.
|
|
826
|
+
# This is supposed to make it easier to understand to the user
|
|
827
|
+
# that the error comes from the app and not our platform.
|
|
828
|
+
if exc.detail == "Not Found":
|
|
829
|
+
return JSONResponse(
|
|
830
|
+
{"detail": f"Path {request.url.path} not found"}, 404
|
|
831
|
+
)
|
|
832
|
+
else:
|
|
833
|
+
# If it's not a generic 404, just return the original message.
|
|
834
|
+
return JSONResponse({"detail": exc.detail}, 404)
|
|
835
|
+
|
|
836
|
+
routes = self.collect_routes()
|
|
837
|
+
if not routes:
|
|
838
|
+
raise ValueError("An application must have at least one route!")
|
|
839
|
+
|
|
840
|
+
for signature, endpoint in routes.items():
|
|
841
|
+
if signature.is_websocket:
|
|
842
|
+
_app.add_api_websocket_route(
|
|
843
|
+
signature.path,
|
|
844
|
+
endpoint,
|
|
845
|
+
name=endpoint.__name__,
|
|
846
|
+
)
|
|
847
|
+
else:
|
|
848
|
+
_app.add_api_route(
|
|
849
|
+
signature.path,
|
|
850
|
+
endpoint,
|
|
851
|
+
name=endpoint.__name__,
|
|
852
|
+
methods=["POST"],
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
return _app
|
|
856
|
+
|
|
857
|
+
def openapi(self) -> dict[str, Any]:
|
|
858
|
+
"""
|
|
859
|
+
Build the OpenAPI specification for the served function.
|
|
860
|
+
Attach needed metadata for a better integration to fal.
|
|
861
|
+
"""
|
|
862
|
+
return self._build_app().openapi()
|
|
863
|
+
|
|
864
|
+
def serve(self) -> None:
|
|
865
|
+
import uvicorn
|
|
866
|
+
|
|
867
|
+
app = self._build_app()
|
|
868
|
+
uvicorn.run(app, host="0.0.0.0", port=8080)
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
@mainify
|
|
872
|
+
class ServeWrapper(BaseServable):
|
|
873
|
+
_func: Callable
|
|
874
|
+
|
|
875
|
+
def __init__(self, func: Callable):
|
|
876
|
+
self._func = func
|
|
877
|
+
|
|
878
|
+
def collect_routes(self) -> dict[RouteSignature, Callable[..., Any]]:
|
|
879
|
+
return {
|
|
880
|
+
RouteSignature("/"): self._func,
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
def __call__(self, *args, **kwargs) -> None:
|
|
884
|
+
if len(args) != 0 or len(kwargs) != 0:
|
|
885
|
+
print(
|
|
886
|
+
f"[warning] {self._func.__name__} function is served with no arguments."
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
self.serve()
|
|
890
|
+
|
|
891
|
+
|
|
812
892
|
@dataclass
|
|
813
893
|
class IsolatedFunction(Generic[ArgsT, ReturnT]):
|
|
814
894
|
host: Host[ArgsT, ReturnT]
|
fal/app.py
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
|
+
import json
|
|
4
5
|
import os
|
|
5
6
|
import typing
|
|
6
7
|
from contextlib import asynccontextmanager
|
|
7
|
-
from typing import Any, Callable, ClassVar,
|
|
8
|
+
from typing import Any, Callable, ClassVar, TypeVar
|
|
8
9
|
|
|
9
10
|
from fastapi import FastAPI
|
|
10
11
|
|
|
11
12
|
import fal.api
|
|
12
13
|
from fal._serialization import add_serialization_listeners_for
|
|
14
|
+
from fal.api import RouteSignature
|
|
13
15
|
from fal.logging import get_logger
|
|
14
16
|
from fal.toolkit import mainify
|
|
15
17
|
|
|
@@ -19,6 +21,13 @@ EndpointT = TypeVar("EndpointT", bound=Callable[..., Any])
|
|
|
19
21
|
logger = get_logger(__name__)
|
|
20
22
|
|
|
21
23
|
|
|
24
|
+
async def _call_any_fn(fn, *args, **kwargs):
|
|
25
|
+
if inspect.iscoroutinefunction(fn):
|
|
26
|
+
return await fn(*args, **kwargs)
|
|
27
|
+
else:
|
|
28
|
+
return fn(*args, **kwargs)
|
|
29
|
+
|
|
30
|
+
|
|
22
31
|
def wrap_app(cls: type[App], **kwargs) -> fal.api.IsolatedFunction:
|
|
23
32
|
add_serialization_listeners_for(cls)
|
|
24
33
|
|
|
@@ -44,25 +53,19 @@ def wrap_app(cls: type[App], **kwargs) -> fal.api.IsolatedFunction:
|
|
|
44
53
|
**cls.host_kwargs,
|
|
45
54
|
**kwargs,
|
|
46
55
|
metadata=metadata,
|
|
47
|
-
|
|
56
|
+
exposed_port=8080,
|
|
57
|
+
serve=False,
|
|
48
58
|
)
|
|
49
59
|
fn = wrapper(initialize_and_serve)
|
|
60
|
+
fn.options.add_requirements(fal.api.SERVE_REQUIREMENTS)
|
|
50
61
|
if realtime_app:
|
|
51
62
|
fn.options.add_requirements(REALTIME_APP_REQUIREMENTS)
|
|
52
|
-
return fn.on(
|
|
53
|
-
serve=False,
|
|
54
|
-
exposed_port=8080,
|
|
55
|
-
)
|
|
56
63
|
|
|
57
|
-
|
|
58
|
-
@mainify
|
|
59
|
-
class RouteSignature(NamedTuple):
|
|
60
|
-
path: str
|
|
61
|
-
is_websocket: bool = False
|
|
64
|
+
return fn
|
|
62
65
|
|
|
63
66
|
|
|
64
67
|
@mainify
|
|
65
|
-
class App:
|
|
68
|
+
class App(fal.api.BaseServable):
|
|
66
69
|
requirements: ClassVar[list[str]] = []
|
|
67
70
|
machine_type: ClassVar[str] = "S"
|
|
68
71
|
host_kwargs: ClassVar[dict[str, Any]] = {}
|
|
@@ -83,19 +86,6 @@ class App:
|
|
|
83
86
|
"Running apps through SDK is not implemented yet."
|
|
84
87
|
)
|
|
85
88
|
|
|
86
|
-
def setup(self):
|
|
87
|
-
"""Setup the application before serving."""
|
|
88
|
-
|
|
89
|
-
def provide_hints(self) -> list[str]:
|
|
90
|
-
"""Provide hints for routing the application."""
|
|
91
|
-
raise NotImplementedError
|
|
92
|
-
|
|
93
|
-
def serve(self) -> None:
|
|
94
|
-
import uvicorn
|
|
95
|
-
|
|
96
|
-
app = self._build_app()
|
|
97
|
-
uvicorn.run(app, host="0.0.0.0", port=8080)
|
|
98
|
-
|
|
99
89
|
def collect_routes(self) -> dict[RouteSignature, Callable[..., Any]]:
|
|
100
90
|
return {
|
|
101
91
|
signature: endpoint
|
|
@@ -103,22 +93,23 @@ class App:
|
|
|
103
93
|
if (signature := getattr(endpoint, "route_signature", None))
|
|
104
94
|
}
|
|
105
95
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
96
|
+
@asynccontextmanager
|
|
97
|
+
async def lifespan(self, app: FastAPI):
|
|
98
|
+
await _call_any_fn(self.setup)
|
|
99
|
+
try:
|
|
100
|
+
yield
|
|
101
|
+
finally:
|
|
102
|
+
await _call_any_fn(self.teardown)
|
|
109
103
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
self.setup()
|
|
113
|
-
try:
|
|
114
|
-
yield
|
|
115
|
-
finally:
|
|
116
|
-
self.teardown()
|
|
104
|
+
def setup(self):
|
|
105
|
+
"""Setup the application before serving."""
|
|
117
106
|
|
|
118
|
-
|
|
107
|
+
def teardown(self):
|
|
108
|
+
"""Teardown the application after serving."""
|
|
119
109
|
|
|
120
|
-
|
|
121
|
-
|
|
110
|
+
def _add_extra_middlewares(self, app: FastAPI):
|
|
111
|
+
@app.middleware("http")
|
|
112
|
+
async def provide_hints_headers(request, call_next):
|
|
122
113
|
response = await call_next(request)
|
|
123
114
|
try:
|
|
124
115
|
response.headers["X-Fal-Runner-Hints"] = ",".join(self.provide_hints())
|
|
@@ -126,57 +117,18 @@ class App:
|
|
|
126
117
|
# This lets us differentiate between apps that don't provide hints
|
|
127
118
|
# and apps that provide empty hints.
|
|
128
119
|
pass
|
|
129
|
-
except Exception
|
|
120
|
+
except Exception:
|
|
130
121
|
from fastapi.logger import logger
|
|
131
122
|
|
|
132
123
|
logger.exception(
|
|
133
124
|
"Failed to provide hints for %s",
|
|
134
125
|
self.__class__.__name__,
|
|
135
|
-
exc_info=exc,
|
|
136
126
|
)
|
|
137
127
|
return response
|
|
138
128
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
allow_headers=("*"),
|
|
143
|
-
allow_methods=("*"),
|
|
144
|
-
allow_origins=("*"),
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
routes = self.collect_routes()
|
|
148
|
-
if not routes:
|
|
149
|
-
raise ValueError("An application must have at least one route!")
|
|
150
|
-
|
|
151
|
-
for signature, endpoint in routes.items():
|
|
152
|
-
if signature.is_websocket:
|
|
153
|
-
_app.add_api_websocket_route(
|
|
154
|
-
signature.path,
|
|
155
|
-
endpoint,
|
|
156
|
-
name=endpoint.__name__,
|
|
157
|
-
)
|
|
158
|
-
else:
|
|
159
|
-
_app.add_api_route(
|
|
160
|
-
signature.path,
|
|
161
|
-
endpoint,
|
|
162
|
-
name=endpoint.__name__,
|
|
163
|
-
methods=["POST"],
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
return _app
|
|
167
|
-
|
|
168
|
-
def openapi(self) -> dict[str, Any]:
|
|
169
|
-
"""
|
|
170
|
-
Build the OpenAPI specification for the served function.
|
|
171
|
-
Attach needed metadata for a better integration to fal.
|
|
172
|
-
"""
|
|
173
|
-
app = self._build_app()
|
|
174
|
-
spec = app.openapi()
|
|
175
|
-
_mark_order_openapi(spec)
|
|
176
|
-
return spec
|
|
177
|
-
|
|
178
|
-
def teardown(self):
|
|
179
|
-
"""Teardown the application after serving."""
|
|
129
|
+
def provide_hints(self) -> list[str]:
|
|
130
|
+
"""Provide hints for routing the application."""
|
|
131
|
+
raise NotImplementedError
|
|
180
132
|
|
|
181
133
|
|
|
182
134
|
@mainify
|
|
@@ -199,10 +151,7 @@ def endpoint(
|
|
|
199
151
|
|
|
200
152
|
def _fal_websocket_template(
|
|
201
153
|
func: EndpointT,
|
|
202
|
-
|
|
203
|
-
session_timeout: float | None = None,
|
|
204
|
-
input_modal: Any | None = None,
|
|
205
|
-
max_batch_size: int = 1,
|
|
154
|
+
route_signature: RouteSignature,
|
|
206
155
|
) -> EndpointT:
|
|
207
156
|
# A template for fal's realtime websocket endpoints to basically
|
|
208
157
|
# be a boilerplate for the user to fill in their inference function
|
|
@@ -220,14 +169,14 @@ def _fal_websocket_template(
|
|
|
220
169
|
try:
|
|
221
170
|
raw_input = await asyncio.wait_for(
|
|
222
171
|
websocket.receive_bytes(),
|
|
223
|
-
timeout=session_timeout,
|
|
172
|
+
timeout=route_signature.session_timeout,
|
|
224
173
|
)
|
|
225
174
|
except asyncio.TimeoutError:
|
|
226
175
|
return
|
|
227
176
|
|
|
228
177
|
input = msgpack.unpackb(raw_input, raw=False)
|
|
229
|
-
if input_modal:
|
|
230
|
-
input = input_modal(**input)
|
|
178
|
+
if route_signature.input_modal:
|
|
179
|
+
input = route_signature.input_modal(**input)
|
|
231
180
|
|
|
232
181
|
queue.append(input)
|
|
233
182
|
|
|
@@ -237,10 +186,18 @@ def _fal_websocket_template(
|
|
|
237
186
|
websocket: WebSocket,
|
|
238
187
|
) -> None:
|
|
239
188
|
loop = asyncio.get_event_loop()
|
|
240
|
-
|
|
189
|
+
max_allowed_buffering = route_signature.buffering or 1
|
|
190
|
+
outgoing_messages: asyncio.Queue[bytes] = asyncio.Queue(
|
|
191
|
+
maxsize=max_allowed_buffering * 2 # x2 for outgoing timings
|
|
192
|
+
)
|
|
241
193
|
|
|
242
194
|
async def emit(message):
|
|
243
|
-
|
|
195
|
+
if isinstance(message, bytes):
|
|
196
|
+
await websocket.send_bytes(message)
|
|
197
|
+
elif isinstance(message, str):
|
|
198
|
+
await websocket.send_text(message)
|
|
199
|
+
else:
|
|
200
|
+
raise TypeError(f"Can't send message of type {type(message)}")
|
|
244
201
|
|
|
245
202
|
async def background_emitter():
|
|
246
203
|
while True:
|
|
@@ -266,7 +223,7 @@ def _fal_websocket_template(
|
|
|
266
223
|
return None # End of input
|
|
267
224
|
|
|
268
225
|
batch = [input]
|
|
269
|
-
while queue and len(batch) < max_batch_size:
|
|
226
|
+
while queue and len(batch) < route_signature.max_batch_size:
|
|
270
227
|
next_input = queue.popleft()
|
|
271
228
|
if hasattr(input, "can_batch") and not input.can_batch(
|
|
272
229
|
next_input, len(batch)
|
|
@@ -275,7 +232,9 @@ def _fal_websocket_template(
|
|
|
275
232
|
break
|
|
276
233
|
batch.append(next_input)
|
|
277
234
|
|
|
235
|
+
t0 = loop.time()
|
|
278
236
|
output = await loop.run_in_executor(None, func, self, *batch) # type: ignore
|
|
237
|
+
total_time = loop.time() - t0
|
|
279
238
|
if not isinstance(output, dict):
|
|
280
239
|
# Handle pydantic output modal
|
|
281
240
|
if hasattr(output, "dict"):
|
|
@@ -285,18 +244,30 @@ def _fal_websocket_template(
|
|
|
285
244
|
f"Expected a dict or pydantic model as output, got {type(output)}"
|
|
286
245
|
)
|
|
287
246
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
247
|
+
messages = [
|
|
248
|
+
msgpack.packb(output, use_bin_type=True),
|
|
249
|
+
]
|
|
250
|
+
if route_signature.emit_timings:
|
|
251
|
+
# We emit x-fal messages in JSON, no matter what the input/output format is.
|
|
252
|
+
timings = {
|
|
253
|
+
"type": "x-fal-message",
|
|
254
|
+
"action": "timings",
|
|
255
|
+
"timing": total_time,
|
|
256
|
+
}
|
|
257
|
+
messages.append(json.dumps(timings, separators=(",", ":")))
|
|
258
|
+
|
|
259
|
+
for message in messages:
|
|
260
|
+
try:
|
|
261
|
+
outgoing_messages.put_nowait(message)
|
|
262
|
+
except asyncio.QueueFull:
|
|
263
|
+
await emit(message)
|
|
293
264
|
|
|
294
265
|
async def websocket_template(self, websocket: WebSocket) -> None:
|
|
295
266
|
import asyncio
|
|
296
267
|
|
|
297
268
|
await websocket.accept()
|
|
298
269
|
|
|
299
|
-
queue: deque[Any] = deque(maxlen=buffering)
|
|
270
|
+
queue: deque[Any] = deque(maxlen=route_signature.buffering)
|
|
300
271
|
input_task = asyncio.create_task(mirror_input(queue, websocket))
|
|
301
272
|
input_task.add_done_callback(lambda _: queue.append(None))
|
|
302
273
|
output_task = asyncio.create_task(mirror_output(self, queue, websocket))
|
|
@@ -314,7 +285,9 @@ def _fal_websocket_template(
|
|
|
314
285
|
# so we can just close the connection after the
|
|
315
286
|
# processing of the last input is done.
|
|
316
287
|
input_task.result()
|
|
317
|
-
await asyncio.wait_for(
|
|
288
|
+
await asyncio.wait_for(
|
|
289
|
+
output_task, timeout=route_signature.session_timeout
|
|
290
|
+
)
|
|
318
291
|
else:
|
|
319
292
|
assert output_task.done()
|
|
320
293
|
|
|
@@ -362,7 +335,8 @@ def _fal_websocket_template(
|
|
|
362
335
|
"websocket": WebSocket,
|
|
363
336
|
"return": None,
|
|
364
337
|
}
|
|
365
|
-
|
|
338
|
+
websocket_template.route_signature = route_signature # type: ignore
|
|
339
|
+
websocket_template.original_func = func # type: ignore
|
|
366
340
|
return typing.cast(EndpointT, websocket_template)
|
|
367
341
|
|
|
368
342
|
|
|
@@ -395,44 +369,17 @@ def realtime(
|
|
|
395
369
|
else:
|
|
396
370
|
input_modal = None
|
|
397
371
|
|
|
398
|
-
|
|
399
|
-
|
|
372
|
+
route_signature = RouteSignature(
|
|
373
|
+
path=path,
|
|
374
|
+
is_websocket=True,
|
|
375
|
+
input_modal=input_modal,
|
|
400
376
|
buffering=buffering,
|
|
401
377
|
session_timeout=session_timeout,
|
|
402
|
-
input_modal=input_modal,
|
|
403
378
|
max_batch_size=max_batch_size,
|
|
404
379
|
)
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
380
|
+
return _fal_websocket_template(
|
|
381
|
+
original_func,
|
|
382
|
+
route_signature,
|
|
383
|
+
)
|
|
408
384
|
|
|
409
385
|
return marker_fn
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
def _mark_order_openapi(spec: dict[str, Any]):
|
|
413
|
-
"""
|
|
414
|
-
Add x-fal-order-* keys to the OpenAPI specification to help the rendering of UI.
|
|
415
|
-
|
|
416
|
-
NOTE: We rely on the fact that fastapi and Python dicts keep the order of properties.
|
|
417
|
-
"""
|
|
418
|
-
|
|
419
|
-
def mark_order(obj: dict[str, Any], key: str):
|
|
420
|
-
obj[f"x-fal-order-{key}"] = list(obj[key].keys())
|
|
421
|
-
|
|
422
|
-
mark_order(spec, "paths")
|
|
423
|
-
|
|
424
|
-
def order_schema_object(schema: dict[str, Any]):
|
|
425
|
-
"""
|
|
426
|
-
Mark the order of properties in the schema object.
|
|
427
|
-
They can have 'allOf', 'properties' or '$ref' key.
|
|
428
|
-
"""
|
|
429
|
-
if "allOf" in schema:
|
|
430
|
-
for sub_schema in schema["allOf"]:
|
|
431
|
-
order_schema_object(sub_schema)
|
|
432
|
-
if "properties" in schema:
|
|
433
|
-
mark_order(schema, "properties")
|
|
434
|
-
|
|
435
|
-
for key in spec["components"].get("schemas") or {}:
|
|
436
|
-
order_schema_object(spec["components"]["schemas"][key])
|
|
437
|
-
|
|
438
|
-
return spec
|
fal/apps.py
CHANGED
|
@@ -63,7 +63,10 @@ class RequestHandle:
|
|
|
63
63
|
_creds: Credentials = field(default_factory=get_default_credentials, repr=False)
|
|
64
64
|
|
|
65
65
|
def __post_init__(self):
|
|
66
|
-
|
|
66
|
+
app_id = _backwards_compatible_app_id(self.app_id)
|
|
67
|
+
# drop any extra path components
|
|
68
|
+
user_id, app_name = app_id.split("/")[:2]
|
|
69
|
+
self.app_id = f"{user_id}/{app_name}"
|
|
67
70
|
|
|
68
71
|
def status(self, *, logs: bool = False) -> _Status:
|
|
69
72
|
"""Check the status of an async inference request."""
|
|
@@ -116,7 +119,16 @@ class RequestHandle:
|
|
|
116
119
|
+ f"/requests/{self.request_id}/"
|
|
117
120
|
)
|
|
118
121
|
response = _HTTP_CLIENT.get(url, headers=self._creds.to_headers())
|
|
119
|
-
|
|
122
|
+
try:
|
|
123
|
+
response.raise_for_status()
|
|
124
|
+
except httpx.HTTPStatusError as e:
|
|
125
|
+
if response.headers["Content-Type"] != "application/json":
|
|
126
|
+
raise
|
|
127
|
+
raise httpx.HTTPStatusError(
|
|
128
|
+
f"{response.status_code}: {response.text}",
|
|
129
|
+
request=e.request,
|
|
130
|
+
response=e.response,
|
|
131
|
+
) from e
|
|
120
132
|
|
|
121
133
|
data = response.json()
|
|
122
134
|
return data
|
|
@@ -134,20 +146,23 @@ class RequestHandle:
|
|
|
134
146
|
_HTTP_CLIENT = httpx.Client(headers={"User-Agent": "Fal/Python"})
|
|
135
147
|
|
|
136
148
|
|
|
137
|
-
def run(app_id: str, arguments: dict[str, Any], *, path: str = "
|
|
149
|
+
def run(app_id: str, arguments: dict[str, Any], *, path: str = "") -> dict[str, Any]:
|
|
138
150
|
"""Run an inference task on a Fal app and return the result."""
|
|
139
151
|
|
|
140
152
|
handle = submit(app_id, arguments, path=path)
|
|
141
153
|
return handle.get()
|
|
142
154
|
|
|
143
155
|
|
|
144
|
-
def submit(app_id: str, arguments: dict[str, Any], *, path: str = "
|
|
156
|
+
def submit(app_id: str, arguments: dict[str, Any], *, path: str = "") -> RequestHandle:
|
|
145
157
|
"""Submit an async inference task to the app. Returns a request handle
|
|
146
158
|
which can be used to check the status of the request and retrieve the
|
|
147
159
|
result."""
|
|
148
160
|
|
|
149
161
|
app_id = _backwards_compatible_app_id(app_id)
|
|
150
|
-
url = _QUEUE_URL_FORMAT.format(app_id=app_id)
|
|
162
|
+
url = _QUEUE_URL_FORMAT.format(app_id=app_id)
|
|
163
|
+
if path:
|
|
164
|
+
url += "/" + path.removeprefix("/")
|
|
165
|
+
|
|
151
166
|
creds = get_default_credentials()
|
|
152
167
|
|
|
153
168
|
response = _HTTP_CLIENT.post(
|
|
@@ -206,7 +221,10 @@ def _connect(app_id: str, *, path: str = "/realtime") -> Iterator[_RealtimeConne
|
|
|
206
221
|
from websockets.sync import client
|
|
207
222
|
|
|
208
223
|
app_id = _backwards_compatible_app_id(app_id)
|
|
209
|
-
url = _REALTIME_URL_FORMAT.format(app_id=app_id)
|
|
224
|
+
url = _REALTIME_URL_FORMAT.format(app_id=app_id)
|
|
225
|
+
if path:
|
|
226
|
+
url += "/" + path.removeprefix("/")
|
|
227
|
+
|
|
210
228
|
creds = get_default_credentials()
|
|
211
229
|
|
|
212
230
|
with client.connect(
|
fal/auth/__init__.py
CHANGED
fal/auth/auth0.py
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import functools
|
|
3
4
|
import time
|
|
4
5
|
import warnings
|
|
5
6
|
|
|
6
7
|
import click
|
|
7
|
-
import
|
|
8
|
-
from auth0.authentication.token_verifier import (
|
|
9
|
-
AsymmetricSignatureVerifier,
|
|
10
|
-
TokenVerifier,
|
|
11
|
-
)
|
|
8
|
+
import httpx
|
|
12
9
|
|
|
13
10
|
from fal.console import console
|
|
14
11
|
from fal.console.icons import CHECK_ICON
|
|
@@ -54,7 +51,7 @@ def login() -> dict:
|
|
|
54
51
|
"client_id": AUTH0_CLIENT_ID,
|
|
55
52
|
"scope": AUTH0_SCOPE,
|
|
56
53
|
}
|
|
57
|
-
device_code_response =
|
|
54
|
+
device_code_response = httpx.post(
|
|
58
55
|
f"https://{AUTH0_DOMAIN}/oauth/device/code", data=device_code_payload
|
|
59
56
|
)
|
|
60
57
|
|
|
@@ -81,7 +78,7 @@ def login() -> dict:
|
|
|
81
78
|
|
|
82
79
|
with console.status("Waiting for confirmation...") as status:
|
|
83
80
|
while True:
|
|
84
|
-
token_response =
|
|
81
|
+
token_response = httpx.post(
|
|
85
82
|
f"https://{AUTH0_DOMAIN}/oauth/token", data=token_payload
|
|
86
83
|
)
|
|
87
84
|
|
|
@@ -109,14 +106,12 @@ def refresh(token: str) -> dict:
|
|
|
109
106
|
"refresh_token": token,
|
|
110
107
|
}
|
|
111
108
|
|
|
112
|
-
token_response =
|
|
109
|
+
token_response = httpx.post(
|
|
113
110
|
f"https://{AUTH0_DOMAIN}/oauth/token", data=token_payload
|
|
114
111
|
)
|
|
115
112
|
|
|
116
113
|
token_data = token_response.json()
|
|
117
114
|
if token_response.status_code == 200:
|
|
118
|
-
# DEBUG: print("Authenticated!")
|
|
119
|
-
|
|
120
115
|
validate_id_token(token_data["id_token"])
|
|
121
116
|
|
|
122
117
|
return token_data
|
|
@@ -130,7 +125,7 @@ def revoke(token: str):
|
|
|
130
125
|
"token": token,
|
|
131
126
|
}
|
|
132
127
|
|
|
133
|
-
token_response =
|
|
128
|
+
token_response = httpx.post(
|
|
134
129
|
f"https://{AUTH0_DOMAIN}/oauth/revoke", data=token_payload
|
|
135
130
|
)
|
|
136
131
|
|
|
@@ -142,7 +137,7 @@ def revoke(token: str):
|
|
|
142
137
|
|
|
143
138
|
|
|
144
139
|
def get_user_info(bearer_token: str) -> dict:
|
|
145
|
-
userinfo_response =
|
|
140
|
+
userinfo_response = httpx.post(
|
|
146
141
|
f"https://{AUTH0_DOMAIN}/userinfo",
|
|
147
142
|
headers={"Authorization": bearer_token},
|
|
148
143
|
)
|
|
@@ -153,24 +148,38 @@ def get_user_info(bearer_token: str) -> dict:
|
|
|
153
148
|
return userinfo_response.json()
|
|
154
149
|
|
|
155
150
|
|
|
151
|
+
@functools.lru_cache
|
|
152
|
+
def build_jwk_client():
|
|
153
|
+
from jwt import PyJWKClient
|
|
154
|
+
|
|
155
|
+
return PyJWKClient(AUTH0_JWKS_URL, cache_keys=True)
|
|
156
|
+
|
|
157
|
+
|
|
156
158
|
def validate_id_token(token: str):
|
|
157
159
|
"""
|
|
158
|
-
|
|
159
|
-
`id_token`s are intended for the client (this sdk) only.
|
|
160
|
-
Never send one to another service.
|
|
161
|
-
|
|
162
|
-
:param id_token:
|
|
160
|
+
id_token is intended for the client (this sdk) only. Never send one to another service.
|
|
163
161
|
"""
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
162
|
+
from jwt import decode
|
|
163
|
+
|
|
164
|
+
jwk_client = build_jwk_client()
|
|
165
|
+
|
|
166
|
+
decode(
|
|
167
|
+
token,
|
|
168
|
+
key=jwk_client.get_signing_key_from_jwt(token).key,
|
|
169
|
+
algorithms=AUTH0_ALGORITHMS,
|
|
167
170
|
issuer=AUTH0_ISSUER,
|
|
168
171
|
audience=AUTH0_CLIENT_ID,
|
|
172
|
+
options={
|
|
173
|
+
"verify_signature": True,
|
|
174
|
+
"verify_exp": True,
|
|
175
|
+
"verify_iat": True,
|
|
176
|
+
"verify_aud": True,
|
|
177
|
+
"verify_iss": True,
|
|
178
|
+
},
|
|
169
179
|
)
|
|
170
|
-
tv.verify(token)
|
|
171
180
|
|
|
172
181
|
|
|
173
|
-
def
|
|
182
|
+
def verify_access_token_expiration(token: str):
|
|
174
183
|
from datetime import timedelta
|
|
175
184
|
|
|
176
185
|
from jwt import decode
|
fal/cli.py
CHANGED
|
@@ -29,7 +29,7 @@ PORT_ENVVAR = "FAL_PORT"
|
|
|
29
29
|
DEBUG_ENABLED = False
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
logger = get_logger(__name__)
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
class ExecutionInfo:
|
|
@@ -63,13 +63,13 @@ class MainGroup(click.Group):
|
|
|
63
63
|
qualified_name, attributes={"invocation_id": invocation_id}
|
|
64
64
|
):
|
|
65
65
|
try:
|
|
66
|
-
|
|
66
|
+
logger.debug(
|
|
67
67
|
f"Executing command: {qualified_name}",
|
|
68
68
|
command=qualified_name,
|
|
69
69
|
)
|
|
70
70
|
return super().invoke(ctx)
|
|
71
71
|
except Exception as exception:
|
|
72
|
-
|
|
72
|
+
logger.error(exception)
|
|
73
73
|
if execution_info.debug:
|
|
74
74
|
# Here we supress detailed errors on click lines because
|
|
75
75
|
# they're mostly decorator calls, irrelevant to the dev's error tracing
|
|
@@ -468,6 +468,7 @@ def alias_list_runners(
|
|
|
468
468
|
table.add_column("Runner ID")
|
|
469
469
|
table.add_column("In Flight Requests")
|
|
470
470
|
table.add_column("Expires in")
|
|
471
|
+
table.add_column("Uptime")
|
|
471
472
|
|
|
472
473
|
for runner in runners:
|
|
473
474
|
table.add_row(
|
|
@@ -478,6 +479,7 @@ def alias_list_runners(
|
|
|
478
479
|
if not runner.expiration_countdown
|
|
479
480
|
else f"{runner.expiration_countdown}s"
|
|
480
481
|
),
|
|
482
|
+
f"{runner.uptime} ({runner.uptime.total_seconds()}s)",
|
|
481
483
|
)
|
|
482
484
|
|
|
483
485
|
console.print(table)
|
fal/env.py
CHANGED
fal/logging/__init__.py
CHANGED
|
@@ -5,7 +5,6 @@ from typing import Any
|
|
|
5
5
|
import structlog
|
|
6
6
|
from structlog.typing import EventDict, WrappedLogger
|
|
7
7
|
|
|
8
|
-
from .datadog import submit_to_datadog
|
|
9
8
|
from .style import LEVEL_STYLES
|
|
10
9
|
from .user import add_user_id
|
|
11
10
|
|
|
@@ -45,7 +44,6 @@ structlog.configure(
|
|
|
45
44
|
structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S"),
|
|
46
45
|
structlog.processors.StackInfoRenderer(),
|
|
47
46
|
add_user_id,
|
|
48
|
-
submit_to_datadog,
|
|
49
47
|
_console_log_output,
|
|
50
48
|
],
|
|
51
49
|
wrapper_class=structlog.stdlib.BoundLogger,
|
fal/logging/trace.py
CHANGED
|
@@ -7,6 +7,10 @@ from grpc_interceptor import ClientCallDetails, ClientInterceptor
|
|
|
7
7
|
from opentelemetry import trace
|
|
8
8
|
from opentelemetry.sdk.trace import TracerProvider
|
|
9
9
|
|
|
10
|
+
from fal.logging import get_logger
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
|
+
|
|
10
14
|
provider = TracerProvider()
|
|
11
15
|
# The line below can be used in dev to inspect opentelemetry result
|
|
12
16
|
# It must be imported from opentelemetry.sdk.trace.export
|
|
@@ -41,6 +45,7 @@ class TraceContextInterceptor(ClientInterceptor):
|
|
|
41
45
|
call_details: ClientCallDetails,
|
|
42
46
|
):
|
|
43
47
|
current_span = get_current_span_context()
|
|
48
|
+
|
|
44
49
|
if current_span is not None:
|
|
45
50
|
new_details = call_details._replace(
|
|
46
51
|
metadata=(
|
|
@@ -50,5 +55,7 @@ class TraceContextInterceptor(ClientInterceptor):
|
|
|
50
55
|
("x-fal-invocation-id", current_span.invocation_id),
|
|
51
56
|
)
|
|
52
57
|
)
|
|
53
|
-
|
|
58
|
+
call_details = new_details
|
|
59
|
+
|
|
60
|
+
logger.debug("Calling %s", call_details)
|
|
54
61
|
return method(request_or_iterator, call_details)
|
fal/sdk.py
CHANGED
|
@@ -29,7 +29,7 @@ FAL_SERVERLESS_DEFAULT_KEEP_ALIVE = 10
|
|
|
29
29
|
FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING = 1
|
|
30
30
|
FAL_SERVERLESS_DEFAULT_MIN_CONCURRENCY = 0
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
logger = get_logger(__name__)
|
|
33
33
|
|
|
34
34
|
patch_dill()
|
|
35
35
|
|
|
@@ -39,8 +39,29 @@ class ServerCredentials:
|
|
|
39
39
|
raise NotImplementedError
|
|
40
40
|
|
|
41
41
|
@property
|
|
42
|
-
def
|
|
43
|
-
|
|
42
|
+
def base_options(self) -> dict[str, str | int]:
|
|
43
|
+
import json
|
|
44
|
+
|
|
45
|
+
grpc_ops: dict[str, str | int] = dict(GRPC_OPTIONS)
|
|
46
|
+
grpc_ops["grpc.enable_retries"] = 1
|
|
47
|
+
grpc_ops["grpc.service_config"] = json.dumps(
|
|
48
|
+
{
|
|
49
|
+
"methodConfig": [
|
|
50
|
+
{
|
|
51
|
+
"name": [{}],
|
|
52
|
+
"retryPolicy": {
|
|
53
|
+
"maxAttempts": 5,
|
|
54
|
+
"initialBackoff": "0.1s",
|
|
55
|
+
"maxBackoff": "5s",
|
|
56
|
+
"backoffMultiplier": 2,
|
|
57
|
+
"retryableStatusCodes": ["UNAVAILABLE"],
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
return grpc_ops
|
|
44
65
|
|
|
45
66
|
|
|
46
67
|
class LocalCredentials(ServerCredentials):
|
|
@@ -140,7 +161,7 @@ def get_default_credentials() -> Credentials:
|
|
|
140
161
|
|
|
141
162
|
key_creds = key_credentials()
|
|
142
163
|
if key_creds:
|
|
143
|
-
|
|
164
|
+
logger.debug("Using key credentials")
|
|
144
165
|
return FalServerlessKeyCredentials(key_creds[0], key_creds[1])
|
|
145
166
|
else:
|
|
146
167
|
return AuthenticatedCredentials()
|
|
@@ -183,6 +204,7 @@ class RunnerInfo:
|
|
|
183
204
|
runner_id: str
|
|
184
205
|
in_flight_requests: int
|
|
185
206
|
expiration_countdown: int
|
|
207
|
+
uptime: timedelta
|
|
186
208
|
|
|
187
209
|
|
|
188
210
|
@dataclass
|
|
@@ -270,6 +292,7 @@ def _from_grpc_runner_info(message: isolate_proto.RunnerInfo) -> RunnerInfo:
|
|
|
270
292
|
runner_id=message.runner_id,
|
|
271
293
|
in_flight_requests=message.in_flight_requests,
|
|
272
294
|
expiration_countdown=message.expiration_countdown,
|
|
295
|
+
uptime=timedelta(seconds=message.uptime),
|
|
273
296
|
)
|
|
274
297
|
|
|
275
298
|
|
|
@@ -346,10 +369,14 @@ class FalServerlessConnection:
|
|
|
346
369
|
if self._stub:
|
|
347
370
|
return self._stub
|
|
348
371
|
|
|
349
|
-
options = self.credentials.server_credentials.
|
|
372
|
+
options = self.credentials.server_credentials.base_options
|
|
350
373
|
channel_creds = self.credentials.to_grpc()
|
|
351
374
|
channel = self._stack.enter_context(
|
|
352
|
-
grpc.secure_channel(
|
|
375
|
+
grpc.secure_channel(
|
|
376
|
+
target=self.hostname,
|
|
377
|
+
credentials=channel_creds,
|
|
378
|
+
options=list(options.items()),
|
|
379
|
+
)
|
|
353
380
|
)
|
|
354
381
|
channel = grpc.intercept_channel(channel, TraceContextInterceptor())
|
|
355
382
|
self._stub = isolate_proto.IsolateControllerStub(channel)
|
fal/toolkit/__init__.py
CHANGED
|
@@ -12,3 +12,19 @@ from fal.toolkit.utils import (
|
|
|
12
12
|
download_file,
|
|
13
13
|
download_model_weights,
|
|
14
14
|
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"CompressedFile",
|
|
18
|
+
"File",
|
|
19
|
+
"Image",
|
|
20
|
+
"ImageSizeInput",
|
|
21
|
+
"get_image_size",
|
|
22
|
+
"mainify",
|
|
23
|
+
"optimize",
|
|
24
|
+
"FAL_MODEL_WEIGHTS_DIR",
|
|
25
|
+
"FAL_PERSISTENT_DIR",
|
|
26
|
+
"FAL_REPOSITORY_DIR",
|
|
27
|
+
"clone_repository",
|
|
28
|
+
"download_file",
|
|
29
|
+
"download_model_weights",
|
|
30
|
+
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: fal
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.3
|
|
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 (
|
|
22
|
+
Requires-Dist: isolate-proto (==0.3.3)
|
|
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)
|
|
@@ -19,35 +19,34 @@ openapi_fal_rest/models/lock_reason.py,sha256=3b_foCV6bZKvsbyic3hM1_qzvJk_9ZD_5m
|
|
|
19
19
|
openapi_fal_rest/models/validation_error.py,sha256=I6tB-HbEOmE0ua27erDX5PX5YUynENv_dgPN3SrwTrQ,2091
|
|
20
20
|
openapi_fal_rest/py.typed,sha256=8ZJUsxZiuOy1oJeVhsTWQhTG_6pTVHVXk5hJL79ebTk,25
|
|
21
21
|
openapi_fal_rest/types.py,sha256=4xaUIOliefW-5jz_p-JT2LO7-V0wKWaniHGtjPBQfvQ,993
|
|
22
|
-
fal/__init__.py,sha256
|
|
22
|
+
fal/__init__.py,sha256=6SvCuotCb0tuqSWDZSFDjtySktJ5m1QpVIlefumJpvM,1199
|
|
23
23
|
fal/_serialization.py,sha256=l_dZuSX5BT7SogXw1CalYLfT2H3zy3tfq4y6jHuxZqQ,4201
|
|
24
|
-
fal/api.py,sha256=
|
|
25
|
-
fal/app.py,sha256=
|
|
26
|
-
fal/apps.py,sha256=
|
|
27
|
-
fal/auth/__init__.py,sha256
|
|
28
|
-
fal/auth/auth0.py,sha256=
|
|
24
|
+
fal/api.py,sha256=Qack_oYNkvF4qown3P_oKvyvRfTJkhOG7PL1xpa8FUQ,32872
|
|
25
|
+
fal/app.py,sha256=KAIgvBBpvzp6oY8BpH5hFOLDUpG4bjtwlV5jPGj2IE0,12487
|
|
26
|
+
fal/apps.py,sha256=T387WJDtKpKEytu27b2AVqqo0uijKrRT9ymk6FcRiEw,6705
|
|
27
|
+
fal/auth/__init__.py,sha256=4W_9svpsmohRPhBi4yjx9rAPaUeBTHaJvSRpdRzXA5s,3133
|
|
28
|
+
fal/auth/auth0.py,sha256=hQ3ZTqqsgpL62GsNB9KvjE8k_2hxXMIJb5TNpRmaiYs,5485
|
|
29
29
|
fal/auth/local.py,sha256=lZqp4j32l2xFpY8zYvLoIHHyJrNAJDcm5MxgsLpY_pw,1786
|
|
30
|
-
fal/cli.py,sha256=
|
|
30
|
+
fal/cli.py,sha256=nLk4LJsGvLicA_iW0T1ldYb_igMwYOdC2fQxUsdWCRQ,17236
|
|
31
31
|
fal/console/__init__.py,sha256=ernZ4bzvvliQh5SmrEqQ7lA5eVcbw6Ra2jalKtA7dxg,132
|
|
32
32
|
fal/console/icons.py,sha256=De9MfFaSkO2Lqfne13n3PrYfTXJVIzYZVqYn5BWsdrA,108
|
|
33
33
|
fal/console/ux.py,sha256=4vj1aGA3grRn-ebeMuDLR6u3YjMwUGpqtNgdTG9su5s,485
|
|
34
|
-
fal/env.py,sha256
|
|
34
|
+
fal/env.py,sha256=-fA8x62BbOX3MOuO0maupa-_QJ9PNwr8ogfeG11QUyQ,53
|
|
35
35
|
fal/exceptions/__init__.py,sha256=Q4LCSqIrJ8GFQZWH5BvWL5mDPR0HwYQuIhNvsdiOkEU,938
|
|
36
36
|
fal/exceptions/_base.py,sha256=LeQmx-soL_-s1742WKN18VwTVjUuYP0L0BdQHPJBpM4,460
|
|
37
37
|
fal/exceptions/auth.py,sha256=01Ro7SyGJpwchubdHe14Cl6-Al1jUj16Sy4BvakNWf4,384
|
|
38
38
|
fal/exceptions/handlers.py,sha256=b21a8S13euECArjpgm2N69HsShqLYVqAboIeMoWlWA4,1414
|
|
39
39
|
fal/flags.py,sha256=8OaKkJg_-UvtyRbZf-rW5ZTW3B1xQpzzXnLRNFB7grA,889
|
|
40
|
-
fal/logging/__init__.py,sha256=
|
|
41
|
-
fal/logging/datadog.py,sha256=pC63CrJLF-bXUaF5RUxkdq9wBgklzrMglcdM9gw7teA,2542
|
|
40
|
+
fal/logging/__init__.py,sha256=snqprf7-sKw6oAATS_Yxklf-a3XhLg0vIHICPwLp6TM,1583
|
|
42
41
|
fal/logging/isolate.py,sha256=yDW_P4aR-t53IRmvD2Iprufv1Wn-xQXoBbMB2Ufr59s,2122
|
|
43
42
|
fal/logging/style.py,sha256=ckIgHzvF4DShM5kQh8F133X53z_vF46snuDHVmo_h9g,386
|
|
44
|
-
fal/logging/trace.py,sha256
|
|
43
|
+
fal/logging/trace.py,sha256=OhzB6d4rQZimBc18WFLqH_9BGfqFFumKKTAGSsmWRMg,1904
|
|
45
44
|
fal/logging/user.py,sha256=A8vbZX9z13TPZEDzvlbvCDDdD0EL1KrCP3qHdrT58-A,632
|
|
46
45
|
fal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
47
46
|
fal/rest_client.py,sha256=kGBGmuyHfX1lR910EoKCYPjsyU8MdXawT_cW2q8Sajc,568
|
|
48
|
-
fal/sdk.py,sha256=
|
|
47
|
+
fal/sdk.py,sha256=Z3MQsD8MMQZq_GEC2VjaYChdNafFJtsgdk77-VK6N44,18782
|
|
49
48
|
fal/sync.py,sha256=Ljet584PVFz4r888-0bwV1Kio-tTneF_85TnHvBPvJw,4277
|
|
50
|
-
fal/toolkit/__init__.py,sha256=
|
|
49
|
+
fal/toolkit/__init__.py,sha256=JDNBT_duflp93geeAzw2kFmGzG5odWnPJEXFLXE2nF4,713
|
|
51
50
|
fal/toolkit/exceptions.py,sha256=--WKKYxUop6WFy_vqAPXK6uH8C-JR98gnNXwhHNCb7E,258
|
|
52
51
|
fal/toolkit/file/__init__.py,sha256=YpUU6YziZV1AMuq12L0EDWToS0sgpHSGWsARbiOEHWk,56
|
|
53
52
|
fal/toolkit/file/file.py,sha256=ku4agJiGXU2gdfZmFrU5mDlVsag834zoeskbo-6ErEU,5926
|
|
@@ -61,7 +60,7 @@ fal/toolkit/mainify.py,sha256=E7gE45nZQZoaJdSlIq0mqajcH-IjcuPBWFmKm5hvhAU,406
|
|
|
61
60
|
fal/toolkit/optimize.py,sha256=OIhX0T-efRMgUJDpvL0bujdun5SovZgTdKxNOv01b_Y,1394
|
|
62
61
|
fal/toolkit/utils/__init__.py,sha256=b3zVpm50Upx1saXU7RiV9r9in6-Chs4OU9KRjBv7MYI,83
|
|
63
62
|
fal/toolkit/utils/download_utils.py,sha256=bigcLJjLK1OBAGxpYisJ0-5vcQCh0HAPuCykPrcCNd0,15596
|
|
64
|
-
fal-0.12.
|
|
65
|
-
fal-0.12.
|
|
66
|
-
fal-0.12.
|
|
67
|
-
fal-0.12.
|
|
63
|
+
fal-0.12.3.dist-info/METADATA,sha256=0eR9dtKw9ZU7y2Dxjx9NtXp--hmw7XG24LuTylD5BlE,2930
|
|
64
|
+
fal-0.12.3.dist-info/WHEEL,sha256=vVCvjcmxuUltf8cYhJ0sJMRDLr1XsPuxEId8YDzbyCY,88
|
|
65
|
+
fal-0.12.3.dist-info/entry_points.txt,sha256=nE9GBVV3PdBosudFwbIzZQUe_9lfPR6EH8K_FdDASnM,62
|
|
66
|
+
fal-0.12.3.dist-info/RECORD,,
|
fal/logging/datadog.py
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import sys
|
|
4
|
-
import traceback
|
|
5
|
-
import warnings
|
|
6
|
-
|
|
7
|
-
from datadog_api_client import Configuration, ThreadedApiClient
|
|
8
|
-
from datadog_api_client.v2.api.logs_api import LogsApi
|
|
9
|
-
from datadog_api_client.v2.model.http_log import HTTPLog
|
|
10
|
-
from datadog_api_client.v2.model.http_log_item import HTTPLogItem
|
|
11
|
-
from structlog.typing import EventDict, WrappedLogger
|
|
12
|
-
|
|
13
|
-
from fal.env import CLI_ENV, DATADOG_API_KEY, DATADOG_APP_KEY
|
|
14
|
-
from fal.logging.trace import get_current_span_context
|
|
15
|
-
|
|
16
|
-
if sys.version_info >= (3, 10):
|
|
17
|
-
import importlib.metadata as importlib_metadata
|
|
18
|
-
else:
|
|
19
|
-
import importlib_metadata
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
configuration = Configuration()
|
|
23
|
-
configuration.api_key["apiKeyAuth"] = DATADOG_API_KEY
|
|
24
|
-
configuration.api_key["appKeyAuth"] = DATADOG_APP_KEY
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def _is_error_level(level: str) -> bool:
|
|
28
|
-
return level in ["error", "exception", "critical"]
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def submit_to_datadog(
|
|
32
|
-
logger: WrappedLogger, method_name: str, event_dict: EventDict
|
|
33
|
-
) -> EventDict:
|
|
34
|
-
if configuration.api_key["apiKeyAuth"] is None:
|
|
35
|
-
return event_dict
|
|
36
|
-
|
|
37
|
-
log_data = dict(event_dict)
|
|
38
|
-
event = log_data.pop("event")
|
|
39
|
-
level = log_data.pop("level")
|
|
40
|
-
|
|
41
|
-
current_span = get_current_span_context()
|
|
42
|
-
attributes = log_data.copy()
|
|
43
|
-
tags: dict[str, str] = {}
|
|
44
|
-
if current_span is not None:
|
|
45
|
-
tags["invocation_id"] = current_span.invocation_id
|
|
46
|
-
attributes["dd.trace_id"] = current_span.trace_id
|
|
47
|
-
attributes["dd.span_id"] = current_span.span_id
|
|
48
|
-
|
|
49
|
-
stack = None
|
|
50
|
-
if _is_error_level(method_name):
|
|
51
|
-
attributes["error.message"] = str(event)
|
|
52
|
-
attributes["error.kind"] = type(event).__name__
|
|
53
|
-
stack = traceback.format_exc()
|
|
54
|
-
|
|
55
|
-
ddtags = ",".join([f"{key}:{value}" for (key, value) in tags.items()])
|
|
56
|
-
log_item = HTTPLogItem(
|
|
57
|
-
message=str(event),
|
|
58
|
-
level=level,
|
|
59
|
-
hostname="client",
|
|
60
|
-
service="fal-serverless-cli",
|
|
61
|
-
env=CLI_ENV,
|
|
62
|
-
version=importlib_metadata.version("fal"),
|
|
63
|
-
ddsource="python",
|
|
64
|
-
ddtags=ddtags,
|
|
65
|
-
traceback=stack,
|
|
66
|
-
**attributes,
|
|
67
|
-
)
|
|
68
|
-
with ThreadedApiClient(configuration) as api_client:
|
|
69
|
-
# Deprecation warning of underlying dependencies should not be shown to users
|
|
70
|
-
# TODO enable it only in the prod distribution (better: remove when fixed)
|
|
71
|
-
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
|
72
|
-
|
|
73
|
-
# TODO improve this - add batching
|
|
74
|
-
api_instance = LogsApi(api_client)
|
|
75
|
-
_ = api_instance.submit_log(HTTPLog([log_item]))
|
|
76
|
-
api_client.close()
|
|
77
|
-
|
|
78
|
-
return event_dict
|
|
File without changes
|
|
File without changes
|