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/toolkit/file/file.py CHANGED
@@ -1,14 +1,24 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from pathlib import Path
4
- from typing import Callable
4
+ from tempfile import NamedTemporaryFile, TemporaryDirectory
5
+ from typing import Any, Callable
6
+ from urllib.parse import urlparse
7
+ from zipfile import ZipFile
5
8
 
6
- from fal.toolkit.file.providers.fal import FalFileRepository, InMemoryRepository
9
+ from pydantic import BaseModel, Field, PrivateAttr
10
+ from pydantic.typing import Optional
11
+
12
+ from fal.toolkit.file.providers.fal import (
13
+ FalCDNFileRepository,
14
+ FalFileRepository,
15
+ InMemoryRepository,
16
+ )
7
17
  from fal.toolkit.file.providers.gcp import GoogleStorageRepository
8
18
  from fal.toolkit.file.providers.r2 import R2Repository
9
19
  from fal.toolkit.file.types import FileData, FileRepository, RepositoryId
10
20
  from fal.toolkit.mainify import mainify
11
- from pydantic import BaseModel, Field, PrivateAttr
21
+ from fal.toolkit.utils.download_utils import download_file
12
22
 
13
23
  FileRepositoryFactory = Callable[[], FileRepository]
14
24
 
@@ -17,6 +27,7 @@ BUILT_IN_REPOSITORIES: dict[RepositoryId, FileRepositoryFactory] = {
17
27
  "in_memory": lambda: InMemoryRepository(),
18
28
  "gcp_storage": lambda: GoogleStorageRepository(),
19
29
  "r2": lambda: R2Repository(),
30
+ "cdn": lambda: FalCDNFileRepository(),
20
31
  }
21
32
 
22
33
 
@@ -37,23 +48,22 @@ class File(BaseModel):
37
48
  _file_data: FileData = PrivateAttr()
38
49
  url: str = Field(
39
50
  description="The URL where the file can be downloaded from.",
40
- examples=["https://url.to/generated/file/z9RV14K95DvU.png"],
41
51
  )
