fal 0.12.4__tar.gz → 0.12.6__tar.gz

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 (90) hide show
  1. {fal-0.12.4 → fal-0.12.6}/PKG-INFO +6 -2
  2. {fal-0.12.4 → fal-0.12.6}/README.md +3 -0
  3. {fal-0.12.4 → fal-0.12.6}/pyproject.toml +3 -2
  4. {fal-0.12.4 → fal-0.12.6}/setup.py +4 -3
  5. fal-0.12.6/src/fal/__main__.py +4 -0
  6. {fal-0.12.4 → fal-0.12.6}/src/fal/apps.py +6 -2
  7. {fal-0.12.4 → fal-0.12.6}/src/fal/auth/auth0.py +12 -14
  8. {fal-0.12.4 → fal-0.12.6}/src/fal/cli.py +68 -34
  9. fal-0.12.6/src/fal/console/ux.py +14 -0
  10. {fal-0.12.4 → fal-0.12.6}/src/fal/workflows.py +10 -9
  11. fal-0.12.4/src/fal/console/ux.py +0 -17
  12. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/__init__.py +0 -0
  13. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/api/__init__.py +0 -0
  14. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/api/applications/__init__.py +0 -0
  15. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/api/applications/app_metadata.py +0 -0
  16. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/api/billing/__init__.py +0 -0
  17. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/api/billing/get_user_details.py +0 -0
  18. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/api/files/__init__.py +0 -0
  19. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/api/files/check_dir_hash.py +0 -0
  20. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/api/files/upload_local_file.py +0 -0
  21. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/api/workflows/__init__.py +0 -0
  22. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/api/workflows/create_or_update_workflow_workflows_post.py +0 -0
  23. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/api/workflows/delete_workflow_workflows_user_id_workflow_name_delete.py +0 -0
  24. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/api/workflows/execute_workflow_workflows_user_id_workflow_name_post.py +0 -0
  25. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/api/workflows/get_workflow_workflows_user_id_workflow_name_get.py +0 -0
  26. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/api/workflows/get_workflows_workflows_get.py +0 -0
  27. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/client.py +0 -0
  28. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/errors.py +0 -0
  29. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/models/__init__.py +0 -0
  30. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/models/app_metadata_response_app_metadata.py +0 -0
  31. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/models/body_upload_local_file.py +0 -0
  32. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/models/customer_details.py +0 -0
  33. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/models/execute_workflow_workflows_user_id_workflow_name_post_json_body_type_0.py +0 -0
  34. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/models/execute_workflow_workflows_user_id_workflow_name_post_response_200_type_0.py +0 -0
  35. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/models/hash_check.py +0 -0
  36. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/models/http_validation_error.py +0 -0
  37. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/models/lock_reason.py +0 -0
  38. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/models/page_workflow_item.py +0 -0
  39. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow.py +0 -0
  40. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/models/validation_error.py +0 -0
  41. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents.py +0 -0
  42. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_nodes.py +0 -0
  43. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_output.py +0 -0
  44. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail.py +0 -0
  45. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail_contents_type_0.py +0 -0
  46. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/models/workflow_item.py +0 -0
  47. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/models/workflow_node.py +0 -0
  48. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/models/workflow_node_type.py +0 -0
  49. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema.py +0 -0
  50. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_input.py +0 -0
  51. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_output.py +0 -0
  52. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/py.typed +0 -0
  53. {fal-0.12.4 → fal-0.12.6}/openapi-fal-rest/openapi_fal_rest/types.py +0 -0
  54. {fal-0.12.4 → fal-0.12.6}/src/fal/__init__.py +0 -0
  55. {fal-0.12.4 → fal-0.12.6}/src/fal/_serialization.py +0 -0
  56. {fal-0.12.4 → fal-0.12.6}/src/fal/api.py +0 -0
  57. {fal-0.12.4 → fal-0.12.6}/src/fal/app.py +0 -0
  58. {fal-0.12.4 → fal-0.12.6}/src/fal/auth/__init__.py +0 -0
  59. {fal-0.12.4 → fal-0.12.6}/src/fal/auth/local.py +0 -0
  60. {fal-0.12.4 → fal-0.12.6}/src/fal/console/__init__.py +0 -0
  61. {fal-0.12.4 → fal-0.12.6}/src/fal/console/icons.py +0 -0
  62. {fal-0.12.4 → fal-0.12.6}/src/fal/env.py +0 -0
  63. {fal-0.12.4 → fal-0.12.6}/src/fal/exceptions/__init__.py +0 -0
  64. {fal-0.12.4 → fal-0.12.6}/src/fal/exceptions/_base.py +0 -0
  65. {fal-0.12.4 → fal-0.12.6}/src/fal/exceptions/auth.py +0 -0
  66. {fal-0.12.4 → fal-0.12.6}/src/fal/exceptions/handlers.py +0 -0
  67. {fal-0.12.4 → fal-0.12.6}/src/fal/flags.py +0 -0
  68. {fal-0.12.4 → fal-0.12.6}/src/fal/logging/__init__.py +0 -0
  69. {fal-0.12.4 → fal-0.12.6}/src/fal/logging/isolate.py +0 -0
  70. {fal-0.12.4 → fal-0.12.6}/src/fal/logging/style.py +0 -0
  71. {fal-0.12.4 → fal-0.12.6}/src/fal/logging/trace.py +0 -0
  72. {fal-0.12.4 → fal-0.12.6}/src/fal/logging/user.py +0 -0
  73. {fal-0.12.4 → fal-0.12.6}/src/fal/py.typed +0 -0
  74. {fal-0.12.4 → fal-0.12.6}/src/fal/rest_client.py +0 -0
  75. {fal-0.12.4 → fal-0.12.6}/src/fal/sdk.py +0 -0
  76. {fal-0.12.4 → fal-0.12.6}/src/fal/sync.py +0 -0
  77. {fal-0.12.4 → fal-0.12.6}/src/fal/toolkit/__init__.py +0 -0
  78. {fal-0.12.4 → fal-0.12.6}/src/fal/toolkit/exceptions.py +0 -0
  79. {fal-0.12.4 → fal-0.12.6}/src/fal/toolkit/file/__init__.py +0 -0
  80. {fal-0.12.4 → fal-0.12.6}/src/fal/toolkit/file/file.py +0 -0
  81. {fal-0.12.4 → fal-0.12.6}/src/fal/toolkit/file/providers/fal.py +0 -0
  82. {fal-0.12.4 → fal-0.12.6}/src/fal/toolkit/file/providers/gcp.py +0 -0
  83. {fal-0.12.4 → fal-0.12.6}/src/fal/toolkit/file/providers/r2.py +0 -0
  84. {fal-0.12.4 → fal-0.12.6}/src/fal/toolkit/file/types.py +0 -0
  85. {fal-0.12.4 → fal-0.12.6}/src/fal/toolkit/image/__init__.py +0 -0
  86. {fal-0.12.4 → fal-0.12.6}/src/fal/toolkit/image/image.py +0 -0
  87. {fal-0.12.4 → fal-0.12.6}/src/fal/toolkit/mainify.py +0 -0
  88. {fal-0.12.4 → fal-0.12.6}/src/fal/toolkit/optimize.py +0 -0
  89. {fal-0.12.4 → fal-0.12.6}/src/fal/toolkit/utils/__init__.py +0 -0
  90. {fal-0.12.4 → fal-0.12.6}/src/fal/toolkit/utils/download_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fal
