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.
- supernote/__init__.py +28 -0
- supernote/alembic/__init__.py +0 -0
- supernote/alembic/env.py +121 -0
- supernote/alembic/script.py.mako +28 -0
- supernote/alembic/versions/0543a383957b_initial_schema.py +312 -0
- supernote/alembic.ini +149 -0
- supernote/cli/__init__.py +1 -0
- supernote/cli/admin.py +178 -0
- supernote/cli/client.py +434 -0
- supernote/cli/main.py +61 -0
- supernote/cli/notebook.py +313 -0
- supernote/cli/server.py +89 -0
- supernote/client/__init__.py +28 -0
- supernote/client/admin.py +76 -0
- supernote/client/api.py +135 -0
- supernote/client/api_model.py +231 -0
- supernote/client/auth.py +84 -0
- supernote/client/client.py +365 -0
- supernote/client/device.py +361 -0
- supernote/client/exceptions.py +33 -0
- supernote/client/extended.py +20 -0
- supernote/client/hashing.py +50 -0
- supernote/client/login_client.py +154 -0
- supernote/client/schedule.py +149 -0
- supernote/client/summary.py +156 -0
- supernote/client/web.py +213 -0
- supernote/models/__init__.py +14 -0
- supernote/models/auth.py +380 -0
- supernote/models/base.py +266 -0
- supernote/models/equipment.py +276 -0
- supernote/models/extended.py +124 -0
- supernote/models/file_common.py +95 -0
- supernote/models/file_device.py +571 -0
- supernote/models/file_web.py +602 -0
- supernote/models/schedule.py +663 -0
- supernote/models/summary.py +830 -0
- supernote/models/system.py +431 -0
- supernote/models/user.py +638 -0
- supernote/notebook/__init__.py +67 -0
- supernote/notebook/color.py +93 -0
- supernote/notebook/converter.py +560 -0
- supernote/notebook/decoder.py +398 -0
- supernote/notebook/exceptions.py +43 -0
- supernote/notebook/fileformat.py +463 -0
- supernote/notebook/manipulator.py +420 -0
- supernote/notebook/parser.py +737 -0
- supernote/notebook/utils.py +49 -0
- supernote/py.typed +0 -0
- supernote/server/__init__.py +12 -0
- supernote/server/app.py +428 -0
- supernote/server/config.py +265 -0
- supernote/server/constants.py +26 -0
- supernote/server/db/__init__.py +9 -0
- supernote/server/db/base.py +8 -0
- supernote/server/db/migrations.py +44 -0
- supernote/server/db/models/__init__.py +23 -0
- supernote/server/db/models/device.py +17 -0
- supernote/server/db/models/file.py +107 -0
- supernote/server/db/models/kv.py +17 -0
- supernote/server/db/models/login_record.py +22 -0
- supernote/server/db/models/note_processing.py +100 -0
- supernote/server/db/models/schedule.py +83 -0
- supernote/server/db/models/summary.py +122 -0
- supernote/server/db/models/user.py +30 -0
- supernote/server/db/session.py +88 -0
- supernote/server/events.py +63 -0
- supernote/server/exceptions.py +153 -0
- supernote/server/mcp/__init__.py +1 -0
- supernote/server/mcp/models.py +109 -0
- supernote/server/mcp/server.py +176 -0
- supernote/server/resources/__init__.py +0 -0
- supernote/server/resources/prompts/__init__.py +0 -0
- supernote/server/resources/prompts/ocr/common/context.md +19 -0
- supernote/server/resources/prompts/ocr/common/legend.md +11 -0
- supernote/server/resources/prompts/ocr/daily/prompt.md +12 -0
- supernote/server/resources/prompts/ocr/default/system.md +14 -0
- supernote/server/resources/prompts/ocr/monthly/prompt.md +22 -0
- supernote/server/resources/prompts/ocr/weekly/prompt.md +16 -0
- supernote/server/resources/prompts/summary/common/instruction.md +8 -0
- supernote/server/resources/prompts/summary/daily/prompt.md +6 -0
- supernote/server/resources/prompts/summary/default/prompt.md +4 -0
- supernote/server/resources/prompts/summary/monthly/prompt.md +6 -0
- supernote/server/resources/prompts/summary/weekly/prompt.md +6 -0
- supernote/server/routes/admin.py +109 -0
- supernote/server/routes/auth.py +337 -0
- supernote/server/routes/decorators.py +13 -0
- supernote/server/routes/extended.py +139 -0
- supernote/server/routes/file_device.py +593 -0
- supernote/server/routes/file_web.py +597 -0
- supernote/server/routes/oss.py +317 -0
- supernote/server/routes/schedule.py +223 -0
- supernote/server/routes/summary.py +390 -0
- supernote/server/routes/system.py +57 -0
- supernote/server/services/__init__.py +15 -0
- supernote/server/services/blob.py +214 -0
- supernote/server/services/coordination.py +171 -0
- supernote/server/services/file.py +970 -0
- supernote/server/services/gemini.py +66 -0
- supernote/server/services/integrity.py +112 -0
- supernote/server/services/processor.py +331 -0
- supernote/server/services/processor_modules/__init__.py +145 -0
- supernote/server/services/processor_modules/gemini_embedding.py +112 -0
- supernote/server/services/processor_modules/gemini_ocr.py +149 -0
- supernote/server/services/processor_modules/page_hashing.py +220 -0
- supernote/server/services/processor_modules/png_conversion.py +107 -0
- supernote/server/services/processor_modules/summary.py +302 -0
- supernote/server/services/prompt_loader.py +133 -0
- supernote/server/services/schedule.py +161 -0
- supernote/server/services/search.py +250 -0
- supernote/server/services/summary.py +419 -0
- supernote/server/services/user.py +480 -0
- supernote/server/services/vfs.py +476 -0
- supernote/server/static/favicon.ico +0 -0
- supernote/server/static/index.html +186 -0
- supernote/server/static/js/api/client.js +463 -0
- supernote/server/static/js/components/FileCard.js +55 -0
- supernote/server/static/js/components/FileViewer.js +137 -0
- supernote/server/static/js/components/LoginCard.js +64 -0
- supernote/server/static/js/components/MoveModal.js +74 -0
- supernote/server/static/js/components/RenameModal.js +39 -0
- supernote/server/static/js/components/SummaryPanel.js +101 -0
- supernote/server/static/js/components/SystemPanel.js +158 -0
- supernote/server/static/js/composables/useFileSystem.js +116 -0
- supernote/server/static/js/main.js +241 -0
- supernote/server/static/style.css +14 -0
- supernote/server/utils/hashing.py +18 -0
- supernote/server/utils/note_content.py +76 -0
- supernote/server/utils/paths.py +58 -0
- supernote/server/utils/rate_limit.py +52 -0
- supernote/server/utils/tasks.py +71 -0
- supernote/server/utils/unique_id.py +48 -0
- supernote/server/utils/url_signer.py +218 -0
- supernote-0.13.6.dist-info/METADATA +183 -0
- supernote-0.13.6.dist-info/RECORD +138 -0
- supernote-0.13.6.dist-info/WHEEL +5 -0
- supernote-0.13.6.dist-info/entry_points.txt +3 -0
- supernote-0.13.6.dist-info/licenses/LICENSE +201 -0
- 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)
|
supernote/cli/client.py
ADDED
|
@@ -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()
|