diff --git a/.gitignore b/.gitignore index efa407c..82f9275 100644 --- a/.gitignore +++ b/.gitignore @@ -159,4 +159,4 @@ cython_debug/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ \ No newline at end of file +#.idea/ diff --git a/docs/conf_file.md b/docs/conf_file.md index 4f8b61c..1d6bbb1 100644 --- a/docs/conf_file.md +++ b/docs/conf_file.md @@ -29,7 +29,7 @@ laserstudio_generate_config Here is an example of a configuration file: ```yaml -# yaml-language-server: $schema=https://raw.githubusercontent.com/Ledger-Donjon/laserstudio/main/config_schema/config.schema.json +# yaml-language-server: $schema=https://raw.githubusercontent.com/Ledger-Donjon/laserstudio/main/laserstudio/config_schema/config.schema.json camera: enable: true label: "NIT IR Camera" diff --git a/laserstudio/config_generator/__init__.py b/laserstudio/config_generator/__init__.py index cee0ed3..faa5b4c 100644 --- a/laserstudio/config_generator/__init__.py +++ b/laserstudio/config_generator/__init__.py @@ -1,5 +1,6 @@ -from .config_generator import main +from .config_generator import main as main_cli +from .config_generator_wizard import main as main_gui from .config_generator import ConfigGenerator -from .config_generator_ui import ConfigGeneratorWizard +from .config_generator_wizard import ConfigGeneratorWizard -__all__ = ["main", "ConfigGenerator", "ConfigGeneratorWizard"] +__all__ = ["main_cli", "main_gui", "ConfigGenerator", "ConfigGeneratorWizard"] diff --git a/laserstudio/config_generator/config_generator.py b/laserstudio/config_generator/config_generator.py index 20818b2..7e05aea 100644 --- a/laserstudio/config_generator/config_generator.py +++ b/laserstudio/config_generator/config_generator.py @@ -22,11 +22,12 @@ class ConfigGenerator: def __init__( self, schema_uri="config.schema.json", - base_url="https://raw.githubusercontent.com/Ledger-Donjon/laserstudio/main/config_schema/", + base_url="https://raw.githubusercontent.com/Ledger-Donjon/laserstudio/main/laserstudio/config_schema/", ): self.schema_uri = schema_uri self.base_url = base_url self.logger = logging.getLogger("Config Generator") + self.use_local = True colorama_init() @staticmethod @@ -548,18 +549,16 @@ def get_flags(self): # Check if -L flag is present for retrieve schema from local directory if "-L" in sys.argv: - __dirname = os.path.dirname( - os.path.dirname(os.path.dirname(os.path.realpath(__file__))) - ) - self.base_url = os.path.join(__dirname, "config_schema") + __dirname = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) + self.use_local = True - def load_schema(self): + def load_schema(self, local=True): # Fetch the JSON schema from the URL + if self.use_local: + __dirname = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) + self.base_url = os.path.join(__dirname, "config_schema") set_base_url(self.base_url) - print("Loading schemas... ", end="") - sys.stdout.flush() schema = resolve_references(self.schema_uri) - print("done.") self.logger.info("Schema loaded successfully") self.logger.debug(json.dumps(schema, indent=2)) self.schema = schema diff --git a/laserstudio/config_generator/config_generator_ui.py b/laserstudio/config_generator/config_generator_widgets.py similarity index 79% rename from laserstudio/config_generator/config_generator_ui.py rename to laserstudio/config_generator/config_generator_widgets.py index 91751d1..5dcfb1c 100644 --- a/laserstudio/config_generator/config_generator_ui.py +++ b/laserstudio/config_generator/config_generator_widgets.py @@ -1,17 +1,10 @@ -#!/usr/bin/python3 from PyQt6.QtCore import Qt, QRegularExpression -from PyQt6.QtGui import QRegularExpressionValidator -from PyQt6.QtWidgets import QButtonGroup -from typing import Optional, Union from PyQt6.QtWidgets import ( - QWizardPage, - QWizard, QVBoxLayout, QPushButton, QHBoxLayout, QWidget, QLabel, - QScrollArea, QSpinBox, QDoubleSpinBox, QCheckBox, @@ -23,48 +16,11 @@ QAbstractButton, QStackedWidget, QRadioButton, + QButtonGroup, ) -import sys -import yaml - -try: - from .ref_resolve import set_base_url - from .config_generator import ConfigGenerator, validate, ValidationError - from ..utils.colors import LedgerPalette, LedgerStyle -except ImportError: - from laserstudio.config_generator.ref_resolve import set_base_url - from laserstudio.config_generator.config_generator import ( - ConfigGenerator, - validate, - ValidationError, - ) - from laserstudio.utils.colors import LedgerPalette, LedgerStyle - -from PyQt6.QtWidgets import QApplication - - -class ConfigGeneratorWizard(QWizard): - def __init__(self, schema: dict, parent=None): - super().__init__(parent) - self.setWindowTitle("Configuration File Generator") - self.setWizardStyle(QWizard.WizardStyle.ModernStyle) - - # Initiate the presentation/introduction page - self.addPage(ConfigGeneratorIntroductionPage(self)) - - self.schema = schema - # To be more readable, we will create a page for each top-property of the schema - self.config_generation_pages = list[ConfigPresentationPage]() - top_properties = schema.get("properties", {}) - for key, subschema in top_properties.items(): - self.config_generation_pages.append( - ConfigPresentationPage(self, key, subschema) - ) - [self.addPage(p) for p in self.config_generation_pages] - - # Add the result page - self.config_result_page = ConfigResultPage(self) - self.addPage(self.config_result_page) +from PyQt6.QtGui import QRegularExpressionValidator +from jsonschema import validate, ValidationError +from typing import Optional, Union class AnyOf: @@ -571,98 +527,3 @@ def validate(self): error = QErrorMessage() error.showMessage(f"Error on validation of {e.json_path[2:]}: {e.message}") return True - - -class ConfigGeneratorIntroductionPage(QWizardPage): - def __init__(self, parent: "ConfigGeneratorWizard"): - super().__init__(parent) - self.setTitle("Introduction") - self.setSubTitle( - "This wizard will help you generate a Configuration File for Laser Studio" - ) - layout = QVBoxLayout() - self.setLayout(layout) - label = QLabel( - "

For each page of the generator, fill the properties of the instruments with the desired values.

" - "

You can make optional properties not to be added in the file by unchecking the checkbox next to the field name.

" - "

If you need an information about a property, hover the cursor over its name to see the description.

" - "

At the end the Configuration File will be shown for you and you can save it.

" - "

Get more details about the schema in the documentation.

" - ) - label.setTextFormat(Qt.TextFormat.RichText) - label.setWordWrap(True) - label.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction) - label.setOpenExternalLinks(True) - label.linkActivated.connect(lambda url: print(url)) - layout.addWidget(label) - - -class ConfigResultPage(QWizardPage): - def __init__(self, parent: "ConfigGeneratorWizard"): - super().__init__(parent) - self.setTitle("Configuration File Result") - self.setSubTitle("This is the generated Configuration File") - layout = QVBoxLayout() - self.setLayout(layout) - self.result_label = QLabel() - layout.addWidget(self.result_label) - scroll = QScrollArea() - scroll.setWidgetResizable(True) - - def initializePage(self): - wiz = self.wizard() - assert isinstance(wiz, ConfigGeneratorWizard) - configs = {} - for config_page in wiz.config_generation_pages: - configs[config_page.schema_widget.key] = config_page.schema_widget.json() - self.config = configs - self.result_label.setText(yaml.dump(self.config, indent=2)) - - def validatePage(self) -> bool: - try: - wizard = self.wizard() - assert isinstance(wizard, ConfigGeneratorWizard) - validate(self.config, wizard.schema) - print("Validation successful", self.config) - return True - except ValidationError as e: - self.setSubTitle( - f"Generated JSON is invalid for '{'.'.join([str(k) for k in e.path])}'\n" - + e.message - ) - return False - - -class ConfigPresentationPage(QWizardPage): - def __init__(self, parent: "ConfigGeneratorWizard", key: str, schema: dict): - super().__init__(parent) - layout = QVBoxLayout() - self.setLayout(layout) - scroll = QScrollArea() - scroll.setWidgetResizable(True) - self.schema_widget = SchemaWidget(schema, key, make_flat=True) - scroll.setWidget(self.schema_widget) - layout.addWidget(scroll) - self.setTitle(schema.get("title")) - self.setSubTitle(schema.get("description")) - - def validatePage(self) -> bool: - return self.schema_widget.validate() - - -if __name__ == "__main__": - config_generator = ConfigGenerator() - sys.argv.append("-L") - config_generator.get_flags() - set_base_url(config_generator.base_url) - # Load all schemas - config_generator.load_schema() - SCHEMA = config_generator.schema - assert type(SCHEMA) is dict - - app = QApplication(sys.argv) - app.setStyle(LedgerStyle) - app.setPalette(LedgerPalette) - wizard = ConfigGeneratorWizard(config_generator.schema) - wizard.show() - sys.exit(app.exec()) diff --git a/laserstudio/config_generator/config_generator_wizard.py b/laserstudio/config_generator/config_generator_wizard.py new file mode 100644 index 0000000..6444349 --- /dev/null +++ b/laserstudio/config_generator/config_generator_wizard.py @@ -0,0 +1,170 @@ +#!/usr/bin/python3 +from PyQt6.QtCore import Qt +from PyQt6.QtWidgets import ( + QWizardPage, + QWizard, + QVBoxLayout, + QLabel, + QScrollArea, + QPushButton, + QFileDialog, +) +import sys +import yaml + +try: + from .ref_resolve import set_base_url + from .config_generator import ConfigGenerator, validate, ValidationError + from .config_generator_widgets import SchemaWidget + from ..utils.colors import LedgerPalette, LedgerStyle +except ImportError: + from laserstudio.config_generator.ref_resolve import set_base_url + from laserstudio.config_generator.config_generator import ( + ConfigGenerator, + validate, + ValidationError, + ) + from laserstudio.config_generator.config_generator_widgets import SchemaWidget + from laserstudio.utils.colors import LedgerPalette, LedgerStyle + +from PyQt6.QtWidgets import QApplication + + +class ConfigGeneratorWizard(QWizard): + def __init__(self, schema: dict, parent=None): + super().__init__(parent) + self.setWindowTitle("Configuration File Generator") + self.setWizardStyle(QWizard.WizardStyle.ModernStyle) + + # Initiate the presentation/introduction page + self.addPage(ConfigGeneratorIntroductionPage(self)) + + self.schema = schema + # To be more readable, we will create a page for each top-property of the schema + self.config_generation_pages = list[ConfigPresentationPage]() + top_properties = schema.get("properties", {}) + for key, subschema in top_properties.items(): + self.config_generation_pages.append( + ConfigPresentationPage(self, key, subschema) + ) + [self.addPage(p) for p in self.config_generation_pages] + + # Add the result page + self.config_result_page = ConfigResultPage(self) + self.addPage(self.config_result_page) + + +class ConfigGeneratorIntroductionPage(QWizardPage): + def __init__(self, parent: "ConfigGeneratorWizard"): + super().__init__(parent) + self.setTitle("Introduction") + self.setSubTitle( + "This wizard will help you generate a Configuration File for Laser Studio" + ) + layout = QVBoxLayout() + self.setLayout(layout) + label = QLabel( + "

For each page of the generator, fill the properties of the instruments with the desired values.

" + "

You can make optional properties not to be added in the file by unchecking the checkbox next to the field name.

" + "

If you need an information about a property, hover the cursor over its name to see the description.

" + "

At the end the Configuration File will be shown for you and you can save it.

" + "

Get more details about the schema in the documentation.

" + ) + label.setTextFormat(Qt.TextFormat.RichText) + label.setWordWrap(True) + label.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction) + label.setOpenExternalLinks(True) + label.linkActivated.connect(lambda url: print(url)) + layout.addWidget(label) + + +class ConfigResultPage(QWizardPage): + def __init__(self, parent: "ConfigGeneratorWizard"): + super().__init__(parent) + self.setTitle("Configuration File Result") + self.setSubTitle( + "This is the generated Configuration File. Click Finish to use it in Laser Studio." + ) + layout = QVBoxLayout() + save = QPushButton("Save Configuration File") + save.clicked.connect(self.save_config) + + self.setLayout(layout) + self.result_label = QLabel() + layout.addWidget(self.result_label) + scroll = QScrollArea() + scroll.setWidgetResizable(True) + + layout.addWidget(save) + + def initializePage(self): + wiz = self.wizard() + assert isinstance(wiz, ConfigGeneratorWizard) + configs = {} + for config_page in wiz.config_generation_pages: + configs[config_page.schema_widget.key] = config_page.schema_widget.json() + self.config = configs + self.result_label.setText(yaml.dump(self.config, indent=2)) + + def validatePage(self) -> bool: + try: + wizard = self.wizard() + assert isinstance(wizard, ConfigGeneratorWizard) + validate(self.config, wizard.schema) + print("Validation successful", self.config) + return True + except ValidationError as e: + self.setSubTitle( + f"Generated JSON is invalid for '{'.'.join([str(k) for k in e.path])}'\n" + + e.message + ) + return False + + def save_config(self): + filename, _ = QFileDialog.getSaveFileName( + self, + "Save Configuration File", + "config.yaml", + "YAML Files (*.yaml);;All Files (*)", + ) + if filename: + with open(filename, "w") as f: + f.write(yaml.dump(self.config, indent=2)) + + +class ConfigPresentationPage(QWizardPage): + def __init__(self, parent: "ConfigGeneratorWizard", key: str, schema: dict): + super().__init__(parent) + layout = QVBoxLayout() + self.setLayout(layout) + scroll = QScrollArea() + scroll.setWidgetResizable(True) + self.schema_widget = SchemaWidget(schema, key, make_flat=True) + scroll.setWidget(self.schema_widget) + layout.addWidget(scroll) + self.setTitle(schema.get("title")) + self.setSubTitle(schema.get("description")) + + def validatePage(self) -> bool: + return self.schema_widget.validate() + + +def main(): + config_generator = ConfigGenerator() + config_generator.get_flags() + set_base_url(config_generator.base_url) + # Load all schemas + config_generator.load_schema() + SCHEMA = config_generator.schema + assert type(SCHEMA) is dict + + app = QApplication(sys.argv) + app.setStyle(LedgerStyle) + app.setPalette(LedgerPalette) + wizard = ConfigGeneratorWizard(config_generator.schema) + wizard.show() + sys.exit(app.exec()) + + +if __name__ == "__main__": + main() diff --git a/laserstudio/config_generator/ref_resolve.py b/laserstudio/config_generator/ref_resolve.py index a633892..bebb04f 100644 --- a/laserstudio/config_generator/ref_resolve.py +++ b/laserstudio/config_generator/ref_resolve.py @@ -9,9 +9,7 @@ # Fetch the JSON schema from the URL -BASE_URL = ( - "https://raw.githubusercontent.com/Ledger-Donjon/laserstudio/main/config_schema/" -) +BASE_URL = "https://raw.githubusercontent.com/Ledger-Donjon/laserstudio/main/laserstudio/config_schema/" def set_base_url(url: str): diff --git a/config_schema/camera.schema.json b/laserstudio/config_schema/camera.schema.json similarity index 100% rename from config_schema/camera.schema.json rename to laserstudio/config_schema/camera.schema.json diff --git a/config_schema/config.schema.json b/laserstudio/config_schema/config.schema.json similarity index 100% rename from config_schema/config.schema.json rename to laserstudio/config_schema/config.schema.json diff --git a/config_schema/instrument.schema.json b/laserstudio/config_schema/instrument.schema.json similarity index 100% rename from config_schema/instrument.schema.json rename to laserstudio/config_schema/instrument.schema.json diff --git a/config_schema/laser.schema.json b/laserstudio/config_schema/laser.schema.json similarity index 100% rename from config_schema/laser.schema.json rename to laserstudio/config_schema/laser.schema.json diff --git a/config_schema/probe.schema.json b/laserstudio/config_schema/probe.schema.json similarity index 100% rename from config_schema/probe.schema.json rename to laserstudio/config_schema/probe.schema.json diff --git a/config_schema/rest.schema.json b/laserstudio/config_schema/rest.schema.json similarity index 89% rename from config_schema/rest.schema.json rename to laserstudio/config_schema/rest.schema.json index 57287fe..c263804 100644 --- a/config_schema/rest.schema.json +++ b/laserstudio/config_schema/rest.schema.json @@ -20,8 +20,8 @@ "api_command": { "type": "string", "description": "Default API command.", - "default": "/device", - "examples": ["images/camera", "/motion"] + "default": "", + "examples": ["/images/camera", "/motion"] } }, "required": ["api_command"] diff --git a/config_schema/serial.schema.json b/laserstudio/config_schema/serial.schema.json similarity index 100% rename from config_schema/serial.schema.json rename to laserstudio/config_schema/serial.schema.json diff --git a/config_schema/stage.schema.json b/laserstudio/config_schema/stage.schema.json similarity index 100% rename from config_schema/stage.schema.json rename to laserstudio/config_schema/stage.schema.json diff --git a/pyproject.toml b/pyproject.toml index 1407469..ef78812 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,12 +8,8 @@ authors = [ ] license = "LGPL-3.0-or-later" readme = "README.md" -documentation = "https://laserstudio.readthedocs.org/" +documentation = "https://laserstudio.readthedocs.io/" classifiers = [ - # How mature is this project? Common values are - # 3 - Alpha - # 4 - Beta - # 5 - Production/Stable "Development Status :: 5 - Production/Stable", # Indicate who your project is intended for "Intended Audience :: Science/Research", @@ -24,7 +20,8 @@ classifiers = [ [tool.poetry.scripts] laserstudio = 'laserstudio.__main__:main' laserstudio_listdevices = 'laserstudio.instruments.list_serials:list_devices' -laserstudio_generate_config = 'laserstudio.config_generator.config_generator:main' +laserstudio_generate_config_cli = 'laserstudio.config_generator.config_generator:main_cli' +laserstudio_generate_config_gui = 'laserstudio.config_generator.config_generator:main_gui' [tool.poetry.dependencies] python = ">=3.9 <3.13"