supernote 0.4.1__tar.gz → 0.6.0__tar.gz

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 (51) hide show
  1. {supernote-0.4.1 → supernote-0.6.0}/PKG-INFO +9 -5
  2. {supernote-0.4.1 → supernote-0.6.0}/README.md +8 -4
  3. {supernote-0.4.1 → supernote-0.6.0}/pyproject.toml +1 -1
  4. supernote-0.6.0/supernote/cli/server.py +137 -0
  5. {supernote-0.4.1 → supernote-0.6.0}/supernote/server/__init__.py +2 -2
  6. {supernote-0.4.1 → supernote-0.6.0}/supernote/server/app.py +23 -9
  7. supernote-0.6.0/supernote/server/config.py +159 -0
  8. {supernote-0.4.1 → supernote-0.6.0}/supernote/server/models/file.py +11 -0
  9. {supernote-0.4.1 → supernote-0.6.0}/supernote/server/routes/file.py +99 -33
  10. {supernote-0.4.1 → supernote-0.6.0}/supernote/server/services/file.py +76 -98
  11. {supernote-0.4.1 → supernote-0.6.0}/supernote/server/services/storage.py +101 -132
  12. {supernote-0.4.1 → supernote-0.6.0}/supernote/server/services/user.py +61 -66
  13. {supernote-0.4.1 → supernote-0.6.0}/supernote.egg-info/PKG-INFO +9 -5
  14. supernote-0.4.1/supernote/cli/server.py +0 -109
  15. supernote-0.4.1/supernote/server/config.py +0 -16
  16. {supernote-0.4.1 → supernote-0.6.0}/LICENSE +0 -0
  17. {supernote-0.4.1 → supernote-0.6.0}/setup.cfg +0 -0
  18. {supernote-0.4.1 → supernote-0.6.0}/supernote/__init__.py +0 -0
  19. {supernote-0.4.1 → supernote-0.6.0}/supernote/cli/__init__.py +0 -0
  20. {supernote-0.4.1 → supernote-0.6.0}/supernote/cli/client.py +0 -0
  21. {supernote-0.4.1 → supernote-0.6.0}/supernote/cli/main.py +0 -0
  22. {supernote-0.4.1 → supernote-0.6.0}/supernote/cli/notebook.py +0 -0
  23. {supernote-0.4.1 → supernote-0.6.0}/supernote/client/__init__.py +0 -0
  24. {supernote-0.4.1 → supernote-0.6.0}/supernote/client/api_model.py +0 -0
  25. {supernote-0.4.1 → supernote-0.6.0}/supernote/client/auth.py +0 -0
  26. {supernote-0.4.1 → supernote-0.6.0}/supernote/client/client.py +0 -0
  27. {supernote-0.4.1 → supernote-0.6.0}/supernote/client/cloud_client.py +0 -0
  28. {supernote-0.4.1 → supernote-0.6.0}/supernote/client/exceptions.py +0 -0
  29. {supernote-0.4.1 → supernote-0.6.0}/supernote/client/login_client.py +0 -0
  30. {supernote-0.4.1 → supernote-0.6.0}/supernote/notebook/__init__.py +0 -0
  31. {supernote-0.4.1 → supernote-0.6.0}/supernote/notebook/color.py +0 -0
  32. {supernote-0.4.1 → supernote-0.6.0}/supernote/notebook/converter.py +0 -0
  33. {supernote-0.4.1 → supernote-0.6.0}/supernote/notebook/decoder.py +0 -0
  34. {supernote-0.4.1 → supernote-0.6.0}/supernote/notebook/exceptions.py +0 -0
  35. {supernote-0.4.1 → supernote-0.6.0}/supernote/notebook/fileformat.py +0 -0
  36. {supernote-0.4.1 → supernote-0.6.0}/supernote/notebook/manipulator.py +0 -0
  37. {supernote-0.4.1 → supernote-0.6.0}/supernote/notebook/parser.py +0 -0
  38. {supernote-0.4.1 → supernote-0.6.0}/supernote/notebook/utils.py +0 -0
  39. {supernote-0.4.1 → supernote-0.6.0}/supernote/py.typed +0 -0
  40. {supernote-0.4.1 → supernote-0.6.0}/supernote/server/models/__init__.py +0 -0
  41. {supernote-0.4.1 → supernote-0.6.0}/supernote/server/models/auth.py +0 -0
  42. {supernote-0.4.1 → supernote-0.6.0}/supernote/server/models/base.py +0 -0
  43. {supernote-0.4.1 → supernote-0.6.0}/supernote/server/routes/auth.py +0 -0
  44. {supernote-0.4.1 → supernote-0.6.0}/supernote/server/routes/decorators.py +0 -0
  45. {supernote-0.4.1 → supernote-0.6.0}/supernote/server/routes/system.py +0 -0
  46. {supernote-0.4.1 → supernote-0.6.0}/supernote/server/services/__init__.py +0 -0
  47. {supernote-0.4.1 → supernote-0.6.0}/supernote.egg-info/SOURCES.txt +0 -0
  48. {supernote-0.4.1 → supernote-0.6.0}/supernote.egg-info/dependency_links.txt +0 -0
  49. {supernote-0.4.1 → supernote-0.6.0}/supernote.egg-info/entry_points.txt +0 -0
  50. {supernote-0.4.1 → supernote-0.6.0}/supernote.egg-info/requires.txt +0 -0
  51. {supernote-0.4.1 → supernote-0.6.0}/supernote.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: supernote
