Loading noctua/api/__init__.py +46 −26 Original line number Diff line number Diff line Loading @@ -9,18 +9,20 @@ import importlib from pathlib import Path # Third-party modules from quart import Blueprint, request, abort, jsonify from quart import Blueprint, abort, jsonify, request # Custom modules # Custom modules (Noctua Core) from noctua import devices from noctua.api.basecontext import config_path, ends, resource_registry from noctua.api.sequencer_instance import seq from noctua.api.basecontext import ends, resource_registry, config_path from noctua.utils.logger import log # Other templates from .blocks import blocks_api from .defaults import defaults_api from .sequencer import sequencer_api, BobRun from .guider import guider_api, Guider, Calibrate from .guider import Calibrate, Guider, guider_api from .guider_instance import guider from .sequencer import BobRun, sequencer_api api_blueprint = Blueprint('api', __name__) Loading Loading @@ -65,14 +67,26 @@ def dynamic_import(url_path): if url_path.startswith(("/blocks", "/templates", "/sequencer")): return dev = getattr(devices, ends.get(url_path,"device")) # devices.light instance dev = getattr(devices, ends.get(url_path, "device") ) # devices.light instance cls = dev.__class__.__name__ # string "Switch" mod_name = cls.lower() # string "switch" module = importlib.import_module(f"noctua.api.{mod_name}") # module noctua.api.switch resource_class = getattr(module, ends.get(url_path,"resource")) # class noctua.api.switch.State # Identify supported HTTP methods by checking for lowercase method names in the class methods = [m for m in ["GET", "POST", "PUT", "DELETE"] if hasattr(resource_class, m.lower())] # string "switch" mod_name = cls.lower() module = importlib.import_module( f"noctua.api.{mod_name}") # module noctua.api.switch # class noctua.api.switch.State resource_class = getattr(module, ends.get(url_path, "resource")) # Identify supported HTTP methods by checking for lowercase method # names in the class methods = [ m for m in [ "GET", "POST", "PUT", "DELETE"] if hasattr( resource_class, m.lower())] # Populate internal registry resource_registry[url_path] = resource_class(dev=dev) Loading @@ -83,11 +97,16 @@ def dynamic_import(url_path): # Improved debug print with methods list full_resource_path = f"{module.__name__}.{resource_class.__name__}" log.info(f"Route registered: /api{url_path:<40} {full_resource_path:<48} {str(methods):<25}") log.info( f"Route registered: /api{ url_path:<40} { full_resource_path:<48} { str(methods):<25}") except Exception as e: log.warning(f"Error loading route: {url_path:<40} {e}") # Load all routes from ini for section in ends.sections(): if not section.startswith(("/blocks", "/templates", "/sequencer")): Loading Loading @@ -125,7 +144,8 @@ async def api_catalog(): method_fn = getattr(resource_instance, m.lower(), None) if method_fn: # Estraiamo i metadati del decoratore se presenti, altrimenti None # Estraiamo i metadati del decoratore se presenti, altrimenti # None schema = getattr(method_fn, "_input_schema", None) formatted_endpoint = f"/api/{url_path.strip('/')}/" Loading noctua/api/basecontext.py +1 −0 Original line number Diff line number Diff line # System modules import configparser from pathlib import Path Loading noctua/api/baseresource.py +26 −14 Original line number Diff line number Diff line #!/usr/bin/env python3 # -*- coding: utf-8 -*- # System modules import asyncio from datetime import datetime from quart import request, jsonify # Third-party modules from quart import jsonify, request from quart.views import MethodView # Custom modules # Other templates from .deps import dep_checker # Change this import class BaseResource(MethodView): """ Base class for all API resources, providing dependency management and Loading Loading @@ -75,7 +79,12 @@ class BaseResource(MethodView): return await asyncio.to_thread(func, *args, **kwargs) def make_response(self, response_data, raw_data=None, errors=None, status_code=200): def make_response( self, response_data, raw_data=None, errors=None, status_code=200): """ Build a standardized JSON response dict. Loading Loading @@ -120,6 +129,7 @@ class BaseResource(MethodView): handler = getattr(self, request.method.lower(), None) if handler is None: # Third-party modules from quart import abort abort(405) Loading Loading @@ -151,21 +161,23 @@ def register_error_handlers(app): @app.errorhandler(400) async def bad_request(e): return jsonify({"error": ["Bad Request"], "timestamp": datetime.utcnow().isoformat()}), 400 return jsonify( {"error": ["Bad Request"], "timestamp": datetime.utcnow().isoformat()}), 400 @app.errorhandler(404) async def not_found(e): return jsonify({"error": ["URL not found"], "timestamp": datetime.utcnow().isoformat()}), 404 return jsonify( {"error": ["URL not found"], "timestamp": datetime.utcnow().isoformat()}), 404 @app.errorhandler(405) async def method_not_allowed(e): return jsonify({"error": ["Method not allowed"], "timestamp": datetime.utcnow().isoformat()}), 405 return jsonify({"error": ["Method not allowed"], "timestamp": datetime.utcnow().isoformat()}), 405 @app.errorhandler(424) async def failed_dependency(e): return jsonify({"error": ["Failed Dependency"], "timestamp": datetime.utcnow().isoformat()}), 424 return jsonify({"error": ["Failed Dependency"], "timestamp": datetime.utcnow().isoformat()}), 424 def expects(param_type="single", count=1, unit=None, placeholder=None): Loading noctua/api/blocks.py +22 −16 Original line number Diff line number Diff line Loading @@ -33,7 +33,8 @@ class BlockFile(MethodView): content = await dao.read(name) return content if content is not None else ({"error": "OB not found"}, 404) return content if content is not None else ( {"error": "OB not found"}, 404) async def post(self, name): """ Loading @@ -44,12 +45,14 @@ class BlockFile(MethodView): data = await request.get_json(force=True, silent=True) # Logic to distinguish between "Create from Template" and "Save Editor Data" # Logic to distinguish between "Create from Template" and "Save Editor # Data" if isinstance(data, str): # It's a template name (e.g. "observation") tpl_content = await dao.read_default(data) if tpl_content: final_data = [tpl_content] if not isinstance(tpl_content, list) else tpl_content final_data = [tpl_content] if not isinstance( tpl_content, list) else tpl_content else: return {"error": f"Template {data} not found"}, 404 elif isinstance(data, (dict, list)): Loading Loading @@ -132,5 +135,8 @@ class BlockElement(MethodView): # --- ROUTING --- blocks_api.add_url_rule('/', view_func=BlocksList.as_view('blocks_list')) blocks_api.add_url_rule('/<name>', view_func=BlockFile.as_view('block_file')) blocks_api.add_url_rule('/<name>/', view_func=BlockFile.as_view('block_file_read')) blocks_api.add_url_rule('/<name>/<int:index>', view_func=BlockElement.as_view('block_element')) blocks_api.add_url_rule( '/<name>/', view_func=BlockFile.as_view('block_file_read')) blocks_api.add_url_rule('/<name>/<int:index>', view_func=BlockElement.as_view('block_element')) noctua/api/camera.py +31 −23 Original line number Diff line number Diff line Loading @@ -4,12 +4,15 @@ '''REST API for Camera related operations''' # This module imports from .baseresource import BaseResource, expects from noctua.config import constants # Custom modules (Noctua Core) from noctua.api.sequencer_instance import seq from noctua.config import constants from noctua.utils.logger import log # Other templates # This module imports from .baseresource import BaseResource, expects class FrameBinning(BaseResource): """Binning of the camera.""" Loading @@ -19,6 +22,7 @@ class FrameBinning(BaseResource): """Set a new binning for the camera.""" binning = await self.get_payload() def action(): self.dev.binning = binning Loading @@ -43,6 +47,7 @@ class Gain(BaseResource): """Set a new gain for the camera.""" gain = await self.get_payload() def action(): self.dev.gain = gain Loading @@ -68,6 +73,7 @@ class Cooler(BaseResource): """Set on or off the CCD cooler.""" state = await self.get_payload() def action(): self.dev.cooler = state Loading @@ -85,6 +91,7 @@ class CoolerTemperatureSetpoint(BaseResource): """Set a new temperature of the CCD.""" state = await self.get_payload() def action(): self.dev.temperature = state Loading Loading @@ -124,6 +131,7 @@ class FilterMovement(BaseResource): """Set a new filter.""" target = await self.get_payload() def action(): self.dev.filter = target Loading Loading
noctua/api/__init__.py +46 −26 Original line number Diff line number Diff line Loading @@ -9,18 +9,20 @@ import importlib from pathlib import Path # Third-party modules from quart import Blueprint, request, abort, jsonify from quart import Blueprint, abort, jsonify, request # Custom modules # Custom modules (Noctua Core) from noctua import devices from noctua.api.basecontext import config_path, ends, resource_registry from noctua.api.sequencer_instance import seq from noctua.api.basecontext import ends, resource_registry, config_path from noctua.utils.logger import log # Other templates from .blocks import blocks_api from .defaults import defaults_api from .sequencer import sequencer_api, BobRun from .guider import guider_api, Guider, Calibrate from .guider import Calibrate, Guider, guider_api from .guider_instance import guider from .sequencer import BobRun, sequencer_api api_blueprint = Blueprint('api', __name__) Loading Loading @@ -65,14 +67,26 @@ def dynamic_import(url_path): if url_path.startswith(("/blocks", "/templates", "/sequencer")): return dev = getattr(devices, ends.get(url_path,"device")) # devices.light instance dev = getattr(devices, ends.get(url_path, "device") ) # devices.light instance cls = dev.__class__.__name__ # string "Switch" mod_name = cls.lower() # string "switch" module = importlib.import_module(f"noctua.api.{mod_name}") # module noctua.api.switch resource_class = getattr(module, ends.get(url_path,"resource")) # class noctua.api.switch.State # Identify supported HTTP methods by checking for lowercase method names in the class methods = [m for m in ["GET", "POST", "PUT", "DELETE"] if hasattr(resource_class, m.lower())] # string "switch" mod_name = cls.lower() module = importlib.import_module( f"noctua.api.{mod_name}") # module noctua.api.switch # class noctua.api.switch.State resource_class = getattr(module, ends.get(url_path, "resource")) # Identify supported HTTP methods by checking for lowercase method # names in the class methods = [ m for m in [ "GET", "POST", "PUT", "DELETE"] if hasattr( resource_class, m.lower())] # Populate internal registry resource_registry[url_path] = resource_class(dev=dev) Loading @@ -83,11 +97,16 @@ def dynamic_import(url_path): # Improved debug print with methods list full_resource_path = f"{module.__name__}.{resource_class.__name__}" log.info(f"Route registered: /api{url_path:<40} {full_resource_path:<48} {str(methods):<25}") log.info( f"Route registered: /api{ url_path:<40} { full_resource_path:<48} { str(methods):<25}") except Exception as e: log.warning(f"Error loading route: {url_path:<40} {e}") # Load all routes from ini for section in ends.sections(): if not section.startswith(("/blocks", "/templates", "/sequencer")): Loading Loading @@ -125,7 +144,8 @@ async def api_catalog(): method_fn = getattr(resource_instance, m.lower(), None) if method_fn: # Estraiamo i metadati del decoratore se presenti, altrimenti None # Estraiamo i metadati del decoratore se presenti, altrimenti # None schema = getattr(method_fn, "_input_schema", None) formatted_endpoint = f"/api/{url_path.strip('/')}/" Loading
noctua/api/basecontext.py +1 −0 Original line number Diff line number Diff line # System modules import configparser from pathlib import Path Loading
noctua/api/baseresource.py +26 −14 Original line number Diff line number Diff line #!/usr/bin/env python3 # -*- coding: utf-8 -*- # System modules import asyncio from datetime import datetime from quart import request, jsonify # Third-party modules from quart import jsonify, request from quart.views import MethodView # Custom modules # Other templates from .deps import dep_checker # Change this import class BaseResource(MethodView): """ Base class for all API resources, providing dependency management and Loading Loading @@ -75,7 +79,12 @@ class BaseResource(MethodView): return await asyncio.to_thread(func, *args, **kwargs) def make_response(self, response_data, raw_data=None, errors=None, status_code=200): def make_response( self, response_data, raw_data=None, errors=None, status_code=200): """ Build a standardized JSON response dict. Loading Loading @@ -120,6 +129,7 @@ class BaseResource(MethodView): handler = getattr(self, request.method.lower(), None) if handler is None: # Third-party modules from quart import abort abort(405) Loading Loading @@ -151,21 +161,23 @@ def register_error_handlers(app): @app.errorhandler(400) async def bad_request(e): return jsonify({"error": ["Bad Request"], "timestamp": datetime.utcnow().isoformat()}), 400 return jsonify( {"error": ["Bad Request"], "timestamp": datetime.utcnow().isoformat()}), 400 @app.errorhandler(404) async def not_found(e): return jsonify({"error": ["URL not found"], "timestamp": datetime.utcnow().isoformat()}), 404 return jsonify( {"error": ["URL not found"], "timestamp": datetime.utcnow().isoformat()}), 404 @app.errorhandler(405) async def method_not_allowed(e): return jsonify({"error": ["Method not allowed"], "timestamp": datetime.utcnow().isoformat()}), 405 return jsonify({"error": ["Method not allowed"], "timestamp": datetime.utcnow().isoformat()}), 405 @app.errorhandler(424) async def failed_dependency(e): return jsonify({"error": ["Failed Dependency"], "timestamp": datetime.utcnow().isoformat()}), 424 return jsonify({"error": ["Failed Dependency"], "timestamp": datetime.utcnow().isoformat()}), 424 def expects(param_type="single", count=1, unit=None, placeholder=None): Loading
noctua/api/blocks.py +22 −16 Original line number Diff line number Diff line Loading @@ -33,7 +33,8 @@ class BlockFile(MethodView): content = await dao.read(name) return content if content is not None else ({"error": "OB not found"}, 404) return content if content is not None else ( {"error": "OB not found"}, 404) async def post(self, name): """ Loading @@ -44,12 +45,14 @@ class BlockFile(MethodView): data = await request.get_json(force=True, silent=True) # Logic to distinguish between "Create from Template" and "Save Editor Data" # Logic to distinguish between "Create from Template" and "Save Editor # Data" if isinstance(data, str): # It's a template name (e.g. "observation") tpl_content = await dao.read_default(data) if tpl_content: final_data = [tpl_content] if not isinstance(tpl_content, list) else tpl_content final_data = [tpl_content] if not isinstance( tpl_content, list) else tpl_content else: return {"error": f"Template {data} not found"}, 404 elif isinstance(data, (dict, list)): Loading Loading @@ -132,5 +135,8 @@ class BlockElement(MethodView): # --- ROUTING --- blocks_api.add_url_rule('/', view_func=BlocksList.as_view('blocks_list')) blocks_api.add_url_rule('/<name>', view_func=BlockFile.as_view('block_file')) blocks_api.add_url_rule('/<name>/', view_func=BlockFile.as_view('block_file_read')) blocks_api.add_url_rule('/<name>/<int:index>', view_func=BlockElement.as_view('block_element')) blocks_api.add_url_rule( '/<name>/', view_func=BlockFile.as_view('block_file_read')) blocks_api.add_url_rule('/<name>/<int:index>', view_func=BlockElement.as_view('block_element'))
noctua/api/camera.py +31 −23 Original line number Diff line number Diff line Loading @@ -4,12 +4,15 @@ '''REST API for Camera related operations''' # This module imports from .baseresource import BaseResource, expects from noctua.config import constants # Custom modules (Noctua Core) from noctua.api.sequencer_instance import seq from noctua.config import constants from noctua.utils.logger import log # Other templates # This module imports from .baseresource import BaseResource, expects class FrameBinning(BaseResource): """Binning of the camera.""" Loading @@ -19,6 +22,7 @@ class FrameBinning(BaseResource): """Set a new binning for the camera.""" binning = await self.get_payload() def action(): self.dev.binning = binning Loading @@ -43,6 +47,7 @@ class Gain(BaseResource): """Set a new gain for the camera.""" gain = await self.get_payload() def action(): self.dev.gain = gain Loading @@ -68,6 +73,7 @@ class Cooler(BaseResource): """Set on or off the CCD cooler.""" state = await self.get_payload() def action(): self.dev.cooler = state Loading @@ -85,6 +91,7 @@ class CoolerTemperatureSetpoint(BaseResource): """Set a new temperature of the CCD.""" state = await self.get_payload() def action(): self.dev.temperature = state Loading Loading @@ -124,6 +131,7 @@ class FilterMovement(BaseResource): """Set a new filter.""" target = await self.get_payload() def action(): self.dev.filter = target Loading