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 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(["fastapi==0.99.1", "uvicorn"])
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 ServeWrapper:
734
- _func: Callable
735
-
736
- def __init__(self, func: Callable):
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
- app = self.build_app()
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
- obj[f"x-fal-order-{key}"] = list(obj[key].keys())
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
- if "allOf" in schema:
801
- for sub_schema in schema["allOf"]:
802
- order_schema_object(sub_schema)
803
- if "properties" in schema:
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") or {}:
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, NamedTuple, TypeVar
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
- serve=True,
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
- def _build_app(self) -> FastAPI:
107
- from fastapi import FastAPI
108
- from fastapi.middleware.cors import CORSMiddleware
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
- @asynccontextmanager
111
- async def lifespan(app: FastAPI):
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
- _app = FastAPI(lifespan=lifespan)
107
+ def teardown(self):
108
+ """Teardown the application after serving."""
119
109
 
120
- @_app.middleware("http")
121
- async def provide_hints(request, call_next):
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 as exc:
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
- _app.add_middleware(
140
- CORSMiddleware,
141
- allow_credentials=True,
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
- buffering: int | None = None,
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
- outgoing_messages: asyncio.Queue[bytes] = asyncio.Queue(maxsize=buffering or 1)
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
- await websocket.send_bytes(message)
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
- message = msgpack.packb(output, use_bin_type=True)
289
- try:
290
- outgoing_messages.put_nowait(message)
291
- except asyncio.QueueFull:
292
- await emit(message)
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(output_task, timeout=session_timeout)
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
- callable = _fal_websocket_template(
399
- original_func,
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
- callable.route_signature = RouteSignature(path=path, is_websocket=True) # type: ignore
406
- callable.original_func = original_func # type: ignore
407
- return callable
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
- self.app_id = _backwards_compatible_app_id(self.app_id)
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
- response.raise_for_status()
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 = "/") -> dict[str, Any]:
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 = "/") -> RequestHandle:
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) + path
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) + path
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
@@ -62,7 +62,7 @@ def _fetch_access_token() -> str:
62
62
 
63
63
  if access_token is not None:
64
64
  try:
65
- auth0.validate_access_token(access_token)
65
+ auth0.verify_access_token_expiration(access_token)
66
66
  return access_token
67
67
  except:
68
68
  # access_token expired, will refresh
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 requests
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 = requests.post(
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 = requests.post(
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 = requests.post(
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 = requests.post(
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 = requests.post(
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
- Verify the token and its precedence.
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
- sv = AsymmetricSignatureVerifier(AUTH0_JWKS_URL)
165
- tv = TokenVerifier(
166
- signature_verifier=sv,
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 validate_access_token(token: str):
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
- log = get_logger(__name__)
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
- log.debug(
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
- log.error(exception)
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
@@ -1,7 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  CLI_ENV = "prod"
4
-
5
- DATADOG_API_KEY = "pub4cd6a1c4763c93ad5af2740b2d931145"
6
- DATADOG_APP_KEY = "4981bae640864a409dcfaddd69c2a157523b1585"
7
-
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
- return method(request_or_iterator, new_details)
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
- log = get_logger(__name__)
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 extra_options(self) -> list[tuple[str, str]]:
43
- return GRPC_OPTIONS
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
- log.debug("Using key credentials")
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.extra_options
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(self.hostname, channel_creds, options)
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.2
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,<0.25.0)
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.1,<0.4.0)
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=--wkZvDt27S2vPi3hk8Nc-XRUl4ac_cB6P_12VSqODw,1085
22
+ fal/__init__.py,sha256=6SvCuotCb0tuqSWDZSFDjtySktJ5m1QpVIlefumJpvM,1199
23
23
  fal/_serialization.py,sha256=l_dZuSX5BT7SogXw1CalYLfT2H3zy3tfq4y6jHuxZqQ,4201
24
- fal/api.py,sha256=xEVuBRVKac82MgcHUqw10IDHims5fwv6oxqpwAmAx1Q,30353
25
- fal/app.py,sha256=_LFUjrf6K8qYKKpDyfJR2dOFmumJxZTgbOz64Ltea9g,13660
26
- fal/apps.py,sha256=utg7mGvklL1oyUBbuEDDcVN234yDLVseqnY1DJsT8nY,6126
27
- fal/auth/__init__.py,sha256=-s7kogdp3szq_pAxHGkfOQjrSBT1DlBx3X-Ifo_Ne1o,3124
28
- fal/auth/auth0.py,sha256=S7bl0ti1w8t-OaAggju12xx0lOtqkXy_qML7QP0t4uI,5303
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=dxnsHzsochm-u8oQ3gFy6uVHbllI_14bU54bDpyMOwk,17129
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=g_s2FAtY2-6zyTar_2NUmswHcab--3xozEJW4E6Y9iQ,172
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=tXFlHBtPFydL3Wgzhq72-EzCFKrnDYvZIF0pKYGVxac,1649
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=-_ShrBsBl9jUlGC-Lwe2r-LIkfj21SETQAhSNrWxbGs,1807
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=Reo6xTxvDCNEEawl4GUNQi83RQ_g2isuAVhXw7fz9oU,17918
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=uHhoAG9FjLzxkmF-Qo9fxz8lrL9t6GE-3zsa_O4Q9V0,420
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.2.dist-info/WHEEL,sha256=vVCvjcmxuUltf8cYhJ0sJMRDLr1XsPuxEId8YDzbyCY,88
65
- fal-0.12.2.dist-info/METADATA,sha256=56jV53hKxoUJA81vNfeG1AVXiVciOE10nb6nICONR5M,3078
66
- fal-0.12.2.dist-info/entry_points.txt,sha256=nE9GBVV3PdBosudFwbIzZQUe_9lfPR6EH8K_FdDASnM,62
67
- fal-0.12.2.dist-info/RECORD,,
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