lyrics-transcriber 0.30.0__py3-none-any.whl → 0.30.1__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.
@@ -1,249 +1,225 @@
1
- import dropbox
2
- from dropbox import Dropbox
3
- from dropbox.files import WriteMode
1
+ from dataclasses import dataclass
2
+ from typing import Protocol, BinaryIO, Optional, List, Any
4
3
  import os
5
4
  import time
6
5
  import logging
7
6
  import requests
7
+ from dropbox import Dropbox
8
+ from dropbox.files import WriteMode, FileMetadata
9
+ from dropbox.sharing import RequestedVisibility, SharedLinkSettings
10
+ from dropbox.exceptions import AuthError, ApiError
8
11
 
9
12
  logger = logging.getLogger(__name__)
10
13
 
11
14
 
12
- class DropboxHandler:
13
- def __init__(self, app_key=None, app_secret=None, refresh_token=None, access_token=None):
14
- self.app_key = app_key or os.environ.get("WHISPER_DROPBOX_APP_KEY")
15
- self.app_secret = app_secret or os.environ.get("WHISPER_DROPBOX_APP_SECRET")
16
- self.refresh_token = refresh_token or os.environ.get("WHISPER_DROPBOX_REFRESH_TOKEN")
17
- self.access_token = access_token or os.environ.get("WHISPER_DROPBOX_ACCESS_TOKEN")
18
- self.dbx = Dropbox(self.access_token)
19
-
20
- def _refresh_access_token(self):
21
- """Refresh the access token using the refresh token."""
22
- try:
23
- logger.debug("Attempting to refresh access token")
24
- # Prepare the token refresh request
25
- data = {"grant_type": "refresh_token", "refresh_token": self.refresh_token}
26
- auth = (self.app_key, self.app_secret)
27
-
28
- logger.debug(f"Making refresh token request to Dropbox API")
29
- response = requests.post("https://api.dropbox.com/oauth2/token", data=data, auth=auth)
30
-
31
- logger.debug(f"Received response from Dropbox API. Status code: {response.status_code}")
32
- if response.status_code == 200:
33
- result = response.json()
34
- self.access_token = result["access_token"]
35
- self.dbx = Dropbox(self.access_token)
36
- logger.info("Successfully refreshed access token")
37
- else:
38
- logger.error(f"Failed to refresh token. Status code: {response.status_code}, Response: {response.text}")
15
+ @dataclass
16
+ class DropboxConfig:
17
+ """Configuration for Dropbox client."""
39
18
 
40
- except Exception as e:
41
- logger.error(f"Error refreshing access token: {str(e)}", exc_info=True)
42
- raise
19
+ app_key: Optional[str] = None
20
+ app_secret: Optional[str] = None
21
+ refresh_token: Optional[str] = None
43
22
 
44
- @staticmethod
45
- def _handle_auth_error(func):
46
- """Decorator to handle authentication errors and retry with refreshed token."""
23
+ @classmethod
24
+ def from_env(cls) -> "DropboxConfig":
25
+ """Create config from environment variables."""
26
+ return cls(
27
+ app_key=os.environ.get("WHISPER_DROPBOX_APP_KEY"),
28
+ app_secret=os.environ.get("WHISPER_DROPBOX_APP_SECRET"),
29
+ refresh_token=os.environ.get("WHISPER_DROPBOX_REFRESH_TOKEN"),
30
+ )
47
31
 
