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/auth/auth0.py CHANGED
@@ -1,14 +1,12 @@
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
9
+
12
10
  from fal.console import console
13
11
  from fal.console.icons import CHECK_ICON
14
12
  from fal.console.ux import get_browser
@@ -53,7 +51,7 @@ def login() -> dict:
53
51
  "client_id": AUTH0_CLIENT_ID,
54
52
  "scope": AUTH0_SCOPE,
55
53
  }
56
- device_code_response = requests.post(
54
+ device_code_response = httpx.post(
57
55
  f"https://{AUTH0_DOMAIN}/oauth/device/code", data=device_code_payload
58
56
  )
59
57
 
@@ -80,7 +78,7 @@ def login() -> dict:
80
78
 
81
79
  with console.status("Waiting for confirmation...") as status:
82
80
  while True:
83
- token_response = requests.post(
81
+ token_response = httpx.post(
84
82
  f"https://{AUTH0_DOMAIN}/oauth/token", data=token_payload
85
83
  )
86
84
 
@@ -108,14 +106,12 @@ def refresh(token: str) -> dict:
108
106
  "refresh_token": token,
109
107
  }
110
108
 
111
- token_response = requests.post(
109
+ token_response = httpx.post(
112
110
  f"https://{AUTH0_DOMAIN}/oauth/token", data=token_payload
113
111
  )
114
112
 
115
113
  token_data = token_response.json()
116
114
  if token_response.status_code == 200:
117
- # DEBUG: print("Authenticated!")
118
-
119
115
  validate_id_token(token_data["id_token"])
120
116
 
121
117
  return token_data
@@ -129,7 +125,7 @@ def revoke(token: str):
129
125
  "token": token,
130
126
  }
131
127
 
132
- token_response = requests.post(
128
+ token_response = httpx.post(
133
129
  f"https://{AUTH0_DOMAIN}/oauth/revoke", data=token_payload
134
130
  )
135
131
 
@@ -141,7 +137,7 @@ def revoke(token: str):
141
137
 
142
138
 
143
139
  def get_user_info(bearer_token: str) -> dict:
144
- userinfo_response = requests.post(
140
+ userinfo_response = httpx.post(
145
141
  f"https://{AUTH0_DOMAIN}/userinfo",
146
142
  headers={"Authorization": bearer_token},
147
143
  )
@@ -152,24 +148,38 @@ def get_user_info(bearer_token: str) -> dict:
152
148
  return userinfo_response.json()
153
149
 
154
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
+
155
158
  def validate_id_token(token: str):
156
159
  """
157
- Verify the token and its precedence.
158
- `id_token`s are intended for the client (this sdk) only.
159
- Never send one to another service.
160
-
161
- :param id_token:
160
+ id_token is intended for the client (this sdk) only. Never send one to another service.
162
161
  """
163
- sv = AsymmetricSignatureVerifier(AUTH0_JWKS_URL)
164
- tv = TokenVerifier(
165
- 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,
166
170
  issuer=AUTH0_ISSUER,
167
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
+ },
168
179
  )
169
- tv.verify(token)
170
180
 
171
181
 
172
- def validate_access_token(token: str):
182
+ def verify_access_token_expiration(token: str):
173
183
  from datetime import timedelta
174
184
 
175
185
  from jwt import decode
fal/cli.py CHANGED
@@ -1,28 +1,24 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
- from datetime import datetime
5
4
  from http import HTTPStatus
6
5
  from sys import argv
7
6
  from typing import Literal
8
7
  from uuid import uuid4
9
8
 
10
9
  import click
11
- import fal.auth as auth
10
+ import openapi_fal_rest.api.billing.get_user_details as get_user_details
11
+ from rich.table import Table
12
+
12
13
  import fal
13
- from fal import api, sdk
14
+ import fal.auth as auth
15
+ from fal import _serialization, api, sdk
14
16
  from fal.console import console
15
17
  from fal.exceptions import ApplicationExceptionHandler
16
18
  from fal.logging import get_logger, set_debug_logging
17
- from fal.logging.isolate import IsolateLogPrinter
18
19
  from fal.logging.trace import get_tracer
19
20
  from fal.rest_client import REST_CLIENT
20
21
  from fal.sdk import AliasInfo, KeyScope
21
- from isolate.logs import Log, LogLevel, LogSource
22
- from rich.table import Table
23
-
24
- import openapi_fal_rest.api.billing.get_user_details as get_user_details
25
- import openapi_fal_rest.api.logs.list_since as list_logs
26
22
 
27
23
  DEFAULT_HOST = "api.alpha.fal.ai"
28
24
  HOST_ENVVAR = "FAL_HOST"
@@ -33,7 +29,7 @@ PORT_ENVVAR = "FAL_PORT"
33
29
  DEBUG_ENABLED = False
34
30
 
35
31
 
36
- log = get_logger(__name__)
32
+ logger = get_logger(__name__)
37
33
 
38
34
 
39
35
  class ExecutionInfo:
@@ -67,13 +63,13 @@ class MainGroup(click.Group):
67
63
  qualified_name, attributes={"invocation_id": invocation_id}
68
64
  ):
69
65
  try:
70
- log.debug(
66
+ logger.debug(
71
67
  f"Executing command: {qualified_name}",
72
68
  command=qualified_name,
73
69
  )
74
70
  return super().invoke(ctx)
75
71
  except Exception as exception:
76
- log.error(exception)
72
+ logger.error(exception)
77
73
  if execution_info.debug:
78
74
  # Here we supress detailed errors on click lines because
79
75
  # they're mostly decorator calls, irrelevant to the dev's error tracing
@@ -192,7 +188,7 @@ def key_generate(client: sdk.FalServerlessClient, scope: str, alias: str | None)
192
188
  "This is the only time the secret will be visible.\n"
193
189
  "You will need to generate a new key pair if you lose access to this secret."
194
190
  )
195
- print(f"FAL_KEY_ID='{result[1]}'\nFAL_KEY_SECRET='{result[0]}'")
191
+ print(f"FAL_KEY='{result[1]}:{result[0]}'")
196
192
 
197
193
 
198
194
  @key_cli.command(name="list")
@@ -246,6 +242,10 @@ def load_function_from(
246
242
  if function_name not in module:
247
243
  raise api.FalServerlessError(f"Function '{function_name}' not found in module")
248
244
 
245
+ # The module for the function is set to <run_path> when runpy is used, in which
246
+ # case we want to manually include the packages it is defined in.
247
+ _serialization.include_packages_from_path(file_path)
248
+
249
249
  target = module[function_name]
250
250
  if isinstance(target, type) and issubclass(target, fal.App):
251
251
  target = fal.wrap_app(target, host=host)
@@ -300,18 +300,19 @@ def register_application(
300
300
  )
301
301
 
302
302
  if id:
303
- # TODO: should we centralize this URL format?
304
- gateway_host = host.url.replace("api.", "gateway.")
305
- gateway_host = remove_http_and_port_from_url(gateway_host)
303
+ gateway_host = remove_http_and_port_from_url(host.url)
304
+ gateway_host = (
305
+ gateway_host.replace("api.", "").replace("alpha.", "").replace("ai", "run")
306
+ )
306
307
 
307
308
  if alias:
308
309
  console.print(
309
310
  f"Registered a new revision for function '{alias}' (revision='{id}')."
310
311
  )
311
- console.print(f"URL: https://{user_id}-{alias}.{gateway_host}")
312
+ console.print(f"URL: https://{gateway_host}/{user_id}/{alias}")
312
313
  else:
313
314
  console.print(f"Registered anonymous function '{id}'.")
314
- console.print(f"URL: https://{user_id}-{id}.{gateway_host}")
315
+ console.print(f"URL: https://{gateway_host}/{user_id}/{id}")
315
316
 
316
317
 
317
318
  @function_cli.command("run")
@@ -330,25 +331,9 @@ def run(host: api.FalServerlessHost, file_path: str, function_name: str):
330
331
  def get_logs(
331
332
  host: api.FalServerlessHost, lines: int | None = 100, url: str | None = None
332
333
  ):
333
- log_printer = IsolateLogPrinter(debug=True)
334
- logs_response = list_logs.sync_detailed(
335
- client=REST_CLIENT, limit=lines, url_query=url
334
+ console.print(
335
+ "logs command is deprecated. To see logs, got to fal web page: https://www.fal.ai/dashboard/logs"
336
336
  )
337
- if not logs_response.status_code == 200 or type(logs_response.parsed) != list:
338
- raise api.FalServerlessError(str(logs_response.parsed))
339
- if len(logs_response.parsed) == 0:
340
- console.print("No logs found")
341
- for log in logs_response.parsed:
342
- app = log.app or "fal"
343
-
344
- log_printer.print(
345
- Log(
346
- message=f"{app}: {log.message}",
347
- source=LogSource.USER,
348
- level=LogLevel[log.level],
349
- timestamp=datetime.fromisoformat(log.timestamp),
350
- )
351
- )
352
337
 
353
338
 
354
339
  ##### Alias group #####
@@ -448,7 +433,12 @@ def alias_update(
448
433
  min_concurrency: int | None,
449
434
  ):
450
435
  with client.connect() as connection:
451
- if keep_alive is None and max_multiplexing is None and max_concurrency is None:
436
+ if (
437
+ keep_alive is None
438
+ and max_multiplexing is None
439
+ and max_concurrency is None
440
+ and min_concurrency is None
441
+ ):
452
442
  console.log("No parameters for update were provided, ignoring.")
453
443
  return
454
444
 
@@ -478,31 +468,23 @@ def alias_list_runners(
478
468
  table.add_column("Runner ID")
479
469
  table.add_column("In Flight Requests")
480
470
  table.add_column("Expires in")
471
+ table.add_column("Uptime")
481
472
 
482
473
  for runner in runners:
483
474
  table.add_row(
484
475
  runner.runner_id,
485
476
  str(runner.in_flight_requests),
486
- "N/A (active)"
487
- if not runner.expiration_countdown
488
- else f"{runner.expiration_countdown}s",
477
+ (
478
+ "N/A (active)"
479
+ if not runner.expiration_countdown
480
+ else f"{runner.expiration_countdown}s"
481
+ ),
482
+ f"{runner.uptime} ({runner.uptime.total_seconds()}s)",
489
483
  )
490
484
 
491
485
  console.print(table)
492
486
 
493
487
 
494
- @alias_cli.command("scale")
495
- @click.argument("alias", required=True)
496
- @click.argument("max_concurrency", required=True, type=int)
497
- def alias_scale(alias: str, max_concurrency: int):
498
- alias_update.callback(
499
- alias=alias,
500
- keep_alive=None,
501
- max_multiplexing=None,
502
- max_concurrency=max_concurrency,
503
- ) # type: ignore
504
-
505
-
506
488
  ##### Secrets group #####
507
489
  @click.group
508
490
  @click.option("--host", default=DEFAULT_HOST, envvar=HOST_ENVVAR)
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
-
@@ -2,11 +2,12 @@ from __future__ import annotations
2
2
 
3
3
  from typing import Generic, TypeVar
4
4
 
5
- from fal.console import console
6
- from fal.console.icons import CROSS_ICON
7
5
  from grpc import Call as RpcCall
8
6
  from rich.markdown import Markdown
9
7
 
8
+ from fal.console import console
9
+ from fal.console.icons import CROSS_ICON
10
+
10
11
  from ._base import FalServerlessException
11
12
 
12
13
  ExceptionType = TypeVar("ExceptionType")
fal/flags.py CHANGED
@@ -24,4 +24,9 @@ REST_HOST = GRPC_HOST.replace("api", "rest", 1)
24
24
  REST_SCHEME = "http" if TEST_MODE or AUTH_DISABLED else "https"
25
25
  REST_URL = f"{REST_SCHEME}://{REST_HOST}"
26
26
 
27
+ # fal.run / env.fal.run
28
+ FAL_RUN_HOST = (
29
+ GRPC_HOST.replace("api.", "", 1).replace("alpha.", "", 1).replace(".ai", ".run", 1)
30
+ )
31
+
27
32
  FORCE_SETUP = bool_envvar("FAL_FORCE_SETUP")
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/logging/user.py CHANGED
@@ -1,8 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
- from fal.auth import USER
4
3
  from structlog.typing import EventDict, WrappedLogger
5
4
 
5
+ from fal.auth import USER
6
+
6
7
 
7
8
  def add_user_id(
8
9
  logger: WrappedLogger, method_name: str, event_dict: EventDict
fal/rest_client.py CHANGED
@@ -1,10 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from openapi_fal_rest.client import Client
4
+
3
5
  import fal.flags as flags
4
6
  from fal.sdk import get_default_credentials
5
7
 
6
- from openapi_fal_rest.client import Client
7
-
8
8
 
9
9
  class CredentialsClient(Client):
10
10
  def get_headers(self) -> dict[str, str]:
fal/sdk.py CHANGED
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import enum
4
- import os
5
4
  from contextlib import ExitStack
6
5
  from dataclasses import dataclass, field
7
6
  from datetime import datetime, timedelta
@@ -9,15 +8,16 @@ from enum import Enum
9
8
  from typing import Any, Callable, Generic, Iterator, Literal, TypeVar
10
9
 
11
10
  import grpc
11
+ from isolate.connections.common import is_agent
12
+ from isolate.logs import Log
13
+ from isolate.server.interface import from_grpc, to_serialized_object, to_struct
14
+
12
15
  import isolate_proto
13
16
  from fal import flags
14
17
  from fal._serialization import patch_dill
15
- from fal.auth import USER
18
+ from fal.auth import USER, key_credentials
16
19
  from fal.logging import get_logger
17
20
  from fal.logging.trace import TraceContextInterceptor
18
- from isolate.connections.common import is_agent
19
- from isolate.logs import Log
20
- from isolate.server.interface import from_grpc, to_serialized_object, to_struct
21
21
  from isolate_proto.configuration import GRPC_OPTIONS
22
22
 
23
23
  ResultT = TypeVar("ResultT")
@@ -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):
@@ -123,27 +144,13 @@ class ServerlessSecret:
123
144
  created_at: datetime
124
145
 
125
146
 
126
- def key_credentials() -> FalServerlessKeyCredentials | None:
127
- # Ignore key credentials when the user forces auth by user.
128
- if os.environ.get("FAL_FORCE_AUTH_BY_USER") == "1":
129
- return None
130
-
131
- if "FAL_KEY_ID" in os.environ and "FAL_KEY_SECRET" in os.environ:
132
- return FalServerlessKeyCredentials(
133
- os.environ["FAL_KEY_ID"],
134
- os.environ["FAL_KEY_SECRET"],
135
- )
136
- else:
137
- return None
138
-
139
-
140
- def _get_agent_credentials(original_credentials: Credentials) -> Credentials:
147
+ def get_agent_credentials(original_credentials: Credentials) -> Credentials:
141
148
  """If running inside a fal Serverless box, use the preconfigured credentials
142
149
  instead of the user provided ones."""
143
150
 
144
151
  key_creds = key_credentials()
145
152
  if is_agent() and key_creds:
146
- return key_creds
153
+ return FalServerlessKeyCredentials(key_creds[0], key_creds[1])
147
154
  else:
148
155
  return original_credentials
149
156
 
@@ -154,8 +161,8 @@ def get_default_credentials() -> Credentials:
154
161
 
155
162
  key_creds = key_credentials()
156
163
  if key_creds:
157
- log.debug("Using key credentials")
158
- return key_creds
164
+ logger.debug("Using key credentials")
165
+ return FalServerlessKeyCredentials(key_creds[0], key_creds[1])
159
166
  else:
160
167
  return AuthenticatedCredentials()
161
168
 
@@ -197,6 +204,7 @@ class RunnerInfo:
197
204
  runner_id: str
198
205
  in_flight_requests: int
199
206
  expiration_countdown: int
207
+ uptime: timedelta
200
208
 
201
209
 
202
210
  @dataclass
@@ -284,6 +292,7 @@ def _from_grpc_runner_info(message: isolate_proto.RunnerInfo) -> RunnerInfo:
284
292
  runner_id=message.runner_id,
285
293
  in_flight_requests=message.in_flight_requests,
286
294
  expiration_countdown=message.expiration_countdown,
295
+ uptime=timedelta(seconds=message.uptime),
287
296
  )
288
297
 
289
298
 
@@ -293,9 +302,11 @@ def _from_grpc_register_application_result(
293
302
  ) -> RegisterApplicationResult:
294
303
  return RegisterApplicationResult(
295
304
  logs=[from_grpc(log) for log in message.logs],
296
- result=None
297
- if not message.HasField("result")
298
- else RegisterApplicationResultType(message.result.application_id),
305
+ result=(
306
+ None
307
+ if not message.HasField("result")
308
+ else RegisterApplicationResultType(message.result.application_id)
309
+ ),
299
310
  )
300
311
 
301
312
 
@@ -358,10 +369,14 @@ class FalServerlessConnection:
358
369
  if self._stub:
359
370
  return self._stub
360
371
 
361
- options = self.credentials.server_credentials.extra_options
372
+ options = self.credentials.server_credentials.base_options
362
373
  channel_creds = self.credentials.to_grpc()
363
374
  channel = self._stack.enter_context(
364
- 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
+ )
365
380
  )
366
381
  channel = grpc.intercept_channel(channel, TraceContextInterceptor())
367
382
  self._stub = isolate_proto.IsolateControllerStub(channel)
fal/sync.py CHANGED
@@ -5,14 +5,14 @@ import os
5
5
  import zipfile
6
6
  from pathlib import Path
7
7
 
8
- from fal.rest_client import REST_CLIENT
9
- from pathspec import PathSpec
10
-
11
8
  import openapi_fal_rest.api.files.check_dir_hash as check_dir_hash_api
12
9
  import openapi_fal_rest.api.files.upload_local_file as upload_local_file_api
13
10
  import openapi_fal_rest.models.body_upload_local_file as upload_file_model
14
11
  import openapi_fal_rest.models.hash_check as hash_check_model
15
12
  import openapi_fal_rest.types as rest_types
13
+ from pathspec import PathSpec
14
+
15
+ from fal.rest_client import REST_CLIENT
16
16
 
17
17
 
18
18
  def _check_hash(target_path: str, hash_string: str) -> bool:
fal/toolkit/__init__.py CHANGED
@@ -1,8 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
- from fal.toolkit.file.file import File
3
+ from fal.toolkit.file import CompressedFile, File
4
4
  from fal.toolkit.image.image import Image, ImageSizeInput, get_image_size
5
5
  from fal.toolkit.mainify import mainify
6
+ from fal.toolkit.optimize import optimize
6
7
  from fal.toolkit.utils import (
7
8
  FAL_MODEL_WEIGHTS_DIR,
8
9
  FAL_PERSISTENT_DIR,
@@ -11,3 +12,19 @@ from fal.toolkit.utils import (
11
12
  download_file,
12
13
  download_model_weights,
13
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
+ ]