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
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""Notebook CLI commands."""
|
|
2
|
+
|
|
3
|
+
# Copyright (c) 2020 jya
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
import io
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
|
|
21
|
+
from colour import Color
|
|
22
|
+
|
|
23
|
+
from supernote.notebook import (
|
|
24
|
+
load,
|
|
25
|
+
load_notebook,
|
|
26
|
+
merge,
|
|
27
|
+
parse_metadata,
|
|
28
|
+
reconstruct,
|
|
29
|
+
)
|
|
30
|
+
from supernote.notebook.color import MODE_RGB, ColorPalette
|
|
31
|
+
from supernote.notebook.converter import (
|
|
32
|
+
ImageConverter,
|
|
33
|
+
PdfConverter,
|
|
34
|
+
SvgConverter,
|
|
35
|
+
TextConverter,
|
|
36
|
+
VisibilityOverlay,
|
|
37
|
+
build_visibility_overlay,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def convert_all(converter, total, file_name, save_func, visibility_overlay):
|
|
42
|
+
basename, extension = os.path.splitext(file_name)
|
|
43
|
+
max_digits = len(str(total))
|
|
44
|
+
for i in range(total):
|
|
45
|
+
# append page number between filename and extension
|
|
46
|
+
numbered_filename = basename + "_" + str(i).zfill(max_digits) + extension
|
|
47
|
+
img = converter.convert(i, visibility_overlay)
|
|
48
|
+
save_func(img, numbered_filename)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def convert_and_concat_all(converter, total, file_name, save_func, separator):
|
|
52
|
+
data = []
|
|
53
|
+
for i in range(total):
|
|
54
|
+
data.append(converter.convert(i))
|
|
55
|
+
data = list(map(lambda x: "" if x is None else (x + "\n"), data))
|
|
56
|
+
if len(data) > 0:
|
|
57
|
+
alldata = ((separator + "\n") if separator else "").join(data)
|
|
58
|
+
save_func(alldata, file_name)
|
|
59
|
+
else:
|
|
60
|
+
print("no data")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def convert_to_png(args, notebook, palette):
|
|
64
|
+
converter = ImageConverter(notebook, palette=palette)
|
|
65
|
+
bg_visibility = (
|
|
66
|
+
VisibilityOverlay.INVISIBLE
|
|
67
|
+
if args.exclude_background
|
|
68
|
+
else VisibilityOverlay.DEFAULT
|
|
69
|
+
)
|
|
70
|
+
vo = build_visibility_overlay(background=bg_visibility)
|
|
71
|
+
|
|
72
|
+
def save(img, file_name):
|
|
73
|
+
img.save(file_name, format="PNG")
|
|
74
|
+
|
|
75
|
+
if args.all:
|
|
76
|
+
total = notebook.get_total_pages()
|
|
77
|
+
convert_all(converter, total, args.output, save, vo)
|
|
78
|
+
else:
|
|
79
|
+
img = converter.convert(args.number, visibility_overlay=vo)
|
|
80
|
+
save(img, args.output)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def convert_to_svg(args, notebook, palette):
|
|
84
|
+
converter = SvgConverter(notebook, palette=palette)
|
|
85
|
+
bg_visibility = (
|
|
86
|
+
VisibilityOverlay.INVISIBLE
|
|
87
|
+
if args.exclude_background
|
|
88
|
+
else VisibilityOverlay.DEFAULT
|
|
89
|
+
)
|
|
90
|
+
vo = build_visibility_overlay(background=bg_visibility)
|
|
91
|
+
|
|
92
|
+
def save(svg, file_name):
|
|
93
|
+
if svg is not None:
|
|
94
|
+
with open(file_name, "w") as f:
|
|
95
|
+
f.write(svg)
|
|
96
|
+
else:
|
|
97
|
+
print("no path data")
|
|
98
|
+
|
|
99
|
+
if args.all:
|
|
100
|
+
total = notebook.get_total_pages()
|
|
101
|
+
convert_all(converter, total, args.output, save, vo)
|
|
102
|
+
else:
|
|
103
|
+
svg = converter.convert(args.number, visibility_overlay=vo)
|
|
104
|
+
save(svg, args.output)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def convert_to_pdf(args, notebook, palette):
|
|
108
|
+
use_link = not args.no_link
|
|
109
|
+
use_keyword = args.add_keyword
|
|
110
|
+
converter = PdfConverter(notebook, palette=palette)
|
|
111
|
+
|
|
112
|
+
def save(data, file_name):
|
|
113
|
+
if data is not None:
|
|
114
|
+
with open(file_name, "wb") as f:
|
|
115
|
+
f.write(data)
|
|
116
|
+
else:
|
|
117
|
+
print("no data")
|
|
118
|
+
|
|
119
|
+
if args.all:
|
|
120
|
+
data = converter.convert(
|
|
121
|
+
-1, enable_link=use_link, enable_keyword=use_keyword
|
|
122
|
+
) # minus value means converting all pages
|
|
123
|
+
save(data, args.output)
|
|
124
|
+
else:
|
|
125
|
+
data = converter.convert(
|
|
126
|
+
args.number, enable_link=use_link, enable_keyword=use_keyword
|
|
127
|
+
)
|
|
128
|
+
save(data, args.output)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def convert_to_txt(args, notebook, palette):
|
|
132
|
+
converter = TextConverter(notebook, palette=palette)
|
|
133
|
+
|
|
134
|
+
def save(data, file_name):
|
|
135
|
+
if data is not None:
|
|
136
|
+
with open(file_name, "w") as f:
|
|
137
|
+
f.write(data)
|
|
138
|
+
else:
|
|
139
|
+
print("no data")
|
|
140
|
+
|
|
141
|
+
if args.all:
|
|
142
|
+
total = notebook.get_total_pages()
|
|
143
|
+
convert_and_concat_all(
|
|
144
|
+
converter, total, args.output, save, args.text_page_separator
|
|
145
|
+
)
|
|
146
|
+
else:
|
|
147
|
+
data = converter.convert(args.number)
|
|
148
|
+
save(data, args.output)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def subcommand_convert(args):
|
|
152
|
+
notebook = load_notebook(args.input, policy=args.policy)
|
|
153
|
+
palette = None
|
|
154
|
+
if args.color:
|
|
155
|
+
try:
|
|
156
|
+
colors = parse_color(args.color)
|
|
157
|
+
except ValueError as e:
|
|
158
|
+
print(e, file=sys.stderr)
|
|
159
|
+
sys.exit(1)
|
|
160
|
+
palette = ColorPalette(MODE_RGB, colors)
|
|
161
|
+
if args.type == "png":
|
|
162
|
+
convert_to_png(args, notebook, palette)
|
|
163
|
+
elif args.type == "svg":
|
|
164
|
+
convert_to_svg(args, notebook, palette)
|
|
165
|
+
elif args.type == "pdf":
|
|
166
|
+
convert_to_pdf(args, notebook, palette)
|
|
167
|
+
elif args.type == "txt":
|
|
168
|
+
convert_to_txt(args, notebook, palette)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def subcommand_analyze(args):
|
|
172
|
+
# show all metadata as JSON
|
|
173
|
+
with open(args.input, "rb") as f:
|
|
174
|
+
metadata = parse_metadata(f, policy=args.policy)
|
|
175
|
+
print(metadata.to_json(indent=2))
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def subcommand_merge(args):
|
|
179
|
+
num_input = len(args.input)
|
|
180
|
+
if num_input == 1: # reconstruct a note file
|
|
181
|
+
notebook = load_notebook(args.input[0])
|
|
182
|
+
reconstructed_binary = reconstruct(notebook)
|
|
183
|
+
with open(args.output, "wb") as f:
|
|
184
|
+
f.write(reconstructed_binary)
|
|
185
|
+
else: # merge multiple note files
|
|
186
|
+
with open(args.input[0], "rb") as f:
|
|
187
|
+
merged_binary = f.read()
|
|
188
|
+
for i in range(1, num_input):
|
|
189
|
+
stream = io.BytesIO(merged_binary)
|
|
190
|
+
merged_notebook = load(stream)
|
|
191
|
+
next_notebook = load_notebook(args.input[i])
|
|
192
|
+
merged_binary = merge(merged_notebook, next_notebook)
|
|
193
|
+
with open(args.output, "wb") as f:
|
|
194
|
+
f.write(merged_binary)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def subcommand_reconstruct(args):
|
|
198
|
+
notebook = load_notebook(args.input)
|
|
199
|
+
reconstructed_binary = reconstruct(notebook)
|
|
200
|
+
with open(args.output, "wb") as f:
|
|
201
|
+
f.write(reconstructed_binary)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def parse_color(color_string):
|
|
205
|
+
colorcodes = color_string.split(",")
|
|
206
|
+
if len(colorcodes) != 4:
|
|
207
|
+
raise ValueError(f"few color codes, 4 colors are required: {color_string}")
|
|
208
|
+
black = int(Color(colorcodes[0]).hex_l[1:7], 16)
|
|
209
|
+
darkgray = int(Color(colorcodes[1]).hex_l[1:7], 16)
|
|
210
|
+
gray = int(Color(colorcodes[2]).hex_l[1:7], 16)
|
|
211
|
+
white = int(Color(colorcodes[3]).hex_l[1:7], 16)
|
|
212
|
+
return (black, darkgray, gray, white)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def add_parser(subparsers):
|
|
216
|
+
# 'analyze' subcommand
|
|
217
|
+
parser_analyze = subparsers.add_parser("analyze", help="analyze note file")
|
|
218
|
+
parser_analyze.add_argument("input", type=str, help="input note file")
|
|
219
|
+
parser_analyze.add_argument(
|
|
220
|
+
"--policy",
|
|
221
|
+
choices=["strict", "loose"],
|
|
222
|
+
default="strict",
|
|
223
|
+
help="select parser policy",
|
|
224
|
+
)
|
|
225
|
+
parser_analyze.set_defaults(func=subcommand_analyze)
|
|
226
|
+
|
|
227
|
+
# 'convert' subcommand
|
|
228
|
+
parser_convert = subparsers.add_parser("convert", help="image conversion")
|
|
229
|
+
parser_convert.add_argument("input", type=str, help="input note file")
|
|
230
|
+
parser_convert.add_argument("output", type=str, help="output image file")
|
|
231
|
+
parser_convert.add_argument(
|
|
232
|
+
"-n", "--number", type=int, default=0, help="page number to be converted"
|
|
233
|
+
)
|
|
234
|
+
parser_convert.add_argument(
|
|
235
|
+
"-a", "--all", action="store_true", default=False, help="convert all pages"
|
|
236
|
+
)
|
|
237
|
+
parser_convert.add_argument(
|
|
238
|
+
"-c",
|
|
239
|
+
"--color",
|
|
240
|
+
type=str,
|
|
241
|
+
help="colorize note with comma separated color codes in order of black, darkgray, gray and white.",
|
|
242
|
+
)
|
|
243
|
+
parser_convert.add_argument(
|
|
244
|
+
"-t",
|
|
245
|
+
"--type",
|
|
246
|
+
choices=["png", "svg", "pdf", "txt"],
|
|
247
|
+
default="png",
|
|
248
|
+
help="select conversion file type",
|
|
249
|
+
)
|
|
250
|
+
parser_convert.add_argument(
|
|
251
|
+
"--exclude-background",
|
|
252
|
+
action="store_true",
|
|
253
|
+
default=False,
|
|
254
|
+
help="exclude background and make it transparent (PNG and SVG are supported)",
|
|
255
|
+
)
|
|
256
|
+
parser_convert.add_argument(
|
|
257
|
+
"--pdf-type",
|
|
258
|
+
choices=["original"],
|
|
259
|
+
default="original",
|
|
260
|
+
help="select PDF conversion type",
|
|
261
|
+
)
|
|
262
|
+
parser_convert.add_argument(
|
|
263
|
+
"--no-link", action="store_true", default=False, help="disable links in PDF"
|
|
264
|
+
)
|
|
265
|
+
parser_convert.add_argument(
|
|
266
|
+
"--add-keyword",
|
|
267
|
+
action="store_true",
|
|
268
|
+
default=False,
|
|
269
|
+
help="enable keywords in PDF",
|
|
270
|
+
)
|
|
271
|
+
parser_convert.add_argument(
|
|
272
|
+
"--text-page-separator",
|
|
273
|
+
type=str,
|
|
274
|
+
default="",
|
|
275
|
+
help="page separator string for text conversion",
|
|
276
|
+
)
|
|
277
|
+
parser_convert.add_argument(
|
|
278
|
+
"--policy",
|
|
279
|
+
choices=["strict", "loose"],
|
|
280
|
+
default="strict",
|
|
281
|
+
help="select parser policy",
|
|
282
|
+
)
|
|
283
|
+
parser_convert.set_defaults(func=subcommand_convert)
|
|
284
|
+
|
|
285
|
+
# 'merge' subcommand
|
|
286
|
+
description = """
|
|
287
|
+
(EXPERIMENTAL FEATURE)
|
|
288
|
+
This command merge multiple note files to one.
|
|
289
|
+
Backup your input files to save your data because you might get a corrupted output file.
|
|
290
|
+
"""
|
|
291
|
+
parser_merge = subparsers.add_parser(
|
|
292
|
+
"merge",
|
|
293
|
+
description=description,
|
|
294
|
+
help="merge multiple note files (EXPERIMENTAL FEATURE)",
|
|
295
|
+
)
|
|
296
|
+
parser_merge.add_argument("input", type=str, nargs="+", help="input note files")
|
|
297
|
+
parser_merge.add_argument("output", type=str, help="output note file")
|
|
298
|
+
parser_merge.set_defaults(func=subcommand_merge)
|
|
299
|
+
|
|
300
|
+
# 'reconstruct' subcommand
|
|
301
|
+
description = """
|
|
302
|
+
(EXPERIMENTAL FEATURE)
|
|
303
|
+
This command disassemble and reconstruct a note file for debugging and testing.
|
|
304
|
+
Backup your input file to save your data because you might get a corrupted output file.
|
|
305
|
+
"""
|
|
306
|
+
parser_reconstruct = subparsers.add_parser(
|
|
307
|
+
"reconstruct",
|
|
308
|
+
description=description,
|
|
309
|
+
help="reconstruct a note file (EXPERIMENTAL FEATURE)",
|
|
310
|
+
)
|
|
311
|
+
parser_reconstruct.add_argument("input", type=str, help="input note file")
|
|
312
|
+
parser_reconstruct.add_argument("output", type=str, help="output note file")
|
|
313
|
+
parser_reconstruct.set_defaults(func=subcommand_reconstruct)
|
supernote/cli/server.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from supernote.server import app as server_app
|
|
8
|
+
|
|
9
|
+
# Configure logging
|
|
10
|
+
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
|
11
|
+
logger = logging.getLogger("supernote-cli")
|
|
12
|
+
|
|
13
|
+
# For ephemeral mode
|
|
14
|
+
DEBUG_EMAIL = "debug@example.com"
|
|
15
|
+
DEBUG_PASSWORD = "password"
|
|
16
|
+
LOGIN_COMMAND = "supernote cloud login --url http://{SUPERNOTE_HOST}:{SUPERNOTE_PORT} {DEBUG_EMAIL} --password {DEBUG_PASSWORD}"
|
|
17
|
+
EXAMPLE_COMMAND = "supernote cloud ls"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def serve_run(args: argparse.Namespace) -> None:
|
|
21
|
+
"""Wrapper for serve command to handle ephemeral mode."""
|
|
22
|
+
if getattr(args, "ephemeral", False):
|
|
23
|
+
with tempfile.TemporaryDirectory(prefix="supernote-ephemeral-") as tmp_dir:
|
|
24
|
+
tmp_path = Path(tmp_dir)
|
|
25
|
+
# Create system directory for database
|
|
26
|
+
(tmp_path / "system").mkdir(parents=True, exist_ok=True)
|
|
27
|
+
|
|
28
|
+
# Set environment variables for the server process
|
|
29
|
+
os.environ["SUPERNOTE_EPHEMERAL"] = "true"
|
|
30
|
+
if not os.getenv("SUPERNOTE_PORT"):
|
|
31
|
+
os.environ["SUPERNOTE_PORT"] = "8080"
|
|
32
|
+
if not os.getenv("SUPERNOTE_HOST"):
|
|
33
|
+
os.environ["SUPERNOTE_HOST"] = "127.0.0.1"
|
|
34
|
+
os.environ["SUPERNOTE_STORAGE_DIR"] = str(tmp_path)
|
|
35
|
+
|
|
36
|
+
print(f"Using ephemeral mode with storage directory: {tmp_path}")
|
|
37
|
+
print(f"Created default user: {DEBUG_EMAIL} / {DEBUG_PASSWORD}")
|
|
38
|
+
print("Run command to login:")
|
|
39
|
+
print(
|
|
40
|
+
" "
|
|
41
|
+
+ LOGIN_COMMAND.format(
|
|
42
|
+
SUPERNOTE_HOST=os.getenv("SUPERNOTE_HOST"),
|
|
43
|
+
SUPERNOTE_PORT=os.getenv("SUPERNOTE_PORT"),
|
|
44
|
+
DEBUG_EMAIL=DEBUG_EMAIL,
|
|
45
|
+
DEBUG_PASSWORD=DEBUG_PASSWORD,
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
print("Run command to test:")
|
|
49
|
+
print(" " + EXAMPLE_COMMAND)
|
|
50
|
+
server_app.run(args)
|
|
51
|
+
else:
|
|
52
|
+
server_app.run(args)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def add_parser(subparsers):
|
|
56
|
+
# Common parent parser
|
|
57
|
+
base_parser = argparse.ArgumentParser(add_help=False)
|
|
58
|
+
base_parser.add_argument(
|
|
59
|
+
"--config-dir",
|
|
60
|
+
type=str,
|
|
61
|
+
default=None,
|
|
62
|
+
help="Path to configuration directory (default: config/)",
|
|
63
|
+
)
|
|
64
|
+
parser_serve = subparsers.add_parser(
|
|
65
|
+
"serve",
|
|
66
|
+
parents=[base_parser],
|
|
67
|
+
help="Start the Supernote Private Cloud server",
|
|
68
|
+
)
|
|
69
|
+
parser_serve.add_argument(
|
|
70
|
+
"--ephemeral",
|
|
71
|
+
action="store_true",
|
|
72
|
+
help="Run in isolated temporary environment with random port and debug user",
|
|
73
|
+
)
|
|
74
|
+
parser_serve.set_defaults(func=serve_run)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def main() -> None:
|
|
78
|
+
parser = argparse.ArgumentParser(description="Supernote Server CLI")
|
|
79
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
80
|
+
add_parser(subparsers)
|
|
81
|
+
args = parser.parse_args()
|
|
82
|
+
if hasattr(args, "func"):
|
|
83
|
+
args.func(args)
|
|
84
|
+
else:
|
|
85
|
+
parser.print_help()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
if __name__ == "__main__":
|
|
89
|
+
main()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Client library for accessing Supernote Cloud services.
|
|
2
|
+
|
|
3
|
+
Example:
|
|
4
|
+
async with await Supernote.login("email@example.com", "password", host="http://localhost:8080") as sn:
|
|
5
|
+
# Access Web and Device APIs directly through the session object
|
|
6
|
+
# Example: List root folder using path-based Device API
|
|
7
|
+
result = await sn.device.list_folder("/")
|
|
8
|
+
|
|
9
|
+
# sn.token contains the access token for use with `Supernote.from_token`
|
|
10
|
+
print(sn.token)
|
|
11
|
+
|
|
12
|
+
# Use an existing token:
|
|
13
|
+
sn = Supernote.from_token("your-token", host="http://localhost:8080")
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .api import Supernote
|
|
17
|
+
from .auth import AbstractAuth, ConstantAuth, FileCacheAuth
|
|
18
|
+
from .client import Client
|
|
19
|
+
from .login_client import LoginClient
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"Supernote",
|
|
23
|
+
"Client",
|
|
24
|
+
"AbstractAuth",
|
|
25
|
+
"ConstantAuth",
|
|
26
|
+
"FileCacheAuth",
|
|
27
|
+
"LoginClient",
|
|
28
|
+
]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from supernote.client.client import Client
|
|
4
|
+
from supernote.models.base import BaseResponse
|
|
5
|
+
from supernote.models.user import (
|
|
6
|
+
RetrievePasswordDTO,
|
|
7
|
+
UpdateEmailDTO,
|
|
8
|
+
UpdatePasswordDTO,
|
|
9
|
+
UserRegisterDTO,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AdminClient:
|
|
16
|
+
"""Client for administrative tasks (User Management)."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, client: Client) -> None:
|
|
19
|
+
"""Initialize with an existing client instance."""
|
|
20
|
+
self.client = client
|
|
21
|
+
|
|
22
|
+
async def register(
|
|
23
|
+
self, email: str, password: str, username: str | None = None
|
|
24
|
+
) -> None:
|
|
25
|
+
"""Register a new user."""
|
|
26
|
+
dto = UserRegisterDTO(email=email, password=password, user_name=username)
|
|
27
|
+
await self.client.post_json(
|
|
28
|
+
"/api/user/register", BaseResponse, json=dto.to_dict()
|
|
29
|
+
)
|
|
30
|
+
logger.info(f"Registered user {email}")
|
|
31
|
+
|
|
32
|
+
async def unregister(self) -> None:
|
|
33
|
+
"""Delete currently logged in user."""
|
|
34
|
+
await self.client.post_json("/api/user/unregister", BaseResponse)
|
|
35
|
+
logger.info("Unregistered user")
|
|
36
|
+
|
|
37
|
+
async def update_password(self, new_password: str) -> None:
|
|
38
|
+
"""Update password for currently logged in user."""
|
|
39
|
+
dto = UpdatePasswordDTO(password=new_password)
|
|
40
|
+
await self.client.put_json(
|
|
41
|
+
"/api/user/password", BaseResponse, json=dto.to_dict()
|
|
42
|
+
)
|
|
43
|
+
logger.info("Password updated")
|
|
44
|
+
|
|
45
|
+
async def update_email(self, new_email: str) -> None:
|
|
46
|
+
"""Update email for currently logged in user."""
|
|
47
|
+
dto = UpdateEmailDTO(email=new_email)
|
|
48
|
+
await self.client.put_json("/api/user/email", BaseResponse, json=dto.to_dict())
|
|
49
|
+
logger.info("Email updated")
|
|
50
|
+
|
|
51
|
+
async def retrieve_password(self, email: str, new_password: str) -> None:
|
|
52
|
+
"""Reset password (public endpoint)."""
|
|
53
|
+
dto = RetrievePasswordDTO(email=email, password=new_password)
|
|
54
|
+
await self.client.post_json(
|
|
55
|
+
"/api/official/user/retrieve/password", BaseResponse, json=dto.to_dict()
|
|
56
|
+
)
|
|
57
|
+
logger.info(f"Password reset for {email}")
|
|
58
|
+
|
|
59
|
+
async def admin_create_user(
|
|
60
|
+
self, email: str, password: str, username: str | None = None
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Register a new user through the admin API."""
|
|
63
|
+
dto = UserRegisterDTO(email=email, password=password, user_name=username)
|
|
64
|
+
await self.client.post_json(
|
|
65
|
+
"/api/admin/users", BaseResponse, json=dto.to_dict()
|
|
66
|
+
)
|
|
67
|
+
logger.info(f"Registered user {email}")
|
|
68
|
+
|
|
69
|
+
async def admin_reset_password(self, email: str, password_md5: str) -> None:
|
|
70
|
+
"""Force reset user password through admin API."""
|
|
71
|
+
await self.client.post_json(
|
|
72
|
+
"/api/admin/users/password",
|
|
73
|
+
BaseResponse,
|
|
74
|
+
json={"email": email, "password": password_md5},
|
|
75
|
+
)
|
|
76
|
+
logger.info(f"Password reset for {email}")
|
supernote/client/api.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Supernote session wrapper."""
|
|
2
|
+
|
|
3
|
+
from typing import Self
|
|
4
|
+
|
|
5
|
+
import aiohttp
|
|
6
|
+
|
|
7
|
+
from .auth import AbstractAuth, ConstantAuth
|
|
8
|
+
from .client import Client
|
|
9
|
+
from .device import DeviceClient
|
|
10
|
+
from .login_client import LoginClient
|
|
11
|
+
from .web import WebClient
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Supernote:
|
|
15
|
+
"""A session-managed entry point for Supernote clients.
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
async with await Supernote.login("email@example.com", "password", host="http://localhost:8080") as sn:
|
|
19
|
+
# Access Web and Device APIs directly through the session object
|
|
20
|
+
# Example: List root folder using path-based Device API
|
|
21
|
+
result = await sn.device.list_folder("/")
|
|
22
|
+
|
|
23
|
+
# sn.token contains the access token for use with `Supernote.from_token`
|
|
24
|
+
print(sn.token)
|
|
25
|
+
|
|
26
|
+
Example using an existing token:
|
|
27
|
+
sn = Supernote.from_token("your-token", host="http://localhost:8080")
|
|
28
|
+
# Note: When created this way, you are responsible for closing the session
|
|
29
|
+
# or passing an existing one.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
host: str | None = None,
|
|
35
|
+
session: aiohttp.ClientSession | None = None,
|
|
36
|
+
auth: AbstractAuth | None = None,
|
|
37
|
+
close_session: bool | None = None,
|
|
38
|
+
):
|
|
39
|
+
"""Initialize the Supernote session wrapper."""
|
|
40
|
+
if close_session is not None:
|
|
41
|
+
self._close_session = close_session
|
|
42
|
+
else:
|
|
43
|
+
self._close_session = session is None
|
|
44
|
+
self._session = session or aiohttp.ClientSession()
|
|
45
|
+
self._client = Client(self._session, host=host, auth=auth)
|
|
46
|
+
self._web = WebClient(self._client)
|
|
47
|
+
self._device = DeviceClient(self._client)
|
|
48
|
+
self._login_client = LoginClient(self._client)
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
async def login(
|
|
52
|
+
cls,
|
|
53
|
+
email: str,
|
|
54
|
+
password: str,
|
|
55
|
+
host: str | None = None,
|
|
56
|
+
session: aiohttp.ClientSession | None = None,
|
|
57
|
+
) -> "Supernote":
|
|
58
|
+
"""Log in and return an authenticated Supernote instance."""
|
|
59
|
+
# Create a temporary unauthenticated instance to perform login
|
|
60
|
+
sn = cls(session=session, host=host)
|
|
61
|
+
login_client = LoginClient(sn.client)
|
|
62
|
+
token = await login_client.login(email, password)
|
|
63
|
+
# Return a new authenticated instance sharing the same session
|
|
64
|
+
return sn.with_auth(ConstantAuth(token))
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def from_token(
|
|
68
|
+
cls,
|
|
69
|
+
token: str,
|
|
70
|
+
host: str | None = None,
|
|
71
|
+
session: aiohttp.ClientSession | None = None,
|
|
72
|
+
) -> "Supernote":
|
|
73
|
+
"""Return an authenticated Supernote instance using an existing token."""
|
|
74
|
+
return cls.from_auth(ConstantAuth(token), host=host, session=session)
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def from_auth(
|
|
78
|
+
cls,
|
|
79
|
+
auth: AbstractAuth,
|
|
80
|
+
host: str | None = None,
|
|
81
|
+
session: aiohttp.ClientSession | None = None,
|
|
82
|
+
) -> "Supernote":
|
|
83
|
+
"""Return an authenticated Supernote instance using existing credentials."""
|
|
84
|
+
return cls(host=host, session=session, auth=auth)
|
|
85
|
+
|
|
86
|
+
async def __aenter__(self) -> Self:
|
|
87
|
+
"""Enter the async context manager."""
|
|
88
|
+
return self
|
|
89
|
+
|
|
90
|
+
async def __aexit__(
|
|
91
|
+
self,
|
|
92
|
+
exc_type: type[BaseException] | None,
|
|
93
|
+
exc_val: BaseException | None,
|
|
94
|
+
exc_tb: object | None,
|
|
95
|
+
) -> None:
|
|
96
|
+
"""Exit the async context manager."""
|
|
97
|
+
if self._close_session:
|
|
98
|
+
await self._session.close()
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def web(self) -> WebClient:
|
|
102
|
+
"""Return the web client."""
|
|
103
|
+
return self._web
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def device(self) -> DeviceClient:
|
|
107
|
+
"""Return the device client."""
|
|
108
|
+
return self._device
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def login_client(self) -> LoginClient:
|
|
112
|
+
"""Return the login client."""
|
|
113
|
+
return self._login_client
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def client(self) -> Client:
|
|
117
|
+
"""Return the lower level base client."""
|
|
118
|
+
return self._client
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def token(self) -> str | None:
|
|
122
|
+
"""Return the current access token if authenticated with ConstantAuth."""
|
|
123
|
+
auth = self._client.get_auth()
|
|
124
|
+
if isinstance(auth, ConstantAuth):
|
|
125
|
+
return auth.token
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
def with_auth(self, auth: AbstractAuth) -> Self:
|
|
129
|
+
"""Return a new Supernote instance with the given authentication credentials."""
|
|
130
|
+
return self.__class__(
|
|
131
|
+
session=self._session,
|
|
132
|
+
host=self._client.host,
|
|
133
|
+
auth=auth,
|
|
134
|
+
close_session=self._close_session,
|
|
135
|
+
)
|