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
@@ -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)
@@ -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}")
@@ -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
+ )