fal 0.12.1__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.

Files changed (116) hide show
  1. fal/__init__.py +12 -3
  2. fal/_serialization.py +18 -0
  3. fal/api.py +140 -59
  4. fal/app.py +309 -86
  5. fal/apps.py +92 -8
  6. fal/auth/__init__.py +20 -1
  7. fal/auth/auth0.py +32 -22
  8. fal/cli.py +34 -52
  9. fal/env.py +0 -4
  10. fal/exceptions/handlers.py +3 -2
  11. fal/flags.py +5 -0
  12. fal/logging/__init__.py +0 -2
  13. fal/logging/trace.py +8 -1
  14. fal/logging/user.py +2 -1
  15. fal/rest_client.py +2 -2
  16. fal/sdk.py +46 -31
  17. fal/sync.py +3 -3
  18. fal/toolkit/__init__.py +18 -1
  19. fal/toolkit/file/file.py +98 -11
  20. fal/toolkit/file/providers/fal.py +43 -2
  21. fal/toolkit/file/types.py +1 -1
  22. fal/toolkit/image/image.py +26 -4
  23. fal/toolkit/optimize.py +50 -0
  24. fal/toolkit/utils/download_utils.py +59 -13
  25. {fal-0.12.1.dist-info → fal-0.12.3.dist-info}/METADATA +7 -7
  26. fal-0.12.3.dist-info/RECORD +66 -0
  27. openapi_fal_rest/models/__init__.py +2 -70
  28. openapi_fal_rest/models/customer_details.py +26 -0
  29. openapi_fal_rest/models/lock_reason.py +16 -0
  30. fal/logging/datadog.py +0 -77
  31. fal-0.12.1.dist-info/RECORD +0 -147
  32. openapi_fal_rest/api/admin/get_invoice_users.py +0 -142
  33. openapi_fal_rest/api/admin/get_usage_per_user.py +0 -199
  34. openapi_fal_rest/api/admin/handle_user_lock.py +0 -191
  35. openapi_fal_rest/api/admin/set_billing_type.py +0 -186
  36. openapi_fal_rest/api/applications/get_status_applications_app_user_id_app_alias_or_id_status_get.py +0 -179
  37. openapi_fal_rest/api/billing/delete_payment_method.py +0 -162
  38. openapi_fal_rest/api/billing/get_checkout_page.py +0 -198
  39. openapi_fal_rest/api/billing/get_setup_intent_key.py +0 -141
  40. openapi_fal_rest/api/billing/get_user_invoices.py +0 -152
  41. openapi_fal_rest/api/billing/get_user_payment_methods.py +0 -152
  42. openapi_fal_rest/api/billing/get_user_price.py +0 -186
  43. openapi_fal_rest/api/billing/get_user_spending.py +0 -192
  44. openapi_fal_rest/api/billing/handle_stripe_webhook.py +0 -173
  45. openapi_fal_rest/api/billing/upcoming_invoice.py +0 -143
  46. openapi_fal_rest/api/billing/update_customer_budget.py +0 -183
  47. openapi_fal_rest/api/files/delete.py +0 -162
  48. openapi_fal_rest/api/files/download.py +0 -162
  49. openapi_fal_rest/api/files/file_exists.py +0 -183
  50. openapi_fal_rest/api/files/list_directory.py +0 -173
  51. openapi_fal_rest/api/files/list_root.py +0 -152
  52. openapi_fal_rest/api/files/upload_from_url.py +0 -179
  53. openapi_fal_rest/api/health/__init__.py +0 -0
  54. openapi_fal_rest/api/health/check.py +0 -136
  55. openapi_fal_rest/api/keys/__init__.py +0 -0
  56. openapi_fal_rest/api/keys/create_key.py +0 -188
  57. openapi_fal_rest/api/keys/delete_key.py +0 -162
  58. openapi_fal_rest/api/keys/list_keys.py +0 -152
  59. openapi_fal_rest/api/logs/__init__.py +0 -0
  60. openapi_fal_rest/api/logs/list_since.py +0 -224
  61. openapi_fal_rest/api/requests/__init__.py +0 -0
  62. openapi_fal_rest/api/requests/requests.py +0 -247
  63. openapi_fal_rest/api/storage/__init__.py +0 -0
  64. openapi_fal_rest/api/storage/get_file_link.py +0 -200
  65. openapi_fal_rest/api/storage/initiate_upload.py +0 -172
  66. openapi_fal_rest/api/storage/upload_file.py +0 -172
  67. openapi_fal_rest/api/tokens/__init__.py +0 -0
  68. openapi_fal_rest/api/tokens/create_token.py +0 -166
  69. openapi_fal_rest/api/usage/__init__.py +0 -0
  70. openapi_fal_rest/api/usage/get_custom_usage_per_machine.py +0 -203
  71. openapi_fal_rest/api/usage/get_gateway_request_stats.py +0 -247
  72. openapi_fal_rest/api/usage/get_gateway_request_stats_by_time.py +0 -236
  73. openapi_fal_rest/api/usage/get_gateway_stats_for_yesterday.py +0 -152
  74. openapi_fal_rest/api/usage/get_shared_usage_per_app.py +0 -203
  75. openapi_fal_rest/api/usage/get_usage_records.py +0 -253
  76. openapi_fal_rest/api/usage/per_machine_usage.py +0 -218
  77. openapi_fal_rest/api/usage/per_machine_usage_details.py +0 -173
  78. openapi_fal_rest/api/users/__init__.py +0 -0
  79. openapi_fal_rest/api/users/handle_user_registration.py +0 -228
  80. openapi_fal_rest/models/billing_type.py +0 -9
  81. openapi_fal_rest/models/body_create_token.py +0 -68
  82. openapi_fal_rest/models/body_upload_file.py +0 -75
  83. openapi_fal_rest/models/file_spec.py +0 -110
  84. openapi_fal_rest/models/gateway_stats_by_time.py +0 -115
  85. openapi_fal_rest/models/gateway_usage_stats.py +0 -147
  86. openapi_fal_rest/models/get_gateway_request_stats_by_time_response_get_gateway_request_stats_by_time.py +0 -70
  87. openapi_fal_rest/models/grouped_usage_detail.py +0 -85
  88. openapi_fal_rest/models/handle_stripe_webhook_response_handle_stripe_webhook.py +0 -43
  89. openapi_fal_rest/models/initiate_upload_info.py +0 -64
  90. openapi_fal_rest/models/invoice.py +0 -129
  91. openapi_fal_rest/models/invoice_item.py +0 -85
  92. openapi_fal_rest/models/key_scope.py +0 -9
  93. openapi_fal_rest/models/log_entry.py +0 -104
  94. openapi_fal_rest/models/log_entry_labels.py +0 -43
  95. openapi_fal_rest/models/new_user_key.py +0 -64
  96. openapi_fal_rest/models/payment_method.py +0 -96
  97. openapi_fal_rest/models/per_app_usage_detail.py +0 -88
  98. openapi_fal_rest/models/persisted_usage_record.py +0 -118
  99. openapi_fal_rest/models/persisted_usage_record_meta.py +0 -43
  100. openapi_fal_rest/models/presigned_upload_url.py +0 -64
  101. openapi_fal_rest/models/request_io.py +0 -112
  102. openapi_fal_rest/models/request_io_json_input.py +0 -43
  103. openapi_fal_rest/models/request_io_json_output.py +0 -43
  104. openapi_fal_rest/models/run_type.py +0 -9
  105. openapi_fal_rest/models/stats_timeframe.py +0 -12
  106. openapi_fal_rest/models/status.py +0 -82
  107. openapi_fal_rest/models/status_health.py +0 -10
  108. openapi_fal_rest/models/uploaded_file_result.py +0 -64
  109. openapi_fal_rest/models/url_file_upload.py +0 -57
  110. openapi_fal_rest/models/usage_per_machine_type.py +0 -115
  111. openapi_fal_rest/models/usage_per_user.py +0 -71
  112. openapi_fal_rest/models/usage_run_detail.py +0 -73
  113. openapi_fal_rest/models/user_key_info.py +0 -84
  114. /openapi_fal_rest/api/admin/__init__.py → /fal/py.typed +0 -0
  115. {fal-0.12.1.dist-info → fal-0.12.3.dist-info}/WHEEL +0 -0
  116. {fal-0.12.1.dist-info → fal-0.12.3.dist-info}/entry_points.txt +0 -0
