machineconfig 1.96__py3-none-any.whl → 2.0__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 machineconfig might be problematic. Click here for more details.

Files changed (164) hide show
  1. machineconfig/cluster/cloud_manager.py +22 -26
  2. machineconfig/cluster/data_transfer.py +2 -2
  3. machineconfig/cluster/distribute.py +0 -2
  4. machineconfig/cluster/file_manager.py +4 -4
  5. machineconfig/cluster/job_params.py +1 -1
  6. machineconfig/cluster/loader_runner.py +8 -8
  7. machineconfig/cluster/remote_machine.py +4 -4
  8. machineconfig/cluster/script_execution.py +2 -2
  9. machineconfig/cluster/sessions_managers/archive/create_zellij_template.py +1 -1
  10. machineconfig/cluster/sessions_managers/enhanced_command_runner.py +23 -23
  11. machineconfig/cluster/sessions_managers/wt_local.py +78 -76
  12. machineconfig/cluster/sessions_managers/wt_local_manager.py +91 -91
  13. machineconfig/cluster/sessions_managers/wt_remote.py +39 -39
  14. machineconfig/cluster/sessions_managers/wt_remote_manager.py +94 -91
  15. machineconfig/cluster/sessions_managers/wt_utils/layout_generator.py +56 -54
  16. machineconfig/cluster/sessions_managers/wt_utils/process_monitor.py +49 -49
  17. machineconfig/cluster/sessions_managers/wt_utils/remote_executor.py +18 -18
  18. machineconfig/cluster/sessions_managers/wt_utils/session_manager.py +42 -42
  19. machineconfig/cluster/sessions_managers/wt_utils/status_reporter.py +36 -36
  20. machineconfig/cluster/sessions_managers/zellij_local.py +43 -46
  21. machineconfig/cluster/sessions_managers/zellij_local_manager.py +139 -120
  22. machineconfig/cluster/sessions_managers/zellij_remote.py +35 -35
  23. machineconfig/cluster/sessions_managers/zellij_remote_manager.py +33 -33
  24. machineconfig/cluster/sessions_managers/zellij_utils/example_usage.py +15 -15
  25. machineconfig/cluster/sessions_managers/zellij_utils/layout_generator.py +25 -26
  26. machineconfig/cluster/sessions_managers/zellij_utils/process_monitor.py +49 -49
  27. machineconfig/cluster/sessions_managers/zellij_utils/remote_executor.py +5 -5
  28. machineconfig/cluster/sessions_managers/zellij_utils/session_manager.py +15 -15
  29. machineconfig/cluster/sessions_managers/zellij_utils/status_reporter.py +11 -11
  30. machineconfig/cluster/templates/utils.py +3 -3
  31. machineconfig/jobs/__pycache__/__init__.cpython-311.pyc +0 -0
  32. machineconfig/jobs/python/__pycache__/__init__.cpython-311.pyc +0 -0
  33. machineconfig/jobs/python/__pycache__/python_ve_symlink.cpython-311.pyc +0 -0
  34. machineconfig/jobs/python/check_installations.py +8 -9
  35. machineconfig/jobs/python/python_cargo_build_share.py +2 -2
  36. machineconfig/jobs/python/vscode/link_ve.py +7 -7
  37. machineconfig/jobs/python/vscode/select_interpreter.py +7 -7
  38. machineconfig/jobs/python/vscode/sync_code.py +5 -5
  39. machineconfig/jobs/python_custom_installers/archive/ngrok.py +2 -2
  40. machineconfig/jobs/python_custom_installers/dev/aider.py +3 -3
  41. machineconfig/jobs/python_custom_installers/dev/alacritty.py +3 -3
  42. machineconfig/jobs/python_custom_installers/dev/brave.py +3 -3
  43. machineconfig/jobs/python_custom_installers/dev/bypass_paywall.py +5 -5
  44. machineconfig/jobs/python_custom_installers/dev/code.py +3 -3
  45. machineconfig/jobs/python_custom_installers/dev/cursor.py +9 -9
  46. machineconfig/jobs/python_custom_installers/dev/docker_desktop.py +4 -4
  47. machineconfig/jobs/python_custom_installers/dev/espanso.py +4 -4
  48. machineconfig/jobs/python_custom_installers/dev/goes.py +4 -4
  49. machineconfig/jobs/python_custom_installers/dev/lvim.py +4 -4
  50. machineconfig/jobs/python_custom_installers/dev/nerdfont.py +3 -3
  51. machineconfig/jobs/python_custom_installers/dev/redis.py +3 -3
  52. machineconfig/jobs/python_custom_installers/dev/wezterm.py +3 -3
  53. machineconfig/jobs/python_custom_installers/dev/winget.py +27 -27
  54. machineconfig/jobs/python_custom_installers/docker.py +3 -3
  55. machineconfig/jobs/python_custom_installers/gh.py +7 -7
  56. machineconfig/jobs/python_custom_installers/hx.py +1 -1
  57. machineconfig/jobs/python_custom_installers/warp-cli.py +3 -3
  58. machineconfig/jobs/python_generic_installers/config.json +412 -389
  59. machineconfig/jobs/python_windows_installers/dev/config.json +1 -1
  60. machineconfig/logger.py +50 -0
  61. machineconfig/profile/__pycache__/__init__.cpython-311.pyc +0 -0
  62. machineconfig/profile/__pycache__/create.cpython-311.pyc +0 -0
  63. machineconfig/profile/__pycache__/shell.cpython-311.pyc +0 -0
  64. machineconfig/profile/create.py +23 -16
  65. machineconfig/profile/create_hardlinks.py +8 -8
  66. machineconfig/profile/shell.py +41 -37
  67. machineconfig/scripts/__pycache__/__init__.cpython-311.pyc +0 -0
  68. machineconfig/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
  69. machineconfig/scripts/linux/devops +2 -2
  70. machineconfig/scripts/linux/fire +1 -0
  71. machineconfig/scripts/linux/fire_agents +0 -1
  72. machineconfig/scripts/linux/mcinit +27 -0
  73. machineconfig/scripts/python/__pycache__/__init__.cpython-311.pyc +0 -0
  74. machineconfig/scripts/python/__pycache__/__init__.cpython-313.pyc +0 -0
  75. machineconfig/scripts/python/__pycache__/croshell.cpython-311.pyc +0 -0
  76. machineconfig/scripts/python/__pycache__/devops.cpython-311.pyc +0 -0
  77. machineconfig/scripts/python/__pycache__/devops.cpython-313.pyc +0 -0
  78. machineconfig/scripts/python/__pycache__/devops_update_repos.cpython-311.pyc +0 -0
  79. machineconfig/scripts/python/__pycache__/devops_update_repos.cpython-313.pyc +0 -0
  80. machineconfig/scripts/python/__pycache__/fire_agents.cpython-311.pyc +0 -0
  81. machineconfig/scripts/python/__pycache__/fire_jobs.cpython-311.pyc +0 -0
  82. machineconfig/scripts/python/__pycache__/repos.cpython-311.pyc +0 -0
  83. machineconfig/scripts/python/ai/__pycache__/init.cpython-311.pyc +0 -0
  84. machineconfig/scripts/python/ai/__pycache__/mcinit.cpython-311.pyc +0 -0
  85. machineconfig/scripts/python/ai/chatmodes/Thinking-Beast-Mode.chatmode.md +337 -0
  86. machineconfig/scripts/python/ai/chatmodes/Ultimate-Transparent-Thinking-Beast-Mode.chatmode.md +644 -0
  87. machineconfig/scripts/python/ai/chatmodes/deepResearch.chatmode.md +81 -0
  88. machineconfig/scripts/python/ai/configs/.gemini/settings.json +81 -0
  89. machineconfig/scripts/python/ai/instructions/python/dev.instructions.md +45 -0
  90. machineconfig/scripts/python/ai/mcinit.py +103 -0
  91. machineconfig/scripts/python/ai/prompts/allLintersAndTypeCheckers.prompt.md +5 -0
  92. machineconfig/scripts/python/ai/prompts/research-report-skeleton.prompt.md +38 -0
  93. machineconfig/scripts/python/ai/scripts/lint_and_type_check.sh +47 -0
  94. machineconfig/scripts/python/archive/tmate_conn.py +5 -5
  95. machineconfig/scripts/python/archive/tmate_start.py +3 -3
  96. machineconfig/scripts/python/choose_wezterm_theme.py +2 -2
  97. machineconfig/scripts/python/cloud_copy.py +19 -18
  98. machineconfig/scripts/python/cloud_mount.py +9 -7
  99. machineconfig/scripts/python/cloud_repo_sync.py +11 -11
  100. machineconfig/scripts/python/cloud_sync.py +1 -1
  101. machineconfig/scripts/python/croshell.py +14 -14
  102. machineconfig/scripts/python/devops.py +6 -6
  103. machineconfig/scripts/python/devops_add_identity.py +8 -6
  104. machineconfig/scripts/python/devops_add_ssh_key.py +18 -18
  105. machineconfig/scripts/python/devops_backup_retrieve.py +13 -13
  106. machineconfig/scripts/python/devops_devapps_install.py +3 -3
  107. machineconfig/scripts/python/devops_update_repos.py +1 -1
  108. machineconfig/scripts/python/dotfile.py +2 -2
  109. machineconfig/scripts/python/fire_agents.py +183 -41
  110. machineconfig/scripts/python/fire_jobs.py +17 -11
  111. machineconfig/scripts/python/ftpx.py +2 -2
  112. machineconfig/scripts/python/gh_models.py +94 -94
  113. machineconfig/scripts/python/helpers/__pycache__/__init__.cpython-311.pyc +0 -0
  114. machineconfig/scripts/python/helpers/__pycache__/cloud_helpers.cpython-311.pyc +0 -0
  115. machineconfig/scripts/python/helpers/__pycache__/helpers2.cpython-311.pyc +0 -0
  116. machineconfig/scripts/python/helpers/__pycache__/helpers4.cpython-311.pyc +0 -0
  117. machineconfig/scripts/python/helpers/cloud_helpers.py +3 -3
  118. machineconfig/scripts/python/helpers/helpers2.py +1 -1
  119. machineconfig/scripts/python/helpers/helpers4.py +8 -6
  120. machineconfig/scripts/python/helpers/helpers5.py +7 -7
  121. machineconfig/scripts/python/helpers/repo_sync_helpers.py +1 -1
  122. machineconfig/scripts/python/mount_nfs.py +3 -2
  123. machineconfig/scripts/python/mount_nw_drive.py +4 -4
  124. machineconfig/scripts/python/mount_ssh.py +3 -2
  125. machineconfig/scripts/python/repos.py +8 -8
  126. machineconfig/scripts/python/scheduler.py +1 -1
  127. machineconfig/scripts/python/start_slidev.py +8 -7
  128. machineconfig/scripts/python/start_terminals.py +1 -1
  129. machineconfig/scripts/python/viewer.py +40 -40
  130. machineconfig/scripts/python/wifi_conn.py +65 -66
  131. machineconfig/scripts/python/wsl_windows_transfer.py +1 -1
  132. machineconfig/scripts/windows/mcinit.ps1 +4 -0
  133. machineconfig/settings/linters/.ruff.toml +2 -2
  134. machineconfig/settings/shells/ipy/profiles/default/startup/playext.py +71 -71
  135. machineconfig/settings/shells/wt/settings.json +8 -8
  136. machineconfig/setup_linux/web_shortcuts/tmp.sh +2 -0
  137. machineconfig/setup_windows/wt_and_pwsh/set_pwsh_theme.py +10 -7
  138. machineconfig/setup_windows/wt_and_pwsh/set_wt_settings.py +9 -7
  139. machineconfig/utils/ai/browser_user_wrapper.py +5 -5
  140. machineconfig/utils/ai/generate_file_checklist.py +11 -12
  141. machineconfig/utils/ai/url2md.py +1 -1
  142. machineconfig/utils/cloud/onedrive/setup_oauth.py +4 -4
  143. machineconfig/utils/cloud/onedrive/transaction.py +129 -129
  144. machineconfig/utils/code.py +13 -6
  145. machineconfig/utils/installer.py +51 -53
  146. machineconfig/utils/installer_utils/installer_abc.py +21 -10
  147. machineconfig/utils/installer_utils/installer_class.py +42 -16
  148. machineconfig/utils/io_save.py +3 -15
  149. machineconfig/utils/options.py +10 -3
  150. machineconfig/utils/path.py +5 -0
  151. machineconfig/utils/path_reduced.py +201 -149
  152. machineconfig/utils/procs.py +23 -23
  153. machineconfig/utils/scheduling.py +11 -12
  154. machineconfig/utils/ssh.py +270 -0
  155. machineconfig/utils/terminal.py +180 -0
  156. machineconfig/utils/utils.py +1 -2
  157. machineconfig/utils/utils2.py +43 -0
  158. machineconfig/utils/utils5.py +163 -34
  159. machineconfig/utils/ve.py +2 -2
  160. {machineconfig-1.96.dist-info → machineconfig-2.0.dist-info}/METADATA +13 -8
  161. {machineconfig-1.96.dist-info → machineconfig-2.0.dist-info}/RECORD +163 -144
  162. machineconfig/cluster/self_ssh.py +0 -57
  163. {machineconfig-1.96.dist-info → machineconfig-2.0.dist-info}/WHEEL +0 -0
  164. {machineconfig-1.96.dist-info → machineconfig-2.0.dist-info}/top_level.txt +0 -0
