Initial commit for profile Radian

This commit is contained in:
VALLONGOL 2025-11-12 13:36:07 +01:00
parent 0e2a56f4e1
commit 8b686ec80e
7 changed files with 572 additions and 49 deletions

37
pyproject.toml Normal file
View File

@ -0,0 +1,37 @@
# pyproject.toml
# This section defines the build system requirements for the project.
# setuptools is a standard choice.
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
# This section contains the core metadata about your project.
[project]
name = "radian-framework"
version = "0.1.0"
authors = [
{ name = "Luca Vallongo", email = "luca.vallongo@gmail.com" },
]
description = "A framework for Radar Data Integration & Analysis."
readme = "README.md" # Optional: if you have a README file
requires-python = ">=3.8"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License", # Choose a license
"Operating System :: OS Independent",
"Development Status :: 3 - Alpha",
]
# This is the most important part for our use case.
# It lists the external libraries that RADIAN depends on to run.
dependencies = [
"PyYAML", # For reading the .yaml configuration files
]
# This section tells setuptools where to find your actual Python packages.
[tool.setuptools]
packages = ["radian"]
[tool.setuptools.package-data]
radian = ["py.typed"]

View File

@ -0,0 +1,121 @@
# radian/components/base_component.py
from abc import ABC, abstractmethod
from typing import Dict, Any, Optional
import tkinter as tk
# A type alias for configuration dictionaries to improve code readability.
ConfigType = Dict[str, Any]
class BaseComponent(ABC):
"""
Abstract Base Class for all RADIAN components.
This class defines a common interface (a "contract") that each external
tool must implement via an adapter class to be managed by the RADIAN framework.
It ensures that RADIAN can interact with any component in a standardized way.
"""
def __init__(self):
# Each component instance will manage its own configuration.
self.config: ConfigType = self.get_default_config()
@abstractmethod
def get_name(self) -> str:
"""
Returns the user-friendly name of the component.
This name will be displayed in the RADIAN GUI.
Returns:
The string name of the component.
"""
pass
@abstractmethod
def get_description(self) -> str:
"""
Returns a brief description of what the component does.
This can be used for tooltips or help text in the GUI.
Returns:
A short string description.
"""
pass
@abstractmethod
def get_default_config(self) -> ConfigType:
"""
Returns a dictionary with the default configuration parameters
for the component. This is used to initialize the component's state.
Returns:
A dictionary representing the default configuration.
"""
pass
def set_config(self, config: ConfigType) -> None:
"""
Applies a new configuration to the component. This method can be
overridden if complex logic is needed when setting a configuration.
Args:
config: A dictionary with the configuration to apply.
"""
self.config = config
def get_current_config(self) -> ConfigType:
"""
Returns the current configuration of the component.
Returns:
The current configuration dictionary.
"""
return self.config
@abstractmethod
def get_config_ui(self, parent: tk.Frame) -> Optional[tk.Frame]:
"""
Creates and returns a Tkinter Frame containing the component's
specific configuration UI elements. RADIAN will place this frame
in the main content area.
If a component has no configurable parameters, this method can
return None.
Args:
parent: The parent Tkinter widget (a Frame) where the UI
should be built.
Returns:
A Tkinter Frame with all the necessary configuration widgets,
or None if no GUI configuration is needed.
"""
pass
@abstractmethod
def run(self, input_data: Optional[Any] = None) -> Any:
"""
Executes the main logic of the component.
This is the core function for workflow execution. It can accept data
from a previous component and return data for the next one.
Args:
input_data: Data passed from a previous component in a workflow.
Returns:
The output of the component's processing, to be passed to the
next component in a workflow, or None if there is no output.
"""
pass
def get_icon_path(self) -> Optional[str]:
"""
Returns the absolute path to the component's icon file (e.g., .png, .ico).
The recommended size is 32x32 or 64x64.
If not implemented or returns None, a default icon will be used.
Returns:
A string representing the absolute path to the icon, or None.
"""
return None