3
- Version: 0.12.4
3
+ Version: 0.12.6
4
4
  Summary: fal is an easy-to-use Serverless Python Framework
5
5
  Author: Features & Labels
6
6
  Author-email: hello@fal.ai
@@ -29,15 +29,19 @@ Requires-Dist: pathspec (>=0.11.1,<0.12.0)
29
29
  Requires-Dist: pillow (>=10.2.0,<11.0.0)
30
30
  Requires-Dist: portalocker (>=2.7.0,<3.0.0)
31
31
  Requires-Dist: pydantic (<2.0)
32
- Requires-Dist: pyjwt (>=2.8.0,<3.0.0)
32
+ Requires-Dist: pyjwt[crypto] (>=2.8.0,<3.0.0)
33
33
  Requires-Dist: python-dateutil (>=2.8.0,<3.0.0)
34
34
  Requires-Dist: rich (>=13.3.2,<14.0.0)
35
+ Requires-Dist: rich_click
35
36
  Requires-Dist: structlog (>=22.3.0,<23.0.0)
36
37
  Requires-Dist: types-python-dateutil (>=2.8.0,<3.0.0)
37
38
  Requires-Dist: typing-extensions (>=4.7.1,<5.0.0)
38
39
  Requires-Dist: websockets (>=12.0,<13.0)