@@ -15,12 +15,12 @@ Requirements:
15
15
  pip install requests
16
16
 
17
17
  Setup Options:
18
-
18
+
19
19
  Option 1: Direct OAuth2 Setup (Recommended)
20
20
  1. Run setup_oauth_authentication() for first-time setup
21
21
  2. Follow the interactive prompts to authorize
22
22
  3. Tokens will be automatically saved and refreshed
23
-
23
+
24
24
  Option 2: Using existing rclone token
25
25
  1. Update the RCLONE_TOKEN with your rclone token
26
26
  2. Set DRIVE_ID from your rclone config
@@ -63,10 +63,10 @@ _cached_config = None
63
63
  def get_config(section: str = "odp") -> dict[str, Any]:
64
64
  """
65
65
  Get OneDrive configuration from rclone config.
66
-
66
+
67
67
  Args:
68
68
  section: The rclone config section name (default: "odp")
69
-
69
+
70
70
  Returns:
71
71
  Dictionary containing token, drive_id, and drive_type
72
72
  """
@@ -75,20 +75,20 @@ def get_config(section: str = "odp") -> dict[str, Any]:
75
75
  rclone_config = get_rclone_token(section)
76
76
  if not rclone_config:
77
77
  raise Exception(f"Could not find rclone config section '{section}'. Please set up rclone first.")
78
-
78
+
79
79
  # Parse the token from rclone config
80
80
  token_str = rclone_config.get("token", "{}")
81
81
  try:
82
82
  token_data = json.loads(token_str)
83
83
  except json.JSONDecodeError:
84
84
  raise Exception(f"Invalid token format in rclone config section '{section}'")
85
-
85
+
86
86
  _cached_config = {
87
87
  "token": token_data,
88
88
  "drive_id": rclone_config.get("drive_id"),
89
89
  "drive_type": rclone_config.get("drive_type", "personal")
90
90
  }
91
-
91
+
92
92
  return _cached_config
93
93
 
94
94
  def get_token() -> dict[str, Any]:
@@ -121,7 +121,7 @@ OAUTH_TOKEN_ENDPOINT = "https://login.microsoftonline.com/common/oauth2/v2.0/tok
121
121
  def is_token_valid() -> bool:
122
122
  """
123
123
  Check if the current rclone token is still valid.
124
-
124
+
125
125
  Returns:
126
126
  True if token is valid, False otherwise
127
127
  """
@@ -131,16 +131,16 @@ def is_token_valid() -> bool:
131
131
  expiry_str = token.get("expiry")
132
132
  if not expiry_str:
133
133
  return False
134
-
134
+
135
135
  # Remove timezone info for parsing (rclone format includes timezone)
136
136
  if '+' in expiry_str:
137
137
  expiry_str = expiry_str.split('+')[0]
138
138
  elif 'Z' in expiry_str:
139
139
  expiry_str = expiry_str.replace('Z', '')
140
-
140
+
141
141
  expiry_time = datetime.fromisoformat(expiry_str)
142
142
  current_time = datetime.now()
143
-
143
+
144
144
  # Add some buffer time (5 minutes)
145
145
  return expiry_time > current_time + timedelta(minutes=5)
146
146
  except Exception as e:
@@ -151,16 +151,16 @@ def is_token_valid() -> bool:
151
151
  def get_access_token() -> Optional[str]:
152
152
  """
153
153
  Get access token, automatically refreshing if expired.
154
-
154
+
155
155
  Returns:
156
156
  Access token string or None if token cannot be obtained/refreshed
157
157
  """
158
158
  # First try to load token from file if it exists
159
159
  load_token_from_file()
160
-
160
+
161
161
  if not is_token_valid():
162
162
  print("🔄 Access token has expired, attempting to refresh...")
163
-
163
+
164
164
  # Try to refresh the token
165
165
  refreshed_token = refresh_access_token()
166
166
  if refreshed_token:
@@ -171,7 +171,7 @@ def get_access_token() -> Optional[str]:
171
171
  print("1. Run setup_oauth_authentication() to set up OAuth")
172
172
  print("2. Update your rclone token by running: rclone config reconnect odp")
173
173
  return None
174
-
174
+
175
175
  token = get_token()
176
176
  return token.get("access_token")
177
177
 
@@ -179,72 +179,72 @@ def get_access_token() -> Optional[str]:
179
179
  def make_graph_request(method: str, endpoint: str, **kwargs: Any) -> requests.Response:
180
180
  """
181
181
  Make authenticated request to Microsoft Graph API.
182
-
182
+
183
183
  Args:
184
184
  method: HTTP method (GET, POST, PUT, etc.)
185
185
  endpoint: API endpoint (without base URL)
186
186
  **kwargs: Additional arguments for requests
187
-
187
+
188
188
  Returns:
189
189
  Response object
190
-
190
+
191
191
  Raises:
192
192
  Exception: If authentication fails or request fails
193
193
  """
194
194
  token = get_access_token()
195
195
  if not token:
196
196
  raise Exception("Failed to get valid access token")
197
-
197
+
198
198
  headers = kwargs.get('headers', {})
199
199
  headers['Authorization'] = f'Bearer {token}'
200
200
  kwargs['headers'] = headers
201
-
201
+
202
202
  url = f"{GRAPH_API_BASE}/{endpoint.lstrip('/')}"
203
203
  response = requests.request(method, url, **kwargs)
204
-
204
+
205
205
  return response
206
206
 
207
207
 
208
208
  def push_to_onedrive(local_path: str, remote_path: str) -> bool:
209
209
  """
210
210
  Push a file from local system to OneDrive.
211
-
211
+
212
212
  Args:
213
213
  local_path: Path to the local file
214
214
  remote_path: Path where the file should be stored in OneDrive
215
215
  (e.g., "/Documents/myfile.txt")
216
-
216
+
217
217
  Returns:
218
218
  True if successful, False otherwise
219
219
  """
220
220
  local_file = Path(local_path)
221
-
221
+
222
222
  if not local_file.exists():
223
223
  print(f"Local file does not exist: {local_path}")
224
224
  return False
225
-
225
+
226
226
  if not local_file.is_file():
227
227
  print(f"Path is not a file: {local_path}")
228
228
  return False
229
-
229
+
230
230
  # Ensure remote path starts with /
231
231
  if not remote_path.startswith('/'):
232
232
  remote_path = '/' + remote_path
233
-
233
+
234
234
  # Create parent directories if they don't exist
235
235
  remote_dir = os.path.dirname(remote_path)
236
236
  if remote_dir and remote_dir != '/':
237
237
  create_remote_directory(remote_dir)
238
-
238
+
239
239
  try:
240
240
  file_size = local_file.stat().st_size
241
-
241
+
242
242
  # For small files (< 4MB), use simple upload
243
243
  if file_size < 4 * 1024 * 1024:
244
244
  return simple_upload(local_file, remote_path)
245
245
  else:
246
246
  return resumable_upload(local_file, remote_path)
247
-
247
+
248
248
  except Exception as e:
249
249
  print(f"Error uploading file: {e}")
250
250
  return False
@@ -255,21 +255,21 @@ def simple_upload(local_file: Path, remote_path: str) -> bool:
255
255
  try:
256
256
  with open(local_file, 'rb') as f:
257
257
  file_content = f.read()
258
-
258
+
259
259
  # URL encode the remote path and use specific drive
260
260
  encoded_path = quote(remote_path, safe='/')
261
261
  drive_id = get_drive_id()
262
262
  endpoint = f"drives/{drive_id}/root:{encoded_path}:/content"
263
-
263
+
264
264
  response = make_graph_request('PUT', endpoint, data=file_content)
265
-
265
+
266
266
  if response.status_code in [200, 201]:
267
267
  print(f"Successfully uploaded: {local_file} -> {remote_path}")
268
268
  return True
269
269
  else:
270
270
  print(f"Upload failed: {response.status_code} - {response.text}")
271
271
  return False
272
-
272
+
273
273
  except Exception as e:
274
274
  print(f"Simple upload error: {e}")
275
275
  return False
@@ -282,41 +282,41 @@ def resumable_upload(local_file: Path, remote_path: str) -> bool:
282
282
  encoded_path = quote(remote_path, safe='/')
283
283
  drive_id = get_drive_id()
284
284
  endpoint = f"drives/{drive_id}/root:{encoded_path}:/createUploadSession"
285
-
285
+
286
286
  item_data = {
287
287
  "item": {
288
288
  "@microsoft.graph.conflictBehavior": "replace",
289
289
  "name": local_file.name
290
290
  }
291
291
  }
292
-
292
+
293
293
  response = make_graph_request('POST', endpoint, json=item_data)
294
-
294
+
295
295
  if response.status_code != 200:
296
296
  print(f"Failed to create upload session: {response.status_code} - {response.text}")
297
297
  return False
298
-
298
+
299
299
  upload_url = response.json()['uploadUrl']
300
300
  file_size = local_file.stat().st_size
301
301
  chunk_size = 320 * 1024 # 320KB chunks
302
-
302
+
303
303
  with open(local_file, 'rb') as f:
304
304
  bytes_uploaded = 0
305
-
305
+
306
306
  while bytes_uploaded < file_size:
307
307
  chunk_data = f.read(chunk_size)
308
308
  if not chunk_data:
309
309
  break
310
-
310
+
311
311
  chunk_end = min(bytes_uploaded + len(chunk_data) - 1, file_size - 1)
312
-
312
+
313
313
  headers = {
314
314
  'Content-Range': f'bytes {bytes_uploaded}-{chunk_end}/{file_size}',
315
315
  'Content-Length': str(len(chunk_data))
316
316
  }
317
-
317
+
318
318
  chunk_response = requests.put(upload_url, data=chunk_data, headers=headers)
319
-
319
+
320
320
  if chunk_response.status_code in [202, 200, 201]:
321
321
  bytes_uploaded += len(chunk_data)
322
322
  progress = (bytes_uploaded / file_size) * 100
@@ -324,10 +324,10 @@ def resumable_upload(local_file: Path, remote_path: str) -> bool:
324
324
  else:
325
325
  print(f"Chunk upload failed: {chunk_response.status_code} - {chunk_response.text}")
326
326
  return False
327
-
327
+
328
328
  print(f"Successfully uploaded: {local_file} -> {remote_path}")
329
329
  return True
330
-
330
+
331
331
  except Exception as e:
332
332
  print(f"Resumable upload error: {e}")
333
333
  return False
@@ -336,70 +336,70 @@ def resumable_upload(local_file: Path, remote_path: str) -> bool:
336
336
  def pull_from_onedrive(remote_path: str, local_path: str) -> bool:
337
337
  """
338
338
  Pull a file from OneDrive to local system.
339
-
339
+
340
340
  Args:
341
341
  remote_path: Path to the file in OneDrive (e.g., "/Documents/myfile.txt")
342
342
  local_path: Path where the file should be saved locally
343
-
343
+
344
344
  Returns:
345
345
  True if successful, False otherwise
346
346
  """
347
347
  # Ensure remote path starts with /
348
348
  if not remote_path.startswith('/'):
349
349
  remote_path = '/' + remote_path
350
-
350
+
351
351
  try:
352
352
  # Get file metadata and download URL using specific drive
353
353
  encoded_path = quote(remote_path, safe='/')
354
354
  drive_id = get_drive_id()
355
355
  endpoint = f"drives/{drive_id}/root:{encoded_path}"
356
-
356
+
357
357
  response = make_graph_request('GET', endpoint)
358
-
358
+
359
359
  if response.status_code == 404:
360
360
  print(f"File not found in OneDrive: {remote_path}")
361
361
  return False
362
362
  elif response.status_code != 200:
363
363
  print(f"Failed to get file info: {response.status_code} - {response.text}")
364
364
  return False
365
-
365
+
366
366
  file_info = response.json()
367
-
367
+
368
368
  # Check if it's a file (not a folder)
369
369
  if 'folder' in file_info:
370
370
  print(f"Path is a folder, not a file: {remote_path}")
371
371
  return False
372
-
372
+
373
373
  # Get download URL
374
374
  download_url = file_info.get('@microsoft.graph.downloadUrl')
375
375
  if not download_url:
376
376
  print("No download URL available")
377
377
  return False
378
-
378
+
379
379
  # Create local directory if it doesn't exist
380
380
  local_file = Path(local_path)
381
381
  local_file.parent.mkdir(parents=True, exist_ok=True)
382
-
382
+
383
383
  # Download the file
384
384
  download_response = requests.get(download_url, stream=True)
385
385
  download_response.raise_for_status()
386
-
386
+
387
387
  file_size = int(file_info.get('size', 0))
388
388
  bytes_downloaded = 0
389
-
389
+
390
390
  with open(local_file, 'wb') as f:
391
391
  for chunk in download_response.iter_content(chunk_size=8192):
392
392
  if chunk:
393
393
  f.write(chunk)
394
394
  bytes_downloaded += len(chunk)
395
-
395
+
396
396
  if file_size > 0:
397
397
  progress = (bytes_downloaded / file_size) * 100
398
398
  print(f"Download progress: {progress:.1f}%")
399
-
399
+
400
400
  print(f"Successfully downloaded: {remote_path} -> {local_path}")
401
401
  return True
402
-
402
+
403
403
  except Exception as e:
404
404
  print(f"Error downloading file: {e}")
405
405
  return False
@@ -408,64 +408,64 @@ def pull_from_onedrive(remote_path: str, local_path: str) -> bool:
408
408
  def create_remote_directory(remote_path: str) -> bool:
409
409
  """
410
410
  Create a directory in OneDrive if it doesn't exist.
411
-
411
+
412
412
  Args:
413
413
  remote_path: Path to the directory in OneDrive
414
-
414
+
415
415
  Returns:
416
416
  True if successful or already exists, False otherwise
417
417
  """
418
418
  if not remote_path or remote_path == '/':
419
419
  return True
420
-
420
+
421
421
  # Ensure remote path starts with /
422
422
  if not remote_path.startswith('/'):
423
423
  remote_path = '/' + remote_path
424
-
424
+
425
425
  try:
426
426
  # Check if directory already exists using specific drive
427
427
  encoded_path = quote(remote_path, safe='/')
428
428
  drive_id = get_drive_id()
429
429
  endpoint = f"drives/{drive_id}/root:{encoded_path}"
430
-
430
+
431
431
  response = make_graph_request('GET', endpoint)
432
-
432
+
433
433
  if response.status_code == 200:
434
434
  # Directory already exists
435
435
  return True
436
436
  elif response.status_code != 404:
437
437
  print(f"Error checking directory: {response.status_code} - {response.text}")
438
438
  return False
439
-
439
+
440
440
  # Create parent directory first
441
441
  parent_dir = os.path.dirname(remote_path)
442
442
  if parent_dir and parent_dir != '/':
443
443
  if not create_remote_directory(parent_dir):
444
444
  return False
445
-
445
+
446
446
  # Create the directory
447
447
  dir_name = os.path.basename(remote_path)
448
448
  parent_encoded = quote(parent_dir if parent_dir else '/', safe='/')
449
-
449
+
450
450
  if parent_dir and parent_dir != '/':
451
451
  endpoint = f"drives/{drive_id}/root:{parent_encoded}:/children"
452
452
  else:
453
453
  endpoint = f"drives/{drive_id}/root/children"
454
-
454
+
455
455
  folder_data = {
456
456
  "name": dir_name,
457
457
  "folder": {},
458
458
  "@microsoft.graph.conflictBehavior": "replace"
459
459
  }
460
-
460
+
461
461
  response = make_graph_request('POST', endpoint, json=folder_data)
462
-
462
+
463
463
  if response.status_code in [200, 201]:
464
464
  return True
465
465
  else:
466
466
  print(f"Failed to create directory: {response.status_code} - {response.text}")
467
467
  return False
468
-
468
+
469
469
  except Exception as e:
470
470
  print(f"Error creating directory: {e}")
471
471
  return False
@@ -474,7 +474,7 @@ def create_remote_directory(remote_path: str) -> bool:
474
474
  def refresh_access_token() -> Optional[dict[str, Any]]:
475
475
  """
476
476
  Refresh the access token using the refresh token.
477
-
477
+
478
478
  Returns:
479
479
  New token dictionary with access_token, refresh_token, and expiry, or None if failed
480
480
  """
@@ -483,9 +483,9 @@ def refresh_access_token() -> Optional[dict[str, Any]]:
483
483
  if not refresh_token:
484
484
  print("ERROR: No refresh token available!")
485
485
  return None
486
-
486
+
487
487
  print("🔄 Refreshing access token...")
488
-
488
+
489
489
  # Prepare the token refresh request
490
490
  data = {
491
491
  'client_id': CLIENT_ID,
@@ -493,25 +493,25 @@ def refresh_access_token() -> Optional[dict[str, Any]]:
493
493
  'refresh_token': refresh_token,
494
494
  'scope': 'https://graph.microsoft.com/Files.ReadWrite.All offline_access'
495
495
  }
496
-
496
+
497
497
  # Add client secret if available (for confidential clients)
498
498
  if CLIENT_SECRET and CLIENT_SECRET != "your_client_secret_here":
499
499
  data['client_secret'] = CLIENT_SECRET
500
-
500
+
501
501
  headers = {
502
502
  'Content-Type': 'application/x-www-form-urlencoded'
503
503
  }
504
-
504
+
505
505
  try:
506
506
  response = requests.post(OAUTH_TOKEN_ENDPOINT, data=data, headers=headers)
507
-
507
+
508
508
  if response.status_code == 200:
509
509
  token_data = response.json()
510
-
510
+
511
511
  # Calculate expiry time (tokens typically last 1 hour)
512
512
  expires_in = token_data.get('expires_in', 3600) # Default to 1 hour
513
513
  expiry_time = datetime.now() + timedelta(seconds=expires_in)
514
-
514
+
515
515
  # Update the cached token configuration
516
516
  new_token = {
517
517
  'access_token': token_data['access_token'],
@@ -519,27 +519,27 @@ def refresh_access_token() -> Optional[dict[str, Any]]:
519
519
  'refresh_token': token_data.get('refresh_token', refresh_token), # Use new or keep old
520
520
  'expiry': expiry_time.isoformat()
521
521
  }
