Commit d9523051 authored by Davide Ricci's avatar Davide Ricci
Browse files

New js validated!

Source Commit: 4c4ce118
[skip ci]
parent 4c4ce118
Loading
Loading
Loading
Loading
Loading
+44 −24
Original line number Diff line number Diff line
@@ -9,16 +9,18 @@ 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 .sequencer import BobRun, sequencer_api

api_blueprint = Blueprint('api', __name__)

@@ -42,14 +44,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)
@@ -60,11 +74,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")):
@@ -85,7 +104,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('/')}/"

+1 −0
Original line number Diff line number Diff line
# System modules
import configparser
from pathlib import Path

+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
@@ -71,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.

@@ -114,6 +123,7 @@ class BaseResource(MethodView):
        handler = getattr(self, request.method.lower(), None)

        if handler is None:
            # Third-party modules
            from quart import abort
            abort(405)

@@ -144,21 +154,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):
+22 −21
Original line number Diff line number Diff line
@@ -30,8 +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):
        """
@@ -42,12 +42,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)):
@@ -60,7 +62,6 @@ class BlockFile(MethodView):
        await dao.write(name, final_data)
        return final_data, 201


    async def put(self, name):
        """Append a template from defaults to the existing OB."""

@@ -81,7 +82,6 @@ class BlockFile(MethodView):

        return {"error": f"Template '{tpl_name}' not found in defaults"}, 404


    async def delete(self, name):
        """Delete the whole OB file."""

@@ -103,7 +103,6 @@ class BlockElement(MethodView):
        except (IndexError, TypeError, KeyError):
            return {"error": "Index out of range or OB not found"}, 404


    async def put(self, name, index):
        """Update a specific template inside the OB."""

@@ -116,7 +115,6 @@ class BlockElement(MethodView):
        except (IndexError, TypeError, KeyError):
            return {"error": "Index out of range or OB not found"}, 404


    async def delete(self, name, index):
        """Delete a specific template inside the OB."""

@@ -132,5 +130,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'))
+58 −33
Original line number Diff line number Diff line
@@ -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, expects
from noctua.config import constants
from noctua.api.sequencer_instance import seq


class FrameBinning(BaseResource):
    """Binning of the camera."""
@@ -17,12 +21,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"""

@@ -38,12 +44,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"""

@@ -52,12 +60,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."""

@@ -71,6 +81,7 @@ class CoolerWarmup(BaseResource):

        return self.make_response("Warmup sequence aborted")


class Filters(BaseResource):
    """Camera filters names."""

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


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

@@ -92,6 +104,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."""

@@ -108,12 +121,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."""

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


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

@@ -138,6 +155,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."""

@@ -148,6 +166,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."""

@@ -158,6 +177,7 @@ class FrameSmall(BaseResource):
        res = await self.run_blocking(self.dev.small_frame)
        return self.make_response(res)


class SnapshotRaw(BaseResource):
    """The acquired image."""

@@ -186,6 +206,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."""

@@ -196,6 +217,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."""

@@ -221,6 +243,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"""

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


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

@@ -288,6 +312,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