View File

@ -0,0 +1,125 @@
# radian/components/component_manager.py
import sys
import importlib.util
import inspect
from pathlib import Path
from typing import Dict, Optional, Type
import yaml
from radian.utils import logger
from radian.components.base_component import BaseComponent
log = logger.get_logger(__name__)
# The expected name of the file containing the adapter class within a component's project.
ADAPTER_FILENAME = "adapter.py"
# The expected name of the adapter class. This can be made more flexible later if needed.
ADAPTER_CLASS_NAME = "RadianAdapter"
class ComponentManager:
"""
Discovers, loads, and manages all RADIAN components (plugins).
It reads a configuration file to find components and dynamically loads
them, making them available to the rest of the application.
"""
def __init__(self, config_path: Path):
self.config_path = config_path
self.components: Dict[str, BaseComponent] = {}
log.info(f"ComponentManager initialized with config path: {config_path}")
def load_components(self):
"""
Loads all components specified in the configuration file.
This is the main entry point for the manager.
"""
log.info("Starting component discovery and loading...")
try:
with open(self.config_path, 'r', encoding='utf-8') as f:
config = yaml.safe_load(f)
except FileNotFoundError:
log.error(f"Component configuration file not found at: {self.config_path}")
return
except yaml.YAMLError as e:
log.error(f"Error parsing component YAML file: {e}")
return
if not config or 'components' not in config:
log.warning("Component configuration file is empty or missing 'components' key.")
return
for name, comp_config in config['components'].items():
if 'path' in comp_config:
log.info(f"Found component '{name}' configured with a local path.")
self._load_component_from_path(name, Path(comp_config['path']))
else:
log.warning(f"Component '{name}' is missing a 'path' key. Skipping.")
log.info(f"Component loading complete. {len(self.components)} components loaded.")
def _load_component_from_path(self, name: str, component_path: Path):
"""Dynamically loads a single component from a local directory path."""
adapter_file_path = component_path / ADAPTER_FILENAME
if not adapter_file_path.is_file():
log.error(f"Component '{name}': Adapter file '{ADAPTER_FILENAME}' not found in {component_path}. Skipping.")
return
try:
# Create a unique module name to avoid conflicts
module_name = f"radian.external_components.{name}"
# Create a module spec from the file path
spec = importlib.util.spec_from_file_location(module_name, adapter_file_path)
if not spec or not spec.loader:
log.error(f"Component '{name}': Could not create module spec from {adapter_file_path}.")
return
# Create a new module object
module = importlib.util.module_from_spec(spec)
# Add the component's root directory to sys.path TEMPORARILY
# so that its internal imports (e.g., from . import logic) work.
sys.path.insert(0, str(component_path))
# Execute the module's code
spec.loader.exec_module(module)
# IMPORTANT: Remove the path from sys.path to avoid side effects
sys.path.pop(0)
# Find the adapter class within the loaded module
AdapterClass = self._find_adapter_class(module)
if AdapterClass:
instance = AdapterClass()
self.components[name] = instance
log.info(f"Successfully loaded and instantiated component: '{name}'")
else:
log.error(f"Component '{name}': No valid adapter class inheriting from BaseComponent found in {adapter_file_path}.")
except Exception as e:
# Clean up path if an error occurred during exec_module
if str(component_path) in sys.path:
sys.path.remove(str(component_path))
log.error(f"Failed to load component '{name}' from {component_path}. Error: {e}", exc_info=True)
def _find_adapter_class(self, module) -> Optional[Type[BaseComponent]]:
"""Inspects a module to find a class that inherits from BaseComponent."""
for name, obj in inspect.getmembers(module, inspect.isclass):
# Check if it's a class defined in this module (not imported)
# and if it's a subclass of BaseComponent (but not BaseComponent itself)
if obj is not BaseComponent and issubclass(obj, BaseComponent):
return obj
return None
def get_component(self, name: str) -> Optional[BaseComponent]:
"""Returns the loaded instance of a component by its name."""
return self.components.get(name)
def get_all_components(self) -> Dict[str, BaseComponent]:
"""Returns a dictionary of all loaded components."""
return self.components

View File

@ -0,0 +1,16 @@
# config/components.yaml
components:
test_component:
# !!! IMPORTANT: Use the actual path to the 'test_component' FOLDER you created !!!
path: "C:/src/____GitProjects/RADIAN/radian_plugins/test_component"
# Comment out the other components for now, as they don't have an adapter.py yet.
#
# radar_data_reader:
# path: "C:/path/to/your/radar_data_reader_project"
# git_repo: "https://github.com/your_username/radar_data_reader.git"
#
# flight_monitor:
# path: "C:/path/to/your/FlightMonitor_project"
# git_repo: "https://github.com/your_username/FlightMonitor.git"

View File

@ -1,8 +1,9 @@
# radian/core/main_controller.py # radian/core/main_controller.py
from pathlib import Path
from radian.gui.main_window import MainWindow from radian.gui.main_window import MainWindow
from radian.utils import logger from radian.utils import logger
from radian.config.logging_config import LOGGING_CONFIG from radian.config.logging_config import LOGGING_CONFIG
from radian.components.component_manager import ComponentManager
# Get a logger for this specific module # Get a logger for this specific module
log = logger.get_logger(__name__) log = logger.get_logger(__name__)
@ -16,26 +17,49 @@ class MainController:
""" """
def __init__(self): def __init__(self):
self.app: MainWindow | None = None self.app: MainWindow | None = None
self.component_manager: ComponentManager | None = None
def run(self): def run(self):
"""
Initializes and starts the RADIAN application.
"""
# 1. Initialize the main window (the GUI) # 1. Initialize the main window (the GUI)
self.app = MainWindow() self.app = MainWindow()
# 2. Set up the centralized logging system # 2. Set up the centralized logging system
self._setup_logging() self._setup_logging()
# 3. Set up a graceful shutdown procedure # 3. Initialize and run the component manager
self._load_components()
# 4. Populate the GUI with the loaded components
self._populate_gui_with_components()
# 5. Set up a graceful shutdown procedure
self.app.protocol("WM_DELETE_WINDOW", self._on_shutdown) self.app.protocol("WM_DELETE_WINDOW", self._on_shutdown)
log.info("RADIAN application started.")
log.info("RADIAN application started.") log.info("RADIAN application started.")
log.info("Welcome to the Radar Data Integration & Analysis Network.") log.info("Welcome to the Radar Data Integration & Analysis Network.")
# 4. Start the Tkinter main event loop # --- TEST: Log i componenti caricati ---
if self.component_manager:
loaded_names = self.component_manager.get_all_components().keys()
if loaded_names:
log.info(f"Available components: {list(loaded_names)}")
else:
log.warning("No components were loaded. Check config/components.yaml and paths.")
# 5. Start the Tkinter main event loop
self.app.mainloop() self.app.mainloop()
def _load_components(self):
"""Initializes the ComponentManager and loads all plugins."""
log.info("Initializing Component Manager...")
# Assume config file is in 'config/components.yaml' relative to the project root
# This needs a robust way to find the project root. For now, let's assume...
config_path = Path(__file__).parent.parent / "config" / "components.yaml"
self.component_manager = ComponentManager(config_path.resolve())
self.component_manager.load_components()
def _setup_logging(self): def _setup_logging(self):
"""Initializes the basic logger and adds the Tkinter handler.""" """Initializes the basic logger and adds the Tkinter handler."""
# Set up the queue, processor, and file/console handlers # Set up the queue, processor, and file/console handlers
@ -64,3 +88,40 @@ class MainController:
# Destroy the main window # Destroy the main window
if self.app: if self.app:
self.app.destroy() self.app.destroy()
def _populate_gui_with_components(self):
"""Gets components from the manager and adds them to the rich GUI list."""
if not self.component_manager or not self.app:
return
all_components = self.component_manager.get_all_components()
for comp_id, component in all_components.items():
self.app.add_component_to_list(
component_id=comp_id,
name=component.get_name(),
icon_path=component.get_icon_path(),
callback=self._on_component_selected # Passiamo il metodo come callback
)
def _on_component_selected(self, component_id: str): # Ora riceve l'id direttamente
"""Called when a user clicks on a component widget in the list."""
if not self.app or not self.component_manager:
return
component = self.component_manager.get_component(component_id)
if not component:
return
log.info(f"Component '{component.get_name()}' selected.")
# Deseleziona gli altri (feedback visivo)
# (Implementazione più avanzata in futuro)
component_ui_frame = component.get_config_ui(self.app.content_frame)
if component_ui_frame:
self.app.display_component_ui(component_ui_frame)
else:
self.app.clear_content_area()
log.info(f"Component '{component.get_name()}' has no configuration UI.")

View File

@ -2,71 +2,119 @@
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import Dict, Any, Optional, Callable
from PIL import Image, ImageTk # Assicurati di aver installato Pillow: pip install Pillow
class MainWindow(tk.Tk): class MainWindow(tk.Tk):
"""
The main window of the RADIAN application.
It sets up the main application frame, including the layout for
the components' UI and the logging area.
"""
def __init__(self): def __init__(self):
super().__init__() super().__init__()
# --- Basic Window Configuration ---
self.title("RADIAN - Radar Data Integration & Analysis Network") self.title("RADIAN - Radar Data Integration & Analysis Network")
self._set_fullscreen() self._set_fullscreen()
# --- Main Layout Structure --- # Store PhotoImage objects to prevent garbage collection
# The main container frame self.photo_images: Dict[str, ImageTk.PhotoImage] = {}
main_frame = ttk.Frame(self) main_frame = ttk.Frame(self)
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# Configure the grid layout (2 rows, 1 column) main_frame.rowconfigure(0, weight=1)
main_frame.rowconfigure(0, weight=3) # Content area takes 3/4 of the space main_frame.columnconfigure(0, weight=1, minsize=250) # Colonna lista, peso minore ma con minsize
main_frame.rowconfigure(1, weight=1) # Log area takes 1/4 of the space main_frame.columnconfigure(1, weight=5)
main_frame.columnconfigure(0, weight=1)
# --- Create Widgets --- self._create_components_list_area(main_frame)
self._create_content_area(main_frame)
self._create_log_area(main_frame) right_panel = ttk.Frame(main_frame)
right_panel.grid(row=0, column=1, sticky="nsew")
right_panel.rowconfigure(0, weight=3)
right_panel.rowconfigure(1, weight=1)
right_panel.columnconfigure(0, weight=1)
self._create_content_area(right_panel)
self._create_log_area(right_panel)
def _set_fullscreen(self): def _set_fullscreen(self):
"""Sets the window to a maximized state."""
# 'zoomed' is the standard state for a maximized window in Tkinter.
# This is generally more reliable across different systems than using attributes.
self.state('zoomed') self.state('zoomed')
def _create_components_list_area(self, parent: ttk.Frame):
list_frame = ttk.LabelFrame(parent, text="Components", padding="10")
list_frame.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
list_frame.rowconfigure(0, weight=1)
list_frame.columnconfigure(0, weight=1)
# --- Nuovo approccio con Canvas e Frame scrollabile ---
canvas = tk.Canvas(list_frame)
scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=canvas.yview)
self.scrollable_frame = ttk.Frame(canvas)
self.scrollable_frame.bind(
"<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
)
canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
canvas.grid(row=0, column=0, sticky="nsew")
scrollbar.grid(row=0, column=1, sticky="ns")
def add_component_to_list(self, component_id: str, name: str, icon_path: Optional[str], callback: Callable):
"""Adds a single, rich component widget to the scrollable list."""
# Frame per il singolo componente
comp_frame = ttk.Frame(self.scrollable_frame, padding=5)
comp_frame.pack(fill="x", expand=True, pady=2)
# Carica l'icona
try:
if icon_path:
img = Image.open(icon_path).resize((48, 48), Image.Resampling.LANCZOS)
photo = ImageTk.PhotoImage(img)
self.photo_images[component_id] = photo # Salva il riferimento!
icon_label = ttk.Label(comp_frame, image=photo)
icon_label.grid(row=0, column=0, rowspan=2, padx=(0, 10))
else:
# Placeholder se non c'è icona
icon_label = ttk.Label(comp_frame, text="", font=("Segoe UI", 24))
icon_label.grid(row=0, column=0, rowspan=2, padx=(0, 10))
except Exception as e:
print(f"Error loading icon for {name}: {e}")
icon_label = ttk.Label(comp_frame, text="!", font=("Segoe UI", 24))
icon_label.grid(row=0, column=0, rowspan=2, padx=(0, 10))
# Nome del componente
name_label = ttk.Label(comp_frame, text=name, font=("Segoe UI", 12, "bold"))
name_label.grid(row=0, column=1, sticky="w")
# --- Eventi di Click ---
# Usa una lambda per passare l'id del componente al callback
click_handler = lambda e, c_id=component_id: callback(c_id)
comp_frame.bind("<Button-1>", click_handler)
name_label.bind("<Button-1>", click_handler)
icon_label.bind("<Button-1>", click_handler)
def _create_content_area(self, parent: ttk.Frame): def _create_content_area(self, parent: ttk.Frame):
"""Creates the frame that will host the components' UI."""
content_frame = ttk.LabelFrame(parent, text="Component View", padding="10") content_frame = ttk.LabelFrame(parent, text="Component View", padding="10")
content_frame.grid(row=0, column=0, sticky="nsew", padx=5, pady=5) content_frame.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
# This is a placeholder. In the future, this frame will be populated
# by the ComponentManager with the UI of the selected component.
placeholder_label = ttk.Label(content_frame, text="Component UI will be displayed here.")
placeholder_label.pack(expand=True)
self.content_frame = content_frame self.content_frame = content_frame
def _create_log_area(self, parent: ttk.Frame): def _create_log_area(self, parent: ttk.Frame):
"""Creates the scrollable text widget for logging."""
log_frame = ttk.LabelFrame(parent, text="System Log", padding="10") log_frame = ttk.LabelFrame(parent, text="System Log", padding="10")
log_frame.grid(row=1, column=0, sticky="nsew", padx=5, pady=5) log_frame.grid(row=1, column=0, sticky="nsew", padx=5, pady=5)
# Configure grid to make the text widget fill the frame
log_frame.rowconfigure(0, weight=1) log_frame.rowconfigure(0, weight=1)
log_frame.columnconfigure(0, weight=1) log_frame.columnconfigure(0, weight=1)
log_text_widget = tk.Text(log_frame, wrap=tk.WORD, state=tk.DISABLED, height=10)
# The text widget for displaying logs log_text_widget.grid(row=0, column=0, sticky="nsew")
log_text = tk.Text(log_frame, wrap=tk.WORD, state=tk.DISABLED, height=10) scrollbar = ttk.Scrollbar(log_frame, orient=tk.VERTICAL, command=log_text_widget.yview)
log_text.grid(row=0, column=0, sticky="nsew")
# The scrollbar for the text widget
scrollbar = ttk.Scrollbar(log_frame, orient=tk.VERTICAL, command=log_text.yview)
scrollbar.grid(row=0, column=1, sticky="ns") scrollbar.grid(row=0, column=1, sticky="ns")
log_text_widget.configure(yscrollcommand=scrollbar.set)
self.log_text = log_text_widget
# Link the scrollbar to the text widget def clear_content_area(self):
log_text.configure(yscrollcommand=scrollbar.set) for widget in self.content_frame.winfo_children():
widget.destroy()
self.log_text = log_text def display_component_ui(self, component_ui_frame: tk.Frame):
self.clear_content_area()
component_ui_frame.pack(fill=tk.BOTH, expand=True)

