Loading noctua/api/__init__.py +21 −10 Original line number Diff line number Diff line Loading @@ -9,8 +9,10 @@ from pathlib import Path # Third-party modules from quart import Blueprint # Custom modules # Custom modules (Noctua Core) from noctua import devices # Other templates from .blocks import blocks_api from .defaults import defaults_api from .sequencer import sequencer_api Loading @@ -27,6 +29,7 @@ ends = configparser.ConfigParser() config_path = Path(__file__).parent.parent / "config" / "api.ini" ends.read(config_path) def dynamic_import(url_path): """Dynamically import into this module api.ini files.""" Loading @@ -37,20 +40,28 @@ def dynamic_import(url_path): res_class_name = ends.get(url_path, "resource") # CoolerWarmup dev_name = ends.get(url_path, "device") # cam dev_instance = getattr(devices, dev_name) # noctua.devices.stx.Camera object # noctua.devices.stx.Camera object dev_instance = getattr(devices, dev_name) mod_name = dev_instance.__class__.__name__.lower() # camera module = importlib.import_module(f"noctua.api.{mod_name}") # noctua.api.camera resource_class = getattr(module, res_class_name) # noctua.api.camera.CoolerWarmup module = importlib.import_module( f"noctua.api.{mod_name}") # noctua.api.camera # noctua.api.camera.CoolerWarmup resource_class = getattr(module, res_class_name) # Register dynamic route as MethodView # This automatically maps GET/POST/PUT/DELETE to class methods view = resource_class.as_view(url_path, dev=dev_instance) api_blueprint.add_url_rule(url_path, view_func=view) print(f"Route registered: /api{url_path:<40} {module.__name__:>5}.{resource_class.__name__}") print( f"Route registered: /api{ url_path:<40} { module.__name__:>5}.{ resource_class.__name__}") except Exception as e: print(f"Route skipped: /api{url_path:<40} -- {str(e):>5}") for section in ends.sections(): dynamic_import(section) noctua/api/baseresource.py +22 −12 Original line number Diff line number Diff line Loading @@ -4,18 +4,21 @@ '''Base class for REST API''' # System modules # System modules import asyncio from datetime import datetime # Third-party modules from quart import request, jsonify from quart import jsonify, request from quart.views import MethodView class BaseResource(MethodView): """ Equivalent to flask_restx Resource. Methods get(), post(), put(), delete() will be called automatically. """ def __init__(self, dev): self.dev = dev Loading @@ -35,14 +38,20 @@ class BaseResource(MethodView): async def run_blocking(self, func, *args, **kwargs): 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): """ Standardized JSON response format. If errors is provided as a list, it uses that. Otherwise, it pulls from self.dev.error. """ # Logic: use manual errors if provided, otherwise check the device for errors # Logic: use manual errors if provided, otherwise check the device for # errors dev_errors = getattr(self.dev, 'error', []) final_errors = errors if errors is not None else dev_errors Loading @@ -59,6 +68,7 @@ class BaseResource(MethodView): return jsonify(res), status_code def register_error_handlers(app): now = datetime.utcnow().isoformat() Loading @@ -72,9 +82,9 @@ def register_error_handlers(app): @app.errorhandler(405) async def method_not_allowed(e): return jsonify({"error": ["Method not allowed"], "timestamp": now}), 405 return jsonify( {"error": ["Method not allowed"], "timestamp": now}), 405 @app.errorhandler(500) async def internal_error(e): return jsonify({"error": [str(e)], "timestamp": now}), 500 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 −31 Original line number Diff line number Diff line Loading @@ -3,10 +3,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 @@ -15,12 +19,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 @@ -35,12 +41,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 @@ -48,12 +56,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 Filters(BaseResource): """Camera filters names.""" Loading @@ -64,6 +74,7 @@ class Filters(BaseResource): res = constants.filter_number return self.make_response(res) class Filter(BaseResource): """Camera filter information.""" Loading @@ -74,6 +85,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 @@ -88,12 +100,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 @@ -101,6 +115,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 @@ -108,6 +123,7 @@ class FrameCustom(BaseResource): res = await self.run_blocking(action) return self.make_response(res) class FrameFull(BaseResource): """Camera full frame.""" Loading @@ -117,6 +133,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 @@ -127,6 +144,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 @@ -137,6 +155,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 @@ -161,6 +180,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 @@ -171,6 +191,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 @@ -196,6 +217,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 @@ -230,6 +252,7 @@ class SnapshotRecenter(BaseResource): seq.tpl.recenter = False return self.make_response(False) class SnapshotDomeslewing(BaseResource): """Get dome slewing status""" Loading @@ -253,6 +276,7 @@ class SnapshotDomeslewing(BaseResource): seq.tpl.domeslewing = False return self.make_response(False) class CoolerWarmup(BaseResource): """Manage the warmup of the CCD.""" Loading @@ -266,6 +290,7 @@ class CoolerWarmup(BaseResource): return self.make_response("Warmup sequence aborted") class Connection(BaseResource): '''Manage the connection to ASCOM.''' Loading @@ -275,6 +300,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 noctua/api/common.py +5 −2 Original line number Diff line number Diff line # System modules import asyncio from datetime import datetime from dataclasses import dataclass from typing import Optional, Any from datetime import datetime from typing import Any, Optional @dataclass class StandardResponse: Loading @@ -9,6 +11,7 @@ class StandardResponse: error: Optional[list] = None timestamp: str = datetime.utcnow().isoformat() async def wrap_blocking(func, *args, **kwargs): """Esegue una funzione sincrona (L1) in un thread separato""" return await asyncio.to_thread(func, *args, **kwargs) Loading
noctua/api/__init__.py +21 −10 Original line number Diff line number Diff line Loading @@ -9,8 +9,10 @@ from pathlib import Path # Third-party modules from quart import Blueprint # Custom modules # Custom modules (Noctua Core) from noctua import devices # Other templates from .blocks import blocks_api from .defaults import defaults_api from .sequencer import sequencer_api Loading @@ -27,6 +29,7 @@ ends = configparser.ConfigParser() config_path = Path(__file__).parent.parent / "config" / "api.ini" ends.read(config_path) def dynamic_import(url_path): """Dynamically import into this module api.ini files.""" Loading @@ -37,20 +40,28 @@ def dynamic_import(url_path): res_class_name = ends.get(url_path, "resource") # CoolerWarmup dev_name = ends.get(url_path, "device") # cam dev_instance = getattr(devices, dev_name) # noctua.devices.stx.Camera object # noctua.devices.stx.Camera object dev_instance = getattr(devices, dev_name) mod_name = dev_instance.__class__.__name__.lower() # camera module = importlib.import_module(f"noctua.api.{mod_name}") # noctua.api.camera resource_class = getattr(module, res_class_name) # noctua.api.camera.CoolerWarmup module = importlib.import_module( f"noctua.api.{mod_name}") # noctua.api.camera # noctua.api.camera.CoolerWarmup resource_class = getattr(module, res_class_name) # Register dynamic route as MethodView # This automatically maps GET/POST/PUT/DELETE to class methods view = resource_class.as_view(url_path, dev=dev_instance) api_blueprint.add_url_rule(url_path, view_func=view) print(f"Route registered: /api{url_path:<40} {module.__name__:>5}.{resource_class.__name__}") print( f"Route registered: /api{ url_path:<40} { module.__name__:>5}.{ resource_class.__name__}") except Exception as e: print(f"Route skipped: /api{url_path:<40} -- {str(e):>5}") for section in ends.sections(): dynamic_import(section)
noctua/api/baseresource.py +22 −12 Original line number Diff line number Diff line Loading @@ -4,18 +4,21 @@ '''Base class for REST API''' # System modules # System modules import asyncio from datetime import datetime # Third-party modules from quart import request, jsonify from quart import jsonify, request from quart.views import MethodView class BaseResource(MethodView): """ Equivalent to flask_restx Resource. Methods get(), post(), put(), delete() will be called automatically. """ def __init__(self, dev): self.dev = dev Loading @@ -35,14 +38,20 @@ class BaseResource(MethodView): async def run_blocking(self, func, *args, **kwargs): 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): """ Standardized JSON response format. If errors is provided as a list, it uses that. Otherwise, it pulls from self.dev.error. """ # Logic: use manual errors if provided, otherwise check the device for errors # Logic: use manual errors if provided, otherwise check the device for # errors dev_errors = getattr(self.dev, 'error', []) final_errors = errors if errors is not None else dev_errors Loading @@ -59,6 +68,7 @@ class BaseResource(MethodView): return jsonify(res), status_code def register_error_handlers(app): now = datetime.utcnow().isoformat() Loading @@ -72,9 +82,9 @@ def register_error_handlers(app): @app.errorhandler(405) async def method_not_allowed(e): return jsonify({"error": ["Method not allowed"], "timestamp": now}), 405 return jsonify( {"error": ["Method not allowed"], "timestamp": now}), 405 @app.errorhandler(500) async def internal_error(e): return jsonify({"error": [str(e)], "timestamp": now}), 500
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 −31 Original line number Diff line number Diff line Loading @@ -3,10 +3,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 @@ -15,12 +19,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 @@ -35,12 +41,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 @@ -48,12 +56,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 Filters(BaseResource): """Camera filters names.""" Loading @@ -64,6 +74,7 @@ class Filters(BaseResource): res = constants.filter_number return self.make_response(res) class Filter(BaseResource): """Camera filter information.""" Loading @@ -74,6 +85,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 @@ -88,12 +100,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 @@ -101,6 +115,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 @@ -108,6 +123,7 @@ class FrameCustom(BaseResource): res = await self.run_blocking(action) return self.make_response(res) class FrameFull(BaseResource): """Camera full frame.""" Loading @@ -117,6 +133,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 @@ -127,6 +144,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 @@ -137,6 +155,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 @@ -161,6 +180,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 @@ -171,6 +191,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 @@ -196,6 +217,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 @@ -230,6 +252,7 @@ class SnapshotRecenter(BaseResource): seq.tpl.recenter = False return self.make_response(False) class SnapshotDomeslewing(BaseResource): """Get dome slewing status""" Loading @@ -253,6 +276,7 @@ class SnapshotDomeslewing(BaseResource): seq.tpl.domeslewing = False return self.make_response(False) class CoolerWarmup(BaseResource): """Manage the warmup of the CCD.""" Loading @@ -266,6 +290,7 @@ class CoolerWarmup(BaseResource): return self.make_response("Warmup sequence aborted") class Connection(BaseResource): '''Manage the connection to ASCOM.''' Loading @@ -275,6 +300,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
noctua/api/common.py +5 −2 Original line number Diff line number Diff line # System modules import asyncio from datetime import datetime from dataclasses import dataclass from typing import Optional, Any from datetime import datetime from typing import Any, Optional @dataclass class StandardResponse: Loading @@ -9,6 +11,7 @@ class StandardResponse: error: Optional[list] = None timestamp: str = datetime.utcnow().isoformat() async def wrap_blocking(func, *args, **kwargs): """Esegue una funzione sincrona (L1) in un thread separato""" return await asyncio.to_thread(func, *args, **kwargs)