522
-
522
+
523
523
  # Update the cached config
524
524
  global _cached_config
525
525
  if _cached_config is not None:
526
526
  _cached_config["token"] = new_token
527
527
  else:
528
528
  clear_config_cache() # Force reload on next access
529
-
529
+
530
530
  print("✅ Access token refreshed successfully!")
531
531
  print(f"🕒 New token expires at: {expiry_time}")
532
-
532
+
533
533
  # Optionally save the new token to a file for persistence
534
534
  save_token_to_file(new_token)
535
-
535
+
536
536
  return new_token
537
-
537
+
538
538
  else:
539
539
  print(f"❌ Token refresh failed: {response.status_code}")
540
540
  print(f"Response: {response.text}")
541
541
  return None
542
-
542
+
543
543
  except Exception as e:
544
544
  print(f"❌ Error refreshing token: {e}")
545
545
  return None
@@ -548,31 +548,31 @@ def refresh_access_token() -> Optional[dict[str, Any]]:
548
548
  def save_token_to_file(token_data: dict[str, Any], file_path: Optional[str] = None) -> bool:
549
549
  """
550
550
  Save token data to a file for persistence.
551
-
551
+
552
552
  Args:
553
553
  token_data: Token dictionary to save
554
554
  file_path: Optional path to save the token file
555
-
555
+
556
556
  Returns:
557
557
  True if successful, False otherwise
558
558
  """
559
559
  if not file_path:
560
560
  # Default to a hidden file in user's home directory
561
561
  file_path = os.path.expanduser("~/.onedrive_token.json")
562
-
562
+
563
563
  try:
564
564
  # Create directory if it doesn't exist
565
565
  os.makedirs(os.path.dirname(file_path), exist_ok=True)
566
-
566
+
567
567
  with open(file_path, 'w') as f:
568
568
  json.dump(token_data, f, indent=2)
569
-
569
+
570
570
  # Set restrictive permissions (readable only by owner)
571
571
  os.chmod(file_path, 0o600)
572
-
572
+
573
573
  print(f"💾 Token saved to: {file_path}")
574
574
  return True
575
-
575
+
576
576
  except Exception as e:
577
577
  print(f"❌ Error saving token: {e}")
578
578
  return False
@@ -581,34 +581,34 @@ def save_token_to_file(token_data: dict[str, Any], file_path: Optional[str] = No
581
581
  def load_token_from_file(file_path: Optional[str] = None) -> Optional[dict[str, Any]]:
582
582
  """
583
583
  Load token data from a file.
584
-
584
+
585
585
  Args:
586
586
  file_path: Optional path to load the token file from
587
-
587
+
588
588
  Returns:
589
589
  Token dictionary or None if failed
590
590
  """
591
591
  if not file_path:
592
592
  file_path = os.path.expanduser("~/.onedrive_token.json")
593
-
593
+
594
594
  try:
595
595
  if os.path.exists(file_path):
596
596
  with open(file_path, 'r') as f:
597
597
  token_data = json.load(f)
598
-
598
+
599
599
  # Update the cached config token
600
600
  global _cached_config
601
601
  if _cached_config is not None:
602
602
  _cached_config["token"] = token_data
603
603
  else:
604
604
  clear_config_cache() # Force reload on next access
605
-
605
+
606
606
  print(f"📂 Token loaded from: {file_path}")
607
607
  return token_data
608
608
  else:
609
609
  print(f"ℹ️ No saved token file found at: {file_path}")
610
610
  return None
611
-
611
+
612
612
  except Exception as e:
613
613
  print(f"❌ Error loading token: {e}")
614
614
  return None
@@ -618,12 +618,12 @@ def get_authorization_url() -> str:
618
618
  """
619
619
  Generate the authorization URL for initial OAuth setup.
620
620
  This is needed only for the first-time setup to get the initial tokens.
621
-
621
+
622
622
  Returns:
623
623
  Authorization URL string
624
624
  """
625
625
  from urllib.parse import urlencode
626
-
626
+
627
627
  params = {
628
628
  'client_id': CLIENT_ID,
629
629
  'response_type': 'code',
@@ -632,7 +632,7 @@ def get_authorization_url() -> str:
632
632
  'scope': 'https://graph.microsoft.com/Files.ReadWrite.All offline_access',
633
633
  'state': 'onedrive_auth'
634
634
  }
635
-
635
+
636
636
  auth_url = f"https://login.microsoftonline.com/common/oauth2/v2.0/authorize?{urlencode(params)}"
637
637
  return auth_url
638
638
 
@@ -641,10 +641,10 @@ def exchange_authorization_code(authorization_code: str) -> Optional[dict[str, A
641
641
  """
642
642
  Exchange authorization code for initial tokens.
643
643
  This is used during the first-time OAuth setup.
644
-
644
+
645
645
  Args:
646
646
  authorization_code: The authorization code received from the callback
647
-
647
+
648
648
  Returns:
649
649
  Token dictionary or None if failed
650
650
  """
@@ -655,32 +655,32 @@ def exchange_authorization_code(authorization_code: str) -> Optional[dict[str, A
655
655
  'redirect_uri': REDIRECT_URI,
656
656
  'scope': 'https://graph.microsoft.com/Files.ReadWrite.All offline_access'
657
657
  }
658
-
658
+
659
659
  # Add client secret if available
660
660
  if CLIENT_SECRET and CLIENT_SECRET != "your_client_secret_here":
661
661
  data['client_secret'] = CLIENT_SECRET
662
-
662
+
663
663
  headers = {
664
664
  'Content-Type': 'application/x-www-form-urlencoded'
665
665
  }
666
-
666
+
667
667
  try:
668
668
  response = requests.post(OAUTH_TOKEN_ENDPOINT, data=data, headers=headers)
669
-
669
+
670
670
  if response.status_code == 200:
671
671
  token_data = response.json()
672
-
672
+
673
673
  # Calculate expiry time
674
674
  expires_in = token_data.get('expires_in', 3600)
675
675
  expiry_time = datetime.now() + timedelta(seconds=expires_in)
676
-
676
+
677
677
  new_token = {
678
678
  'access_token': token_data['access_token'],
679
679
  'token_type': token_data.get('token_type', 'Bearer'),
680
680
  'refresh_token': token_data['refresh_token'],
681
681
  'expiry': expiry_time.isoformat()
682
682
  }
683
-
683
+
684
684
  # Update cached config and save
685
685
  global _cached_config
686
686
  if _cached_config is not None:
@@ -688,15 +688,15 @@ def exchange_authorization_code(authorization_code: str) -> Optional[dict[str, A
688
688
  else:
689
689
  clear_config_cache() # Force reload on next access
690
690
  save_token_to_file(new_token)
691
-
691
+
692
692
  print("✅ Initial tokens obtained successfully!")
693
693
  return new_token
694
-
694
+
695
695
  else:
696
696
  print(f"❌ Token exchange failed: {response.status_code}")
697
697
  print(f"Response: {response.text}")
698
698
  return None
699
-
699
+
700
700
  except Exception as e:
701
701
  print(f"❌ Error exchanging authorization code: {e}")
702
702
  return None
@@ -709,7 +709,7 @@ def setup_oauth_authentication():
709
709
  """
710
710
  print("🔧 Setting up OneDrive OAuth Authentication")
711
711
  print("=" * 50)
712
-
712
+
713
713
  if CLIENT_ID == "your_client_id_here":
714
714
  print("❌ You need to set up Azure App Registration first!")
715
715
  print("\n📋 Setup Instructions:")
@@ -725,21 +725,21 @@ def setup_oauth_authentication():
725
725
  print(" export ONEDRIVE_CLIENT_ID='your_client_id'")
726
726
  print(" export ONEDRIVE_REDIRECT_URI='http://localhost:8080/callback'")
727
727
  return
728
-
728
+
729
729
  print(f"Using Client ID: {CLIENT_ID}")
730
730
  print(f"Redirect URI: {REDIRECT_URI}")
731
-
731
+
732
732
  # Generate authorization URL
733
733
  auth_url = get_authorization_url()
734
734
  print("\n🌐 Please visit this URL to authorize the application:")
735
735
  print(f"{auth_url}")
736
-
736
+
737
737
  print("\n📋 After authorization, you'll be redirected to:")
738
738
  print(f"{REDIRECT_URI}?code=AUTHORIZATION_CODE&state=onedrive_auth")
739
739
  print("\n🔑 Copy the 'code' parameter from the URL and paste it below:")
740
-
740
+
741
741
  auth_code = input("Authorization Code: ").strip()
742
-
742
+
743
743
  if auth_code:
744
744
  token_data = exchange_authorization_code(auth_code)
745
745
  if token_data:
@@ -755,18 +755,18 @@ def setup_oauth_authentication():
755
755
  if __name__ == "__main__":
756
756
  # Try to load existing token from file
757
757
  load_token_from_file()
758
-
758
+
759
759
  print("OneDrive transaction functions loaded.")
760
760
  try:
761
761
  config = get_config()
762
762
  print(f"Drive ID: {get_drive_id()}")
763
763
  print(f"Drive Type: {get_drive_type()}")
764
-
764
+
765
765
  if is_token_valid():
766
766
  print("✅ Token is valid and ready to use")
767
767
  else:
768
768
  print("⚠️ Token has expired or is invalid")
769
-
769
+
770
770
  # Try to refresh automatically
771
771
  if refresh_access_token():
772
772
  print("✅ Token refreshed successfully")
@@ -778,7 +778,7 @@ if __name__ == "__main__":
778
778
  except Exception as e:
779
779
  print(f"❌ Error loading rclone config: {e}")
780
780
  print("Please ensure rclone is configured with an 'odp' section")
781
-
781
+
782
782
  print("\n📚 Available Functions:")
783
783
  print("• push_to_onedrive(local_path, remote_path)")
784
784
  print("• pull_from_onedrive(remote_path, local_path)")
@@ -786,11 +786,11 @@ if __name__ == "__main__":
786
786
  print("• setup_oauth_authentication() - First-time OAuth setup")
787
787
  print("• save_token_to_file(token_data) - Save tokens for persistence")
788
788
  print("• load_token_from_file() - Load saved tokens")
789
-
789
+
790
790
  print("\n💡 Example usage:")
791
791
  print("push_to_onedrive('/home/user/document.pdf', '/Documents/document.pdf')")
792
792
  print("pull_from_onedrive('/Documents/document.pdf', '/home/user/downloaded.pdf')")
793
-
793
+
794
794
  # Uncomment to test with a file
795
795
  # push_to_onedrive('/home/alex/Downloads/users.xlsx', '/Documents/users.xlsx')
796
796