39
40
  Description-Content-Type: text/markdown
40
41
 
42
+ [![PyPI](https://img.shields.io/pypi/v/fal.svg?logo=PyPI)](https://pypi.org/project/fal)
43
+ [![Tests](https://img.shields.io/github/actions/workflow/status/fal-ai/fal/integration_tests.yaml?label=Tests)](https://github.com/fal-ai/fal/actions)
44
+
41
45
  # fal
42
46
 
43
47
  fal is a serverless Python runtime that lets you run and scale code in the cloud with no infra management.
@@ -1,3 +1,6 @@
1
+ [![PyPI](https://img.shields.io/pypi/v/fal.svg?logo=PyPI)](https://pypi.org/project/fal)
2
+ [![Tests](https://img.shields.io/github/actions/workflow/status/fal-ai/fal/integration_tests.yaml?label=Tests)](https://github.com/fal-ai/fal/actions)
3
+
1
4
  # fal
2
5
 
3
6
  fal is a serverless Python runtime that lets you run and scale code in the cloud with no infra management.
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "fal"
3
- version = "0.12.4"
3
+ version = "0.12.6"
4
4
  description = "fal is an easy-to-use Serverless Python Framework"
5
5
  authors = ["Features & Labels <hello@fal.ai>"]
6
6
  readme = "README.md"
@@ -24,6 +24,7 @@ grpc-interceptor = "^0.15.0"
24
24
  colorama = "^0.4.6"
25
25
  portalocker = "^2.7.0"
26
26
  rich = "^13.3.2"
27
+ rich_click = "*"
27
28
  packaging = ">=21.3"
28
29
  pathspec = "^0.11.1"
29
30
  pydantic = "<2.0"
@@ -42,7 +43,7 @@ importlib-metadata = { version = ">=4.4", python = "<3.10" }
42
43
  msgpack = "^1.0.7"
43
44
  websockets = "^12.0"
44
45
  pillow = "^10.2.0"
45
- pyjwt = "^2.8.0"
46
+ pyjwt = { version = "^2.8.0", extras = ["crypto"] }
46
47
 
47
48
  [tool.poetry.group.dev.dependencies]
48
49
  openapi-python-client = "^0.14.1"
@@ -52,9 +52,10 @@ install_requires = \
52
52
  'pillow>=10.2.0,<11.0.0',
53
53
  'portalocker>=2.7.0,<3.0.0',
54
54
  'pydantic<2.0',
55
- 'pyjwt>=2.8.0,<3.0.0',
55
+ 'pyjwt[crypto]>=2.8.0,<3.0.0',
56
56
  'python-dateutil>=2.8.0,<3.0.0',
57
57
  'rich>=13.3.2,<14.0.0',
58
+ 'rich_click',
58
59
  'structlog>=22.3.0,<23.0.0',
59
60
  'types-python-dateutil>=2.8.0,<3.0.0',
60
61
  'typing-extensions>=4.7.1,<5.0.0',
@@ -68,9 +69,9 @@ entry_points = \
68
69
 
69
70
  setup_kwargs = {
70
71
  'name': 'fal',
71
- 'version': '0.12.4',
72
+ 'version': '0.12.6',
72
73
  'description': 'fal is an easy-to-use Serverless Python Framework',
73
- 'long_description': '# fal\n\nfal is a serverless Python runtime that lets you run and scale code in the cloud with no infra management.\n\nWith fal, you can build pipelines, serve ML models and scale them up to many users. You scale down to 0 when you don\'t use any resources.\n\n## Quickstart\n\nFirst, you need to install the `fal` package. You can do so using pip:\n```shell\npip install fal\n```\n\nThen you need to authenticate:\n```shell\nfal auth login\n```\n\nYou can also use fal keys that you can get from [our dashboard](https://fal.ai/dashboard/keys).\n\nNow can use the fal package in your Python scripts as follows:\n\n```py\nimport fal\n\n@fal.function(\n "virtualenv",\n requirements=["pyjokes"],\n)\ndef tell_joke() -> str:\n import pyjokes\n\n joke = pyjokes.get_joke()\n return joke\n\nprint("Joke from the clouds: ", tell_joke())\n```\n\nA new virtual environment will be created by fal in the cloud and the set of requirements that we passed will be installed as soon as this function is called. From that point on, our code will be executed as if it were running locally, and the joke prepared by the pyjokes library will be returned.\n\n## Next steps\n\nIf you would like to find out more about the capabilities of fal, check out to the [docs](https://fal.ai/docs). You can learn more about persistent storage, function caches and deploying your functions as API endpoints.\n',
74
+ 'long_description': '[![PyPI](https://img.shields.io/pypi/v/fal.svg?logo=PyPI)](https://pypi.org/project/fal)\n[![Tests](https://img.shields.io/github/actions/workflow/status/fal-ai/fal/integration_tests.yaml?label=Tests)](https://github.com/fal-ai/fal/actions)\n\n# fal\n\nfal is a serverless Python runtime that lets you run and scale code in the cloud with no infra management.\n\nWith fal, you can build pipelines, serve ML models and scale them up to many users. You scale down to 0 when you don\'t use any resources.\n\n## Quickstart\n\nFirst, you need to install the `fal` package. You can do so using pip:\n```shell\npip install fal\n```\n\nThen you need to authenticate:\n```shell\nfal auth login\n```\n\nYou can also use fal keys that you can get from [our dashboard](https://fal.ai/dashboard/keys).\n\nNow can use the fal package in your Python scripts as follows:\n\n```py\nimport fal\n\n@fal.function(\n "virtualenv",\n requirements=["pyjokes"],\n)\ndef tell_joke() -> str:\n import pyjokes\n\n joke = pyjokes.get_joke()\n return joke\n\nprint("Joke from the clouds: ", tell_joke())\n```\n\nA new virtual environment will be created by fal in the cloud and the set of requirements that we passed will be installed as soon as this function is called. From that point on, our code will be executed as if it were running locally, and the joke prepared by the pyjokes library will be returned.\n\n## Next steps\n\nIf you would like to find out more about the capabilities of fal, check out to the [docs](https://fal.ai/docs). You can learn more about persistent storage, function caches and deploying your functions as API endpoints.\n',
74
75
  'author': 'Features & Labels',
75
76
  'author_email': 'hello@fal.ai',
76
77
  'maintainer': 'None',
@@ -0,0 +1,4 @@
1
+ from .cli import cli
2
+
3
+ if __name__ == "__main__":
4
+ cli()
@@ -65,8 +65,12 @@ class RequestHandle:
65
65
  def __post_init__(self):
66
66
  app_id = _backwards_compatible_app_id(self.app_id)
67
67
  # drop any extra path components
68
- user_id, app_name = app_id.split("/")[:2]
69
- self.app_id = f"{user_id}/{app_name}"
68
+ parts = app_id.split("/")[:3]
69
+ if parts[0] != "workflows":
70
+ # if the app_id is not a workflow, only keep the first two parts
71
+ parts = parts[:2]
72
+
73
+ self.app_id = "/".join(parts)
70
74
 
71
75
  def status(self, *, logs: bool = False) -> _Status:
72
76
  """Check the status of an async inference request."""
@@ -9,7 +9,7 @@ import httpx
9
9
 
10
10
  from fal.console import console
11
11
  from fal.console.icons import CHECK_ICON
12
- from fal.console.ux import get_browser
12
+ from fal.console.ux import maybe_open_browser_tab
13
13
 
14
14
  WEBSITE_URL = "https://fal.ai"
15
15
 
@@ -27,19 +27,17 @@ def logout_url(return_url: str):
27
27
 
28
28
 
29
29
  def _open_browser(url: str, code: str | None) -> None:
30
- browser = get_browser()
31
- console.print()
30
+ maybe_open_browser_tab(url)
32
31
 
33
- if browser is not None and click.confirm(
34
- "Open browser automatically ('no' to show URL)?", default=True, err=True
35
- ):
36
- browser.open_new_tab(url)
37
- else:
38
- console.print(f"On your computer or mobile device navigate to: {url}")
39
- if code:
40
- console.print(
41
- f"Confirm it shows the following code: [markdown.code]{code}[/]"
42
- )
32
+ console.print(
33
+ "If browser didn't open automatically, on your computer or mobile device navigate to"
34
+ )
35
+ console.print(url)
36
+
37
+ if code:
38
+ console.print(
39
+ f"\nConfirm it shows the following code: [markdown.code]{code}[/]\n"
40
+ )
43
41
 
44
42
 
45
43
  def login() -> dict:
@@ -183,7 +181,7 @@ def validate_id_token(token: str):
183
181
  def verify_access_token_expiration(token: str):
184
182
  from jwt import decode
185
183
 
186
- leeway = 60 * 30 * 60 # 30 minutes
184
+ leeway = 30 * 60 # 30 minutes
187
185
  decode(
188
186
  token,
189
187
  leeway=-leeway, # negative to consider expired before actual expiration
@@ -1,14 +1,16 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
+ from dataclasses import dataclass, field
4
5
  from http import HTTPStatus
5
6
  from sys import argv
6
- from typing import Literal
7
+ from typing import Any, Callable, Literal
7
8
  from uuid import uuid4
8
9
 
9
10
  import click
10
11
  import openapi_fal_rest.api.billing.get_user_details as get_user_details
11
12
  from rich.table import Table
13
+ from rich_click import RichCommand, RichGroup
12
14
 
13
15
  import fal
14
16
  import fal.auth as auth
@@ -32,16 +34,29 @@ DEBUG_ENABLED = False
32
34
  logger = get_logger(__name__)
33
35
 
34
36
 
35
- class ExecutionInfo:
36
- debug: bool
37
- invocation_id: str
37
+ @dataclass
38
+ class State:
39
+ debug: bool = False
40
+ invocation_id: str = field(default_factory=lambda: str(uuid4()))
38
41
 
39
- def __init__(self, debug=False):
40
- self.debug = debug
41
- self.invocation_id = str(uuid4())
42
42
 
43
+ def debug_option(*param_decls: str, **kwargs: Any) -> Callable[[click.FC], click.FC]:
44
+ def callback(ctx: click.Context, param: click.Parameter, value: bool) -> None:
45
+ state = ctx.ensure_object(State)
46
+ state.debug = value
47
+ set_debug_logging(value)
43
48
 
44
- class MainGroup(click.Group):
49
+ if not param_decls:
50
+ param_decls = ("--debug",)
51
+
52
+ kwargs.setdefault("is_flag", True)
53
+ kwargs.setdefault("expose_value", False)
54
+ kwargs.setdefault("callback", callback)
55
+ kwargs.setdefault("help", "Enable detailed errors and verbose logging.")
56
+ return click.option(*param_decls, **kwargs)
57
+
58
+
59
+ class MainGroup(RichGroup):
45
60
  """A custom implementation of the top-level group
46
61
  (i.e. called on all commands and subcommands).
47
62
 
@@ -54,13 +69,13 @@ class MainGroup(click.Group):
54
69
  _tracer = get_tracer(__name__)
55
70
 
56
71
  def invoke(self, ctx):
57
- execution_info = ExecutionInfo(debug=ctx.params["debug"])
72
+ from click.exceptions import Abort, ClickException, Exit
73
+
74
+ state = ctx.ensure_object(State)
58
75
  qualified_name = " ".join([ctx.info_name] + argv[1:])
59
- invocation_id = execution_info.invocation_id
60
- set_debug_logging(execution_info.debug)
61
76
 
62
77
  with self._tracer.start_as_current_span(
63
- qualified_name, attributes={"invocation_id": invocation_id}
78
+ qualified_name, attributes={"invocation_id": state.invocation_id}
64
79
  ):
65
80
  try:
66
81
  logger.debug(
@@ -68,22 +83,25 @@ class MainGroup(click.Group):
68
83
  command=qualified_name,
69
84
  )
70
85
  return super().invoke(ctx)
86
+ except (EOFError, KeyboardInterrupt, ClickException, Exit, Abort):
87
+ # let click's main handle these
88
+ raise
71
89
  except Exception as exception:
72
90
  logger.error(exception)
73
- if execution_info.debug:
91
+ if state.debug:
74
92
  # Here we supress detailed errors on click lines because
75
93
  # they're mostly decorator calls, irrelevant to the dev's error tracing
76
94
  console.print_exception(suppress=[click])
77
95
  console.print()
78
96
  console.print(
79
- f"The [markdown.code]invocation_id[/] for this operation is: [white]{invocation_id}[/]"
97
+ f"The [markdown.code]invocation_id[/] for this operation is: [white]{state.invocation_id}[/]"
80
98
  )
81
99
  else:
82
100
  self._exception_handler.handle(exception)
83
101
 
84
102
  def add_command(
85
103
  self,
86
- cmd: click.Command,
104
+ cmd: RichCommand,
87
105
  name: str | None = None,
88
106
  aliases: list[str] | None = None,
89
107
  ) -> None:
@@ -108,7 +126,7 @@ class MainGroup(click.Group):
108
126
  self.add_command(alias_cmd, alias)
109
127
 
110
128
 
111
- class AliasCommand(click.Command):
129
+ class AliasCommand(RichCommand):
112
130
  def __init__(self, wrapped):
113
131
  self._wrapped = wrapped
114
132
 
@@ -123,35 +141,34 @@ class AliasCommand(click.Command):
123
141
  return self._wrapped.__getattribute__(__name)
124
142
 
125
143
 
126
- @click.group(
127
- cls=MainGroup,
128
- context_settings={"allow_interspersed_args": True},
129
- )
130
- @click.option(
131
- "--debug", is_flag=True, help="Enable detailed errors and verbose logging."
132
- )
144
+ @click.group(cls=MainGroup)
133
145
  @click.version_option()
134
- def cli(debug):
146
+ @debug_option()
147
+ def cli():
135
148
  pass
136
149
 
137
150
 
138
151
  ###### Auth group ######
139
- @click.group
152
+ @click.group(cls=RichGroup)
153
+ @debug_option()
140
154
  def auth_cli():
141
155
  pass
142
156
 
143
157
 
144
158
  @auth_cli.command(name="login")
159
+ @debug_option()
145
160
  def auth_login():
146
161
  auth.login()
147
162
 
148
163
 
149
164
  @auth_cli.command(name="logout")
165
+ @debug_option()
150
166
  def auth_logout():
151
167
  auth.logout()
152
168
 
153
169
 
154
170
  @auth_cli.command(name="hello", hidden=True)
171
+ @debug_option()
155
172
  def auth_test():
156
173
  """
157
174
  To test auth.
@@ -160,9 +177,10 @@ def auth_test():
160
177
 
161
178
 
162
179
  ###### Key group ######
163
- @click.group
180
+ @click.group(cls=RichGroup)
164
181
  @click.option("--host", default=DEFAULT_HOST, envvar=HOST_ENVVAR)
165
182
  @click.option("--port", default=DEFAULT_PORT, envvar=PORT_ENVVAR, hidden=True)
183
+ @debug_option()
166
184
  @click.pass_context
167
185
  def key_cli(ctx, host: str, port: str):
168
186
  ctx.obj = sdk.FalServerlessClient(f"{host}:{port}")
@@ -181,6 +199,7 @@ def key_cli(ctx, host: str, port: str):
181
199
  default=None,
182
200
  help="An alias for the key.",
183
201
  )
202
+ @debug_option()
184
203
  @click.pass_obj
185
204
  def key_generate(client: sdk.FalServerlessClient, scope: str, alias: str | None):
186
205
  with client.connect() as connection:
@@ -195,6 +214,7 @@ def key_generate(client: sdk.FalServerlessClient, scope: str, alias: str | None)
195
214
 
196
215
 
197
216
  @key_cli.command(name="list")
217
+ @debug_option()
198
218
  @click.pass_obj
199
219
  def key_list(client: sdk.FalServerlessClient):
200
220
  table = Table(title="Keys")
@@ -215,6 +235,7 @@ def key_list(client: sdk.FalServerlessClient):
215
235
 
216
236
  @key_cli.command(name="revoke")
217
237
  @click.argument("key_id", required=True)
238
+ @debug_option()
218
239
  @click.pass_obj
219
240
  def key_revoke(client: sdk.FalServerlessClient, key_id: str):
220
241
  with client.connect() as connection:
@@ -226,9 +247,10 @@ ALIAS_AUTH_OPTIONS = ["public", "private", "shared"]
226
247
  ALIAS_AUTH_TYPE = Literal["public", "private", "shared"]
227
248
 
228
249
 
229
- @click.group
250
+ @click.group(cls=RichGroup)
230
251
  @click.option("--host", default=DEFAULT_HOST, envvar=HOST_ENVVAR)
231
252
  @click.option("--port", default=DEFAULT_PORT, envvar=PORT_ENVVAR, hidden=True)
253
+ @debug_option()
232
254
  @click.pass_context
233
255
  def function_cli(ctx, host: str, port: str):
234
256
  ctx.obj = api.FalServerlessHost(f"{host}:{port}")
@@ -270,6 +292,7 @@ def load_function_from(
270
292
  )
271
293
  @click.argument("file_path", required=True)
272
294
  @click.argument("function_name", required=True)
295
+ @debug_option()
273
296
  @click.pass_obj
274
297
  def register_application(
275
298
  host: api.FalServerlessHost,
@@ -321,6 +344,7 @@ def register_application(
321
344
  @function_cli.command("run")
322
345
  @click.argument("file_path", required=True)
323
346
  @click.argument("function_name", required=True)
347
+ @debug_option()
324
348
  @click.pass_obj
325
349
  def run(host: api.FalServerlessHost, file_path: str, function_name: str):
326
350
  isolated_function = load_function_from(host, file_path, function_name)
@@ -330,6 +354,7 @@ def run(host: api.FalServerlessHost, file_path: str, function_name: str):
330
354
  @function_cli.command("logs")
331
355
  @click.option("--lines", default=100)
332
356
  @click.option("--url", default=None)
357
+ @debug_option()
333
358
  @click.pass_obj
334
359
  def get_logs(
335
360
  host: api.FalServerlessHost, lines: int | None = 100, url: str | None = None
@@ -340,9 +365,10 @@ def get_logs(
340
365
 
341
366
 
342
367
  ##### Alias group #####
343
- @click.group
368
+ @click.group(cls=RichGroup)
344
369
  @click.option("--host", default=DEFAULT_HOST, envvar=HOST_ENVVAR)
345
370
  @click.option("--port", default=DEFAULT_PORT, envvar=PORT_ENVVAR, hidden=True)
371
+ @debug_option()
346
372
  @click.pass_context
347
373
  def alias_cli(ctx, host: str, port: str):
348
374
  ctx.obj = api.FalServerlessClient(f"{host}:{port}")
@@ -383,6 +409,7 @@ def _alias_table(aliases: list[AliasInfo]):
383
409
  type=click.Choice(ALIAS_AUTH_OPTIONS),
384
410
  default="private",
385
411
  )
412
+ @debug_option()
386
413
  @click.pass_obj
387
414
  def alias_set(
388
415
  client: api.FalServerlessClient,
@@ -396,6 +423,7 @@ def alias_set(
396
423
 
397
424
  @alias_cli.command("delete")
398
425
  @click.argument("alias", required=True)
426
+ @debug_option()
399
427
  @click.pass_obj
400
428
  def alias_delete(client: api.FalServerlessClient, alias: str):
401
429
  with client.connect() as connection:
@@ -405,6 +433,7 @@ def alias_delete(client: api.FalServerlessClient, alias: str):
405
433
 
406
434
 
407
435
  @alias_cli.command("list")
436
+ @debug_option()
408
437
  @click.pass_obj
409
438
  def alias_list(client: api.FalServerlessClient):
410
439
  with client.connect() as connection:
@@ -426,6 +455,7 @@ def alias_list(client: api.FalServerlessClient):
426
455
  # "auth_mode",
427
456
  # type=click.Choice(ALIAS_AUTH_OPTIONS),
428
457
  # )
458
+ @debug_option()
429
459
  @click.pass_obj
430
460
  def alias_update(
431
461
  client: api.FalServerlessClient,
@@ -459,6 +489,7 @@ def alias_update(
459
489
 
460
490
  @alias_cli.command("runners")
461
491
  @click.argument("alias", required=True)
492
+ @debug_option()
462
493
  @click.pass_obj
463
494
  def alias_list_runners(
464
495
  client: api.FalServerlessClient,
@@ -489,15 +520,17 @@ def alias_list_runners(
489
520
 
490
521
 
491
522
  ##### Secrets group #####
492
- @click.group
523
+ @click.group(cls=RichGroup)
493
524
  @click.option("--host", default=DEFAULT_HOST, envvar=HOST_ENVVAR)
494
525
  @click.option("--port", default=DEFAULT_PORT, envvar=PORT_ENVVAR, hidden=True)
526
+ @debug_option()
495
527
  @click.pass_context
496
528
  def secrets_cli(ctx, host: str, port: str):
497
529
  ctx.obj = sdk.FalServerlessClient(f"{host}:{port}")
498
530
 
499
531
 
500
532
  @secrets_cli.command("list")
533
+ @debug_option()
501
534
  @click.pass_obj
502
535
  def list_secrets(client: api.FalServerlessClient):
503
536
  table = Table(title="Secrets")
@@ -514,6 +547,7 @@ def list_secrets(client: api.FalServerlessClient):
514
547
  @secrets_cli.command("set")
515
548
  @click.argument("secret_name", required=True)
516
549
  @click.argument("secret_value", required=True)
550
+ @debug_option()
517
551
  @click.pass_obj
518
552
  def set_secret(client: api.FalServerlessClient, secret_name: str, secret_value: str):
519
553
  with client.connect() as connection:
@@ -523,6 +557,7 @@ def set_secret(client: api.FalServerlessClient, secret_name: str, secret_value:
523
557
 
524
558
  @secrets_cli.command("delete")
525
559
  @click.argument("secret_name", required=True)
560
+ @debug_option()
526
561
  @click.pass_obj
527
562
  def delete_secret(client: api.FalServerlessClient, secret_name: str):
528
563
  with client.connect() as connection:
@@ -574,11 +609,10 @@ def _get_user_id() -> str:
574
609
  raise api.FalServerlessError(content["detail"])
575
610
  try:
576
611
  full_user_id = user_details_response.parsed.user_id
577
- user_id = full_user_id.split("|")[1]
612
+ _provider, _, user_id = full_user_id.partition("|")
613
+ if not user_id:
614
+ user_id = full_user_id
615
+
578
616
  return user_id
579
617
  except Exception as e:
580
618
  raise api.FalServerlessError(f"Could not parse the user data: {e}")
581
-
582
-
583
- if __name__ == "__main__":
584
- cli()
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ import webbrowser
4
+
5
+
6
+ def maybe_open_browser_tab(url) -> None:
7
+ try:
8
+ # Avoids unwanted output in the console from the standard `webbrowser.open()`.
9
+ # See https://stackoverflow.com/a/19199794
10
+ browser = webbrowser.get()
11
+
12
+ browser.open_new_tab(url)
13
+ except webbrowser.Error:
14
+ pass
@@ -12,6 +12,7 @@ import rich
12
12
  from openapi_fal_rest.api.workflows import (
13
13
  create_or_update_workflow_workflows_post as publish_workflow,
14
14
  )
15
+ from openapi_fal_rest.models.http_validation_error import HTTPValidationError
15
16
  from pydantic import BaseModel
16
17
  from rich.syntax import Syntax
17
18
 
@@ -363,7 +364,7 @@ class Workflow:
363
364
 
364
365
  to_dict = to_json
365
366
 
366
- def publish(self, title: str, *, is_public: bool = True) -> None:
367
+ def publish(self, title: str, *, is_public: bool = True):
367
368
  workflow_contents = publish_workflow.TypedWorkflow(
368
369
  name=self.name,
369
370
  title=title,
@@ -376,14 +377,14 @@ class Workflow:
376
377
  )
377
378
  if isinstance(published_workflow, Exception):
378
379
  raise published_workflow
379
-
380
- return (
381
- REST_CLIENT.base_url
382
- + "/workflows/"
383
- + published_workflow.user_id
384
- + "/"
385
- + published_workflow.name
386
- )
380
+ if isinstance(published_workflow, HTTPValidationError):
381
+ raise RuntimeError(published_workflow.detail)
382
+ if not published_workflow:
383
+ raise RuntimeError("Failed to publish the workflow")
384
+
385
+ # NOTE: dropping the provider prefix from the user_id
386
+ user_id_part = published_workflow.user_id.split("|")[-1]
387
+ return f"{user_id_part}/{published_workflow.name}"
387
388
 
388
389
 
389
390
  def create_workflow(
@@ -1,17 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import webbrowser
4
-
5
-
6
- def get_browser() -> webbrowser.BaseBrowser | None:
7
- """Gets a reference to the default browser, if available.
8
-
9
- This allows us to decide on a flow before showing the actual browser window.
10
- It also avoids unwanted output in the console from the standard `webbrowser.open()`.
11
-
12
- See https://stackoverflow.com/a/19199794
13
- """
14
- try:
15
- return webbrowser.get()
16
- except webbrowser.Error:
17
- return None
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes