appxc 0.0.3.dev384__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.
- appxc/__init__.py +25 -0
- appxc/build/__init__.py +6 -0
- appxc/build/pyinstaller.py +409 -0
- appxc/config/__init__.py +5 -0
- appxc/config/cleanup_parts.py +18 -0
- appxc/config/config.py +152 -0
- appxc/email/__init__.py +3 -0
- appxc/email/sendmail.py +145 -0
- appxc/fileversions.py +197 -0
- appxc/gui/__init__.py +22 -0
- appxc/gui/application.py +169 -0
- appxc/gui/common.py +551 -0
- appxc/gui/config.py +139 -0
- appxc/gui/locale.py +19 -0
- appxc/gui/login.py +206 -0
- appxc/gui/manual_config_update.py +111 -0
- appxc/gui/registration_admin.py +385 -0
- appxc/gui/registration_user.py +313 -0
- appxc/gui/setting_base.py +176 -0
- appxc/gui/setting_dict.py +335 -0
- appxc/gui/setting_select.py +356 -0
- appxc/locale/appxc-gui.pot +131 -0
- appxc/locale/de/LC_MESSAGES/appxc-gui.po +134 -0
- appxc/locale/en/LC_MESSAGES/appxc-gui.po +131 -0
- appxc/logging.py +144 -0
- appxc/options.py +202 -0
- appxc/pyinstaller/__init__.py +18 -0
- appxc/pyinstaller/hook-appxc_private.py +10 -0
- appxc/registry/__init__.py +12 -0
- appxc/registry/_public_encryption.py +41 -0
- appxc/registry/_registration_request.py +92 -0
- appxc/registry/_registration_response.py +71 -0
- appxc/registry/_registry_base.py +81 -0
- appxc/registry/_signature.py +57 -0
- appxc/registry/_user_db.py +291 -0
- appxc/registry/_user_id.py +43 -0
- appxc/registry/registry.py +821 -0
- appxc/registry/shared_storage.py +202 -0
- appxc/registry/shared_sync.py +76 -0
- appxc/security/__init__.py +6 -0
- appxc/security/private_storage.py +79 -0
- appxc/security/security.py +588 -0
- appxc/setting/__init__.py +45 -0
- appxc/setting/base_types.py +299 -0
- appxc/setting/setting.py +604 -0
- appxc/setting/setting_dict.py +631 -0
- appxc/setting/setting_extension.py +94 -0
- appxc/setting/setting_select.py +271 -0
- appxc/stateful.py +210 -0
- appxc/storage/__init__.py +33 -0
- appxc/storage/buffer.py +164 -0
- appxc/storage/ftp.py +157 -0
- appxc/storage/local.py +88 -0
- appxc/storage/meta_data.py +30 -0
- appxc/storage/ram.py +100 -0
- appxc/storage/serializer.py +34 -0
- appxc/storage/serializer_compact.py +76 -0
- appxc/storage/serializer_json.py +160 -0
- appxc/storage/serializer_raw.py +19 -0
- appxc/storage/storable.py +61 -0
- appxc/storage/storage.py +821 -0
- appxc/storage/storage_to_bytes.py +65 -0
- appxc/storage/sync.py +262 -0
- appxc/utility/__init__.py +5 -0
- appxc/utility/ntptime.py +111 -0
- appxc-0.0.3.dev384.dist-info/METADATA +51 -0
- appxc-0.0.3.dev384.dist-info/RECORD +70 -0
- appxc-0.0.3.dev384.dist-info/WHEEL +4 -0
- appxc-0.0.3.dev384.dist-info/entry_points.txt +2 -0
- appxc-0.0.3.dev384.dist-info/licenses/LICENSE +50 -0
appxc/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Copyright 2023-2026 the contributors of APPXC (github.com/alexander-nbg/appxc)
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Facade for APPXC basic classes"""
|
|
4
|
+
# Basic classes exposed here shall only have dependencies to python builtin
|
|
5
|
+
# modules. This facade shall not expose any object from sub-modules. Rationale:
|
|
6
|
+
# using APPXC shall not enforce loading of unnecessary dependencies which are
|
|
7
|
+
# typically present in sub-modules.
|
|
8
|
+
|
|
9
|
+
# try:
|
|
10
|
+
# from ._version import __version__ as __version__
|
|
11
|
+
# except ImportError:
|
|
12
|
+
# # Package was not installed; version metadata is unknown (e.g. running directly
|
|
13
|
+
# # from source without hatch-vcs build hook execution).
|
|
14
|
+
# __version__: str = "0.0.0.dev0+unknown"
|
|
15
|
+
|
|
16
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
__version__ = version("package-name")
|
|
20
|
+
except PackageNotFoundError:
|
|
21
|
+
# package is not installed
|
|
22
|
+
__version__ = "0.0.0.dev0+unknown"
|
|
23
|
+
|
|
24
|
+
from .options import Options
|
|
25
|
+
from .stateful import Stateful
|
appxc/build/__init__.py
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
# Copyright 2026 the contributors of APPXC (github.com/alexander-nbg/appxc)
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Cross-platform PyInstaller build script.
|
|
4
|
+
|
|
5
|
+
This script provides a unified way to build Python applications using PyInstaller
|
|
6
|
+
across different platforms (Linux, Windows, macOS). It handles virtual environment
|
|
7
|
+
creation, dependency installation, and packaging.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import platform
|
|
12
|
+
import shutil
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
import time
|
|
16
|
+
import traceback
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
# Build directory paths (module-level constants)
|
|
20
|
+
BUILD_DIR = Path("./build")
|
|
21
|
+
ENV_PATH = BUILD_DIR / ".env"
|
|
22
|
+
DIST_PATH = BUILD_DIR / "dist"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def elapsed_time(start: float) -> str:
|
|
26
|
+
"""Return elapsed time in seconds with one decimal place."""
|
|
27
|
+
return f"{time.time() - start:.1f}s"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class _StepLogger:
|
|
31
|
+
"""Context manager for logging build steps with timing."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, message: str):
|
|
34
|
+
self.message = message
|
|
35
|
+
self.start_time = None
|
|
36
|
+
|
|
37
|
+
def __enter__(self):
|
|
38
|
+
print(f"\n{'=' * 60}")
|
|
39
|
+
print(self.message)
|
|
40
|
+
print(f"{'-' * 60}")
|
|
41
|
+
self.start_time = time.time()
|
|
42
|
+
return self
|
|
43
|
+
|
|
44
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
45
|
+
if exc_type is None:
|
|
46
|
+
print(f"{'-' * 60}")
|
|
47
|
+
print(f"Done ({elapsed_time(self.start_time)}); {self.message}")
|
|
48
|
+
print(f"{'=' * 60}")
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _run_command(
|
|
53
|
+
cmd: list[str],
|
|
54
|
+
shell: bool = False,
|
|
55
|
+
check: bool = True,
|
|
56
|
+
verbose: bool = False,
|
|
57
|
+
**kwargs,
|
|
58
|
+
) -> subprocess.CompletedProcess:
|
|
59
|
+
"""Run a command and return the result."""
|
|
60
|
+
if verbose:
|
|
61
|
+
print(f"Running: {' '.join(cmd)}")
|
|
62
|
+
|
|
63
|
+
# Suppress output unless verbose or capture_output is set
|
|
64
|
+
should_capture = not verbose and "capture_output" not in kwargs
|
|
65
|
+
|
|
66
|
+
run_kwargs = {
|
|
67
|
+
"shell": shell,
|
|
68
|
+
**kwargs,
|
|
69
|
+
}
|
|
70
|
+
if should_capture:
|
|
71
|
+
run_kwargs["capture_output"] = True
|
|
72
|
+
run_kwargs["text"] = True
|
|
73
|
+
|
|
74
|
+
result = subprocess.run(cmd, check=check, **run_kwargs) # noqa: S603 cmd originating from internal function.
|
|
75
|
+
|
|
76
|
+
# Only print if there's an error or warning
|
|
77
|
+
if should_capture and result.stderr:
|
|
78
|
+
print(result.stderr, file=sys.stderr)
|
|
79
|
+
|
|
80
|
+
return result
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def create_venv(cleanup: bool = False, verbose: bool = False) -> None:
|
|
84
|
+
"""Create a virtual environment at ./build/.env"""
|
|
85
|
+
# Remove existing environment if cleanup is requested
|
|
86
|
+
if cleanup and ENV_PATH.exists():
|
|
87
|
+
print(f" Cleaning: Removing existing environment at {ENV_PATH}")
|
|
88
|
+
shutil.rmtree(ENV_PATH)
|
|
89
|
+
|
|
90
|
+
# Create parent directory
|
|
91
|
+
ENV_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
92
|
+
|
|
93
|
+
# Use system default Python
|
|
94
|
+
if not ENV_PATH.exists():
|
|
95
|
+
_run_command(["python", "-m", "venv", str(ENV_PATH)], verbose=verbose)
|
|
96
|
+
print(f" Virtual environment created at {ENV_PATH}")
|
|
97
|
+
else:
|
|
98
|
+
print(f" Virtual environment already exists at {ENV_PATH}")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_activation_script() -> str:
|
|
102
|
+
"""Get the activation script command for the virtual environment."""
|
|
103
|
+
if platform.system() == "Windows":
|
|
104
|
+
return str(ENV_PATH / "Scripts" / "activate.bat")
|
|
105
|
+
return f"source {ENV_PATH / 'bin' / 'activate'}"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _run_in_venv(
|
|
109
|
+
cmd: list[str],
|
|
110
|
+
verbose: bool = False,
|
|
111
|
+
**kwargs,
|
|
112
|
+
) -> subprocess.CompletedProcess:
|
|
113
|
+
"""Run a command within the virtual environment."""
|
|
114
|
+
# Use the full path to the venv executables (works on all platforms)
|
|
115
|
+
if platform.system() == "Windows":
|
|
116
|
+
venv_bin = ENV_PATH / "Scripts"
|
|
117
|
+
exe_suffix = ".exe"
|
|
118
|
+
else:
|
|
119
|
+
venv_bin = ENV_PATH / "bin"
|
|
120
|
+
exe_suffix = ""
|
|
121
|
+
|
|
122
|
+
# Replace python/pip/pyinstaller with full venv path
|
|
123
|
+
if cmd[0] in ["python", "pip", "pyinstaller"]:
|
|
124
|
+
cmd[0] = str(venv_bin / f"{cmd[0]}{exe_suffix}")
|
|
125
|
+
|
|
126
|
+
return _run_command(cmd, verbose=verbose, **kwargs)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def install_requirements(
|
|
130
|
+
requirements_files: list[Path],
|
|
131
|
+
editable_packages: list[Path],
|
|
132
|
+
verbose: bool = False,
|
|
133
|
+
) -> None:
|
|
134
|
+
"""Install requirements in the virtual environment."""
|
|
135
|
+
# Install requirements files
|
|
136
|
+
for req_file in requirements_files:
|
|
137
|
+
if req_file.exists():
|
|
138
|
+
print(f" Installing from {req_file}")
|
|
139
|
+
_run_in_venv(["pip", "install", "-r", str(req_file)], verbose=verbose)
|
|
140
|
+
else:
|
|
141
|
+
print(f" Warning: Requirements file {req_file} not found, skipping")
|
|
142
|
+
|
|
143
|
+
# Install editable packages
|
|
144
|
+
for package in editable_packages:
|
|
145
|
+
if package.exists():
|
|
146
|
+
print(f" Installing editable package from {package}")
|
|
147
|
+
_run_in_venv(["pip", "install", "-e", str(package)], verbose=verbose)
|
|
148
|
+
else:
|
|
149
|
+
print(f" Warning: Editable package path {package} not found, skipping")
|
|
150
|
+
|
|
151
|
+
# Install PyInstaller
|
|
152
|
+
print(" Installing PyInstaller")
|
|
153
|
+
_run_in_venv(["pip", "install", "pyinstaller"], verbose=verbose)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def build(
|
|
157
|
+
main_file: Path,
|
|
158
|
+
debug_build: bool = False,
|
|
159
|
+
hidden_imports: list[str] | None = None,
|
|
160
|
+
strip: bool = True,
|
|
161
|
+
verbose: bool = False,
|
|
162
|
+
) -> None:
|
|
163
|
+
"""Build executable via PyInstaller"""
|
|
164
|
+
# Base command
|
|
165
|
+
cmd = [
|
|
166
|
+
"python",
|
|
167
|
+
"-m",
|
|
168
|
+
"PyInstaller",
|
|
169
|
+
str(main_file),
|
|
170
|
+
"--onefile",
|
|
171
|
+
"--clean",
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
# Add hidden imports
|
|
175
|
+
if hidden_imports:
|
|
176
|
+
for imp in hidden_imports:
|
|
177
|
+
cmd.extend(["--hidden-import", imp])
|
|
178
|
+
|
|
179
|
+
# Add strip option (only for Linux/macOS)
|
|
180
|
+
if strip and platform.system() != "Windows":
|
|
181
|
+
cmd.append("--strip")
|
|
182
|
+
|
|
183
|
+
# Add debug options
|
|
184
|
+
if debug_build:
|
|
185
|
+
cmd.extend(["--debug", "all"])
|
|
186
|
+
cmd.extend(["--name", f"{main_file.stem}_debug"])
|
|
187
|
+
|
|
188
|
+
# Set dist path
|
|
189
|
+
cmd.extend(["--distpath", str(DIST_PATH)])
|
|
190
|
+
|
|
191
|
+
_run_in_venv(cmd, verbose=verbose)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def copy_additional_files(files: list[Path]) -> None:
|
|
195
|
+
"""Copy additional files to the dist directory."""
|
|
196
|
+
DIST_PATH.mkdir(parents=True, exist_ok=True)
|
|
197
|
+
|
|
198
|
+
for file_path in files:
|
|
199
|
+
if file_path.exists():
|
|
200
|
+
dest = DIST_PATH / file_path.name
|
|
201
|
+
print(f" Copying {file_path} -> {dest}")
|
|
202
|
+
shutil.copy2(file_path, dest)
|
|
203
|
+
else:
|
|
204
|
+
print(f" Warning: File {file_path} not found, skipping")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def save_build_info(verbose: bool = False) -> None:
|
|
208
|
+
"""Save Python version and package information."""
|
|
209
|
+
DIST_PATH.mkdir(parents=True, exist_ok=True)
|
|
210
|
+
info_file = DIST_PATH / "python_versions.txt"
|
|
211
|
+
|
|
212
|
+
with open(info_file, "w") as f:
|
|
213
|
+
# Get Python version
|
|
214
|
+
result = _run_in_venv(
|
|
215
|
+
["python", "--version"],
|
|
216
|
+
capture_output=True,
|
|
217
|
+
text=True,
|
|
218
|
+
verbose=verbose,
|
|
219
|
+
)
|
|
220
|
+
f.write(result.stdout)
|
|
221
|
+
|
|
222
|
+
# Get pip freeze
|
|
223
|
+
result = _run_in_venv(
|
|
224
|
+
["pip", "freeze"],
|
|
225
|
+
capture_output=True,
|
|
226
|
+
text=True,
|
|
227
|
+
verbose=verbose,
|
|
228
|
+
)
|
|
229
|
+
f.write(result.stdout)
|
|
230
|
+
|
|
231
|
+
print(f" Build information saved to {info_file}")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def main():
|
|
235
|
+
"""Main entry point"""
|
|
236
|
+
parser = argparse.ArgumentParser(
|
|
237
|
+
description="APPXC PyInstaller build script",
|
|
238
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
239
|
+
epilog="""
|
|
240
|
+
Examples:
|
|
241
|
+
%(prog)s --main-file app.py
|
|
242
|
+
%(prog)s -m app.py --additional-files data.csv config.ini
|
|
243
|
+
%(prog)s -m app.py -a template.xlsx --clean --verbose
|
|
244
|
+
%(prog)s -m app.py --hidden-imports babel.numbers --no-strip
|
|
245
|
+
|
|
246
|
+
Build outputs:
|
|
247
|
+
- Virtual environment: ./build/.env
|
|
248
|
+
- Distribution files: ./build/dist/
|
|
249
|
+
""",
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Mandatory arguments
|
|
253
|
+
parser.add_argument(
|
|
254
|
+
"-m",
|
|
255
|
+
"--main-file",
|
|
256
|
+
required=True,
|
|
257
|
+
type=Path,
|
|
258
|
+
help="Main Python file to build",
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Optional arguments
|
|
262
|
+
parser.add_argument(
|
|
263
|
+
"-a",
|
|
264
|
+
"--additional-files",
|
|
265
|
+
type=Path,
|
|
266
|
+
nargs="*",
|
|
267
|
+
default=[],
|
|
268
|
+
help="Additional files to copy to dist directory",
|
|
269
|
+
)
|
|
270
|
+
parser.add_argument(
|
|
271
|
+
"-r",
|
|
272
|
+
"--requirements",
|
|
273
|
+
type=Path,
|
|
274
|
+
nargs="*",
|
|
275
|
+
default=[],
|
|
276
|
+
help=("Additional requirements files to install (default: requirements.txt)"),
|
|
277
|
+
)
|
|
278
|
+
parser.add_argument(
|
|
279
|
+
"--editable-packages",
|
|
280
|
+
type=Path,
|
|
281
|
+
nargs="*",
|
|
282
|
+
default=[],
|
|
283
|
+
help="Paths to packages to install in editable mode (default: appxc)",
|
|
284
|
+
)
|
|
285
|
+
parser.add_argument(
|
|
286
|
+
"--hidden-imports",
|
|
287
|
+
nargs="*",
|
|
288
|
+
default=[],
|
|
289
|
+
help="Hidden imports for PyInstaller",
|
|
290
|
+
)
|
|
291
|
+
parser.add_argument(
|
|
292
|
+
"--no-strip",
|
|
293
|
+
action="store_true",
|
|
294
|
+
help="Disable strip option for PyInstaller",
|
|
295
|
+
)
|
|
296
|
+
parser.add_argument(
|
|
297
|
+
"--debug",
|
|
298
|
+
action="store_true",
|
|
299
|
+
help="Also create a debug build",
|
|
300
|
+
)
|
|
301
|
+
parser.add_argument(
|
|
302
|
+
"--clean",
|
|
303
|
+
action="store_true",
|
|
304
|
+
help="Clean build directory before building",
|
|
305
|
+
)
|
|
306
|
+
parser.add_argument(
|
|
307
|
+
"--verbose",
|
|
308
|
+
action="store_true",
|
|
309
|
+
help="Show detailed command output",
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
args = parser.parse_args()
|
|
313
|
+
|
|
314
|
+
# Convert to absolute paths
|
|
315
|
+
main_file = args.main_file.resolve()
|
|
316
|
+
|
|
317
|
+
# Set default requirements if none provided
|
|
318
|
+
requirements_files = args.requirements or [
|
|
319
|
+
Path("requirements.txt"),
|
|
320
|
+
]
|
|
321
|
+
|
|
322
|
+
# Set default editable packages if none provided
|
|
323
|
+
editable_packages = args.editable_packages or [
|
|
324
|
+
Path("appxc"),
|
|
325
|
+
]
|
|
326
|
+
|
|
327
|
+
print(f"{'=' * 60}")
|
|
328
|
+
print("PyInstaller Build Script")
|
|
329
|
+
print(f"{'=' * 60}")
|
|
330
|
+
print(f"Platform: {platform.system()}")
|
|
331
|
+
print(f"Main file: {main_file}")
|
|
332
|
+
print("Build directory: ./build/")
|
|
333
|
+
print(f"Clean build: {args.clean}")
|
|
334
|
+
print(f"Verbose: {args.verbose}")
|
|
335
|
+
print(f"{'=' * 60}")
|
|
336
|
+
|
|
337
|
+
# Validate main file exists
|
|
338
|
+
if not main_file.exists():
|
|
339
|
+
print(f"Error: Main file {main_file} not found!")
|
|
340
|
+
sys.exit(1)
|
|
341
|
+
|
|
342
|
+
start_total = time.time()
|
|
343
|
+
|
|
344
|
+
try:
|
|
345
|
+
# Clean build directory if requested
|
|
346
|
+
if args.clean and BUILD_DIR.exists():
|
|
347
|
+
print(f"\nCleaning build directory: {BUILD_DIR}")
|
|
348
|
+
shutil.rmtree(BUILD_DIR)
|
|
349
|
+
|
|
350
|
+
# Create virtual environment
|
|
351
|
+
with _StepLogger("Creating virtual environment"):
|
|
352
|
+
create_venv(cleanup=args.clean, verbose=args.verbose)
|
|
353
|
+
|
|
354
|
+
# Install requirements
|
|
355
|
+
with _StepLogger("Installing requirements"):
|
|
356
|
+
install_requirements(
|
|
357
|
+
requirements_files,
|
|
358
|
+
editable_packages,
|
|
359
|
+
verbose=args.verbose,
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
# Build with PyInstaller (release)
|
|
363
|
+
with _StepLogger("Building release version with PyInstaller"):
|
|
364
|
+
build(
|
|
365
|
+
main_file,
|
|
366
|
+
debug_build=False,
|
|
367
|
+
hidden_imports=args.hidden_imports or None,
|
|
368
|
+
strip=not args.no_strip,
|
|
369
|
+
verbose=args.verbose,
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
# Build debug version if requested
|
|
373
|
+
if args.debug:
|
|
374
|
+
with _StepLogger("Building debug version with PyInstaller"):
|
|
375
|
+
build(
|
|
376
|
+
main_file,
|
|
377
|
+
debug_build=True,
|
|
378
|
+
hidden_imports=args.hidden_imports or None,
|
|
379
|
+
strip=not args.no_strip,
|
|
380
|
+
verbose=args.verbose,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
# Copy additional files
|
|
384
|
+
if args.additional_files:
|
|
385
|
+
with _StepLogger("Copying additional files to dist"):
|
|
386
|
+
copy_additional_files(args.additional_files)
|
|
387
|
+
|
|
388
|
+
# Save build info
|
|
389
|
+
with _StepLogger("Saving build information"):
|
|
390
|
+
save_build_info(verbose=args.verbose)
|
|
391
|
+
|
|
392
|
+
print(f"\n{'=' * 60}")
|
|
393
|
+
print(
|
|
394
|
+
f"Build completed successfully! (Total time: {elapsed_time(start_total)})",
|
|
395
|
+
)
|
|
396
|
+
print(f"{'=' * 60}")
|
|
397
|
+
|
|
398
|
+
except subprocess.CalledProcessError as e:
|
|
399
|
+
print(f"\nError: Build failed with exit code {e.returncode}")
|
|
400
|
+
sys.exit(1)
|
|
401
|
+
except Exception as e:
|
|
402
|
+
print(f"\nError: {e}")
|
|
403
|
+
|
|
404
|
+
traceback.print_exc()
|
|
405
|
+
sys.exit(1)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
if __name__ == "__main__":
|
|
409
|
+
main()
|
appxc/config/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Copyright 2024-2026 the contributors of APPXC (github.com/alexander-nbg/appxc)
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
# TODO: reactivate storing and loading sections as INI files
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
import configparser
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ToolConfigParser(configparser.ConfigParser):
|
|
11
|
+
"""Internal helper with predefined settings for ConfigParser()"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, **kwargs):
|
|
14
|
+
# Note that we need to change the comment prefix to something else and
|
|
15
|
+
# allow "no values" to keep them as keys without values. Other
|
|
16
|
+
# functions must then ignore the "#" keys.
|
|
17
|
+
super().__init__(comment_prefixes="/", allow_no_value=True, **kwargs)
|
|
18
|
+
self.optionxform = str
|
appxc/config/config.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Copyright 2024-2026 the contributors of APPXC (github.com/alexander-nbg/appxc)
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Provide Configuration Handling
|
|
4
|
+
|
|
5
|
+
The configuration concept is accumulating APPXC SettingDict objects as
|
|
6
|
+
sections.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from collections.abc import Mapping
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from appxc import logging
|
|
13
|
+
from appxc.setting import SettingDict
|
|
14
|
+
from appxc.storage import RamStorage, Storage
|
|
15
|
+
|
|
16
|
+
# TODO: config refactoring
|
|
17
|
+
# 1) I need the serialize/deserialize on dictionaries as storable >> to mary
|
|
18
|
+
# it with a factory
|
|
19
|
+
# >> Storage does not exist until storing or loading!
|
|
20
|
+
# 2) Same as above based on configparser. Intended is to combine with
|
|
21
|
+
# unsecured storage but could theoretically be secured.
|
|
22
|
+
#
|
|
23
|
+
# NOTE: (1) and (2) could initially be the same. I do not care much about
|
|
24
|
+
# storage size such I could implement only (1)
|
|
25
|
+
#
|
|
26
|
+
# 3) option config must become section-specific
|
|
27
|
+
# 4) fields must become private
|
|
28
|
+
# 5) get on section must return the same dict as used internally!
|
|
29
|
+
# !! The options maintaining dict must use appropriate types
|
|
30
|
+
# ?? Will it become necessary to define the type on option definition
|
|
31
|
+
# ** It would be nice to configure an option directly on creation
|
|
32
|
+
|
|
33
|
+
# TODO: support of translations for section naming since they will pop up in
|
|
34
|
+
# menus. Note that this applies likewise to settings in the SettingDict's.
|
|
35
|
+
|
|
36
|
+
# TODO: Consider enforcing full lower-case (or full upper-case) section names.
|
|
37
|
+
# Currently, this is not fixed and when checking for existing sections, it's
|
|
38
|
+
# never clear whether upper() should be applied. The best solution would be to
|
|
39
|
+
# support the ["this-section" in config] syntax where the implementation
|
|
40
|
+
# handles the intended case-insensitivity the current usage is like
|
|
41
|
+
# ["this-section" in config.sections()]. This problem applies likewise to
|
|
42
|
+
# SettingDict. If touched, it should also consider the translations solution.
|
|
43
|
+
|
|
44
|
+
# TODO: recover INI file handling:
|
|
45
|
+
# 1) Loading sections from INI file
|
|
46
|
+
# 2) Store all sections to INI file
|
|
47
|
+
#
|
|
48
|
+
# Note: Section-wise INI handling would imply each section providing it's part.
|
|
49
|
+
# But the file is not section-specific.<
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class AppxcConfigError(Exception):
|
|
53
|
+
"""General config error"""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Config:
|
|
57
|
+
"""Organize configuration settings.
|
|
58
|
+
|
|
59
|
+
Configuration typically splits into several sets of properties for USER or
|
|
60
|
+
tool access related options. A Config object collects those configuration
|
|
61
|
+
sections (internally as SettingDict objects) and adds some
|
|
62
|
+
convenience:
|
|
63
|
+
* load/store of all sections
|
|
64
|
+
* store/load into one human readable INI file to assist the tool
|
|
65
|
+
developers. !! Everything including passwords !!
|
|
66
|
+
|
|
67
|
+
In an application, it is recommended to initialize the the aplication parts
|
|
68
|
+
with the APPXC SettingDict's. They are shared by value and can be loaded
|
|
69
|
+
after initialization.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
log = logging.get_logger(__name__ + ".Config")
|
|
73
|
+
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
default_storage_factory: Storage.Factory | None = None,
|
|
77
|
+
):
|
|
78
|
+
self._default_storage_factory = default_storage_factory
|
|
79
|
+
self._sections: dict[str, SettingDict] = {}
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def sections(self) -> list[str]:
|
|
83
|
+
"""Return list of sections."""
|
|
84
|
+
return list(self._sections.keys())
|
|
85
|
+
|
|
86
|
+
def section(self, section: str) -> SettingDict:
|
|
87
|
+
"""Access a section"""
|
|
88
|
+
if section not in self._sections:
|
|
89
|
+
raise AppxcConfigError(
|
|
90
|
+
f"Cannot access section {section}, it does not exist. "
|
|
91
|
+
f"Existing are: {self.sections}.",
|
|
92
|
+
)
|
|
93
|
+
return self._sections[section]
|
|
94
|
+
|
|
95
|
+
def add_section(
|
|
96
|
+
self,
|
|
97
|
+
section: str,
|
|
98
|
+
storage_factory: Storage.Factory | None = None,
|
|
99
|
+
settings: Mapping[str, Any] | None = None,
|
|
100
|
+
) -> SettingDict:
|
|
101
|
+
"""Add section if not yet existing."""
|
|
102
|
+
# ensure section does not yet exist:
|
|
103
|
+
if section in self._sections:
|
|
104
|
+
raise AppxcConfigError(
|
|
105
|
+
f"Cannot add section {section}, it does already exist.",
|
|
106
|
+
)
|
|
107
|
+
# define storage
|
|
108
|
+
if storage_factory is not None:
|
|
109
|
+
storage = storage_factory(section)
|
|
110
|
+
elif self._default_storage_factory is not None:
|
|
111
|
+
storage = self._default_storage_factory(section)
|
|
112
|
+
else:
|
|
113
|
+
storage = RamStorage()
|
|
114
|
+
# construct section:
|
|
115
|
+
# * SettingDict will take over the section name
|
|
116
|
+
self._sections[section] = SettingDict(
|
|
117
|
+
storage=storage,
|
|
118
|
+
settings=settings,
|
|
119
|
+
name=section,
|
|
120
|
+
)
|
|
121
|
+
# * Config will, by default, not raise exceptions on import when
|
|
122
|
+
# a config option is new or missing
|
|
123
|
+
export_options = SettingDict.ExportOptions(
|
|
124
|
+
exception_on_new_key=False,
|
|
125
|
+
exception_on_missing_key=False,
|
|
126
|
+
).get_state()
|
|
127
|
+
self._sections[section].get_state_kwargs = {"options": export_options}
|
|
128
|
+
self._sections[section].set_state_kwargs = {"options": export_options}
|
|
129
|
+
|
|
130
|
+
self.log.info("added section: %s", section)
|
|
131
|
+
return self._sections[section]
|
|
132
|
+
|
|
133
|
+
def remove_section(self, section: str):
|
|
134
|
+
"""Remove section"""
|
|
135
|
+
if section in self._sections:
|
|
136
|
+
del self._sections[section]
|
|
137
|
+
self.log.info("removed section: %s", section)
|
|
138
|
+
else:
|
|
139
|
+
raise AppxcConfigError(
|
|
140
|
+
f"Cannot remove section, it does not exist: {section}",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
def store(self):
|
|
144
|
+
"""Store all sections"""
|
|
145
|
+
for section in self._sections.values():
|
|
146
|
+
section.store()
|
|
147
|
+
|
|
148
|
+
def load(self):
|
|
149
|
+
"""Load all sections"""
|
|
150
|
+
for section in self._sections.values():
|
|
151
|
+
if section.exists():
|
|
152
|
+
section.load()
|
appxc/email/__init__.py
ADDED