fal/__init__.py CHANGED
@@ -1,12 +1,10 @@
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
9
- from fal.app import App, endpoint, wrap_app
7
+ from fal.app import App, endpoint, realtime, wrap_app
10
8
  from fal.sdk import FalServerlessKeyCredentials
11
9
  from fal.sync import sync_dir
12
10
 
@@ -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/_serialization.py CHANGED
@@ -1,9 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from functools import wraps
4
+ from pathlib import Path
4
5
 
5
6
  import dill
6
7
  from dill import _dill
8
+
7
9
  from fal.toolkit import mainify
8
10
 
9
11
  # each @fal.function gets added to this set so that we can
@@ -49,12 +51,28 @@ def by_value_locator(obj, pickler=None, og_locator=_dill._locate_function):
49
51
  _dill._locate_function = by_value_locator
50
52
 
51
53
 
54
+ def include_packages_from_path(raw_path: str):
55
+ path = Path(raw_path).resolve()
56
+ parent = path
57
+ while (parent.parent / "__init__.py").exists():
58
+ parent = parent.parent
59
+
60
+ if parent != path:
61
+ _PACKAGES.add(parent.name)
62
+
63
+
52
64
  def add_serialization_listeners_for(obj):
53
65
  module_name = getattr(obj, "__module__", None)
