Commit e286c874 authored by vertighel's avatar vertighel
Browse files

Now we have: working /templates api to read defaults; working /blocks api to...

Now we have: working /templates api to read defaults; working  /blocks api to CRUD from defaults into ob/ directory. Finally we have /sequencer/run api to check, start, stop, pause, resume templates based on json data, not json files. I'm thinking the best way to run directly files
parent b92645d1
Loading
Loading
Loading
Loading
+10 −6
Original line number Diff line number Diff line
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# System modules
import configparser
import importlib
from pathlib import Path

# Third-party modules
from quart import Blueprint

# Custom modules
from noctua import devices
from .blocks import blocks_api
from .defaults import defaults_api
from .templates import templates_api
from .sequencer import sequencer_api

api_blueprint = Blueprint('api', __name__)

# Register Manual Blueprints (Namespaces)
# Register special Namespaces
api_blueprint.register_blueprint(blocks_api, url_prefix='/blocks')
api_blueprint.register_blueprint(defaults_api, url_prefix='/templates')
api_blueprint.register_blueprint(templates_api, url_prefix='/sequencer')
api_blueprint.register_blueprint(sequencer_api, url_prefix='/sequencer')

# Load api.ini
# Load api endpoint config file
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."""

    try:
        # Avoid overriding routes already handled by manual Blueprints
        if url_path.startswith(('/blocks', '/sequencer')):
        # Skip manual routes
        if url_path.startswith(("/blocks", "/sequencer", "/templates")):
            return

        res_class_name = ends.get(url_path, "resource")
+34 −12
Original line number Diff line number Diff line
@@ -24,35 +24,57 @@ class BaseResource(MethodView):
        return datetime.utcnow().isoformat()

    async def get_payload(self):
        return await request.get_json(force=True)
        """
        Parse JSON payload. silent=True prevents Quart from
        returning a 400 error before execution.

        """
        
        return await request.get_json(force=True, silent=True)

    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, status_code=200):
        errors = getattr(self.dev, 'error', [])
        if errors and status_code == 200:
            status_code = 500 
    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
        dev_errors = getattr(self.dev, 'error', [])
        final_errors = errors if errors is not None else dev_errors

        # Force a non-200 status code if there are errors
        if final_errors and status_code == 200:
            status_code = 400

        res = {
            "raw": raw_data if raw_data is not None else response_data,
            "response": response_data,
            "error": errors if errors else None,
            "error": final_errors if final_errors else None,
            "timestamp": self.timestamp,
        }

        return jsonify(res), status_code
    
def register_error_handlers(app):
    now = datetime.utcnow().isoformat()
    
    @app.errorhandler(400)
    async def bad_request(e):
        return jsonify({"error": ["Bad Request"], "timestamp": datetime.utcnow().isoformat()}), 400
        return jsonify({"error": ["Bad Request"], "timestamp": now}), 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": now}), 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": now}), 405
    
    @app.errorhandler(500)
    async def internal_error(e):
        return jsonify({"error": [str(e)], "timestamp": datetime.utcnow().isoformat()}), 500
        return jsonify({"error": [str(e)], "timestamp": now}), 500
    
+17 −5
Original line number Diff line number Diff line
@@ -26,11 +26,14 @@ class BlockFile(MethodView):

    async def get(self, name):
        """Show the whole OB content."""
        
        content = await dao.read(name)
        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 its content."""
        """Create a new OB. If payload is a valid default name, use
        its content."""
        
        data = []
        try:
            tpl_name = await request.get_json(force=True, silent=True)
@@ -48,6 +51,7 @@ class BlockFile(MethodView):
    
    async def put(self, name):
        """Append a template from defaults to the existing OB."""
        
        content = await dao.read(name)
        if content is None:
            return {"error": "OB not found"}, 404
