supernote 0.13.6__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.
Files changed (138) hide show
  1. supernote/__init__.py +28 -0
  2. supernote/alembic/__init__.py +0 -0
  3. supernote/alembic/env.py +121 -0
  4. supernote/alembic/script.py.mako +28 -0
  5. supernote/alembic/versions/0543a383957b_initial_schema.py +312 -0
  6. supernote/alembic.ini +149 -0
  7. supernote/cli/__init__.py +1 -0
  8. supernote/cli/admin.py +178 -0
  9. supernote/cli/client.py +434 -0
  10. supernote/cli/main.py +61 -0
  11. supernote/cli/notebook.py +313 -0
  12. supernote/cli/server.py +89 -0
  13. supernote/client/__init__.py +28 -0
  14. supernote/client/admin.py +76 -0
  15. supernote/client/api.py +135 -0
  16. supernote/client/api_model.py +231 -0
  17. supernote/client/auth.py +84 -0
  18. supernote/client/client.py +365 -0
  19. supernote/client/device.py +361 -0
  20. supernote/client/exceptions.py +33 -0
  21. supernote/client/extended.py +20 -0
  22. supernote/client/hashing.py +50 -0
  23. supernote/client/login_client.py +154 -0
  24. supernote/client/schedule.py +149 -0
  25. supernote/client/summary.py +156 -0
  26. supernote/client/web.py +213 -0
  27. supernote/models/__init__.py +14 -0
  28. supernote/models/auth.py +380 -0
  29. supernote/models/base.py +266 -0
  30. supernote/models/equipment.py +276 -0
  31. supernote/models/extended.py +124 -0
  32. supernote/models/file_common.py +95 -0
  33. supernote/models/file_device.py +571 -0
  34. supernote/models/file_web.py +602 -0
  35. supernote/models/schedule.py +663 -0
  36. supernote/models/summary.py +830 -0
  37. supernote/models/system.py +431 -0
  38. supernote/models/user.py +638 -0
  39. supernote/notebook/__init__.py +67 -0
  40. supernote/notebook/color.py +93 -0
  41. supernote/notebook/converter.py +560 -0
  42. supernote/notebook/decoder.py +398 -0
  43. supernote/notebook/exceptions.py +43 -0
  44. supernote/notebook/fileformat.py +463 -0
  45. supernote/notebook/manipulator.py +420 -0
  46. supernote/notebook/parser.py +737 -0
  47. supernote/notebook/utils.py +49 -0
  48. supernote/py.typed +0 -0
  49. supernote/server/__init__.py +12 -0
  50. supernote/server/app.py +428 -0
  51. supernote/server/config.py +265 -0
  52. supernote/server/constants.py +26 -0
  53. supernote/server/db/__init__.py +9 -0
  54. supernote/server/db/base.py +8 -0
  55. supernote/server/db/migrations.py +44 -0
  56. supernote/server/db/models/__init__.py +23 -0
  57. supernote/server/db/models/device.py +17 -0
  58. supernote/server/db/models/file.py +107 -0
  59. supernote/server/db/models/kv.py +17 -0
  60. supernote/server/db/models/login_record.py +22 -0
  61. supernote/server/db/models/note_processing.py +100 -0
  62. supernote/server/db/models/schedule.py +83 -0
  63. supernote/server/db/models/summary.py +122 -0
  64. supernote/server/db/models/user.py +30 -0
  65. supernote/server/db/session.py +88 -0
  66. supernote/server/events.py +63 -0
  67. supernote/server/exceptions.py +153 -0
  68. supernote/server/mcp/__init__.py +1 -0
  69. supernote/server/mcp/models.py +109 -0
  70. supernote/server/mcp/server.py +176 -0
  71. supernote/server/resources/__init__.py +0 -0
  72. supernote/server/resources/prompts/__init__.py +0 -0
  73. supernote/server/resources/prompts/ocr/common/context.md +19 -0
  74. supernote/server/resources/prompts/ocr/common/legend.md +11 -0
  75. supernote/server/resources/prompts/ocr/daily/prompt.md +12 -0
  76. supernote/server/resources/prompts/ocr/default/system.md +14 -0
  77. supernote/server/resources/prompts/ocr/monthly/prompt.md +22 -0
  78. supernote/server/resources/prompts/ocr/weekly/prompt.md +16 -0
  79. supernote/server/resources/prompts/summary/common/instruction.md +8 -0
  80. supernote/server/resources/prompts/summary/daily/prompt.md +6 -0
  81. supernote/server/resources/prompts/summary/default/prompt.md +4 -0
  82. supernote/server/resources/prompts/summary/monthly/prompt.md +6 -0
  83. supernote/server/resources/prompts/summary/weekly/prompt.md +6 -0
  84. supernote/server/routes/admin.py +109 -0
  85. supernote/server/routes/auth.py +337 -0
  86. supernote/server/routes/decorators.py +13 -0
  87. supernote/server/routes/extended.py +139 -0
  88. supernote/server/routes/file_device.py +593 -0
  89. supernote/server/routes/file_web.py +597 -0
  90. supernote/server/routes/oss.py +317 -0
  91. supernote/server/routes/schedule.py +223 -0
  92. supernote/server/routes/summary.py +390 -0
  93. supernote/server/routes/system.py +57 -0
  94. supernote/server/services/__init__.py +15 -0
  95. supernote/server/services/blob.py +214 -0
  96. supernote/server/services/coordination.py +171 -0
  97. supernote/server/services/file.py +970 -0
  98. supernote/server/services/gemini.py +66 -0
  99. supernote/server/services/integrity.py +112 -0
  100. supernote/server/services/processor.py +331 -0
  101. supernote/server/services/processor_modules/__init__.py +145 -0
  102. supernote/server/services/processor_modules/gemini_embedding.py +112 -0
  103. supernote/server/services/processor_modules/gemini_ocr.py +149 -0
  104. supernote/server/services/processor_modules/page_hashing.py +220 -0
  105. supernote/server/services/processor_modules/png_conversion.py +107 -0
  106. supernote/server/services/processor_modules/summary.py +302 -0
  107. supernote/server/services/prompt_loader.py +133 -0
  108. supernote/server/services/schedule.py +161 -0
  109. supernote/server/services/search.py +250 -0
  110. supernote/server/services/summary.py +419 -0
  111. supernote/server/services/user.py +480 -0
  112. supernote/server/services/vfs.py +476 -0
  113. supernote/server/static/favicon.ico +0 -0
  114. supernote/server/static/index.html +186 -0
  115. supernote/server/static/js/api/client.js +463 -0
  116. supernote/server/static/js/components/FileCard.js +55 -0
  117. supernote/server/static/js/components/FileViewer.js +137 -0
  118. supernote/server/static/js/components/LoginCard.js +64 -0
  119. supernote/server/static/js/components/MoveModal.js +74 -0
  120. supernote/server/static/js/components/RenameModal.js +39 -0
  121. supernote/server/static/js/components/SummaryPanel.js +101 -0
  122. supernote/server/static/js/components/SystemPanel.js +158 -0
  123. supernote/server/static/js/composables/useFileSystem.js +116 -0
  124. supernote/server/static/js/main.js +241 -0
  125. supernote/server/static/style.css +14 -0
  126. supernote/server/utils/hashing.py +18 -0
  127. supernote/server/utils/note_content.py +76 -0
  128. supernote/server/utils/paths.py +58 -0
  129. supernote/server/utils/rate_limit.py +52 -0
  130. supernote/server/utils/tasks.py +71 -0
  131. supernote/server/utils/unique_id.py +48 -0
  132. supernote/server/utils/url_signer.py +218 -0
  133. supernote-0.13.6.dist-info/METADATA +183 -0
  134. supernote-0.13.6.dist-info/RECORD +138 -0
  135. supernote-0.13.6.dist-info/WHEEL +5 -0
  136. supernote-0.13.6.dist-info/entry_points.txt +3 -0
  137. supernote-0.13.6.dist-info/licenses/LICENSE +201 -0
  138. supernote-0.13.6.dist-info/top_level.txt +1 -0
