[](https://gist.github.com/cheerfulstoic/d107229326a01ff0f333a1d3476e068d)
[](https://github.com/Adalfarus/dancer/actions)
[](https://github.com/Adalfarus/dancer/blob/main/LICENSE)
[//]: # (<div style="display: flex; align-items: center; width: 100%;">)
[//]: # ( <img src="project_data/dancer.png" style="height: 10vw;">)
[//]: # ( <p style="margin: 0 0 0 2vw; font-size: 10vw; color: #3b4246;">Dancer</p>)
[//]: # (</div>)
<img src="https://github.com/adalfarus/dancer/blob/main/project_data/img.png">
dancer is a simple, and user-friendly Python library for creating competent apps.
## Compatibility
π© (Works perfectly); π¨ (Untested); π§ (Some Issues); π₯ (Unusable)
| OS | UX & README instructions | Tests | More Complex Functionalities |
|--------------------------|--------------------------|-------|------------------------------|
| Windows | π© | π© | π© |
| MacOS | π¨ | π© | π¨ |
| Linux (Ubuntu 22.04 LTS) | π© | π© | π© |
## Features
* **Makes user-specific storage easy**
β Automatically sets up per-user directories (e.g., config, logs, plugins, styling) to isolate application data and avoid conflicts across users.
* **Comes with various data storage and encryption built-in \[IN THE FULL RELEASE]**
β Full release will offer structured storage layers, file-based persistence, encryption helpers, and cryptographic utilitiesβsome adapted from [`aplustools`](https://pypi.org/project/aplustools/).
* **Sets up a good environment for your app to run in**
β Automatically constructs an organized project tree, validates Python + OS compatibility, allows for easy restarts when compiled, and supports dynamic extension loading.
* **Supports Windows, Linux and MacOS**
β Fully cross-platform with native support for common operating systems and runtime configurations.
> β οΈ These features are part of a larger toolkit. The examples below (GUI/TUI bootstrapping via `config` and `start`) are **only one part** of what this package enables.
## Installation
You can install `dancer` via pip:
```sh
pip install dancer --upgrade
```
If you want to use Qt-specific components:
```sh
pip install dancer[qt] --upgrade
```
This installs PySide6 which enables you to use e.g. the class DefaultQtGUIApp which has features like theme management, and more.
Or clone the repository and install manually:
```sh
git clone https://github.com/Adalfarus/dancer.git
cd dancer
python -m build
```
## Usage
Here are a few quick examples of how to use `dancer`:
### TUI Server Example
This creates a simple Flask-based TUI server. It uses `DefaultServerTUI` to manage config state, run settings, and integrate logging, but it does **not** rely on dancers Qt components. The Qt-based system tray would not make enough use of the advanced features they provide.
> This example focuses on config validation, modular settings loading, Flask integration, and minimal runtime bootstrapping.
````python
from dancer import config, start, DefaultServerTUI
from werkzeug.serving import make_server, BaseWSGIServer
from argparse import ArgumentParser, Namespace
from threading import Thread
import logging
import json
import sys
import os
from PySide6.QtWidgets import QApplication, QSystemTrayIcon, QMenu, QPlainTextEdit
from PySide6.QtGui import QIcon, QAction
import typing as _ty
class QtConsoleHandler(logging.Handler):
def __init__(self, widget: QPlainTextEdit):
super().__init__()
self.widget = widget
def emit(self, record):
msg = self.format(record)
self.widget.appendPlainText(msg)
class App(DefaultServerTUI):
def __init__(self, parsed_args: Namespace, logging_mode: int) -> None:
super().__init__(os.path.abspath("./latest.log"), parsed_args, logging_mode, always_restart=True)
from common.app import
create_app # This is a relative import from appdata, so we can only do it after we ran config()
self.logger.info("Creating Flask Server ...")
self.config_path: str = os.path.abspath("./config/core.json")
settings_path: str = parsed_args.load_config_path or self.config_path
self.app_settings: dict[str, _ty.Any] = self.load_settings_from_file(settings_path)
self.app_settings.update(vars(parsed_args))
self.write_settings_to_file(self.app_settings.copy(), self.config_path)
self.console_window = None
self.app = create_app(self.app_settings)
self.ssl_context: None | tuple[str, str] = None
self.server: BaseWSGIServer | None = None
self.thread: Thread | None = None
if parsed_args.run_ssl:
if config.INDEV:
self.ssl_context = ("./cert.pem", "./key.pem")
else:
self.ssl_context = (self.app_settings["certfile"], self.app_settings["keyfile"])
@staticmethod
def load_settings_from_file(file_path: str) -> dict[str, _ty.Any]:
if not os.path.exists(file_path):
return {}
with open(file_path, "r") as f:
return json.loads(f.read())
@staticmethod
def write_settings_to_file(settings: dict[str, _ty.Any], file_path: str) -> None:
with open(file_path, "w") as f:
f.write(json.dumps(settings, indent=4))
def exec(self) -> int:
self.server = make_server(host="0.0.0.0", port=3030, app=self.app, ssl_context=self.ssl_context)
self.logger.info("Starting Flask Server ...")
self.thread = Thread(target=self.server.serve_forever)
self.thread.start()
self.logger.info("Starting GUI ...")
self.qapp = QApplication([])
self.qapp.setQuitOnLastWindowClosed(False)
self.console_window = QPlainTextEdit(None)
self.handler = QtConsoleHandler(self.console_window)
formatter = logging.Formatter(
'[%(asctime)s.%(msecs)03d] [%(levelname)s] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
self.handler.setFormatter(formatter)
self.logger.add_handler(self.handler)
tray = QSystemTrayIcon()
tray.setIcon(QIcon("media/icon.png")) # This image has to exist, otherwise there won't be a tray element
tray.setVisible(True)
tray.setToolTip(config.PROGRAM_NAME)
menu = QMenu()
menu.setStyleSheet("""
QMenu {
color: #000000;
background-color: #ffffff;
border-radius: 5px;
}
QMenu::item {
padding: 2px 10px;
margin: 2px 2px;
font-weight: bold;
}
QMenu::item:selected {
background-color: #f2f2f2;
}
""")
title_action = QAction(f"π§ {config.PROGRAM_NAME} Menu")
title_action.setEnabled(False)
menu.addAction(title_action)
menu.addSeparator()
about_action = QAction("About")
about_action.triggered.connect(self.open_about)
menu.addAction(about_action)
settings_action = QAction("Settings")
settings_action.triggered.connect(self.open_settings)
menu.addAction(settings_action)
console_action = QAction("Console")
console_action.triggered.connect(self.open_console)
menu.addAction(console_action)
restart_action = QAction("Restart")
restart_action.triggered.connect(self.restart)
menu.addAction(restart_action)
quit_action = QAction("Quit")
quit_action.triggered.connect(self.qapp.quit)
menu.addAction(quit_action)
for action in menu.actions():
action.setIconVisibleInMenu(False)
tray.setContextMenu(menu)
self.logger.info("Entering application loop ...")
return self.qapp.exec()
def open_settings(self) -> None:
pass
def open_console(self) -> None:
self.console_window.show()
def open_about(self) -> None:
pass
def restart(self) -> None:
self.qapp.exit(
1000) # Exit code 1000 is the default exit code for a restart in dancer (only works in compiled builds)
def close(self) -> None:
if hasattr(self, "server") and self.server is not None:
self.server.shutdown()
if hasattr(self, "thread") and self.thread is not None:
self.thread.join()
if __name__ == "__main__":
app_info = config.AppConfig(
True, False, # These flags are INDEV and INDEV_KEEP_RUNTIME_FILES.
# Indev enables behavior such as replacing all appdata files.
# They can also be checked by importing dancer.config in another file and accessing the e.g. .INDEV attribute.
"ContentView Server", # This is the program name
"contentview_server", # This is the normalized program name
100, "a0", # This is the version and version_add
{"Windows": {"10": ("any",), "11": ("any",)}}, # These are the supported OS versions (major: (minor,))
[(3, 10), (3, 11), (3, 12), (3, 13)], # These are the supported Python versions
{ # This is the directory structure that gets created in appdata
"config": {},
"core": {
"common": {},
"plugins": {},
"extensions": {
"library": {},
"providers": {}
}
},
"data": {
"static": {},
"templates": {}
}
},
["./"] # These are the relative paths from default-config that are added to sys.paths
)
config.do(
app_info) # This completes the config, there is no reason to manually do it except if you want specialized behavior.
if config.INDEV:
os.environ["FLASK_SECRET_KEY"] = "..."
os.environ["JWT_SECRET_KEY"] = "..."
os.environ["PEPPER"] = "..."
os.environ["COOKIE_SECRET_KEY"] = "..."
parser = ArgumentParser(description=f"{config.PROGRAM_NAME}")
parser.add_argument("library_path")
parser.add_argument("--host", default="0.0.0.0")
parser.add_argument("--port", type=int, default=3030)
parser.add_argument("--static-dir", type=str, default=None)
parser.add_argument("--templates-dir", type=str, default=None)
parser.add_argument("--load-config-path", type=str, default=None)
parser.add_argument("--comicview-client", type=str, default=None)
parser.add_argument("--no-ssl", dest="run_ssl", action="store_false")
parser.add_argument("--certfile", type=str)
parser.add_argument("--keyfile", type=str)
parser.add_argument("--api-only", action="store_true")
parser.add_argument("--frontend-only", action="store_true")
parser.add_argument("--admin-user", action="store_true")
parser.add_argument("--rate-limit", action="store_true")
start(App,
parser) # Here we pass the App class and the parser (optional) dancer will always add logging-level to the parser.
````
### Hybrid Example, a bit of TUI and a bit of GUI
You can look at this example in more detail at: https://github.com/adalfarus/unicode-writer
### GUI Example (Qt)
This shows how to structure a GUI app using `DefaultAppGUIQt`. Unlike the TUI example, this version makes full use of file-based settings, asset management, extension loading, and more. It is meant for apps with interactive UIs, not simple CLI or server tools.
You can look at this example in more detail at: https://github.com/Giesbrt/Automaten/blob/dancer-start/src/main.py
````python
from dancer import config, start
from dancer.qt import DefaultAppGUIQt
# Here we do the config setup at the top of the file
# This is done so we can access the user-specific modules in appdata as imports right after.
app_info = config.AppConfig(
False, True,
"N.E.F.S.' Simulator",
"nefs_simulator",
1400, "b4",
{"Windows": {"10": ("any",), "11": ("any",)}},
[(3, 10), (3, 11), (3, 12), (3, 13)],
{
"config": {},
"core": {
"libs": {},
"modules": {}
},
"data": {
"assets": {
"app_icons": {}
},
"styling": {
"styles": {},
"themes": {}
},
"logs": {}
}
},
["./core/common", "./core/plugins", "./"]
)
config.do(app_info)
# Std Lib imports
from pathlib import Path as PLPath
from argparse import ArgumentParser, Namespace
from traceback import format_exc
from functools import partial
from string import Template
import multiprocessing
import threading
import logging
import sys
import os
# Third party imports
from packaging.version import Version, InvalidVersion
from returns import result as _result
import stdlib_list
import requests
# PySide6
from PySide6.QtWidgets import QApplication, QMessageBox, QSizePolicy
from PySide6.QtGui import QIcon, QDesktopServices, Qt, QPalette
from PySide6.QtCore import QUrl
# aplustools
from aplustools.io.env import diagnose_shutdown_blockers
# Internal imports (why we did config.do( ... ) early)
from automaton.UIAutomaton import UiAutomaton
from automaton.automatonProvider import AutomatonProvider
from serializer import serialize, deserialize
from storage import AppSettings
from gui import MainWindow
from abstractions import IMainWindow, IBackend, IAppSettings
from automaton import start_backend
from utils.IOManager import IOManager
from utils.staticSignal import SignalCache
from automaton.UiSettingsProvider import UiSettingsProvider
from customPythonHandler import CustomPythonHandler
from extensions_loader import Extensions_Loader
from automaton.base.QAutomatonInputWidget import QAutomatonInputOutput
# Standard typing imports for aps
import collections.abc as _a
import typing as _ty
import types as _ts
hiddenimports = list(stdlib_list.stdlib_list())
multiprocessing.freeze_support()
class App(DefaultAppGUIQt):
def __init__(self, parsed_args: Namespace, logging_level: int) -> None:
self.base_app_dir = config.base_app_dir
self.data_folder = os.path.join(self.base_app_dir, "data")
self.core_folder = os.path.join(self.base_app_dir, "core")
self.extensions_folder = os.path.join(self.base_app_dir, "extensions")
self.config_folder = os.path.join(self.base_app_dir, "config")
self.styling_folder = os.path.join(self.data_folder, "styling")
settings = AppSettings()
settings.init(config, self.config_folder)
super().__init__(MainWindow, settings,
os.path.join(self.styling_folder, "themes"),
os.path.join(self.styling_folder, "styles"),
os.path.join(self.data_folder, "logs"),
parsed_args, logging_level,
setup_thread_pool=True)
try:
recent_files = []
for file in self.settings.get_recent_files():
if os.path.exists(file):
recent_files.append(file)
self.settings.set_recent_files(tuple(recent_files))
self.offload_work("load_extensions", self.set_extensions,
lambda: Extensions_Loader(self.base_app_dir).load_content())
self.extensions = None
self.wait_for_manual_completion("load_extensions", check_interval=0.1)
# ... (rest of the initialization)
self.backend: IBackend = start_backend(self.settings)
self.backend_stop_event: threading.Event = threading.Event()
self.backend_thread: threading.Thread = threading.Thread(target=self.backend.run_infinite,
args=(self.backend_stop_event,))
self.backend_thread.start()
except Exception as e:
raise Exception("Exception during App initialization") from e
def set_extensions(self, extensions):
pass
# ... (other methods)
def timer_tick(self, index: int) -> None:
super().timer_tick(index)
if index == 0: # Default 500ms timer
SignalCache().invoke()
def close(self) -> None:
super().close()
if hasattr(self, "backend_thread") and self.backend_thread.is_alive():
self.backend_stop_event.set()
self.backend_thread.join()
if __name__ == "__main__":
parser = ArgumentParser(description=f"{config.PROGRAM_NAME}")
parser.add_argument("input", nargs="?", default="", help="Path to the input file.")
start(App, parser)
results: str = diagnose_shutdown_blockers(return_result=True)
````
## Naming convention, dependencies and library information
[PEP 8 -- Style Guide for Python Code](https://peps.python.org/pep-0008/#naming-conventions)
For modules I use 'lowercase', classes are 'CapitalizedWords' and functions and methods are 'lower_case_with_underscores'.
### Information
Further details will be added in the full release. The package is designed as a **development toolkit**, not just an application runner. More features like threading, crypto, state validation, and I/O extensions are in the works.
## Contributing
We welcome contributions! Please see our [contributing guidelines](https://github.com/adalfarus/dancer/blob/main/CONTRIBUTING.md) for more details on how you can contribute to dancer.
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
4. Push to the branch (`git push origin feature/AmazingFeature`)
5. Open a pull request
### Aps Build master
You can use the aps_build_master script for your os to make your like a lot easier.
It supports running tests, installing, building and much more as well as chaining together as many commands as you like.
This example runs test, build the project and then installs it
````commandline
call .\aps_build_master.bat 234
````
````shell
sudo apt install python3-pip
sudo apt install python3-venv
chmod +x ./aps_build_master.sh
./aps_build_master.sh 234
````
## License
dancer is licensed under the LGPL-2.1 License - see the [LICENSE](https://github.com/adalfarus/dancer/blob/main/LICENSE) file for details.
Raw data
{
"_id": null,
"home_page": null,
"name": "dncer",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.10",
"maintainer_email": "Cariel Becker <cariel.becker@gmx.de>",
"keywords": "general, tools, app tools, production, apt",
"author": null,
"author_email": "Cariel Becker <cariel.becker@gmx.de>",
"download_url": "https://files.pythonhosted.org/packages/51/e7/41a58a735cb6ee87d816b00c89b6ae0b0416b6498906757479f7c83dc8b6/dncer-0.0.0.1a9.tar.gz",
"platform": null,
"description": "[](https://gist.github.com/cheerfulstoic/d107229326a01ff0f333a1d3476e068d)\n[](https://github.com/Adalfarus/dancer/actions)\n[](https://github.com/Adalfarus/dancer/blob/main/LICENSE)\n\n[//]: # (<div style=\"display: flex; align-items: center; width: 100%;\">)\n\n[//]: # ( <img src=\"project_data/dancer.png\" style=\"height: 10vw;\">)\n\n[//]: # ( <p style=\"margin: 0 0 0 2vw; font-size: 10vw; color: #3b4246;\">Dancer</p>)\n\n[//]: # (</div>)\n<img src=\"https://github.com/adalfarus/dancer/blob/main/project_data/img.png\">\n\ndancer is a simple, and user-friendly Python library for creating competent apps.\n\n## Compatibility\n\ud83d\udfe9 (Works perfectly); \ud83d\udfe8 (Untested); \ud83d\udfe7 (Some Issues); \ud83d\udfe5 (Unusable)\n\n| OS | UX & README instructions | Tests | More Complex Functionalities |\n|--------------------------|--------------------------|-------|------------------------------|\n| Windows | \ud83d\udfe9 | \ud83d\udfe9 | \ud83d\udfe9 |\n| MacOS | \ud83d\udfe8 | \ud83d\udfe9 | \ud83d\udfe8 |\n| Linux (Ubuntu 22.04 LTS) | \ud83d\udfe9 | \ud83d\udfe9 | \ud83d\udfe9 |\n\n## Features\n\n* **Makes user-specific storage easy**\n\n \u2192 Automatically sets up per-user directories (e.g., config, logs, plugins, styling) to isolate application data and avoid conflicts across users.\n\n* **Comes with various data storage and encryption built-in \\[IN THE FULL RELEASE]**\n\n \u2192 Full release will offer structured storage layers, file-based persistence, encryption helpers, and cryptographic utilities\u2014some adapted from [`aplustools`](https://pypi.org/project/aplustools/).\n\n* **Sets up a good environment for your app to run in**\n\n \u2192 Automatically constructs an organized project tree, validates Python + OS compatibility, allows for easy restarts when compiled, and supports dynamic extension loading.\n\n* **Supports Windows, Linux and MacOS**\n\n \u2192 Fully cross-platform with native support for common operating systems and runtime configurations.\n\n> \u26a0\ufe0f These features are part of a larger toolkit. The examples below (GUI/TUI bootstrapping via `config` and `start`) are **only one part** of what this package enables.\n\n## Installation\n\nYou can install `dancer` via pip:\n\n```sh\npip install dancer --upgrade\n```\n\nIf you want to use Qt-specific components:\n\n```sh\npip install dancer[qt] --upgrade\n```\n\nThis installs PySide6 which enables you to use e.g. the class DefaultQtGUIApp which has features like theme management, and more.\n\nOr clone the repository and install manually:\n\n```sh\ngit clone https://github.com/Adalfarus/dancer.git\ncd dancer\npython -m build\n```\n\n## Usage\n\nHere are a few quick examples of how to use `dancer`:\n\n### TUI Server Example\n\nThis creates a simple Flask-based TUI server. It uses `DefaultServerTUI` to manage config state, run settings, and integrate logging, but it does **not** rely on dancers Qt components. The Qt-based system tray would not make enough use of the advanced features they provide.\n\n> This example focuses on config validation, modular settings loading, Flask integration, and minimal runtime bootstrapping.\n\n````python\nfrom dancer import config, start, DefaultServerTUI\n\nfrom werkzeug.serving import make_server, BaseWSGIServer\nfrom argparse import ArgumentParser, Namespace\nfrom threading import Thread\nimport logging\nimport json\nimport sys\nimport os\n\nfrom PySide6.QtWidgets import QApplication, QSystemTrayIcon, QMenu, QPlainTextEdit\nfrom PySide6.QtGui import QIcon, QAction\n\nimport typing as _ty\n\n\nclass QtConsoleHandler(logging.Handler):\n def __init__(self, widget: QPlainTextEdit):\n super().__init__()\n self.widget = widget\n\n def emit(self, record):\n msg = self.format(record)\n self.widget.appendPlainText(msg)\n\n\nclass App(DefaultServerTUI):\n def __init__(self, parsed_args: Namespace, logging_mode: int) -> None:\n super().__init__(os.path.abspath(\"./latest.log\"), parsed_args, logging_mode, always_restart=True)\n from common.app import\n create_app # This is a relative import from appdata, so we can only do it after we ran config()\n self.logger.info(\"Creating Flask Server ...\")\n\n self.config_path: str = os.path.abspath(\"./config/core.json\")\n settings_path: str = parsed_args.load_config_path or self.config_path\n self.app_settings: dict[str, _ty.Any] = self.load_settings_from_file(settings_path)\n self.app_settings.update(vars(parsed_args))\n self.write_settings_to_file(self.app_settings.copy(), self.config_path)\n\n self.console_window = None\n self.app = create_app(self.app_settings)\n self.ssl_context: None | tuple[str, str] = None\n self.server: BaseWSGIServer | None = None\n self.thread: Thread | None = None\n\n if parsed_args.run_ssl:\n if config.INDEV:\n self.ssl_context = (\"./cert.pem\", \"./key.pem\")\n else:\n self.ssl_context = (self.app_settings[\"certfile\"], self.app_settings[\"keyfile\"])\n\n @staticmethod\n def load_settings_from_file(file_path: str) -> dict[str, _ty.Any]:\n if not os.path.exists(file_path):\n return {}\n with open(file_path, \"r\") as f:\n return json.loads(f.read())\n\n @staticmethod\n def write_settings_to_file(settings: dict[str, _ty.Any], file_path: str) -> None:\n with open(file_path, \"w\") as f:\n f.write(json.dumps(settings, indent=4))\n\n def exec(self) -> int:\n self.server = make_server(host=\"0.0.0.0\", port=3030, app=self.app, ssl_context=self.ssl_context)\n self.logger.info(\"Starting Flask Server ...\")\n self.thread = Thread(target=self.server.serve_forever)\n self.thread.start()\n self.logger.info(\"Starting GUI ...\")\n\n self.qapp = QApplication([])\n self.qapp.setQuitOnLastWindowClosed(False)\n\n self.console_window = QPlainTextEdit(None)\n self.handler = QtConsoleHandler(self.console_window)\n formatter = logging.Formatter(\n '[%(asctime)s.%(msecs)03d] [%(levelname)s] %(message)s',\n datefmt='%Y-%m-%d %H:%M:%S'\n )\n self.handler.setFormatter(formatter)\n self.logger.add_handler(self.handler)\n\n tray = QSystemTrayIcon()\n tray.setIcon(QIcon(\"media/icon.png\")) # This image has to exist, otherwise there won't be a tray element\n tray.setVisible(True)\n tray.setToolTip(config.PROGRAM_NAME)\n\n menu = QMenu()\n menu.setStyleSheet(\"\"\"\n QMenu {\n color: #000000; \n background-color: #ffffff; \n border-radius: 5px;\n }\n QMenu::item {\n padding: 2px 10px; \n margin: 2px 2px; \n font-weight: bold;\n }\n QMenu::item:selected {\n background-color: #f2f2f2;\n }\n \"\"\")\n\n title_action = QAction(f\"\ud83d\udd27 {config.PROGRAM_NAME} Menu\")\n title_action.setEnabled(False)\n menu.addAction(title_action)\n menu.addSeparator()\n\n about_action = QAction(\"About\")\n about_action.triggered.connect(self.open_about)\n menu.addAction(about_action)\n\n settings_action = QAction(\"Settings\")\n settings_action.triggered.connect(self.open_settings)\n menu.addAction(settings_action)\n\n console_action = QAction(\"Console\")\n console_action.triggered.connect(self.open_console)\n menu.addAction(console_action)\n\n restart_action = QAction(\"Restart\")\n restart_action.triggered.connect(self.restart)\n menu.addAction(restart_action)\n\n quit_action = QAction(\"Quit\")\n quit_action.triggered.connect(self.qapp.quit)\n menu.addAction(quit_action)\n\n for action in menu.actions():\n action.setIconVisibleInMenu(False)\n\n tray.setContextMenu(menu)\n self.logger.info(\"Entering application loop ...\")\n return self.qapp.exec()\n\n def open_settings(self) -> None:\n pass\n\n def open_console(self) -> None:\n self.console_window.show()\n\n def open_about(self) -> None:\n pass\n\n def restart(self) -> None:\n self.qapp.exit(\n 1000) # Exit code 1000 is the default exit code for a restart in dancer (only works in compiled builds)\n\n def close(self) -> None:\n if hasattr(self, \"server\") and self.server is not None:\n self.server.shutdown()\n if hasattr(self, \"thread\") and self.thread is not None:\n self.thread.join()\n\n\nif __name__ == \"__main__\":\n app_info = config.AppConfig(\n True, False, # These flags are INDEV and INDEV_KEEP_RUNTIME_FILES.\n # Indev enables behavior such as replacing all appdata files.\n # They can also be checked by importing dancer.config in another file and accessing the e.g. .INDEV attribute.\n \"ContentView Server\", # This is the program name\n \"contentview_server\", # This is the normalized program name\n 100, \"a0\", # This is the version and version_add\n {\"Windows\": {\"10\": (\"any\",), \"11\": (\"any\",)}}, # These are the supported OS versions (major: (minor,))\n [(3, 10), (3, 11), (3, 12), (3, 13)], # These are the supported Python versions\n { # This is the directory structure that gets created in appdata\n \"config\": {},\n \"core\": {\n \"common\": {},\n \"plugins\": {},\n \"extensions\": {\n \"library\": {},\n \"providers\": {}\n }\n },\n \"data\": {\n \"static\": {},\n \"templates\": {}\n }\n },\n [\"./\"] # These are the relative paths from default-config that are added to sys.paths\n )\n config.do(\n app_info) # This completes the config, there is no reason to manually do it except if you want specialized behavior.\n\n if config.INDEV:\n os.environ[\"FLASK_SECRET_KEY\"] = \"...\"\n os.environ[\"JWT_SECRET_KEY\"] = \"...\"\n os.environ[\"PEPPER\"] = \"...\"\n os.environ[\"COOKIE_SECRET_KEY\"] = \"...\"\n\n parser = ArgumentParser(description=f\"{config.PROGRAM_NAME}\")\n parser.add_argument(\"library_path\")\n parser.add_argument(\"--host\", default=\"0.0.0.0\")\n parser.add_argument(\"--port\", type=int, default=3030)\n parser.add_argument(\"--static-dir\", type=str, default=None)\n parser.add_argument(\"--templates-dir\", type=str, default=None)\n parser.add_argument(\"--load-config-path\", type=str, default=None)\n parser.add_argument(\"--comicview-client\", type=str, default=None)\n parser.add_argument(\"--no-ssl\", dest=\"run_ssl\", action=\"store_false\")\n parser.add_argument(\"--certfile\", type=str)\n parser.add_argument(\"--keyfile\", type=str)\n\n parser.add_argument(\"--api-only\", action=\"store_true\")\n parser.add_argument(\"--frontend-only\", action=\"store_true\")\n parser.add_argument(\"--admin-user\", action=\"store_true\")\n parser.add_argument(\"--rate-limit\", action=\"store_true\")\n\n start(App,\n parser) # Here we pass the App class and the parser (optional) dancer will always add logging-level to the parser.\n````\n\n### Hybrid Example, a bit of TUI and a bit of GUI\n\nYou can look at this example in more detail at: https://github.com/adalfarus/unicode-writer\n\n### GUI Example (Qt)\n\nThis shows how to structure a GUI app using `DefaultAppGUIQt`. Unlike the TUI example, this version makes full use of file-based settings, asset management, extension loading, and more. It is meant for apps with interactive UIs, not simple CLI or server tools.\n\nYou can look at this example in more detail at: https://github.com/Giesbrt/Automaten/blob/dancer-start/src/main.py\n\n````python\nfrom dancer import config, start\nfrom dancer.qt import DefaultAppGUIQt\n\n# Here we do the config setup at the top of the file\n# This is done so we can access the user-specific modules in appdata as imports right after.\napp_info = config.AppConfig(\n False, True,\n \"N.E.F.S.' Simulator\",\n \"nefs_simulator\",\n 1400, \"b4\",\n {\"Windows\": {\"10\": (\"any\",), \"11\": (\"any\",)}},\n [(3, 10), (3, 11), (3, 12), (3, 13)],\n {\n \"config\": {},\n \"core\": {\n \"libs\": {},\n \"modules\": {}\n },\n \"data\": {\n \"assets\": {\n \"app_icons\": {}\n },\n \"styling\": {\n \"styles\": {},\n \"themes\": {}\n },\n \"logs\": {}\n }\n },\n [\"./core/common\", \"./core/plugins\", \"./\"]\n)\nconfig.do(app_info)\n\n# Std Lib imports\nfrom pathlib import Path as PLPath\nfrom argparse import ArgumentParser, Namespace\nfrom traceback import format_exc\nfrom functools import partial\nfrom string import Template\nimport multiprocessing\nimport threading\nimport logging\nimport sys\nimport os\n\n# Third party imports\nfrom packaging.version import Version, InvalidVersion\nfrom returns import result as _result\nimport stdlib_list\nimport requests\n# PySide6\nfrom PySide6.QtWidgets import QApplication, QMessageBox, QSizePolicy\nfrom PySide6.QtGui import QIcon, QDesktopServices, Qt, QPalette\nfrom PySide6.QtCore import QUrl\n# aplustools\nfrom aplustools.io.env import diagnose_shutdown_blockers\n\n# Internal imports (why we did config.do( ... ) early)\nfrom automaton.UIAutomaton import UiAutomaton\nfrom automaton.automatonProvider import AutomatonProvider\nfrom serializer import serialize, deserialize\nfrom storage import AppSettings\nfrom gui import MainWindow\nfrom abstractions import IMainWindow, IBackend, IAppSettings\nfrom automaton import start_backend\nfrom utils.IOManager import IOManager\nfrom utils.staticSignal import SignalCache\nfrom automaton.UiSettingsProvider import UiSettingsProvider\nfrom customPythonHandler import CustomPythonHandler\nfrom extensions_loader import Extensions_Loader\nfrom automaton.base.QAutomatonInputWidget import QAutomatonInputOutput\n\n# Standard typing imports for aps\nimport collections.abc as _a\nimport typing as _ty\nimport types as _ts\n\nhiddenimports = list(stdlib_list.stdlib_list())\nmultiprocessing.freeze_support()\n\n\nclass App(DefaultAppGUIQt):\n def __init__(self, parsed_args: Namespace, logging_level: int) -> None:\n self.base_app_dir = config.base_app_dir\n self.data_folder = os.path.join(self.base_app_dir, \"data\")\n self.core_folder = os.path.join(self.base_app_dir, \"core\")\n self.extensions_folder = os.path.join(self.base_app_dir, \"extensions\")\n self.config_folder = os.path.join(self.base_app_dir, \"config\")\n self.styling_folder = os.path.join(self.data_folder, \"styling\")\n\n settings = AppSettings()\n settings.init(config, self.config_folder)\n\n super().__init__(MainWindow, settings,\n os.path.join(self.styling_folder, \"themes\"),\n os.path.join(self.styling_folder, \"styles\"),\n os.path.join(self.data_folder, \"logs\"),\n parsed_args, logging_level,\n setup_thread_pool=True)\n\n try:\n recent_files = []\n for file in self.settings.get_recent_files():\n if os.path.exists(file):\n recent_files.append(file)\n self.settings.set_recent_files(tuple(recent_files))\n\n self.offload_work(\"load_extensions\", self.set_extensions,\n lambda: Extensions_Loader(self.base_app_dir).load_content())\n self.extensions = None\n\n self.wait_for_manual_completion(\"load_extensions\", check_interval=0.1)\n\n # ... (rest of the initialization)\n\n self.backend: IBackend = start_backend(self.settings)\n self.backend_stop_event: threading.Event = threading.Event()\n self.backend_thread: threading.Thread = threading.Thread(target=self.backend.run_infinite,\n args=(self.backend_stop_event,))\n self.backend_thread.start()\n\n except Exception as e:\n raise Exception(\"Exception during App initialization\") from e\n\n def set_extensions(self, extensions):\n pass\n\n # ... (other methods)\n\n def timer_tick(self, index: int) -> None:\n super().timer_tick(index)\n if index == 0: # Default 500ms timer\n SignalCache().invoke()\n\n def close(self) -> None:\n super().close()\n if hasattr(self, \"backend_thread\") and self.backend_thread.is_alive():\n self.backend_stop_event.set()\n self.backend_thread.join()\n\n\nif __name__ == \"__main__\":\n parser = ArgumentParser(description=f\"{config.PROGRAM_NAME}\")\n parser.add_argument(\"input\", nargs=\"?\", default=\"\", help=\"Path to the input file.\")\n\n start(App, parser)\n\n results: str = diagnose_shutdown_blockers(return_result=True)\n````\n\n## Naming convention, dependencies and library information\n[PEP 8 -- Style Guide for Python Code](https://peps.python.org/pep-0008/#naming-conventions)\n\nFor modules I use 'lowercase', classes are 'CapitalizedWords' and functions and methods are 'lower_case_with_underscores'.\n\n### Information\nFurther details will be added in the full release. The package is designed as a **development toolkit**, not just an application runner. More features like threading, crypto, state validation, and I/O extensions are in the works.\n\n## Contributing\n\nWe welcome contributions! Please see our [contributing guidelines](https://github.com/adalfarus/dancer/blob/main/CONTRIBUTING.md) for more details on how you can contribute to dancer.\n\n1. Fork the repository\n2. Create your feature branch (`git checkout -b feature/AmazingFeature`)\n3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)\n4. Push to the branch (`git push origin feature/AmazingFeature`)\n5. Open a pull request\n\n### Aps Build master\n\nYou can use the aps_build_master script for your os to make your like a lot easier.\nIt supports running tests, installing, building and much more as well as chaining together as many commands as you like.\n\nThis example runs test, build the project and then installs it\n````commandline\ncall .\\aps_build_master.bat 234\n````\n\n````shell\nsudo apt install python3-pip\nsudo apt install python3-venv\nchmod +x ./aps_build_master.sh\n./aps_build_master.sh 234\n````\n\n## License\n\ndancer is licensed under the LGPL-2.1 License - see the [LICENSE](https://github.com/adalfarus/dancer/blob/main/LICENSE) file for details.\n",
"bugtrack_url": null,
"license": null,
"summary": "An app framework",
"version": "0.0.0.1a9",
"project_urls": {
"Documentation": "https://github.com/adalfarus/dancer/wiki",
"Home": "https://pypi.org/project/dancer/",
"Issue tracker": "https://github.com/adalfarus/dancer/issues",
"Repository": "https://github.com/adalfarus/dancer"
},
"split_keywords": [
"general",
" tools",
" app tools",
" production",
" apt"
],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "f5738106d7f305ccc5d3bc1e8de3119cb8e730690310539162f5dbf0cb87930d",
"md5": "16c78e99b4d23efd5cb7b31c5da5d7d6",
"sha256": "c6dd65664b0dafe2cbdce275368031eecad009d399978ce8a43f4cd1409e4da9"
},
"downloads": -1,
"filename": "dncer-0.0.0.1a9-py3-none-any.whl",
"has_sig": false,
"md5_digest": "16c78e99b4d23efd5cb7b31c5da5d7d6",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.10",
"size": 67450,
"upload_time": "2025-07-26T15:54:37",
"upload_time_iso_8601": "2025-07-26T15:54:37.876409Z",
"url": "https://files.pythonhosted.org/packages/f5/73/8106d7f305ccc5d3bc1e8de3119cb8e730690310539162f5dbf0cb87930d/dncer-0.0.0.1a9-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "51e741a58a735cb6ee87d816b00c89b6ae0b0416b6498906757479f7c83dc8b6",
"md5": "764949242ac358316b116cec98a8cb3f",
"sha256": "e40d43f09e85ffcc9a62b66a4b64c50ba9bf4c967ab78e4cc1e21ea7ddc3c5aa"
},
"downloads": -1,
"filename": "dncer-0.0.0.1a9.tar.gz",
"has_sig": false,
"md5_digest": "764949242ac358316b116cec98a8cb3f",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.10",
"size": 60507,
"upload_time": "2025-07-26T15:54:39",
"upload_time_iso_8601": "2025-07-26T15:54:39.228357Z",
"url": "https://files.pythonhosted.org/packages/51/e7/41a58a735cb6ee87d816b00c89b6ae0b0416b6498906757479f7c83dc8b6/dncer-0.0.0.1a9.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-07-26 15:54:39",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "adalfarus",
"github_project": "dancer",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "dncer"
}