48
- def wrapper(self, *args, **kwargs):
49
- try:
50
- logger.debug(f"Executing {func.__name__} with args: {args}, kwargs: {kwargs}")
51
- return func(self, *args, **kwargs)
52
- except (dropbox.exceptions.AuthError, dropbox.exceptions.ApiError) as e:
53
- logger.debug(f"Caught error in {func.__name__}: {str(e)}")
54
- if "expired_access_token" in str(e):
55
- logger.info(f"Access token expired in {func.__name__}, attempting refresh")
56
- self._refresh_access_token()
57
- logger.debug(f"Retrying {func.__name__} after token refresh")
58
- return func(self, *args, **kwargs)
59
- logger.error(f"Unhandled Dropbox error in {func.__name__}: {str(e)}")
60
- raise
61
-
62
- return wrapper
63
-
64
- @_handle_auth_error
65
- def upload_with_retry(self, file, path, max_retries=3):
32
+
33
+ class DropboxAPI(Protocol):
34
+ """Protocol for Dropbox API operations."""
35
+
36
+ def files_upload(self, f: bytes, path: str, mode: WriteMode) -> Any: ...
37
+ def files_list_folder(self, path: str, recursive: bool = False) -> Any: ...
38
+ def files_list_folder_continue(self, cursor: str) -> Any: ...
39
+ def files_download(self, path: str) -> tuple[Any, Any]: ...
40
+ def files_download_to_file(self, download_path: str, path: str) -> None: ...
41
+ def files_get_metadata(self, path: str) -> Any: ...
42
+ def sharing_create_shared_link_with_settings(self, path: str, settings: SharedLinkSettings) -> Any: ...
43
+ def sharing_list_shared_links(self, path: str) -> Any: ...
44
+
45
+
46
+ class DropboxHandler:
47
+ """Handles Dropbox storage operations with automatic token refresh."""
48
+
49
+ def __init__(
50
+ self,
51
+ config: Optional[DropboxConfig] = None,
52
+ client: Optional[DropboxAPI] = None,
53
+ ):
54
+ """Initialize the Dropbox handler."""
55
+ self.config = config or DropboxConfig.from_env()
56
+ self._validate_config()
57
+
58
+ self.client = client or Dropbox(
59
+ app_key=self.config.app_key,
60
+ app_secret=self.config.app_secret,
61
+ oauth2_refresh_token=self.config.refresh_token,
62
+ )
63
+
64
+ def _validate_config(self) -> None:
65
+ """Validate the configuration."""
66
+ logger.debug("Validating DropboxConfig with values:")
67
+ logger.debug(f"app_key: {self.config.app_key[:4] + '...' if self.config.app_key else 'None'}")
68
+ logger.debug(f"app_secret: {self.config.app_secret[:4] + '...' if self.config.app_secret else 'None'}")
69
+ logger.debug(f"refresh_token: {self.config.refresh_token[:4] + '...' if self.config.refresh_token else 'None'}")
70
+
71
+ missing = []
72
+ if not self.config.app_key:
73
+ missing.append("app_key")
74
+ if not self.config.app_secret:
75
+ missing.append("app_secret")
76
+ if not self.config.refresh_token:
77
+ missing.append("refresh_token")
78
+
79
+ if missing:
80
+ error_msg = f"Missing required Dropbox configuration: {', '.join(missing)}"
81
+ logger.error(error_msg)
82
+ raise ValueError(error_msg)
83
+
84
+ def upload_with_retry(self, file: BinaryIO, path: str, max_retries: int = 3) -> None:
66
85
  """Upload a file to Dropbox with retries."""
67
86
  for attempt in range(max_retries):
68
87
  try:
69
88
  logger.debug(f"Attempting file upload to {path} (attempt {attempt + 1}/{max_retries})")
70
89
  file.seek(0)
71
- self.dbx.files_upload(file.read(), path, mode=WriteMode.overwrite)
90
+ self.client.files_upload(file.read(), path, mode=WriteMode.overwrite)
72
91
  logger.debug(f"Successfully uploaded file to {path}")
73
92
  return
74
- except dropbox.exceptions.ApiError as e:
93
+ except ApiError as e:
75
94
  logger.warning(f"Upload attempt {attempt + 1} failed: {str(e)}")
76
95
  if attempt == max_retries - 1:
77
- logger.error(f"All upload attempts failed for {path}: {str(e)}")
96
+ logger.error(f"All upload attempts failed for {path}")
78
97
  raise
79
- sleep_time = 1 * (attempt + 1)
80
- logger.debug(f"Waiting {sleep_time} seconds before retry")
81
- time.sleep(sleep_time)
98
+ time.sleep(1 * (attempt + 1))
82
99
 
83
- @_handle_auth_error
84
- def upload_string_with_retry(self, content, path, max_retries=3):
100
+ def upload_string_with_retry(self, content: str, path: str, max_retries: int = 3) -> None:
85
101
  """Upload a string content to Dropbox with retries."""
86
102
  for attempt in range(max_retries):
87
103
  try:
88
104
  logger.debug(f"Attempting string upload to {path} (attempt {attempt + 1}/{max_retries})")
89
- self.dbx.files_upload(content.encode(), path, mode=WriteMode.overwrite)
105
+ self.client.files_upload(content.encode(), path, mode=WriteMode.overwrite)
90
106
  logger.debug(f"Successfully uploaded string content to {path}")
91
107
  return
92
- except dropbox.exceptions.ApiError as e:
108
+ except ApiError as e:
93
109
  logger.warning(f"Upload attempt {attempt + 1} failed: {str(e)}")
94
110
  if attempt == max_retries - 1:
95
- logger.error(f"All upload attempts failed for {path}: {str(e)}")
111
+ logger.error(f"All upload attempts failed for {path}")
96
112
  raise
97
- sleep_time = 1 * (attempt + 1)
98
- logger.debug(f"Waiting {sleep_time} seconds before retry")
99
- time.sleep(sleep_time)
113
+ time.sleep(1 * (attempt + 1))
100
114
 
101
- @_handle_auth_error
102
- def list_folder_recursive(self, path=""):
115
+ def list_folder_recursive(self, path: str = "") -> List[FileMetadata]:
103
116
  """List all files in a folder recursively."""
104
117
  try:
105
118
  logger.debug(f"Listing files recursively from {path}")
106
119
  entries = []
107
- result = self.dbx.files_list_folder(path, recursive=True)
120
+ result = self.client.files_list_folder(path, recursive=True)
108
121
 
109
122
  while True:
110
123
  entries.extend(result.entries)
111
124
  if not result.has_more:
112
125
  break
113
- logger.debug("Fetching more results from Dropbox")
114
- result = self.dbx.files_list_folder_continue(result.cursor)
126
+ result = self.client.files_list_folder_continue(result.cursor)
115
127
 
116
128
  return entries
117
-
118
- except (dropbox.exceptions.AuthError, dropbox.exceptions.ApiError):
119
- # Let the decorator handle these
120
- raise
121
129
  except Exception as e:
122
- logger.error(f"Error listing files from Dropbox: {str(e)}", exc_info=True)
130
+ logger.error(f"Error listing files: {str(e)}", exc_info=True)
123
131
  raise
124
132
 
125
- @_handle_auth_error
126
- def download_file_content(self, path):
133
+ def download_file_content(self, path: str) -> bytes:
127
134
  """Download and return the content of a file."""
128
135
  try:
129
136
  logger.debug(f"Downloading file content from {path}")
130
- return self.dbx.files_download(path)[1].content
137
+ return self.client.files_download(path)[1].content
131
138
  except Exception as e:
132
- logger.error(f"Error downloading file from {path}: {str(e)}", exc_info=True)
139
+ logger.error(f"Error downloading file: {str(e)}", exc_info=True)
133
140
  raise
134
141
 
135
- @_handle_auth_error
136
- def download_folder(self, dropbox_path, local_path):
142
+ def download_folder(self, dropbox_path: str, local_path: str) -> None:
137
143
  """Download all files from a Dropbox folder to a local path."""
138
144
  try:
139
145
  logger.debug(f"Downloading folder {dropbox_path} to {local_path}")
146
+ entries = self.list_folder_recursive(dropbox_path)
140
147
 
141
- # List all files in the folder
142
- result = self.dbx.files_list_folder(dropbox_path, recursive=True)
143
- entries = result.entries
144
-
145
- # Continue fetching if there are more files
146
- while result.has_more:
147
- result = self.dbx.files_list_folder_continue(result.cursor)
148
- entries.extend(result.entries)
149
-
150
- # Download each file
151
148
  for entry in entries:
152
- if isinstance(entry, dropbox.files.FileMetadata):
153
- # Calculate relative path from the root folder
149
+ if isinstance(entry, FileMetadata):
154
150
  rel_path = entry.path_display[len(dropbox_path) :].lstrip("/")
155
151
  local_file_path = os.path.join(local_path, rel_path)
156
152
 
157
- # Create directories if they don't exist
158
153
  os.makedirs(os.path.dirname(local_file_path), exist_ok=True)
159
-
160
- # Download the file
161
154
  logger.debug(f"Downloading {entry.path_display} to {local_file_path}")
162
- self.dbx.files_download_to_file(local_file_path, entry.path_display)
163
-
164
- logger.debug(f"Successfully downloaded folder {dropbox_path} to {local_path}")
155
+ self.client.files_download_to_file(local_file_path, entry.path_display)
165
156
 
157
+ logger.debug(f"Successfully downloaded folder {dropbox_path}")
166
158
  except Exception as e:
167
- logger.error(f"Error downloading folder {dropbox_path}: {str(e)}", exc_info=True)
159
+ logger.error(f"Error downloading folder: {str(e)}", exc_info=True)
168
160
  raise
169
161
 
170
- @_handle_auth_error
171
- def upload_folder(self, local_path, dropbox_path):
162
+ def upload_folder(self, local_path: str, dropbox_path: str) -> None:
172
163
  """Upload all files from a local folder to a Dropbox path."""
173
164
  try:
174
165
  logger.debug(f"Uploading folder {local_path} to {dropbox_path}")