42
- content_type: str = Field(
52
+ content_type: Optional[str] = Field(
43
53
  description="The mime type of the file.",
44
54
  examples=["image/png"],
45
55
  )
46
- file_name: str = Field(
56
+ file_name: Optional[str] = Field(
47
57
  description="The name of the file. It will be auto-generated if not provided.",
48
58
  examples=["z9RV14K95DvU.png"],
49
59
  )
50
- file_size: int = Field(
60
+ file_size: Optional[int] = Field(
51
61
  description="The size of the file in bytes.", examples=[4404019]
52
62
  )
53
63
 
54
64
  def __init__(self, **kwargs):
55
65
  if "file_data" in kwargs:
56
- data = kwargs.pop("file_data")
66
+ data: FileData = kwargs.pop("file_data")
57
67
  repository = kwargs.pop("repository", None)
58
68
 
59
69
  repo = (
@@ -74,12 +84,39 @@ class File(BaseModel):
74
84
 
75
85
  super().__init__(**kwargs)
76
86
 
87
+ # Pydantic custom validator for input type conversion
88
+ @classmethod
89
+ def __get_validators__(cls):
90
+ yield cls.__convert_from_str
91
+
92
+ @classmethod
93
+ def __convert_from_str(cls, value: Any):
94
+ if isinstance(value, str):
95
+ parsed_url = urlparse(value)
96
+ if parsed_url.scheme not in ["http", "https", "data"]:
97
+ raise ValueError(f"value must be a valid URL")
98
+ return cls._from_url(parsed_url.geturl())
99
+
100
+ return value
101
+
102
+ @classmethod
103
+ def _from_url(
104
+ cls,
105
+ url: str,
106
+ ) -> File:
107
+ return cls(
108
+ url=url,
109
+ content_type=None,
110
+ file_name=None,
111
+ repository=DEFAULT_REPOSITORY,
112
+ )
113
+
77
114
  @classmethod
78
115
  def from_bytes(
79
116
  cls,
80
117
  data: bytes,
81
- content_type: str | None = None,
82
- file_name: str | None = None,
118
+ content_type: Optional[str] = None,
119
+ file_name: Optional[str] = None,
83
120
  repository: FileRepository | RepositoryId = DEFAULT_REPOSITORY,
84
121
  ) -> File:
85
122
  return cls(
@@ -91,7 +128,7 @@ class File(BaseModel):
91
128
  def from_path(
92
129
  cls,
93
130
  path: str | Path,
94
- content_type: str | None = None,
131
+ content_type: Optional[str] = None,
95
132
  repository: FileRepository | RepositoryId = DEFAULT_REPOSITORY,
96
133
  ) -> File:
97
134
  file_path = Path(path)
@@ -104,4 +141,54 @@ class File(BaseModel):
104
141
  )
105
142
 
106
143
  def as_bytes(self) -> bytes:
144
+ if getattr(self, "_file_data", None) is None:
145
+ raise ValueError("File has not been downloaded")
146
+
107
147
  return self._file_data.data
148
+
149
+ def save(self, path: str | Path, overwrite: bool = False) -> Path:
150
+ file_path = Path(path).resolve()
151
+
152
+ if file_path.exists() and not overwrite:
153
+ raise FileExistsError(f"File {file_path} already exists")
154
+
155
+ downloaded_path = download_file(self.url, target_dir=file_path.parent)
156
+ downloaded_path.rename(file_path)
157
+
158
+ return file_path
159
+
160
+
161
+ @mainify
162
+ class CompressedFile(File):
163
+ _extract_dir: Optional[TemporaryDirectory] = PrivateAttr(default=None)
164
+
165
+ def __init__(self, **kwargs):
166
+ super().__init__(**kwargs)
167
+ self._extract_dir = None
168
+
169
+ def __iter__(self):
170
+ if not self._extract_dir:
171
+ self._extract_files()
172
+
173
+ files = Path(self._extract_dir.name).iterdir() # type: ignore
174
+ return iter(files)
175
+
176
+ def _extract_files(self):
177
+ self._extract_dir = TemporaryDirectory()
178
+
179
+ with NamedTemporaryFile() as temp_file:
180
+ file_path = temp_file.name
181
+ self.save(file_path, overwrite=True)
182
+
183
+ with ZipFile(file_path) as zip_file:
184
+ zip_file.extractall(self._extract_dir.name)
185
+
186
+ def glob(self, pattern: str):
187
+ if not self._extract_dir:
188
+ self._extract_files()
189
+
190
+ return Path(self._extract_dir.name).glob(pattern) # type: ignore
191
+
192
+ def __del__(self):
193
+ if self._extract_dir:
194
+ self._extract_dir.cleanup()
@@ -7,18 +7,23 @@ from dataclasses import dataclass
7
7
  from urllib.error import HTTPError
8
8
  from urllib.request import Request, urlopen
9
9
 
10
+ from fal.auth import key_credentials
10
11
  from fal.toolkit.exceptions import FileUploadException
11
12
  from fal.toolkit.file.types import FileData, FileRepository
12
13
  from fal.toolkit.mainify import mainify
13
14
 
15
+ _FAL_CDN = "https://fal-cdn.batuhan-941.workers.dev"
16
+
14
17
 
15
18
  @mainify
16
19
  @dataclass
17
20
  class FalFileRepository(FileRepository):
18
21
  def save(self, file: FileData) -> str:
19
- key_id = os.environ.get("FAL_KEY_ID")
20
- key_secret = os.environ.get("FAL_KEY_SECRET")
22
+ key_creds = key_credentials()
23
+ if not key_creds:
24
+ raise FileUploadException("FAL_KEY must be set")
21
25
 
26
+ key_id, key_secret = key_creds
22
27
  headers = {
23
28
  "Authorization": f"Key {key_id}:{key_secret}",
24
29
  "Accept": "application/json",
@@ -70,3 +75,39 @@ class FalFileRepository(FileRepository):
70
75
  class InMemoryRepository(FileRepository):
71
76
  def save(self, file: FileData) -> str:
72
77
  return f'data:{file.content_type};base64,{b64encode(file.data).decode("utf-8")}'
78
+
79
+
80
+ @mainify
81
+ @dataclass
82
+ class FalCDNFileRepository(FileRepository):
83
+ def save(self, file: FileData) -> str:
84
+ headers = {
85
+ **self.auth_headers,
86
+ "Accept": "application/json",
87
+ "Content-Type": file.content_type,
88
+ }
89
+
90
+ url = os.getenv("FAL_CDN_HOST", _FAL_CDN) + "/files/upload"
91
+ request = Request(url, headers=headers, method="POST", data=file.data)
92
+ try:
93
+ with urlopen(request) as response:
94
+ result = json.load(response)
95
+ except HTTPError as e:
96
+ raise FileUploadException(
97
+ f"Error initiating upload. Status {e.status}: {e.reason}"
98
+ )
99
+
100
+ access_url = result["access_url"]
101
+ return access_url
102
+
103
+ @property
104
+ def auth_headers(self) -> dict[str, str]:
105
+ key_creds = key_credentials()
106
+ if not key_creds:
107
+ raise FileUploadException("FAL_KEY must be set")
108
+
109
+ key_id, key_secret = key_creds
110
+ return {
111
+ "Authorization": f"Bearer {key_id}:{key_secret}",
112
+ "User-Agent": "fal/0.1.0",
113
+ }
fal/toolkit/file/types.py CHANGED
@@ -31,7 +31,7 @@ class FileData:
31
31
  self.file_name = file_name
32
32
 
33
33
 
34
- RepositoryId = Literal["fal", "in_memory", "gcp_storage", "r2"]
34
+ RepositoryId = Literal["fal", "in_memory", "gcp_storage", "r2", "cdn"]
35
35
 
36
36
 
37
37
  @mainify
@@ -1,12 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import io
4
+ from tempfile import NamedTemporaryFile
4
5
  from typing import TYPE_CHECKING, Literal, Optional, Union
5
6
 
7
+ from pydantic import BaseModel, Field
8
+
6
9
  from fal.toolkit.file.file import DEFAULT_REPOSITORY, File
7
10
  from fal.toolkit.file.types import FileData, FileRepository, RepositoryId
8
11
  from fal.toolkit.mainify import mainify
9
- from pydantic import BaseModel, Field
12
+ from fal.toolkit.utils.download_utils import _download_file_python
10
13
 
11
14
  if TYPE_CHECKING:
12
15
  from PIL import Image as PILImage
@@ -43,7 +46,7 @@ IMAGE_SIZE_PRESETS: dict[ImageSizePreset, ImageSize] = {
43
46
 
44
47
  ImageSizeInput = Union[ImageSize, ImageSizePreset]
45
48
 
46
-
49
+ @mainify
47
50
  def get_image_size(source: ImageSizeInput) -> ImageSize:
48
51
  if isinstance(source, ImageSize):
49
52
  return source
@@ -53,8 +56,6 @@ def get_image_size(source: ImageSizeInput) -> ImageSize:
53
56
  raise TypeError(f"Invalid value for ImageSize: {source}")
54
57
 
55
58
 
56
- get_image_size.__module__ = "__main__"
57
-
58
59
  ImageFormat = Literal["png", "jpeg", "jpg", "webp", "gif"]
59
60
 
60
61
 
@@ -117,3 +118,24 @@ class Image(File):
117
118
  raw_image = f.getvalue()
118
119
 
119
120
  return cls.from_bytes(raw_image, format, size, file_name, repository)
121
+
122
+ def to_pil(self, mode: str = "RGB") -> PILImage.Image:
123
+ try:
124
+ from PIL import Image as PILImage
125
+ from PIL import ImageOps
126
+ except ImportError:
127
+ raise ImportError(
128
+ "The PIL package is required to use Image.to_pil()."
129
+ )
130
+
131
+ # Stream the image data from url to a temp file and convert it to a PIL image
132
+ with NamedTemporaryFile() as temp_file:
133
+ temp_file_path = temp_file.name
134
+
135
+ _download_file_python(self.url, temp_file_path)
136
+
137
+ img = PILImage.open(temp_file_path).convert(mode)
138
+ img = ImageOps.exif_transpose(img)
139
+
140
+ return img
141
+
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import traceback
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from fal.toolkit.mainify import mainify
8
+
9
+ if TYPE_CHECKING:
10
+ import torch
11
+
12
+
13
+ @mainify
14
+ def optimize(
15
+ module: torch.nn.Module, *, optimization_config: dict[str, Any] | None = None
16
+ ) -> torch.nn.Module:
17
+ """Optimize the given torch module with dynamic compilation and
18
+ quantization techniques. Only applicable under fal's cloud environment.
19
+
20
+ Warning: This function is experimental and may not work as expected.
21
+ """
22
+ import runpy
23
+
24
+ import torch.nn as nn
25
+
26
+ if not isinstance(module, nn.Module):
27
+ raise TypeError(f"Expected a torch.nn.Module, got {type(module)}.")
28
+
29
+ optimizer_path = os.environ.get("FAL_SPATIAL_OPTIMIZER", None)
30
+ if optimizer_path is None:
31
+ print(
32
+ "[WARNING] FAL_SPATIAL_OPTIMIZER is not set, falling back"
33
+ "to default torch execution"
34
+ )
35
+ return module
36
+
37
+ try:
38
+ spatial_optimizer = runpy.run_path(optimizer_path, run_name="spatial_optimizer")
39
+
40
+ return spatial_optimizer["optimize"](
41
+ module,
42
+ optimization_config=optimization_config,
43
+ )
44
+ except Exception as e:
45
+ print(
46
+ "[WARNING] Failed to optimize module, falling back "
47
+ "to default torch execution."
48
+ )
49
+ traceback.print_exc()
50
+ return module
@@ -3,15 +3,16 @@ from __future__ import annotations
3
3
  import hashlib
4
4
  import shutil
5
5
  import subprocess
6
+ import sys
6
7
  from functools import lru_cache
7
- from pathlib import Path
8
+ from pathlib import Path, PurePath
8
9
  from tempfile import TemporaryDirectory
9
10
  from urllib.parse import urlparse
10
11
  from urllib.request import Request, urlopen
11
12
 
12
13
  from fal.toolkit.mainify import mainify
13
14
 
14
- FAL_PERSISTENT_DIR = Path("/data")
15
+ FAL_PERSISTENT_DIR = PurePath("/data")
15
16
  FAL_REPOSITORY_DIR = FAL_PERSISTENT_DIR / ".fal" / "repos"
16
17
  FAL_MODEL_WEIGHTS_DIR = FAL_PERSISTENT_DIR / ".fal" / "model_weights"
17
18
 
@@ -71,8 +72,13 @@ def _get_remote_file_properties(url: str) -> tuple[str, int]:
71
72
  content_length = int(response.headers.get("Content-Length", -1))
72
73
 
73
74
  if not file_name:
74
- url_path = urlparse(url).path
75
- file_name = Path(url_path).name or _hash_url(url)
75
+ parsed_url = urlparse(url)
76
+
77
+ if parsed_url.scheme == "data":
78
+ file_name = _hash_url(url)
79
+ else:
80
+ url_path = parsed_url.path
81
+ file_name = Path(url_path).name or _hash_url(url)
76
82
 
77
83
  return file_name, content_length
78
84
 
@@ -148,7 +154,7 @@ def download_file(
148
154
 
149
155
  # If target_dir is not an absolute path, use "/data" as the relative directory
150
156
  if not target_dir_path.is_absolute():
151
- target_dir_path = FAL_PERSISTENT_DIR / target_dir_path
157
+ target_dir_path = FAL_PERSISTENT_DIR / target_dir_path # type: ignore[assignment]
152
158
 
153
159
  target_path = target_dir_path.resolve() / file_name
154
160
 
@@ -180,7 +186,7 @@ def download_file(
180
186
 
181
187
 
182
188
  @mainify
183
- def _download_file_python(url: str, target_path: Path) -> Path:
189
+ def _download_file_python(url: str, target_path: Path | str) -> Path:
184
190
  """Download a file from a given URL and save it to a specified path using a
185
191
  Python interface.
186
192
 
@@ -215,7 +221,7 @@ def _download_file_python(url: str, target_path: Path) -> Path:
215
221
  finally:
216
222
  Path(temp_file.name).unlink(missing_ok=True)
217
223
 
218
- return target_path
224
+ return Path(target_path)
219
225
 
220
226
 
221
227
  @mainify
@@ -290,7 +296,7 @@ def download_model_weights(url: str, force: bool = False):
290
296
  A Path object representing the full path to the downloaded model weights.
291
297
  """
292
298
  # This is not a protected path, so the user may change stuff internally
293
- weights_dir = FAL_MODEL_WEIGHTS_DIR / _hash_url(url)
299
+ weights_dir = Path(FAL_MODEL_WEIGHTS_DIR / _hash_url(url))
294
300
 
295
301
  if weights_dir.exists() and not force:
296
302
  try:
@@ -315,6 +321,7 @@ def clone_repository(
315
321
  target_dir: str | Path | None = None,
316
322
  repo_name: str | None = None,
317
323
  force: bool = False,
324
+ include_to_path: bool = False,
318
325
  ) -> Path:
319
326
  """Clones a Git repository from the specified HTTPS URL into a local
320
327
  directory.
@@ -334,20 +341,34 @@ def clone_repository(
334
341
  If not provided, the repository's name from the URL is used.
335
342
  force: If `True`, the repository is cloned even if it already exists locally
336
343
  and its commit hash matches the provided commit hash. Defaults to `False`.
344
+ include_to_path: If `True`, the cloned repository is added to the `sys.path`.
345
+ Defaults to `False`.
337
346
 
338
347
  Returns:
339
348
  A Path object representing the full path to the cloned Git repository.
340
349
  """
341
- target_dir = target_dir or FAL_REPOSITORY_DIR
350
+ target_dir = target_dir or FAL_REPOSITORY_DIR # type: ignore[assignment]
342
351
  repo_name = repo_name or Path(https_url).stem
343
352
 
344
- local_repo_path = Path(target_dir) / repo_name
353
+ local_repo_path = Path(target_dir) / repo_name # type: ignore[arg-type]
345
354
 
346
355
  if local_repo_path.exists():
347
356
  local_repo_commit_hash = _get_git_revision_hash(local_repo_path)
348
357
  if local_repo_commit_hash == commit_hash and not force:
358
+ if include_to_path:
359
+ __add_local_path_to_sys_path(local_repo_path)
349
360
  return local_repo_path
350
361
  else:
362
+ if local_repo_commit_hash != commit_hash:
363
+ print(
364
+ f"Local repository '{local_repo_path}' has a different commit hash "
365
+ f"({local_repo_commit_hash}) than the one provided ({commit_hash})."
366
+ )
367
+ elif force:
368
+ print(
369
+ f"Local repository '{local_repo_path}' already exists. "
370
+ f"Forcing re-download."
371
+ )
351
372
  print(f"Removing the existing repository: {local_repo_path} ")
352
373
  shutil.rmtree(local_repo_path)
353
374
 
@@ -362,6 +383,7 @@ def clone_repository(
362
383
  clone_command = [
363
384
  "git",
364
385
  "clone",
386
+ "--recursive",
365
387
  https_url,
366
388
  temp_dir_path,
367
389
  ]
@@ -380,13 +402,37 @@ def clone_repository(
380
402
 
381
403
  raise error
382
404
 
405
+ if include_to_path:
406
+ __add_local_path_to_sys_path(local_repo_path)
407
+
383
408
  return local_repo_path
384
409
 
385
410
 
411
+ @mainify
412
+ def __add_local_path_to_sys_path(local_path: Path | str):
413
+ local_path_str = str(local_path)
414
+
415
+ if local_path_str not in sys.path:
416
+ sys.path.insert(0, local_path_str)
417
+
418
+
386
419
  @mainify
387
420
  def _get_git_revision_hash(repo_path: Path) -> str:
388
421
  import subprocess
389
422
 
390
- return subprocess.check_output(
391
- ["git", "rev-parse", "HEAD"], cwd=repo_path, text=True
392
- ).strip()
423
+ try:
424
+ return subprocess.check_output(
425
+ ["git", "rev-parse", "HEAD"],
426
+ cwd=repo_path,
427
+ text=True,
428
+ stderr=subprocess.STDOUT,
429
+ ).strip()
430
+ except subprocess.CalledProcessError as error:
431
+ if "not a git repository" in error.output:
432
+ print(f"Repository '{repo_path}' is not a git repository.")
433
+ return ""
434
+
435
+ print(
436
+ f"{error}\nFailed to get the commit hash of the repository '{repo_path}' ."
437
+ )
438
+ raise error
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fal
3
- Version: 0.12.1
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,31 +11,31 @@ 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)
24
+ Requires-Dist: msgpack (>=1.0.7,<2.0.0)
27
25
  Requires-Dist: opentelemetry-api (>=1.15.0,<2.0.0)
28
26
  Requires-Dist: opentelemetry-sdk (>=1.15.0,<2.0.0)
29
27
  Requires-Dist: packaging (>=21.3)
30
28
  Requires-Dist: pathspec (>=0.11.1,<0.12.0)
29
+ Requires-Dist: pillow (>=10.2.0,<11.0.0)
31
30
  Requires-Dist: portalocker (>=2.7.0,<3.0.0)
32
31
  Requires-Dist: pydantic (<2.0)
32
+ Requires-Dist: pyjwt (>=2.8.0,<3.0.0)
33
33
  Requires-Dist: python-dateutil (>=2.8.0,<3.0.0)
34
- Requires-Dist: requests (>=2.28.1,<3.0.0)
35
34
  Requires-Dist: rich (>=13.3.2,<14.0.0)
36
35
  Requires-Dist: structlog (>=22.3.0,<23.0.0)
37
36
  Requires-Dist: types-python-dateutil (>=2.8.0,<3.0.0)
38
37
  Requires-Dist: typing-extensions (>=4.7.1,<5.0.0)
38
+ Requires-Dist: websockets (>=12.0,<13.0)
39
39
  Description-Content-Type: text/markdown
40
40
 
41
41
  # fal
@@ -0,0 +1,66 @@
1
+ openapi_fal_rest/__init__.py,sha256=sqsyB55QptrijXTCVFQfIJ6uC__vXez1i5KNvYplk5w,151
2
+ openapi_fal_rest/api/__init__.py,sha256=87ApBzKyGb5zsgTMOkQXDqsLZCmaSFoJMwbGzCDQZMw,47
3
+ openapi_fal_rest/api/applications/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ openapi_fal_rest/api/applications/app_metadata.py,sha256=GqG6Q7jt8Jcyhb3ms_6i0M1B3cy205y3_A8W-AGEapY,5120
5
+ openapi_fal_rest/api/billing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ openapi_fal_rest/api/billing/get_user_details.py,sha256=2HQHRUQj8QwqSKgiV_USBdXCxGlfaVTBbLiPaDsMBUM,4013
7
+ openapi_fal_rest/api/files/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ openapi_fal_rest/api/files/check_dir_hash.py,sha256=zPNlOwG4YVvnhgfrleQtYLhI1lG0t8YQ1CU3TyvXvfk,4747
9
+ openapi_fal_rest/api/files/upload_local_file.py,sha256=p2lM7hswGbs8KNLg1Pp6vwV7x-1PKtWX-aYmaHUHSDU,5649
10
+ openapi_fal_rest/client.py,sha256=G6BpJg9j7-JsrAUGddYwkzeWRYickBjPdcVgXoPzxuE,2817
11
+ openapi_fal_rest/errors.py,sha256=8mXSxdfSGzxT82srdhYbR0fHfgenxJXaUtMkaGgb6iU,470
12
+ openapi_fal_rest/models/__init__.py,sha256=u2YVZnZwu9YqDPasBnh9GLVIkEJj4PFCThf97Pblx4o,601
13
+ openapi_fal_rest/models/app_metadata_response_app_metadata.py,sha256=swJMfWvbjlMF8dmv-KEqcR9If0UjsRogwj9UqBBlkpc,1251
14
+ openapi_fal_rest/models/body_upload_local_file.py,sha256=rOTEbYBXfwZk8TsywZWSPPQQEfJgvsLIufT6A40RJZs,1980
15
+ openapi_fal_rest/models/customer_details.py,sha256=XQBaO-A5DI54nCau5ZIR0jhCAmsBJKrtDgApuu6PFrU,3912
16
+ openapi_fal_rest/models/hash_check.py,sha256=T9R7n4EdadCxbFUZvresZZFPYwDfyJMZVNxY6wIJEE8,1352
17
+ openapi_fal_rest/models/http_validation_error.py,sha256=2nhqlv8RX2qp6VR7hb8-SKtzJWXSZ0J95ThW9J4agJo,2131
18
+ openapi_fal_rest/models/lock_reason.py,sha256=3b_foCV6bZKvsbyic3hM1_qzvJk_9ZD_5mS1GzSawdw,703
19
+ openapi_fal_rest/models/validation_error.py,sha256=I6tB-HbEOmE0ua27erDX5PX5YUynENv_dgPN3SrwTrQ,2091
20
+ openapi_fal_rest/py.typed,sha256=8ZJUsxZiuOy1oJeVhsTWQhTG_6pTVHVXk5hJL79ebTk,25
21
+ openapi_fal_rest/types.py,sha256=4xaUIOliefW-5jz_p-JT2LO7-V0wKWaniHGtjPBQfvQ,993
22
+ fal/__init__.py,sha256=6SvCuotCb0tuqSWDZSFDjtySktJ5m1QpVIlefumJpvM,1199
23
+ fal/_serialization.py,sha256=l_dZuSX5BT7SogXw1CalYLfT2H3zy3tfq4y6jHuxZqQ,4201
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
+ fal/auth/local.py,sha256=lZqp4j32l2xFpY8zYvLoIHHyJrNAJDcm5MxgsLpY_pw,1786
30
+ fal/cli.py,sha256=nLk4LJsGvLicA_iW0T1ldYb_igMwYOdC2fQxUsdWCRQ,17236
31
+ fal/console/__init__.py,sha256=ernZ4bzvvliQh5SmrEqQ7lA5eVcbw6Ra2jalKtA7dxg,132
32
+ fal/console/icons.py,sha256=De9MfFaSkO2Lqfne13n3PrYfTXJVIzYZVqYn5BWsdrA,108
33
+ fal/console/ux.py,sha256=4vj1aGA3grRn-ebeMuDLR6u3YjMwUGpqtNgdTG9su5s,485
34
+ fal/env.py,sha256=-fA8x62BbOX3MOuO0maupa-_QJ9PNwr8ogfeG11QUyQ,53
35
+ fal/exceptions/__init__.py,sha256=Q4LCSqIrJ8GFQZWH5BvWL5mDPR0HwYQuIhNvsdiOkEU,938
36
+ fal/exceptions/_base.py,sha256=LeQmx-soL_-s1742WKN18VwTVjUuYP0L0BdQHPJBpM4,460
37
+ fal/exceptions/auth.py,sha256=01Ro7SyGJpwchubdHe14Cl6-Al1jUj16Sy4BvakNWf4,384
38
+ fal/exceptions/handlers.py,sha256=b21a8S13euECArjpgm2N69HsShqLYVqAboIeMoWlWA4,1414
39
+ fal/flags.py,sha256=8OaKkJg_-UvtyRbZf-rW5ZTW3B1xQpzzXnLRNFB7grA,889
40
+ fal/logging/__init__.py,sha256=snqprf7-sKw6oAATS_Yxklf-a3XhLg0vIHICPwLp6TM,1583
41
+ fal/logging/isolate.py,sha256=yDW_P4aR-t53IRmvD2Iprufv1Wn-xQXoBbMB2Ufr59s,2122
42
+ fal/logging/style.py,sha256=ckIgHzvF4DShM5kQh8F133X53z_vF46snuDHVmo_h9g,386
43
+ fal/logging/trace.py,sha256=OhzB6d4rQZimBc18WFLqH_9BGfqFFumKKTAGSsmWRMg,1904
44
+ fal/logging/user.py,sha256=A8vbZX9z13TPZEDzvlbvCDDdD0EL1KrCP3qHdrT58-A,632
45
+ fal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
46
+ fal/rest_client.py,sha256=kGBGmuyHfX1lR910EoKCYPjsyU8MdXawT_cW2q8Sajc,568
47
+ fal/sdk.py,sha256=Z3MQsD8MMQZq_GEC2VjaYChdNafFJtsgdk77-VK6N44,18782
48
+ fal/sync.py,sha256=Ljet584PVFz4r888-0bwV1Kio-tTneF_85TnHvBPvJw,4277
49
+ fal/toolkit/__init__.py,sha256=JDNBT_duflp93geeAzw2kFmGzG5odWnPJEXFLXE2nF4,713
50
+ fal/toolkit/exceptions.py,sha256=--WKKYxUop6WFy_vqAPXK6uH8C-JR98gnNXwhHNCb7E,258
51
+ fal/toolkit/file/__init__.py,sha256=YpUU6YziZV1AMuq12L0EDWToS0sgpHSGWsARbiOEHWk,56
52
+ fal/toolkit/file/file.py,sha256=ku4agJiGXU2gdfZmFrU5mDlVsag834zoeskbo-6ErEU,5926
53
+ fal/toolkit/file/providers/fal.py,sha256=hO59loXzGP4Vg-Q1FFR56nWbbI6BccJRnFsEI6z6EQE,3404
54
+ fal/toolkit/file/providers/gcp.py,sha256=Bq5SJSghXF8YfFnbZ83_mPdrWs2dFhi8ytODp92USgk,1962
55
+ fal/toolkit/file/providers/r2.py,sha256=xJtZfX3cfzJgLXS3F8mHArbrHi0_QBpIMy5M4-tS8H8,2586
56
+ fal/toolkit/file/types.py,sha256=MTIj6Y_ioL4CiMZXMiqx74vlmUifc3SNvcrWAXQfULE,1109
57
+ fal/toolkit/image/__init__.py,sha256=liEq0CqkRqUQ1udnnyGVFBwCXUhR2f6o5ffbtbAlP8o,57
58
+ fal/toolkit/image/image.py,sha256=bF1PzO4cJoFGJFpQYeG0sNaGuw3cC1zmobmbZrxbPFY,4339
59
+ fal/toolkit/mainify.py,sha256=E7gE45nZQZoaJdSlIq0mqajcH-IjcuPBWFmKm5hvhAU,406
60
+ fal/toolkit/optimize.py,sha256=OIhX0T-efRMgUJDpvL0bujdun5SovZgTdKxNOv01b_Y,1394
61
+ fal/toolkit/utils/__init__.py,sha256=b3zVpm50Upx1saXU7RiV9r9in6-Chs4OU9KRjBv7MYI,83
62
+ fal/toolkit/utils/download_utils.py,sha256=bigcLJjLK1OBAGxpYisJ0-5vcQCh0HAPuCykPrcCNd0,15596
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,,