machineconfig 1.97__py3-none-any.whl → 2.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.

Potentially problematic release.


This version of machineconfig might be problematic. Click here for more details.

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