3
- Version: 0.4.1
3
+ Version: 0.6.0
4
4
  Summary: All-in-one toolkit for Supernote devices: parse notebooks, self-host services, access services
5
5
  Author-email: jya <jya@wizmy.net>, Allen Porter <allen.porter@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -82,8 +82,11 @@ limitation. All credit goes to the original authors of [supernote-tool](https://
82
82
  ### Run Private Server
83
83
 
84
84
  ```bash
85
- # Configure users (you will be prompted for a password)
86
- supernote-server user add alice
85
+ # Generate configuration
86
+ supernote-server config init > config.yaml
87
+
88
+ # Generate users (outputs YAML, append to users.yaml)
89
+ supernote-server user add alice >> users.yaml
87
90
 
88
91
  # Start server
89
92
  supernote-server serve
@@ -95,9 +98,10 @@ supernote-server serve
95
98
  # Build image
96
99
  docker build -t supernote-server .
97
100
 
98
- # Create user
101
+ # Generate initial configuration
99
102
  mkdir config
100
- docker run --rm -it -v $(pwd)/config:/config supernote-server supernote-server user add alice
103
+ docker run --rm supernote-server supernote-server config init > config/config.yaml
104
+ docker run --rm -it supernote-server supernote-server user add alice >> config/users.yaml
101
105
 
102
106
  # Run server
103
107
  docker run -d -p 8080:8080 \
@@ -52,8 +52,11 @@ limitation. All credit goes to the original authors of [supernote-tool](https://
52
52
  ### Run Private Server
53
53
 
54
54
  ```bash
55
- # Configure users (you will be prompted for a password)
56
- supernote-server user add alice
55
+ # Generate configuration
56
+ supernote-server config init > config.yaml
57
+
58
+ # Generate users (outputs YAML, append to users.yaml)
59
+ supernote-server user add alice >> users.yaml
57
60
 
58
61
  # Start server
59
62
  supernote-server serve
@@ -65,9 +68,10 @@ supernote-server serve
65
68
  # Build image
66
69
  docker build -t supernote-server .
67
70
 
68
- # Create user
71
+ # Generate initial configuration
69
72
  mkdir config
70
- docker run --rm -it -v $(pwd)/config:/config supernote-server supernote-server user add alice
73
+ docker run --rm supernote-server supernote-server config init > config/config.yaml
74
+ docker run --rm -it supernote-server supernote-server user add alice >> config/users.yaml
71
75
 
72
76
  # Run server
73
77
  docker run -d -p 8080:8080 \
@@ -4,7 +4,7 @@ requires = ["setuptools>=77.0"]
4
4
 
5
5
  [project]
6
6
  name = "supernote"
7
- version = "0.4.1"
7
+ version = "0.6.0"
8
8
  license = "Apache-2.0"
9
9
  license-files = ["LICENSE"]
10
10
  description = "All-in-one toolkit for Supernote devices: parse notebooks, self-host services, access services"
@@ -0,0 +1,137 @@
1
+ """Server CLI commands."""
2
+
3
+ import argparse
4
+ import getpass
5
+ import secrets
6
+
7
+ import yaml
8
+
9
+ from supernote.server import app as server_app
10
+ from supernote.server.config import AuthConfig, ServerConfig, UsersConfig
11
+ from supernote.server.services.user import UserService
12
+
13
+
14
+ def get_auth_config(config_dir: str | None) -> AuthConfig:
15
+ # Load base config
16
+ server_config = ServerConfig.load(config_dir)
17
+ return server_config.auth
18
+
19
+
20
+ def list_users(config_dir: str | None):
21
+ config = get_auth_config(config_dir)
22
+ service = UserService(config)
23
+ for user in service.list_users():
24
+ status = "active" if user.is_active else "inactive"
25
+ print(f"{user.username} ({status})")
26
+
27
+
28
+ def add_user(username: str, password: str | None = None):
29
+ if password is None:
30
+ password = getpass.getpass(f"Password for {username}: ")
31
+
32
+ # Generate the YAML snippet for a single user
33
+ user_entry = UserService.create_user_entry(username, password)
34
+ users_config = UsersConfig(users=[user_entry])
35
+
36
+ # Print YAML to stdout
37
+ print("# Generated Supernote User Entry")
38
+ print("# Append this to your users.yaml file.")
39
+ print(yaml.safe_dump(users_config.to_dict(), default_flow_style=False))
40
+
41
+
42
+ def config_init():
43
+ """Output a default config.yaml with a generated secret."""
44
+ secret = secrets.token_hex(32)
45
+ config = ServerConfig()
46
+ config.auth.secret_key = secret
47
+
48
+ print("# Generated Supernote Server Configuration")
49
+ print("# Save this as config.yaml.")
50
+ print(yaml.safe_dump(config.to_dict(), default_flow_style=False))
51
+
52
+
53
+ def generate_secret():
54
+ """Output a 32-byte random hex secret."""
55
+ print(secrets.token_hex(32))
56
+
57
+
58
+ def add_parser(subparsers):
59
+ # Common parent parser for server-related commands that need config access
60
+ base_parser = argparse.ArgumentParser(add_help=False)
61
+ base_parser.add_argument(
62
+ "--config-dir",
63
+ type=str,
64
+ default=None,
65
+ help="Path to configuration directory (default: config/)",
66
+ )
67
+
68
+ # 'serve' subcommand
69
+ parser_serve = subparsers.add_parser(
70
+ "serve",
71
+ parents=[base_parser],
72
+ help="Start the Supernote Private Cloud server",
73
+ )
74
+ parser_serve.set_defaults(func=server_app.run)
75
+
76
+ # User management
77
+ parser_user = subparsers.add_parser("user", help="User management commands")
78
+ user_subparsers = parser_user.add_subparsers(dest="user_command")
79
+
80
+ # user list
81
+ parser_user_list = user_subparsers.add_parser(
82
+ "list",
83
+ parents=[base_parser],
84
+ help="List all users in the current config file",
85
+ )
86
+ parser_user_list.set_defaults(
87
+ func=lambda args: list_users(getattr(args, "config_dir", None))
88
+ )
89
+
90
+ # user add
91
+ parser_user_add = user_subparsers.add_parser(
92
+ "add",
93
+ parents=[base_parser],
94
+ help="Generate a user configuration snippet",
95
+ )
96
+ parser_user_add.add_argument("username", type=str, help="Username for the new user")
97
+ parser_user_add.add_argument(
98
+ "--password", type=str, help="Password (if omitted, prompt interactively)"
99
+ )
100
+ parser_user_add.set_defaults(
101
+ func=lambda args: add_user(args.username, args.password)
102
+ )
103
+
104
+ # config group
105
+ parser_config = subparsers.add_parser("config", help="Configuration utilities")
106
+ config_subparsers = parser_config.add_subparsers(dest="config_command")
107
+
108
+ # config init
109
+ parser_config_init = config_subparsers.add_parser(
110
+ "init",
111
+ parents=[base_parser],
112
+ help="Generate a default config.yaml",
113
+ )
114
+ parser_config_init.set_defaults(func=lambda _: config_init())
115
+
116
+ # config secret
117
+ parser_config_secret = config_subparsers.add_parser(
118
+ "generate-secret",
119
+ parents=[base_parser],
120
+ help="Generate a secure random secret",
121
+ )
122
+ parser_config_secret.set_defaults(func=lambda _: generate_secret())
123
+
124
+
125
+ def main():
126
+ parser = argparse.ArgumentParser(description="Supernote Server CLI")
127
+ subparsers = parser.add_subparsers(dest="command")
128
+ add_parser(subparsers)
129
+ args = parser.parse_args()
130
+ if hasattr(args, "func"):
131
+ args.func(args)
132
+ else:
133
+ parser.print_help()
134
+
135
+
136
+ if __name__ == "__main__":
137
+ main()
@@ -3,10 +3,10 @@
3
3
  """
4
4
 
5
5
  from .app import create_app, run
6
- from .config import Config
6
+ from .config import ServerConfig
7
7
 
8
8
  __all__ = [
9
9
  "create_app",
10
10
  "run",
11
- "Config",
11
+ "ServerConfig",
12
12
  ]
@@ -6,9 +6,9 @@ from typing import Any, Awaitable, Callable
6
6
 
7
7
  from aiohttp import web
8
8
 
9
- from supernote.server.services.user import JWT_ALGORITHM, JWT_SECRET
9
+ from supernote.server.services.user import JWT_ALGORITHM
10
10
 
11
- from . import config
11
+ from .config import ServerConfig
12
12
  from .models.base import create_error_response
13
13
  from .routes import auth, file, system
14
14
  from .services.file import FileService
@@ -56,8 +56,11 @@ async def trace_middleware(
56
56
  "body": body_str,
57
57
  }
58
58
 
59
+ # Get config from app
60
+ server_config: ServerConfig = request.app["config"]
61
+
59
62
  try:
60
- with open(config.TRACE_LOG_FILE, "a") as f:
63
+ with open(server_config.trace_log_file, "a") as f:
61
64
  f.write(json.dumps(log_entry) + "\n")
62
65
  f.flush()
63
66
  except Exception as e:
@@ -92,8 +95,12 @@ async def jwt_auth_middleware(
92
95
 
93
96
  import jwt
94
97
 
98
+ server_config: ServerConfig = request.app["config"]
99
+
95
100
  try:
96
- payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
101
+ payload = jwt.decode(
102
+ token, server_config.auth.secret_key, algorithms=[JWT_ALGORITHM]
103
+ )
97
104
  except jwt.InvalidTokenError:
98
105
  return web.json_response(
99
106
  create_error_response("Invalid token").to_dict(), status=401
@@ -102,16 +109,21 @@ async def jwt_auth_middleware(
102
109
  return await handler(request)
103
110
 
104
111
 
105
- def create_app() -> web.Application:
112
+ def create_app(config: ServerConfig | None = None) -> web.Application:
113
+ if config is None:
114
+ config = ServerConfig.load()
115
+
106
116
  app = web.Application(middlewares=[trace_middleware, jwt_auth_middleware])
117
+ app["config"] = config
107
118
 
108
119
  # Initialize services
109
- storage_root = Path(config.STORAGE_DIR)
120
+ storage_root = Path(config.storage_dir)
110
121
  temp_root = storage_root / "temp"
111
122
  storage_service = StorageService(storage_root, temp_root)
112
123
  app["storage_service"] = storage_service
113
- app["user_service"] = UserService(config.USER_CONFIG_FILE)
124
+ app["user_service"] = UserService(config.auth)
114
125
  app["file_service"] = FileService(storage_service)
126
+ app["sync_locks"] = {} # user -> (equipment_no, expiry_time)
115
127
 
116
128
  # Register routes
117
129
  app.add_routes(system.routes)
@@ -125,5 +137,7 @@ def create_app() -> web.Application:
125
137
 
126
138
  def run(args: Any) -> None:
127
139
  logging.basicConfig(level=logging.DEBUG)
128
- app = create_app()
129
- web.run_app(app, host=config.HOST, port=config.PORT)
140
+ config_dir = getattr(args, "config_dir", None)
141
+ config = ServerConfig.load(config_dir)
142
+ app = create_app(config)
143
+ web.run_app(app, host=config.host, port=config.port)
@@ -0,0 +1,159 @@
1
+ import logging
2
+ import os
3
+ import secrets
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import yaml
9
+ from mashumaro.mixins.dict import DataClassDictMixin
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ @dataclass
15
+ class AuthConfig(DataClassDictMixin):
16
+ secret_key: str = ""
17
+ expiration_hours: int = 24
18
+ users_file: str = "users.yaml"
19
+
20
+
21
+ @dataclass
22
+ class UserEntry(DataClassDictMixin):
23
+ username: str
24
+ password_md5: str
25
+ is_active: bool = True
26
+ devices: list[str] = field(default_factory=list)
27
+ profile: dict[str, Any] = field(default_factory=dict)
28
+ mobile: str | None = None
29
+ email: str | None = None
30
+ avatar: str | None = None
31
+ signature: str | None = None
32
+
33
+
34
+ @dataclass
35
+ class UsersConfig(DataClassDictMixin):
36
+ users: list[UserEntry] = field(default_factory=list)
37
+
38
+ @classmethod
39
+ def load(cls, path: str | Path) -> "UsersConfig":
40
+ path = Path(path)
41
+ if not path.exists():
42
+ return cls()
43
+ try:
44
+ with open(path, "r") as f:
45
+ data = yaml.safe_load(f) or {}
46
+ return cls.from_dict(data)
47
+ except Exception as e:
48
+ logger.warning(f"Failed to load users file {path}: {e}")
49
+ return cls()
50
+
51
+ def save(self, path: str | Path) -> None:
52
+ path = Path(path)
53
+ if not path.parent.exists():
54
+ path.parent.mkdir(parents=True, exist_ok=True)
55
+ with open(path, "w") as f:
56
+ yaml.safe_dump(self.to_dict(), f, default_flow_style=False)
57
+
58
+
59
+ @dataclass
60
+ class ServerConfig(DataClassDictMixin):
61
+ host: str = "0.0.0.0"
62
+ port: int = 8080
63
+ trace_log_file: str = "data/server_trace.log"
64
+ storage_dir: str = "storage"
65
+ auth: AuthConfig = field(default_factory=AuthConfig)
66
+
67
+ @classmethod
68
+ def load(cls, config_dir: str | Path | None = None) -> "ServerConfig":
69
+ """Load configuration from directory."""
70
+ # 1. Determine config directory
71
+ if config_dir is None:
72
+ config_dir = os.getenv("SUPERNOTE_CONFIG_DIR", "config")
73
+
74
+ config_dir_path = Path(config_dir)
75
+ config_file = config_dir_path / "config.yaml"
76
+
77
+ # 2. Load from YAML if exists
78
+ file_data: dict[str, Any] = {}
79
+ if file_exists := config_file.exists():
80
+ try:
81
+ with open(config_file, "r") as f:
82
+ file_data = yaml.safe_load(f) or {}
83
+ except Exception as e:
84
+ logger.warning(f"Failed to load config file {config_file}: {e}")
85
+
86
+ # 3. Create config object using mashumaro
87
+ try:
88
+ config = cls.from_dict(file_data)
89
+ except Exception as e:
90
+ logger.warning(
91
+ f"Failed to parse config file {config_file}, using defaults: {e}"
92
+ )
93
+ config = cls()
94
+
95
+ # Resolve users_file relative to config_dir if it's just a filename (not already a path)
96
+ users_path = Path(config.auth.users_file)
97
+ if not users_path.is_absolute() and len(users_path.parts) == 1:
98
+ config.auth.users_file = str(config_dir_path / users_path)
99
+
100
+ # 4. Generate secret if missing AND not provided by env var
101
+ env_secret = os.getenv("SUPERNOTE_JWT_SECRET")
102
+ secret_generated = False
103
+
104
+ if not config.auth.secret_key and not env_secret:
105
+ logger.warning("No JWT secret key configured. Generating a new random key.")
106
+ config.auth.secret_key = secrets.token_hex(32)
107
+ secret_generated = True
108
+
109
+ # 5. Save config if it was missing or if we generated a secret
110
+ if not file_exists or secret_generated:
111
+ try:
112
+ # Ensure directory exists
113
+ if not config_file.parent.exists():
114
+ config_file.parent.mkdir(parents=True, exist_ok=True)
115
+
116
+ # Prepare config for saving (restore relative paths if possible)
117
+ config_to_save = cls.from_dict(config.to_dict())
118
+ try:
119
+ users_path_abs = Path(config.auth.users_file)
120
+ if users_path_abs.is_absolute() and users_path_abs.is_relative_to(
121
+ config_dir_path
122
+ ):
123
+ config_to_save.auth.users_file = str(
124
+ users_path_abs.relative_to(config_dir_path)
125
+ )
126
+ except ValueError:
127
+ pass
128
+
129
+ with open(config_file, "w") as f:
130
+ yaml.safe_dump(
131
+ config_to_save.to_dict(), f, default_flow_style=False
132
+ )
133
+
134
+ if secret_generated:
135
+ logger.info(
136
+ f"Saved new configuration with generated secret to {config_file}"
137
+ )
138
+ else:
139
+ logger.info(f"Created default configuration file at {config_file}")
140
+ except Exception as e:
141
+ logger.warning(f"Failed to save configuration to {config_file}: {e}")
142
+
143
+ # 6. Apply env var override (runtime only, not saved)
144
+ if env_secret:
145
+ config.auth.secret_key = env_secret
146
+
147
+ if os.getenv("SUPERNOTE_HOST"):
148
+ config.host = os.getenv("SUPERNOTE_HOST", config.host)
149
+
150
+ if os.getenv("SUPERNOTE_PORT"):
151
+ try:
152
+ config.port = int(os.getenv("SUPERNOTE_PORT", str(config.port)))
153
+ except ValueError:
154
+ pass
155
+
156
+ if os.getenv("SUPERNOTE_STORAGE_DIR"):
157
+ config.storage_dir = os.getenv("SUPERNOTE_STORAGE_DIR", config.storage_dir)
158
+
159
+ return config
@@ -25,6 +25,17 @@ class SyncStartRequest(DataClassJSONMixin):
25
25
  code_generation_options = [TO_DICT_ADD_OMIT_NONE_FLAG] # type: ignore[list-item]
26
26
 
27
27
 
28
+ @dataclass
29
+ class SyncEndRequest(DataClassJSONMixin):
30
+ equipment_no: str = field(metadata=field_options(alias="equipmentNo"))
31
+ flag: str | None = None # "true" or "false"
32
+
33
+ class Config(BaseConfig):
34
+ serialize_by_alias = True
35
+ omit_none = True
36
+ code_generation_options = [TO_DICT_ADD_OMIT_NONE_FLAG] # type: ignore[list-item]
37
+
38
+
28
39
  @dataclass
29
40
  class ListFolderRequest(DataClassJSONMixin):
30
41
  path: str = "/"