supernote/cli/admin.py ADDED
@@ -0,0 +1,178 @@
1
+ """Admin CLI commands."""
2
+
3
+ import asyncio
4
+ import getpass
5
+ import hashlib
6
+ import sys
7
+
8
+ from supernote.client.admin import AdminClient
9
+ from supernote.client.exceptions import ApiException
10
+
11
+ from .client import create_session
12
+
13
+
14
+ async def add_user_async(
15
+ url: str, email: str, password: str, display_name: str | None = None
16
+ ):
17
+ """Async implementation of add user."""
18
+ async with create_session(url) as session:
19
+ print(f"Attempting to register user '{email}' on {url}...")
20
+
21
+ admin_client = AdminClient(session.client)
22
+
23
+ # Hash password
24
+ password_md5 = hashlib.md5(password.encode()).hexdigest()
25
+
26
+ # Try Public Registration
27
+ try:
28
+ await admin_client.register(
29
+ email=email,
30
+ password=password_md5,
31
+ username=display_name,
32
+ )
33
+ print("Success! User created (Public Registration).")
34
+ return
35
+ except ApiException:
36
+ # If failed, we don't have detailed status code in simple try/except unless we check exception type
37
+ # But client raises ApiException
38
+ pass
39
+
40
+ # Try Admin Creation API
41
+ print("Public registration failed or disabled. Attempting Admin creation...")
42
+ try:
43
+ await admin_client.admin_create_user(
44
+ email=email,
45
+ password=password_md5,
46
+ username=display_name,
47
+ )
48
+ print("Success! User created (Admin API).")
49
+ except Exception as e:
50
+ print(f"Admin creation failed: {e}")
51
+ sys.exit(1)
52
+
53
+
54
+ async def list_users_async():
55
+ """Async implementation of list users."""
56
+ async with create_session() as session:
57
+ client = session.client
58
+ try:
59
+ users = await client.get_json("/api/admin/users", list)
60
+
61
+ print(f"\nTotal Users: {len(users)}\n")
62
+ print(f"{'Username':<30} {'Email':<30} {'Capacity':<10}")
63
+ print("-" * 75)
64
+ for u in users:
65
+ # users is a list of dicts if we passed list to get_json, or we need a VO
66
+ # client.get_json returns data_cls.from_json(result)
67
+ # But list doesn't have from_json.
68
+ # We should probably use raw get or define a List[UserVO] type if we can.
69
+ # For now let's just use raw request to get json list safely.
70
+ pass
71
+ except Exception:
72
+ pass
73
+
74
+ # Re-doing list fetch with raw request because of typing limitation in client.get_json for list[dict]
75
+ try:
76
+ resp = await client.get("/api/admin/users")
77
+ users = await resp.json()
78
+
79
+ print(f"\nTotal Users: {len(users)}\n")
80
+ print(f"{'Email':<30} {'Name':<20} {'Capacity':<10}")
81
+ print("-" * 65)
82
+ for u in users:
83
+ print(
84
+ f"{u.get('email', 'N/A'):<30} {u.get('userName', 'N/A'):<20} {u.get('totalCapacity', '0'):<10}"
85
+ )
86
+
87
+ except Exception as e:
88
+ print(f"Failed to list users: {e}")
89
+
90
+
91
+ def add_user(args):
92
+ password = args.password
93
+ if not password:
94
+ password = getpass.getpass(f"Password for {args.email}: ")
95
+
96
+ display_name = args.name or args.email.split("@")[0]
97
+ asyncio.run(add_user_async(args.url, args.email, password, display_name))
98
+
99
+
100
+ def list_users(args):
101
+ asyncio.run(list_users_async())
102
+
103
+
104
+ async def reset_password_async(url: str, email: str, password: str):
105
+ """Async implementation of password reset."""
106
+ async with create_session(url) as session:
107
+ print(f"Attempting to reset password for '{email}' on {url}...")
108
+ admin_client = AdminClient(session.client)
109
+
110
+ password_md5 = hashlib.md5(password.encode()).hexdigest()
111
+
112
+ try:
113
+ await admin_client.admin_reset_password(email, password_md5)
114
+ print("Success! Password Reset.")
115
+ except Exception as e:
116
+ print(f"Failed to reset password: {e}")
117
+ sys.exit(1)
118
+
119
+
120
+ def reset_password(args):
121
+ password = args.password
122
+ if not password:
123
+ password = getpass.getpass(f"New password for {args.email}: ")
124
+ confirm = getpass.getpass("Confirm password: ")
125
+ if password != confirm:
126
+ print("Passwords do not match.")
127
+ sys.exit(1)
128
+
129
+ asyncio.run(reset_password_async(args.url, args.email, password))
130
+
131
+
132
+ def add_parser(subparsers):
133
+ # 'admin' parent command
134
+ parser_admin = subparsers.add_parser(
135
+ "admin",
136
+ help="Administrative commands for Supernote Server",
137
+ )
138
+ parser_admin.add_argument("--url", type=str, help="URL of the Supernote Server")
139
+
140
+ admin_subparsers = parser_admin.add_subparsers(dest="admin_command")
141
+
142
+ # --- User Management ---
143
+ parser_user = admin_subparsers.add_parser("user", help="User management commands")
144
+ user_subparsers = parser_user.add_subparsers(dest="user_command")
145
+
146
+ # user list
147
+ parser_user_list = user_subparsers.add_parser(
148
+ "list",
149
+ help="List all users",
150
+ )
151
+ parser_user_list.set_defaults(func=list_users)
152
+
153
+ # user add
154
+ parser_user_add = user_subparsers.add_parser(
155
+ "add",
156
+ help="Add a new user",
157
+ )
158
+ parser_user_add.add_argument(
159
+ "email", type=str, help="Email address for the new user"
160
+ )
161
+ parser_user_add.add_argument(
162
+ "--password", type=str, help="Password (if omitted, prompt interactively)"
163
+ )
164
+ parser_user_add.add_argument(
165
+ "--name", type=str, help="Display name (optional)", default=None
166
+ )
167
+ parser_user_add.set_defaults(func=add_user)
168
+
169
+ # user reset-password
170
+ parser_user_reset = user_subparsers.add_parser(
171
+ "reset-password",
172
+ help="Reset user password",
173
+ )
174
+ parser_user_reset.add_argument("email", type=str, help="Email address of the user")
175
+ parser_user_reset.add_argument(
176
+ "--password", type=str, help="New password (if omitted, prompt interactively)"
177
+ )
178
+ parser_user_reset.set_defaults(func=reset_password)
@@ -0,0 +1,434 @@
1
+ """Client CLI commands."""
2
+
3
+ import asyncio
4
+ import getpass
5
+ import logging
6
+ import os
7
+ import sys
8
+ import traceback
9
+ from contextlib import asynccontextmanager
10
+
11
+ from supernote.client import Supernote
12
+ from supernote.client.auth import ConstantAuth, FileCacheAuth
13
+ from supernote.client.exceptions import SmsVerificationRequired, SupernoteException
14
+
15
+ _LOGGER = logging.getLogger(__name__)
16
+
17
+
18
+ def load_cached_auth(url: str | None = None) -> tuple[FileCacheAuth, str]:
19
+ """Load cached credentials."""
20
+ cache_path = os.path.expanduser("~/.cache/supernote.pkl")
21
+ if not os.path.exists(cache_path) and not url:
22
+ print(f"Error: No cached credentials found at {cache_path}")
23
+ print("Please run 'supernote cloud-login --url <URL>' first.")
24
+ sys.exit(1)
25
+
26
+ auth = FileCacheAuth(cache_path)
27
+ if not url:
28
+ url = auth.get_host()
29
+ if not url:
30
+ print("Error: No server URL found in cached credentials.")
31
+ print("Please run 'supernote cloud-login --url <URL>' first.")
32
+ sys.exit(1)
33
+
34
+ return auth, url
35
+
36
+
37
+ @asynccontextmanager
38
+ async def create_session(url: str | None = None) -> Supernote:
39
+ """Initialize Supernote session with cached credentials."""
40
+ auth, url = load_cached_auth(url)
41
+ async with Supernote.from_auth(auth, host=url) as sn:
42
+ yield sn
43
+
44
+
45
+ def setup_logging(verbose: bool = False) -> None:
46
+ """Setup logging configuration."""
47
+ level = logging.DEBUG if verbose else logging.INFO
48
+ logging.basicConfig(
49
+ level=level,
50
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
51
+ stream=sys.stdout,
52
+ )
53
+ # Enable debug logging for cloud modules and aiohttp
54
+ if verbose:
55
+ logging.getLogger("supernote").setLevel(logging.DEBUG)
56
+ logging.getLogger("aiohttp").setLevel(logging.DEBUG)
57
+
58
+
59
+ async def async_cloud_login(
60
+ email: str, password: str, url: str, verbose: bool = False
61
+ ) -> None:
62
+ """Perform cloud login with detailed debugging output.
63
+
64
+ Args:
65
+ email: User email/account
66
+ password: User password
67
+ url: Server URL (e.g. http://localhost:8080)
68
+ verbose: Enable verbose HTTP logging
69
+ """
70
+ setup_logging(verbose)
71
+
72
+ print("=" * 80)
73
+ print("Supernote Cloud Login Debugging Tool")
74
+ print("=" * 80)
75
+ print(f"Email: {email}")
76
+ print(f"URL: {url}")
77
+ print(
78
+ f"Verbose Mode: {'ENABLED' if verbose else 'DISABLED (use -v or --verbose for detailed logs)'}"
79
+ )
80
+ print("=" * 80)
81
+ print()
82
+
83
+ try:
84
+ # Step 1: Login
85
+ print("Step 1: Starting login flow...")
86
+ print(" - This will get CSRF token")
87
+ print(" - Call token endpoint")
88
+ print(" - Get random code")
89
+ print(" - Encode password")
90
+ print(" - Submit login request")
91
+ print()
92
+
93
+ try:
94
+ sn = await Supernote.login(email, password, host=url)
95
+ except SmsVerificationRequired as err:
96
+ sn = await _handle_sms_verification(email, url, err)
97
+
98
+ async with sn:
99
+ access_token = sn.token
100
+ print("✓ Login successful!")
101
+ print(
102
+ f"Access Token: {access_token[:20]}..."
103
+ if access_token and len(access_token) > 20
104
+ else access_token
105
+ )
106
+ print()
107
+
108
+ # Save token to cache
109
+ cache_path = os.path.expanduser("~/.cache/supernote.pkl")
110
+ print(f"Saving credentials to {cache_path}...")
111
+ auth = FileCacheAuth(cache_path)
112
+ auth.save_credentials(access_token, url)
113
+ print("✓ Credentials saved!")
114
+ print()
115
+
116
+ await _run_login_sanity_tests(sn)
117
+
118
+ except SupernoteException as err:
119
+ print()
120
+ print("=" * 80)
121
+ print(f"✗ Error during login flow: {err}")
122
+ print("=" * 80)
123
+ if verbose:
124
+ traceback.print_exc()
125
+ sys.exit(1)
126
+ except Exception as err:
127
+ print()
128
+ print("=" * 80)
129
+ print(f"✗ Unexpected error: {err}")
130
+ print("=" * 80)
131
+ if verbose:
132
+ traceback.print_exc()
133
+ sys.exit(1)
134
+
135
+
136
+ async def _handle_sms_verification(
137
+ email: str, url: str, err: SmsVerificationRequired
138
+ ) -> Supernote:
139
+ """Handle SMS verification flow."""
140
+ async with Supernote(host=url) as temp_sn:
141
+ print()
142
+ print("!" * 80)
143
+ print("SMS Verification Required")
144
+ print("!" * 80)
145
+ print(f"Message: {err}")
146
+ print("The server has sent an SMS verification code to your phone.")
147
+ print()
148
+
149
+ print("Requesting SMS verification code...")
150
+ await temp_sn.login_client.request_sms_code(email)
151
+ print("SMS code requested successfully.")
152
+ print()
153
+
154
+ code = input("Enter verification code: ").strip()
155
+ print()
156
+ print("Submitting verification code...")
157
+
158
+ access_token = await temp_sn.login_client.sms_login(email, code, err.timestamp)
159
+ # Create authenticated session
160
+ return temp_sn.with_auth(ConstantAuth(access_token))
161
+
162
+
163
+ async def _run_login_sanity_tests(sn: Supernote) -> None:
164
+ """Run basic functionality tests after login."""
165
+ print("Step 3: Testing basic functionality...")
166
+
167
+ # Test 1: Query user
168
+ print(" Test 1: Querying user information...")
169
+ try:
170
+ user_resp = await sn.web.query_user()
171
+ print(" ✓ User query successful!")
172
+ if user_resp.user:
173
+ user = user_resp.user
174
+ print(f" - User Name: {user.user_name}")
175
+ print(f" - Email: {user.email}")
176
+ print(f" - Country Code: {user.country_code}")
177
+ if user.file_server:
178
+ print(f" - File Server: {user.file_server}")
179
+ else:
180
+ print(" - No user information returned")
181
+ except SupernoteException as err:
182
+ print(f" ✗ User query failed: {err}")
183
+ print()
184
+
185
+ # Test 2: Listing files
186
+ print(" Test 2: Listing files in root directory...")
187
+ try:
188
+ list_resp = await sn.device.list_folder("/")
189
+ print(" ✓ File list successful!")
190
+ files = list_resp.entries
191
+ print(f" - Files count: {len(files)}")
192
+
193
+ if files:
194
+ print(" - First few files:")
195
+ for file in files[:5]:
196
+ folder_marker = "📁" if file.tag == "folder" else "📄"
197
+ print(f" {folder_marker} {file.name} (ID: {file.id})")
198
+ else:
199
+ print(" - No files found")
200
+ except SupernoteException as err:
201
+ print(f" ✗ File list failed: {err}")
202
+ print()
203
+
204
+ print("=" * 80)
205
+ print("All tests completed successfully!")
206
+ print("=" * 80)
207
+
208
+
209
+ def subcommand_cloud_login(args) -> None:
210
+ """Handler for cloud-login subcommand."""
211
+ password = args.password
212
+ if not password:
213
+ password = getpass.getpass(f"Password for {args.email}: ")
214
+
215
+ asyncio.run(async_cloud_login(args.email, password, args.url, args.verbose))
216
+
217
+
218
+ async def async_cloud_ls(path: str, verbose: bool = False) -> None:
219
+ """List files in Supernote Cloud using cached credentials."""
220
+ setup_logging(verbose)
221
+
222
+ try:
223
+ async with create_session() as sn:
224
+ print(f"Using server: {sn.client.host}")
225
+
226
+ print(f"Listing files in {path}...")
227
+ list_resp = await sn.device.list_folder(path)
228
+ files = list_resp.entries
229
+
230
+ print(f"Total files: {len(files)}")
231
+ if files:
232
+ for file in files:
233
+ folder_marker = "📁" if file.tag == "folder" else "📄"
234
+ print(f"{folder_marker} {file.name} (ID: {file.id})")
235
+
236
+ except SupernoteException as err:
237
+ print(f"Error: {err}")
238
+ if verbose:
239
+ traceback.print_exc()
240
+ sys.exit(1)
241
+ except Exception as err:
242
+ print(f"Unexpected error: {err}")
243
+ if verbose:
244
+ traceback.print_exc()
245
+ sys.exit(1)
246
+
247
+
248
+ def subcommand_cloud_ls(args) -> None:
249
+ """Handler for cloud-ls subcommand."""
250
+ asyncio.run(async_cloud_ls(args.path, args.verbose))
251
+
252
+
253
+ async def async_cloud_upload(
254
+ local_path: str, remote_path: str, verbose: bool = False
255
+ ) -> None:
256
+ """Upload a file to Supernote Cloud."""
257
+ setup_logging(verbose)
258
+
259
+ if not os.path.exists(local_path):
260
+ print(f"Error: Local file not found: {local_path}")
261
+ sys.exit(1)
262
+
263
+ try:
264
+ with open(local_path, "rb") as f:
265
+ content = f.read()
266
+
267
+ async with create_session() as sn:
268
+ print(f"Uploading {local_path} to {remote_path}...")
269
+ await sn.device.upload_content(remote_path, content)
270
+ print("✓ Upload successful!")
271
+ except SupernoteException as err:
272
+ print(f"Error: {err}")
273
+ sys.exit(1)
274
+
275
+
276
+ def subcommand_cloud_upload(args) -> None:
277
+ """Handler for cloud-upload subcommand."""
278
+ # If remote_path is not provided, use the same filename in the root directory
279
+ remote_path = args.remote_path
280
+ if not remote_path:
281
+ remote_path = "/" + os.path.basename(args.local_path)
282
+ # If remote_path is a directory (ends with /), append the local filename
283
+ elif remote_path.endswith("/"):
284
+ remote_path += os.path.basename(args.local_path)
285
+
286
+ asyncio.run(async_cloud_upload(args.local_path, remote_path, args.verbose))
287
+
288
+
289
+ async def async_cloud_download(
290
+ remote_path: str, local_path: str, verbose: bool = False
291
+ ) -> None:
292
+ """Download a file from Supernote Cloud."""
293
+ setup_logging(verbose)
294
+
295
+ try:
296
+ async with create_session() as sn:
297
+ print(f"Downloading {remote_path} to {local_path}...")
298
+ content = await sn.device.download_content(path=remote_path)
299
+
300
+ with open(local_path, "wb") as f:
301
+ f.write(content)
302
+ print(f"✓ Downloaded to {local_path}")
303
+ except SupernoteException as err:
304
+ print(f"Error: {err}")
305
+ sys.exit(1)
306
+
307
+
308
+ def subcommand_cloud_download(args) -> None:
309
+ """Handler for cloud-download subcommand."""
310
+ local_path = args.local_path
311
+ if not local_path:
312
+ local_path = os.path.basename(args.remote_path)
313
+ elif os.path.isdir(local_path):
314
+ local_path = os.path.join(local_path, os.path.basename(args.remote_path))
315
+
316
+ asyncio.run(async_cloud_download(args.remote_path, local_path, args.verbose))
317
+
318
+
319
+ async def async_cloud_mkdir(path: str, verbose: bool = False) -> None:
320
+ """Create a folder in Supernote Cloud."""
321
+ setup_logging(verbose)
322
+
323
+ try:
324
+ async with create_session() as sn:
325
+ print(f"Creating folder: {path}...")
326
+ await sn.device.create_folder(path, equipment_no="WEB")
327
+ print("✓ Folder created successfully!")
328
+ except SupernoteException as err:
329
+ print(f"Error: {err}")
330
+ sys.exit(1)
331
+
332
+
333
+ def subcommand_cloud_mkdir(args) -> None:
334
+ """Handler for cloud-mkdir subcommand."""
335
+ asyncio.run(async_cloud_mkdir(args.path, args.verbose))
336
+
337
+
338
+ async def async_cloud_rm(path: str, verbose: bool = False) -> None:
339
+ """Remove a file or folder from Supernote Cloud."""
340
+ setup_logging(verbose)
341
+
342
+ try:
343
+ async with create_session() as sn:
344
+ print(f"Removing {path}...")
345
+ await sn.device.delete_by_path(path)
346
+ print("✓ Removed successfully!")
347
+ except SupernoteException as err:
348
+ print(f"Error: {err}")
349
+ sys.exit(1)
350
+
351
+
352
+ def subcommand_cloud_rm(args) -> None:
353
+ """Handler for cloud-rm subcommand."""
354
+ asyncio.run(async_cloud_rm(args.path, args.verbose))
355
+
356
+
357
+ def add_parser(subparsers):
358
+ """Add the cloud subparser to the main subparsers."""
359
+ cloud_parser = subparsers.add_parser("cloud", help="Interact with Supernote Cloud")
360
+ cloud_subparsers = cloud_parser.add_subparsers(
361
+ dest="cloud_command", help="Cloud command"
362
+ )
363
+
364
+ # 'cloud login' subcommand
365
+ parser_login = cloud_subparsers.add_parser(
366
+ "login", help="Authenticate with Supernote Cloud"
367
+ )
368
+ parser_login.add_argument("email", type=str, help="user email/account")
369
+ parser_login.add_argument(
370
+ "--password", type=str, help="user password (prompt if omitted)"
371
+ )
372
+ parser_login.add_argument(
373
+ "--url", type=str, required=True, help="Server URL (e.g. http://localhost:8080)"
374
+ )
375
+ parser_login.add_argument(
376
+ "-v", "--verbose", action="store_true", help="Enable verbose logging"
377
+ )
378
+ parser_login.set_defaults(func=subcommand_cloud_login)
379
+
380
+ # 'cloud ls' subcommand
381
+ parser_ls = cloud_subparsers.add_parser("ls", help="List files in Supernote Cloud")
382
+ parser_ls.add_argument(
383
+ "path", type=str, nargs="?", default="/", help="Path to list"
384
+ )
385
+ parser_ls.add_argument(
386
+ "-v", "--verbose", action="store_true", help="Enable verbose logging"
387
+ )
388
+ parser_ls.set_defaults(func=subcommand_cloud_ls)
389
+
390
+ # 'cloud upload' subcommand
391
+ parser_upload = cloud_subparsers.add_parser(
392
+ "upload", help="Upload a file to Supernote Cloud"
393
+ )
394
+ parser_upload.add_argument("local_path", type=str, help="Local file path")
395
+ parser_upload.add_argument(
396
+ "remote_path", type=str, nargs="?", help="Cloud destination path (optional)"
397
+ )
398
+ parser_upload.add_argument(
399
+ "-v", "--verbose", action="store_true", help="Enable verbose logging"
400
+ )
401
+ parser_upload.set_defaults(func=subcommand_cloud_upload)
402
+
403
+ # 'cloud download' subcommand
404
+ parser_download = cloud_subparsers.add_parser(
405
+ "download", help="Download a file from Supernote Cloud"
406
+ )
407
+ parser_download.add_argument("remote_path", type=str, help="Cloud file path")
408
+ parser_download.add_argument(
409
+ "local_path", type=str, nargs="?", help="Local destination path (optional)"
410
+ )
411
+ parser_download.add_argument(
412
+ "-v", "--verbose", action="store_true", help="Enable verbose logging"
413
+ )
414
+ parser_download.set_defaults(func=subcommand_cloud_download)
415
+
416
+ # 'cloud mkdir' subcommand
417
+ parser_mkdir = cloud_subparsers.add_parser(
418
+ "mkdir", help="Create a folder in Supernote Cloud"
419
+ )
420
+ parser_mkdir.add_argument("path", type=str, help="Cloud folder path")
421
+ parser_mkdir.add_argument(
422
+ "-v", "--verbose", action="store_true", help="Enable verbose logging"
423
+ )
424
+ parser_mkdir.set_defaults(func=subcommand_cloud_mkdir)
425
+
426
+ # 'cloud rm' subcommand
427
+ parser_rm = cloud_subparsers.add_parser(
428
+ "rm", help="Remove a file or folder from Supernote Cloud"
429
+ )
430
+ parser_rm.add_argument("path", type=str, help="Cloud file or folder path")
431
+ parser_rm.add_argument(
432
+ "-v", "--verbose", action="store_true", help="Enable verbose logging"
433
+ )
434
+ parser_rm.set_defaults(func=subcommand_cloud_rm)
supernote/cli/main.py ADDED
@@ -0,0 +1,61 @@
1
+ """Main CLI entry point."""
2
+
3
+ import argparse
4
+ import importlib
5
+ import sys
6
+
7
+ SUBPARERS = ["notebook", "client", "server", "admin"]
8
+
9
+
10
+ def main():
11
+ parser = argparse.ArgumentParser(
12
+ prog="supernote",
13
+ description="Supernote toolkit for parsing, self hosting, and service access",
14
+ )
15
+
16
+ subparsers = parser.add_subparsers(dest="command", help="Command to run")
17
+
18
+ # We load subparsers lazily to handle missing optional dependencies
19
+ # (e.g. 'client' requires aiohttp, 'server' requires sqlalchemy).
20
+ for name in SUBPARERS:
21
+ try:
22
+ mod = importlib.import_module(f".{name}", package="supernote.cli")
23
+ mod.add_parser(subparsers)
24
+ except (ImportError, ModuleNotFoundError) as err:
25
+ # Add a placeholder parser that explains the missing dependency
26
+ p = subparsers.add_parser(
27
+ name, help=f"(Disabled: missing dependencies: {err})"
28
+ )
29
+
30
+ def make_error_handler(name: str, err: Exception):
31
+ def error_handler(args: argparse.Namespace):
32
+ print(
33
+ f"Error: Command '{name}' failed to load due to missing dependencies: {err}"
34
+ )
35
+ error_extra = name
36
+ if name == "admin":
37
+ error_extra = "client"
38
+ print(
39
+ f"Try installing with: pip install 'supernote[{error_extra}]'"
40
+ )
41
+ sys.exit(1)
42
+
43
+ return error_handler
44
+
45
+ p.set_defaults(func=make_error_handler(name, err))
46
+
47
+ args = parser.parse_args()
48
+ if args.command is None:
49
+ parser.print_help()
50
+ sys.exit(1)
51
+
52
+ # Dispatch
53
+ if hasattr(args, "func"):
54
+ args.func(args)
55
+ else:
56
+ parser.print_help()
57
+ sys.exit(1)
58
+
59
+
60
+ if __name__ == "__main__":
61
+ main()