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

Check dependencies fixed by Claude. Ansi Colors on logs

Source Commit: da46e648
[skip ci]
parent da46e648
Loading
Loading
Loading
Loading
Loading
+36 −16
Original line number Diff line number Diff line
@@ -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
@@ -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.
@@ -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)
@@ -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")):
+1 −0
Original line number Diff line number Diff line
# System modules
import configparser
from pathlib import Path

+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


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

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

@@ -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
+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 −32
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
from noctua.config import constants
from noctua.api.sequencer_instance import seq


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

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

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

@@ -68,6 +78,7 @@ class CoolerWarmup(BaseResource):

        return self.make_response("Warmup sequence aborted")


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

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


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

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

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

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


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

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

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

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

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

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

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

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


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

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