Commit 47b3272c authored by Davide Ricci's avatar Davide Ricci
Browse files

Added stage to APIs

Source Commit: 6c6807bc
[skip ci]
parent 6c6807bc
Loading
Loading
Loading
Loading
Loading
+21 −10
Original line number Diff line number Diff line
@@ -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
@@ -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."""

@@ -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)
+22 −12
Original line number Diff line number Diff line
@@ -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

@@ -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

@@ -59,6 +68,7 @@ class BaseResource(MethodView):

        return jsonify(res), status_code


def register_error_handlers(app):
    now = datetime.utcnow().isoformat()

@@ -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
    
+18 −14
Original line number Diff line number Diff line
@@ -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."""

@@ -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)."""

@@ -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
@@ -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

+57 −31
Original line number Diff line number Diff line
@@ -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."""
@@ -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"""

@@ -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"""

@@ -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."""

@@ -64,6 +74,7 @@ class Filters(BaseResource):
        res = constants.filter_number
        return self.make_response(res)


class Filter(BaseResource):
    """Camera filter information."""

@@ -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."""

@@ -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."""

@@ -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
@@ -108,6 +123,7 @@ class FrameCustom(BaseResource):
        res = await self.run_blocking(action)
        return self.make_response(res)


class FrameFull(BaseResource):
    """Camera full frame."""

@@ -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."""

@@ -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."""

@@ -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."""

@@ -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."""

@@ -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."""

@@ -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"""

@@ -230,6 +252,7 @@ class SnapshotRecenter(BaseResource):
            seq.tpl.recenter = False
        return self.make_response(False)


class SnapshotDomeslewing(BaseResource):
    """Get dome slewing status"""

@@ -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."""

@@ -266,6 +290,7 @@ class CoolerWarmup(BaseResource):

        return self.make_response("Warmup sequence aborted")


class Connection(BaseResource):
    '''Manage the connection to ASCOM.'''

@@ -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.'''

+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:
@@ -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