Loading noctua/api/__init__.py +36 −16 Original line number Diff line number Diff line Loading @@ -9,12 +9,14 @@ import importlib from pathlib import Path # Third-party modules from quart import Blueprint, request, abort from quart import Blueprint, abort, 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 # Other templates from .blocks import blocks_api from .defaults import defaults_api from .sequencer import sequencer_api Loading @@ -31,6 +33,7 @@ api_blueprint.register_blueprint(sequencer_api, url_prefix='/sequencer') api_blueprint = Blueprint('api', __name__) def dynamic_import(url_path): """ Import and register resources from api.ini with debug logging of supported methods. Loading @@ -40,14 +43,26 @@ def dynamic_import(url_path): if url_path.startswith(("/blocks", "/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 @@ -58,11 +73,16 @@ def dynamic_import(url_path): # Improved debug print with methods list full_resource_path = f"{module.__name__}.{resource_class.__name__}" print(f"Route registered: /api{url_path:<40} {full_resource_path:<48} {str(methods):<25}") print( f"Route registered: /api{ url_path:<40} { full_resource_path:<48} { str(methods):<25}") except Exception as e: print(f"Error loading {url_path}: {e}") # Load all routes from ini for section in ends.sections(): if not section.startswith(("/blocks", "/sequencer")): 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 +22 −8 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 check_dependencies Loading Loading @@ -72,7 +75,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 @@ -123,10 +131,12 @@ class BaseResource(MethodView): # 1. Validate that the class implements the requested HTTP method handler = getattr(self, request.method.lower(), None) if handler is None: # Third-party modules from quart import abort abort(405) # 2. Validate the dependency chain (reads ?force=true from query string) # 2. Validate the dependency chain (reads ?force=true from query # string) force = request.args.get('force') == 'true' is_available, err_msg = await check_dependencies(api_path, force) Loading @@ -153,16 +163,20 @@ 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 noctua/api/blocks.py +18 −14 Original line number Diff line number Diff line Loading @@ -7,12 +7,13 @@ from quart import Blueprint, request from quart.views import MethodView # Custom modules # Custom modules (Noctua Core) from noctua.utils.data_access_object import ObservationBlockObject blocks_api = Blueprint('blocks', __name__) dao = ObservationBlockObject() class BlocksList(MethodView): """Manage the collection of OB files.""" Loading @@ -21,6 +22,7 @@ class BlocksList(MethodView): res = await dao.list_available() return res class BlockFile(MethodView): """Manage a specific OB file (Create, Append, Delete, Read All).""" Loading @@ -28,7 +30,8 @@ class BlockFile(MethodView): """Show the whole OB content.""" 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): """Create a new OB. If payload is a valid default name, use Loading @@ -42,7 +45,8 @@ class BlockFile(MethodView): tpl_content = await dao.read_default(tpl_name) if tpl_content: # Ensure it's a list for the OB file data = [tpl_content] if not isinstance(tpl_content, list) else tpl_content data = [tpl_content] if not isinstance( tpl_content, list) else tpl_content except Exception: pass Loading noctua/api/camera.py +57 −32 Original line number Diff line number Diff line Loading @@ -4,10 +4,14 @@ '''REST API for Camera related operations''' # Custom modules (Noctua Core) from noctua.api.sequencer_instance import seq from noctua.config import constants # Other templates # This module imports from .baseresource import BaseResource from noctua.config import constants from noctua.api.sequencer_instance import seq class FrameBinning(BaseResource): """Binning of the camera.""" Loading @@ -16,12 +20,14 @@ class FrameBinning(BaseResource): """Set a new binning for the camera.""" binning = await self.get_payload() def action(): self.dev.binning = binning return self.dev.binning res = await self.run_blocking(action) return self.make_response(res) class Cooler(BaseResource): """Manage the CCD cooler status""" Loading @@ -36,12 +42,14 @@ class Cooler(BaseResource): """Set on or off the CCD cooler.""" state = await self.get_payload() def action(): self.dev.cooler = state return self.dev.cooler res = await self.run_blocking(action) return self.make_response(res) class CoolerTemperatureSetpoint(BaseResource): """Manage the CCD temperature""" Loading @@ -49,12 +57,14 @@ class CoolerTemperatureSetpoint(BaseResource): """Set a new temperature of the CCD.""" state = await self.get_payload() def action(): self.dev.temperature = state return self.dev.temperature res = await self.run_blocking(action) return self.make_response(res) class CoolerWarmup(BaseResource): """Manage the warmup of the CCD.""" Loading @@ -68,6 +78,7 @@ class CoolerWarmup(BaseResource): return self.make_response("Warmup sequence aborted") class Filters(BaseResource): """Camera filters names.""" Loading @@ -78,6 +89,7 @@ class Filters(BaseResource): res = constants.filter_number return self.make_response(res) class Filter(BaseResource): """Camera filter information.""" Loading @@ -89,6 +101,7 @@ class Filter(BaseResource): res = constants.filter_name.get(raw, "Undef.") return self.make_response(res, raw_data=raw) class FilterMovement(BaseResource): """Manage the camera filter wheel.""" Loading @@ -104,12 +117,14 @@ class FilterMovement(BaseResource): """Set a new filter.""" target = await self.get_payload() def action(): self.dev.filter = target return self.dev.filter res = await self.run_blocking(action) return self.make_response(res) class FrameCustom(BaseResource): """Camera custom frame.""" Loading @@ -117,6 +132,7 @@ class FrameCustom(BaseResource): """Set a custom windowing.""" new_frame = await self.get_payload() def action(): self.dev.binning = new_frame["binning"] # Logic depends on driver implementation of set_window Loading @@ -124,6 +140,7 @@ class FrameCustom(BaseResource): res = await self.run_blocking(action) return self.make_response(res) class FrameFull(BaseResource): """Camera full frame.""" Loading @@ -133,6 +150,7 @@ class FrameFull(BaseResource): res = await self.run_blocking(self.dev.full_frame) return self.make_response(res) class FrameHalf(BaseResource): """Camera frame of half size the full frame.""" Loading @@ -143,6 +161,7 @@ class FrameHalf(BaseResource): res = await self.run_blocking(self.dev.half_frame) return self.make_response(res) class FrameSmall(BaseResource): """Camera frame of 2 arcmin.""" Loading @@ -153,6 +172,7 @@ class FrameSmall(BaseResource): res = await self.run_blocking(self.dev.small_frame) return self.make_response(res) class SnapshotRaw(BaseResource): """The acquired image.""" Loading @@ -177,6 +197,7 @@ class SnapshotRaw(BaseResource): res = await self.run_blocking(self.dev.abort) return self.make_response(res) class SnapshotState(BaseResource): """Manage the state of a raw image.""" Loading @@ -187,6 +208,7 @@ class SnapshotState(BaseResource): res = constants.camera_state.get(raw, "Off") return self.make_response(res, raw_data=raw) class Snapshot(BaseResource): """Manage the acquisition of an image using the sequencer.""" Loading @@ -212,6 +234,7 @@ class Snapshot(BaseResource): res = await self.run_blocking(seq.tpl.abort) return self.make_response(res) class SnapshotRecenter(BaseResource): """Manage the recentering via the the apply_offset function""" Loading Loading @@ -246,6 +269,7 @@ class SnapshotRecenter(BaseResource): seq.tpl.recenter = False return self.make_response(False) class SnapshotDomeslewing(BaseResource): """Get dome slewing status""" Loading Loading @@ -279,6 +303,7 @@ class Connection(BaseResource): res = await self.run_blocking(lambda: self.dev.connection) return self.make_response(res) class Settings(BaseResource): '''General camera settings.''' Loading Loading
noctua/api/__init__.py +36 −16 Original line number Diff line number Diff line Loading @@ -9,12 +9,14 @@ import importlib from pathlib import Path # Third-party modules from quart import Blueprint, request, abort from quart import Blueprint, abort, 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 # Other templates from .blocks import blocks_api from .defaults import defaults_api from .sequencer import sequencer_api Loading @@ -31,6 +33,7 @@ api_blueprint.register_blueprint(sequencer_api, url_prefix='/sequencer') api_blueprint = Blueprint('api', __name__) def dynamic_import(url_path): """ Import and register resources from api.ini with debug logging of supported methods. Loading @@ -40,14 +43,26 @@ def dynamic_import(url_path): if url_path.startswith(("/blocks", "/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 @@ -58,11 +73,16 @@ def dynamic_import(url_path): # Improved debug print with methods list full_resource_path = f"{module.__name__}.{resource_class.__name__}" print(f"Route registered: /api{url_path:<40} {full_resource_path:<48} {str(methods):<25}") print( f"Route registered: /api{ url_path:<40} { full_resource_path:<48} { str(methods):<25}") except Exception as e: print(f"Error loading {url_path}: {e}") # Load all routes from ini for section in ends.sections(): if not section.startswith(("/blocks", "/sequencer")): 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 +22 −8 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 check_dependencies Loading Loading @@ -72,7 +75,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 @@ -123,10 +131,12 @@ class BaseResource(MethodView): # 1. Validate that the class implements the requested HTTP method handler = getattr(self, request.method.lower(), None) if handler is None: # Third-party modules from quart import abort abort(405) # 2. Validate the dependency chain (reads ?force=true from query string) # 2. Validate the dependency chain (reads ?force=true from query # string) force = request.args.get('force') == 'true' is_available, err_msg = await check_dependencies(api_path, force) Loading @@ -153,16 +163,20 @@ 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
noctua/api/blocks.py +18 −14 Original line number Diff line number Diff line Loading @@ -7,12 +7,13 @@ from quart import Blueprint, request from quart.views import MethodView # Custom modules # Custom modules (Noctua Core) from noctua.utils.data_access_object import ObservationBlockObject blocks_api = Blueprint('blocks', __name__) dao = ObservationBlockObject() class BlocksList(MethodView): """Manage the collection of OB files.""" Loading @@ -21,6 +22,7 @@ class BlocksList(MethodView): res = await dao.list_available() return res class BlockFile(MethodView): """Manage a specific OB file (Create, Append, Delete, Read All).""" Loading @@ -28,7 +30,8 @@ class BlockFile(MethodView): """Show the whole OB content.""" 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): """Create a new OB. If payload is a valid default name, use Loading @@ -42,7 +45,8 @@ class BlockFile(MethodView): tpl_content = await dao.read_default(tpl_name) if tpl_content: # Ensure it's a list for the OB file data = [tpl_content] if not isinstance(tpl_content, list) else tpl_content data = [tpl_content] if not isinstance( tpl_content, list) else tpl_content except Exception: pass Loading
noctua/api/camera.py +57 −32 Original line number Diff line number Diff line Loading @@ -4,10 +4,14 @@ '''REST API for Camera related operations''' # Custom modules (Noctua Core) from noctua.api.sequencer_instance import seq from noctua.config import constants # Other templates # This module imports from .baseresource import BaseResource from noctua.config import constants from noctua.api.sequencer_instance import seq class FrameBinning(BaseResource): """Binning of the camera.""" Loading @@ -16,12 +20,14 @@ class FrameBinning(BaseResource): """Set a new binning for the camera.""" binning = await self.get_payload() def action(): self.dev.binning = binning return self.dev.binning res = await self.run_blocking(action) return self.make_response(res) class Cooler(BaseResource): """Manage the CCD cooler status""" Loading @@ -36,12 +42,14 @@ class Cooler(BaseResource): """Set on or off the CCD cooler.""" state = await self.get_payload() def action(): self.dev.cooler = state return self.dev.cooler res = await self.run_blocking(action) return self.make_response(res) class CoolerTemperatureSetpoint(BaseResource): """Manage the CCD temperature""" Loading @@ -49,12 +57,14 @@ class CoolerTemperatureSetpoint(BaseResource): """Set a new temperature of the CCD.""" state = await self.get_payload() def action(): self.dev.temperature = state return self.dev.temperature res = await self.run_blocking(action) return self.make_response(res) class CoolerWarmup(BaseResource): """Manage the warmup of the CCD.""" Loading @@ -68,6 +78,7 @@ class CoolerWarmup(BaseResource): return self.make_response("Warmup sequence aborted") class Filters(BaseResource): """Camera filters names.""" Loading @@ -78,6 +89,7 @@ class Filters(BaseResource): res = constants.filter_number return self.make_response(res) class Filter(BaseResource): """Camera filter information.""" Loading @@ -89,6 +101,7 @@ class Filter(BaseResource): res = constants.filter_name.get(raw, "Undef.") return self.make_response(res, raw_data=raw) class FilterMovement(BaseResource): """Manage the camera filter wheel.""" Loading @@ -104,12 +117,14 @@ class FilterMovement(BaseResource): """Set a new filter.""" target = await self.get_payload() def action(): self.dev.filter = target return self.dev.filter res = await self.run_blocking(action) return self.make_response(res) class FrameCustom(BaseResource): """Camera custom frame.""" Loading @@ -117,6 +132,7 @@ class FrameCustom(BaseResource): """Set a custom windowing.""" new_frame = await self.get_payload() def action(): self.dev.binning = new_frame["binning"] # Logic depends on driver implementation of set_window Loading @@ -124,6 +140,7 @@ class FrameCustom(BaseResource): res = await self.run_blocking(action) return self.make_response(res) class FrameFull(BaseResource): """Camera full frame.""" Loading @@ -133,6 +150,7 @@ class FrameFull(BaseResource): res = await self.run_blocking(self.dev.full_frame) return self.make_response(res) class FrameHalf(BaseResource): """Camera frame of half size the full frame.""" Loading @@ -143,6 +161,7 @@ class FrameHalf(BaseResource): res = await self.run_blocking(self.dev.half_frame) return self.make_response(res) class FrameSmall(BaseResource): """Camera frame of 2 arcmin.""" Loading @@ -153,6 +172,7 @@ class FrameSmall(BaseResource): res = await self.run_blocking(self.dev.small_frame) return self.make_response(res) class SnapshotRaw(BaseResource): """The acquired image.""" Loading @@ -177,6 +197,7 @@ class SnapshotRaw(BaseResource): res = await self.run_blocking(self.dev.abort) return self.make_response(res) class SnapshotState(BaseResource): """Manage the state of a raw image.""" Loading @@ -187,6 +208,7 @@ class SnapshotState(BaseResource): res = constants.camera_state.get(raw, "Off") return self.make_response(res, raw_data=raw) class Snapshot(BaseResource): """Manage the acquisition of an image using the sequencer.""" Loading @@ -212,6 +234,7 @@ class Snapshot(BaseResource): res = await self.run_blocking(seq.tpl.abort) return self.make_response(res) class SnapshotRecenter(BaseResource): """Manage the recentering via the the apply_offset function""" Loading Loading @@ -246,6 +269,7 @@ class SnapshotRecenter(BaseResource): seq.tpl.recenter = False return self.make_response(False) class SnapshotDomeslewing(BaseResource): """Get dome slewing status""" Loading Loading @@ -279,6 +303,7 @@ class Connection(BaseResource): res = await self.run_blocking(lambda: self.dev.connection) return self.make_response(res) class Settings(BaseResource): '''General camera settings.''' Loading