@@ -67,6 +71,7 @@ class BlockFile(MethodView):

    async def delete(self, name):
        """Delete the whole OB file."""
        
        success = await dao.delete(name)
        if success:
            return {"message": f"OB {name} deleted"}, 200
@@ -78,6 +83,7 @@ class BlockElement(MethodView):

    async def get(self, name, index):
        """Show a specific template inside the OB."""
        
        content = await dao.read(name)
        try:
            return content[index - 1]
@@ -86,6 +92,7 @@ class BlockElement(MethodView):

    async def put(self, name, index):
        """Update a specific template inside the OB."""
        
        new_data = await request.get_json(force=True)
        content = await dao.read(name)
        try:
@@ -97,6 +104,7 @@ class BlockElement(MethodView):

    async def delete(self, name, index):
        """Delete a specific template inside the OB."""
        
        content = await dao.read(name)
        try:
            content.pop(index - 1)
@@ -107,7 +115,11 @@ 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('/',
                        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'))
+18 −11
Original line number Diff line number Diff line
@@ -85,6 +85,7 @@ class FilterMovement(BaseResource):
            return self.dev.filter
        res = await self.run_blocking(action)
        return self.make_response(res)
    
class FrameCustom(BaseResource):
    """Camera custom frame."""

@@ -124,24 +125,30 @@ class FrameSmall(BaseResource):
        res = await self.run_blocking(self.dev.small_frame)
        return self.make_response(res)

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

    async def post(self):
        """Start a simple acquisition."""
        """Start a raw image using camera interface."""
        new = await self.get_payload()  # exptime, type
        try:
            res = await self.run_blocking(
                self.dev.start, new["exptime"], new["type"], self.timestamp
            )
        except KeyError as e:
            msg = f"API: Missing keyword: {e}"
            self.dev.error.append(msg)
            return self.make_response(None, status_code=400)

        return self.make_response(res)

    async def delete(self):
        """Stop the acquisition process."""
        """Stop the process."""
        res = await self.run_blocking(self.dev.abort)
        return self.make_response(res)

class SnapshotState(BaseResource):
    """Manage the acquisition of an image."""
    """Manage the state of a raw image."""

    async def get(self):
        """Retrieve the status of the acquisition process."""
@@ -149,11 +156,11 @@ class SnapshotState(BaseResource):
        res = constants.camera_state.get(raw, "Off")
        return self.make_response(res, raw_data=raw)

class SnapshotAcquisition(BaseResource):
class Snapshot(BaseResource):
    """Manage the acquisition of an image using the sequencer."""

    async def get(self):
        """Retrieve the status of the acquisition process."""
        """Retrieve the status of the image process."""
        data = {
            "paused": seq.tpl.paused if seq.tpl else None,
            "quitting": seq.tpl.aborted if seq.tpl else None,
@@ -161,14 +168,14 @@ class SnapshotAcquisition(BaseResource):
        return self.make_response(data)

    async def post(self):
        """Start a new acquisition."""
        """Start a new image."""
        data = await self.get_payload()
        # Templates are Layer 2
        await self.run_blocking(seq.tpl.run, data)
        return self.make_response(getattr(seq.tpl, 'filenames', []))

    async def delete(self):
        """Stop the acquisition process."""
        """Stop the image process."""
        res = await self.run_blocking(seq.tpl.abort)
        return self.make_response(res)

+105 −0
Original line number Diff line number Diff line
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

'''REST API to launch observation blocks'''
'''REST API to manage the Sequencer and Observation Block loading'''

# Third-party modules
from quart import Blueprint
@@ -11,43 +11,18 @@ from noctua.api.sequencer_instance import seq
from noctua.utils.data_access_object import ObservationBlockObject as DAO
from .baseresource import BaseResource

templates_api = Blueprint('sequencer', __name__)

sequencer_api = Blueprint('sequencer', __name__)
dao = DAO()

class BobList(BaseResource):
    """Show and modify a list of OBs to be sequenced"""

    async def get(self):
        """Show all OB in the sequence"""
        res = await self.run_blocking(lambda: dao.todos)
        return self.make_response(res)

    async def post(self):
        """Add a new OB to the sequence"""
        data = await self.get_payload()
        # Original code was a stub, preserved logic
        return self.make_response(data, status_code=201)


class Bob(BaseResource):
    '''Let delete a specific OB'''

    async def delete(self, id):
        '''Delete an OB given its identifier'''
        res = await self.run_blocking(dao.delete, id)
        return self.make_response(res, status_code=204)


class BobRun(BaseResource):
    """Manage the sequencer"""

    async def get(self):
        """Show the sequencer status"""
        # dev is seq here

        s = self.dev.tpl
        if s:
            res = {
            data = {
                "name": s.name,
                "paused": s.paused,
                "quitting": s.aborted,
@@ -55,63 +30,76 @@ class BobRun(BaseResource):
                "error": s.error,
                "filename": s.filename,
            }
            err = self.dev.error
        else:
            res = {
            data = {
                "name": None, "paused": None, "quitting": None,
                "output": None, "error": None, "filename": None,
            }
        return self.make_response(res)
            err = "No tpl object"

        return self.make_response(data, errors=err)

    async def put(self):
        """Pause or resume the template execution. Inverting the logic with
        respect to sequencer class:
        PUT /sequencer/run true: resume,
        PUT /sequencer/run false: pause
        """
        """Pause or resume the template execution."""

        in_execution = await self.get_payload()
        
        def logic():
        def toggle():
            if in_execution:
                self.dev.resume()
            else:
                self.dev.pause()
            return self.dev.tpl.paused if self.dev.tpl else None

        res = await self.run_blocking(logic)
        res = await self.run_blocking(toggle)
        return self.make_response(res)

    async def post(self):
        """Start the sequencer"""
        payload = await self.get_payload()

        def logic():
            if not payload:
                return {"error": "No payload"}
        payload = await self.get_payload()

        if payload is None:
            return self.make_response(
                None, 
                errors=["No payload. Please provide a valid json with template+params"], 
                status_code=400
            )

        def execute_logic():
            # try:
            #     # Try to load a file via the DAO
            #     # ob_info = dao.read(payload)
            #     self.dev.load_file(payload)
            # except Exception as e:
            # print(f"API: exception: {e}")
            # Fallback: assume payload is the OB JSON parameters
            try:
                ob = dao.show(payload)["filename"]
                self.dev.load_file(ob)
                self.dev.execute()
                return self.dev.ob
            except (TypeError, KeyError):
                # If not a known OB file, assume raw json
                self.dev.ob = payload
                self.dev.execute()
                return self.dev.ob
            except Exception as f:
                raise Exception(f"Maybe not a json? {f}")

        res = await self.run_blocking(logic)
        try:
            res = await self.run_blocking(execute_logic)
            return self.make_response(res)
        except Exception as e:
            return self.make_response(None, errors=[str(e)], status_code=400)

    async def delete(self):
        """Stop the sequencer"""
        def logic():

        def stop():
            self.dev.quit()
            return self.dev.ob if getattr(self.dev, "ob", False) else None
            if getattr(self.dev, "ob", False) and self.dev.ob:
                return self.dev.ob
            return None

        res = await self.run_blocking(logic)
        res = await self.run_blocking(stop)
        return self.make_response(res)

# Association of classes to endpoints
templates_api.add_url_rule('/', view_func=BobList.as_view('bob_list', dev=seq))
templates_api.add_url_rule('/<int:id>', view_func=Bob.as_view('bob_detail', dev=seq))
templates_api.add_url_rule('/run', view_func=BobRun.as_view('bob_run', dev=seq))
# --- ROUTING RULES ---
sequencer_api.add_url_rule('/run', view_func=BobRun.as_view('bob_run', dev=seq))
Loading