54
66
  if not module_name:
55
67
  return None
56
68
 
57
69
  _MODULES.add(module_name)
70
+ if module_name == "__main__":
71
+ # When the module is __main__, we need to recursively go up the
72
+ # tree to locate the actual package name.
73
+ import __main__
74
+
75
+ include_packages_from_path(__main__.__file__)
58
76
 
59
77
  if "." in module_name:
60
78
  package_name, *_ = module_name.partition(".")
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,
@@ -23,10 +24,18 @@ from typing import (
23
24
 
24
25
  import dill
25
26
  import dill.detect
26
- import fal.flags as flags
27
27
  import grpc
28
28
  import isolate
29
29
  import yaml
30
+ from fastapi import FastAPI
31
+ from isolate.backends.common import active_python
32
+ from isolate.backends.settings import DEFAULT_SETTINGS
33
+ from isolate.connections import PythonIPC
34
+ from packaging.requirements import Requirement
35
+ from packaging.utils import canonicalize_name
36
+ from typing_extensions import Concatenate, ParamSpec
37
+
38
+ import fal.flags as flags
30
39
  from fal._serialization import add_serialization_listeners_for, patch_dill
31
40
  from fal.logging.isolate import IsolateLogPrinter
32
41
  from fal.sdk import (
@@ -38,16 +47,10 @@ from fal.sdk import (
38
47
  FalServerlessConnection,
39
48
  HostedRunState,
40
49
  MachineRequirements,
41
- _get_agent_credentials,
50
+ get_agent_credentials,
42
51
  get_default_credentials,
43
52
  )
44
53
  from fal.toolkit import mainify
45
- from isolate.backends.common import active_python
46
- from isolate.backends.settings import DEFAULT_SETTINGS
47
- from isolate.connections import PythonIPC
48
- from packaging.requirements import Requirement
49
- from packaging.utils import canonicalize_name
50
- from typing_extensions import Concatenate, ParamSpec
51
54
 
52
55
  ArgsT = ParamSpec("ArgsT")
53
56
  ReturnT = TypeVar("ReturnT", covariant=True)
@@ -55,6 +58,8 @@ ReturnT = TypeVar("ReturnT", covariant=True)
55
58
  BasicConfig = Dict[str, Any]
56
59
  _UNSET = object()
57
60
 
61
+ SERVE_REQUIREMENTS = ["fastapi==0.99.1", "uvicorn"]
62
+
58
63
 
59
64
  @dataclass
60
65
  class FalServerlessError(Exception):
@@ -109,7 +114,7 @@ class Host(Generic[ArgsT, ReturnT]):
109
114
  options.environment[key] = value
110
115
 
111
116
  if options.gateway.get("serve"):
112
- options.add_requirements(["fastapi==0.99.1", "uvicorn"])
117
+ options.add_requirements(SERVE_REQUIREMENTS)
113
118
 
114
119
  return options
115
120
 
@@ -328,7 +333,7 @@ class FalServerlessHost(Host):
328
333
 
329
334
  def __setstate__(self, state: dict[str, Any]) -> None:
330
335
  self.__dict__.update(state)
331
- self.credentials = _get_agent_credentials(self.credentials)
336
+ self.credentials = get_agent_credentials(self.credentials)
332
337
 
333
338
  @property
334
339
  def _connection(self) -> FalServerlessConnection:
@@ -729,53 +734,17 @@ def function( # type: ignore
729
734
 
730
735
 
731
736
  @mainify
732
- class ServeWrapper:
733
- _func: Callable
734
-
735
- def __init__(self, func: Callable):
736
- self._func = func
737
-
738
- def build_app(self):
739
- from fastapi import FastAPI
740
- from fastapi.middleware.cors import CORSMiddleware
741
-
742
- _app = FastAPI()
743
-
744
- _app.add_middleware(
745
- CORSMiddleware,
746
- allow_credentials=True,
747
- allow_headers=("*"),
748
- allow_methods=("*"),
749
- allow_origins=("*"),
750
- )
751
-
752
- _app.add_api_route(
753
- "/",
754
- self._func, # type: ignore
755
- name=self._func.__name__,
756
- methods=["POST"],
757
- )
758
-
759
- return _app
760
-
761
- def __call__(self, *args, **kwargs) -> None:
762
- if len(args) != 0 or len(kwargs) != 0:
763
- print(
764
- f"[warning] {self._func.__name__} function is served with no arguments."
765
- )
766
-
767
- from uvicorn import run
768
-
769
- app = self.build_app()
770
- 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
+ """
771
741
 
772
742
  def openapi(self) -> dict[str, Any]:
773
743
  """
774
744
  Build the OpenAPI specification for the served function.
775
745
  Attach needed metadata for a better integration to fal.
776
746
  """
777
- app = self.build_app()
778
- spec = app.openapi()
747
+ spec = super().openapi()
779
748
  self._mark_order_openapi(spec)
780
749
  return spec
781
750
 
@@ -787,7 +756,8 @@ class ServeWrapper:
787
756
  """
788
757
 
789
758
  def mark_order(obj: dict[str, Any], key: str):
790
- 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())
791
761
 
792
762
  mark_order(spec, "paths")
793
763
 
@@ -796,18 +766,129 @@ class ServeWrapper:
796
766
  Mark the order of properties in the schema object.
797
767
  They can have 'allOf', 'properties' or '$ref' key.
798
768
  """
799
- if "allOf" in schema:
800
- for sub_schema in schema["allOf"]:
801
- order_schema_object(sub_schema)
802
- if "properties" in schema:
803
- mark_order(schema, "properties")
769
+ for sub_schema in schema.get("allOf", []):
770
+ order_schema_object(sub_schema)
771
+
772
+ mark_order(schema, "properties")
804
773
 
805
- for key in spec.get("components", {}).get("schemas") or {}:
774
+ for key in spec.get("components", {}).get("schemas", {}):
806
775
  order_schema_object(spec["components"]["schemas"][key])
807
776
 
808
777
  return spec
809
778
 
810
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
+
811
892
  @dataclass
812
893
  class IsolatedFunction(Generic[ArgsT, ReturnT]):
813
894
  host: Host[ArgsT, ReturnT]