175
-
176
- # Walk through all files in the local folder
177
- for root, dirs, files in os.walk(local_path):
166
+ for root, _, files in os.walk(local_path):
178
167
  for filename in files:
179
168
  local_file_path = os.path.join(root, filename)
180
- # Calculate relative path from local_path
181
169
  rel_path = os.path.relpath(local_file_path, local_path)
182
170
  target_path = f"{dropbox_path}/{rel_path}"
183
171
 
184
172
  logger.debug(f"Uploading {rel_path} to {target_path}")
185
173
  with open(local_file_path, "rb") as f:
186
- self.dbx.files_upload(f.read(), target_path, mode=WriteMode.overwrite)
187
-
188
- logger.debug(f"Successfully uploaded folder {local_path} to {dropbox_path}")
174
+ self.client.files_upload(f.read(), target_path, mode=WriteMode.overwrite)
189
175
 
176
+ logger.debug(f"Successfully uploaded folder {local_path}")
190
177
  except Exception as e:
191
- logger.error(f"Error uploading folder {local_path}: {str(e)}", exc_info=True)
178
+ logger.error(f"Error uploading folder: {str(e)}", exc_info=True)
192
179
  raise
193
180
 
194
- @_handle_auth_error
195
- def create_shared_link(self, path):
181
+ def create_shared_link(self, path: str) -> str:
196
182
  """Create a shared link for a file that's accessible without login."""
197
183
  try:
198
184
  logger.debug(f"Creating shared link for {path}")
199
- shared_link = self.dbx.sharing_create_shared_link_with_settings(
200
- path, settings=dropbox.sharing.SharedLinkSettings(requested_visibility=dropbox.sharing.RequestedVisibility.public)
185
+ shared_link = self.client.sharing_create_shared_link_with_settings(
186
+ path, settings=SharedLinkSettings(requested_visibility=RequestedVisibility.public)
201
187
  )
202
- # Convert dropbox shared link to direct download link
203
188
  return shared_link.url.replace("www.dropbox.com", "dl.dropboxusercontent.com")
204
189
  except Exception as e:
205
190
  logger.error(f"Error creating shared link: {str(e)}", exc_info=True)
206
191
  raise
207
192
 
208
- @_handle_auth_error
209
- def get_existing_shared_link(self, path):
193
+ def get_existing_shared_link(self, path: str) -> Optional[str]:
210
194
  """Get existing shared link for a file if it exists."""
211
195
  try:
212
196
  logger.debug(f"Getting existing shared link for {path}")
213
- shared_links = self.dbx.sharing_list_shared_links(path=path).links
197
+ shared_links = self.client.sharing_list_shared_links(path=path).links
214
198
  if shared_links:
215
- # Convert to direct download link
216
199
  return shared_links[0].url.replace("www.dropbox.com", "dl.dropboxusercontent.com")
217
200
  return None
218
201
  except Exception as e:
219
202
  logger.error(f"Error getting existing shared link: {str(e)}", exc_info=True)
220
203
  return None
221
204
 
222
- @_handle_auth_error
223
- def create_or_get_shared_link(self, path):
205
+ def create_or_get_shared_link(self, path: str) -> str:
224
206
  """Create a shared link or get existing one."""
225
207
  try:
226
- # First try to get existing link
227
208
  existing_link = self.get_existing_shared_link(path)
228
209
  if existing_link:
229
210
  logger.debug(f"Found existing shared link for {path}")
230
211
  return existing_link
231
212
 
232
- # If no existing link, create new one
233
213
  logger.debug(f"Creating new shared link for {path}")
234
- shared_link = self.dbx.sharing_create_shared_link_with_settings(
235
- path, settings=dropbox.sharing.SharedLinkSettings(requested_visibility=dropbox.sharing.RequestedVisibility.public)
236
- )
237
- return shared_link.url.replace("www.dropbox.com", "dl.dropboxusercontent.com")
214
+ return self.create_shared_link(path)
238
215
  except Exception as e:
239
- logger.error(f"Error creating or getting shared link: {str(e)}", exc_info=True)
216
+ logger.error(f"Error creating/getting shared link: {str(e)}", exc_info=True)
240
217
  raise
241
218
 
242
- @_handle_auth_error
243
- def file_exists(self, path):
219
+ def file_exists(self, path: str) -> bool:
244
220
  """Check if a file exists in Dropbox."""
245
221
  try:
246
- self.dbx.files_get_metadata(path)
222
+ self.client.files_get_metadata(path)
247
223
  return True
248
224
  except:
249
225
  return False