View File

@ -0,0 +1,115 @@
# C:/radian_plugins/test_component/adapter.py
import tkinter as tk
from tkinter import ttk
from typing import Optional
from pathlib import Path
# This import works because RADIAN was installed in editable mode.
from radian.components.base_component import BaseComponent, ConfigType
from radian.utils import logger
# Each component should have its own logger instance.
log = logger.get_logger(__name__)
class RadianAdapter(BaseComponent):
"""
A dummy component adapter for testing the RADIAN loading mechanism.
This class implements the full BaseComponent contract.
"""
def __init__(self):
log.info("TestComponent's adapter is being initialized...")
# The call to super().__init__() is important as it sets up
# the initial configuration by calling get_default_config().
super().__init__()
# NOTE: Tkinter variables are now created inside get_config_ui
# to ensure they are fresh each time the UI is built.
def get_name(self) -> str:
"""Returns the user-friendly name of the component."""
return "Test Component"
def get_description(self) -> str:
"""Returns a brief description of what the component does."""
return "A dummy component for demonstrating and testing the plugin system."
def get_default_config(self) -> ConfigType:
"""Defines the default configuration parameters for this component."""
return {
"test_parameter_1": "default_string_value",
"test_parameter_2": True
}
def get_icon_path(self) -> Optional[str]:
"""Returns the path to this component's icon."""
# The icon is expected to be in the same directory as this adapter file.
icon_path = Path(__file__).parent / "test_component.png"
if icon_path.exists():
return str(icon_path)
log.warning(f"Icon not found at: {icon_path}")
return None
def get_config_ui(self, parent: tk.Frame) -> Optional[tk.Frame]:
"""Creates the configuration UI for this test component."""
log.debug(f"Creating config UI for '{self.get_name()}'")
frame = ttk.Frame(parent, padding="10")
frame.columnconfigure(1, weight=1)
# --- Initialize Tkinter variables locally within this method ---
# This ensures they are new for each UI creation and avoids conflicts
# with previously destroyed widgets.
param1_var = tk.StringVar()
param2_var = tk.BooleanVar()
# --- UI Widgets ---
label1 = ttk.Label(frame, text="Test Parameter 1 (String):")
label1.grid(row=0, column=0, padx=5, pady=5, sticky="w")
entry1 = ttk.Entry(frame, textvariable=param1_var)
entry1.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
check2 = ttk.Checkbutton(frame, text="Enable Test Feature", variable=param2_var)
check2.grid(row=1, column=0, columnspan=2, padx=5, pady=5, sticky="w")
# --- Nested helper functions to manage UI state ---
# These functions have access to the local tkinter variables.
def update_ui_from_config():
"""Helper to sync the UI with the current self.config state."""
param1_var.set(self.config.get("test_parameter_1", ""))
param2_var.set(self.config.get("test_parameter_2", False))
def update_config_from_ui(*args):
"""Helper to sync self.config with the current UI state."""
self.config["test_parameter_1"] = param1_var.get()
self.config["test_parameter_2"] = param2_var.get()
log.info(f"TestComponent config updated from UI: {self.config}")
# --- Load current config into UI variables ---
update_ui_from_config()
# --- Bind UI changes back to the config dictionary ---
param1_var.trace_add("write", update_config_from_ui)
param2_var.trace_add("write", update_config_from_ui)
return frame
def run(self, input_data: Optional[any] = None) -> any:
"""Executes the dummy logic of this component."""
log.info(f"--- Running '{self.get_name()}' ---")
if input_data:
log.info(f"Received input data: {input_data}")
log.info(f"Using configuration: {self.config}")
if self.config.get("test_parameter_2"):
log.info("Test Feature is ENABLED.")
else:
log.info("Test Feature is DISABLED.")
output_message = f"Output from {self.get_name()} with param1='{self.config.get('test_parameter_1')}'"
log.info("--- Finished running ---")
return output_message