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.
Files changed (70) hide show
  1. appxc/__init__.py +25 -0
  2. appxc/build/__init__.py +6 -0
  3. appxc/build/pyinstaller.py +409 -0
  4. appxc/config/__init__.py +5 -0
  5. appxc/config/cleanup_parts.py +18 -0
  6. appxc/config/config.py +152 -0
  7. appxc/email/__init__.py +3 -0
  8. appxc/email/sendmail.py +145 -0
  9. appxc/fileversions.py +197 -0
  10. appxc/gui/__init__.py +22 -0
  11. appxc/gui/application.py +169 -0
  12. appxc/gui/common.py +551 -0
  13. appxc/gui/config.py +139 -0
  14. appxc/gui/locale.py +19 -0
  15. appxc/gui/login.py +206 -0
  16. appxc/gui/manual_config_update.py +111 -0
  17. appxc/gui/registration_admin.py +385 -0
  18. appxc/gui/registration_user.py +313 -0
  19. appxc/gui/setting_base.py +176 -0
  20. appxc/gui/setting_dict.py +335 -0
  21. appxc/gui/setting_select.py +356 -0
  22. appxc/locale/appxc-gui.pot +131 -0
  23. appxc/locale/de/LC_MESSAGES/appxc-gui.po +134 -0
  24. appxc/locale/en/LC_MESSAGES/appxc-gui.po +131 -0
  25. appxc/logging.py +144 -0
  26. appxc/options.py +202 -0
  27. appxc/pyinstaller/__init__.py +18 -0
  28. appxc/pyinstaller/hook-appxc_private.py +10 -0
  29. appxc/registry/__init__.py +12 -0
  30. appxc/registry/_public_encryption.py +41 -0
  31. appxc/registry/_registration_request.py +92 -0
  32. appxc/registry/_registration_response.py +71 -0
  33. appxc/registry/_registry_base.py +81 -0
  34. appxc/registry/_signature.py +57 -0
  35. appxc/registry/_user_db.py +291 -0
  36. appxc/registry/_user_id.py +43 -0
  37. appxc/registry/registry.py +821 -0
  38. appxc/registry/shared_storage.py +202 -0
  39. appxc/registry/shared_sync.py +76 -0
  40. appxc/security/__init__.py +6 -0
  41. appxc/security/private_storage.py +79 -0
  42. appxc/security/security.py +588 -0
  43. appxc/setting/__init__.py +45 -0
  44. appxc/setting/base_types.py +299 -0
  45. appxc/setting/setting.py +604 -0
  46. appxc/setting/setting_dict.py +631 -0
  47. appxc/setting/setting_extension.py +94 -0
  48. appxc/setting/setting_select.py +271 -0
  49. appxc/stateful.py +210 -0
  50. appxc/storage/__init__.py +33 -0
  51. appxc/storage/buffer.py +164 -0
  52. appxc/storage/ftp.py +157 -0
  53. appxc/storage/local.py +88 -0
  54. appxc/storage/meta_data.py +30 -0
  55. appxc/storage/ram.py +100 -0
  56. appxc/storage/serializer.py +34 -0
  57. appxc/storage/serializer_compact.py +76 -0
  58. appxc/storage/serializer_json.py +160 -0
  59. appxc/storage/serializer_raw.py +19 -0
  60. appxc/storage/storable.py +61 -0
  61. appxc/storage/storage.py +821 -0
  62. appxc/storage/storage_to_bytes.py +65 -0
  63. appxc/storage/sync.py +262 -0
  64. appxc/utility/__init__.py +5 -0
  65. appxc/utility/ntptime.py +111 -0
  66. appxc-0.0.3.dev384.dist-info/METADATA +51 -0
  67. appxc-0.0.3.dev384.dist-info/RECORD +70 -0
  68. appxc-0.0.3.dev384.dist-info/WHEEL +4 -0
  69. appxc-0.0.3.dev384.dist-info/entry_points.txt +2 -0
  70. 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
@@ -0,0 +1,6 @@
1
+ # Copyright 2026 the contributors of APPXC (github.com/alexander-nbg/appxc)
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Facade for APPXC build utilities"""
4
+
5
+ # TODO: when growing, this module also include other development tools and need
6
+ # renaming.
@@ -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()
@@ -0,0 +1,5 @@
1
+ # Copyright 2024-2026 the contributors of APPXC (github.com/alexander-nbg/appxc)
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Facade for APPXC config module"""
4
+
5
+ from .config import AppxcConfigError, Config
@@ -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()
@@ -0,0 +1,3 @@
1
+ # Copyright 2026 the contributors of APPXC (github.com/alexander-nbg/appxc)
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Facade for APPXC email module"""