Commit b93e0471 authored by vertighel's avatar vertighel
Browse files

Avoid stressing devices while check dependencies. Manage json error in netio

parent da46e648
Loading
Loading
Loading
Loading
+7 −16
Original line number Diff line number Diff line
@@ -7,8 +7,7 @@ from quart import request, jsonify
from quart.views import MethodView

# Custom modules
from .deps import check_dependencies

from .deps import dep_checker # Change this import

class BaseResource(MethodView):
    """
@@ -109,26 +108,19 @@ class BaseResource(MethodView):

    async def dispatch_request(self, *args, **kwargs):
        """
        Override the default dispatch to enforce dependency checks and validate
        method existence before execution.

        Returns
        -------
        Response
            The Quart response object or a JSON error.
        Override the default dispatch to enforce cached dependency checks.
        """
        # Determine the logical path for dependency checking
        api_path = request.path.replace("/api", "", 1)

        # 1. Validate that the class implements the requested HTTP method
        handler = getattr(self, request.method.lower(), None)
        
        if handler is None:
            from quart import abort
            abort(405)

        # 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)
        
        # Use dep_checker instead of check_dependencies
        is_available, err_msg = await dep_checker.get_status(api_path, force)

        if not is_available:
            return jsonify({
@@ -138,7 +130,6 @@ class BaseResource(MethodView):
                "timestamp": self.timestamp,
            }), 424

        # 3. Proceed to standard MethodView dispatching
        return await super().dispatch_request(*args, **kwargs)
    

+87 −36
Original line number Diff line number Diff line
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# System modules
import time

'''Shared dependency resolution for API resources and the stream layer.'''
# Third-party modules

# Custom modules
from noctua.api.basecontext import ends, resource_registry


async def check_dependencies(path: str, force: bool = False) -> tuple[bool, str | None]:
class DependencyCache:
    """
    Recursively verify the dependency chain for a given endpoint path.
    Manages dependency verification with a short-lived internal cache.
    """

    def __init__(self, ttl=0.8):
        """
        Initialize the dependency cache.

        Parameters
        ----------
        ttl : float
            Time-to-live for cache entries in seconds.
        """
        self._cache = {}
        self._ttl = ttl

    Walks the ``depends-on`` chain declared in ``api.ini`` and calls each
    parent resource's ``get()`` method directly, so the check always reflects
    the live hardware state rather than any cached value.

    async def get_status(self, path, force=False):
        """
        Check if a resource is available based on its dependencies.

        Parameters
        ----------
        path : str
        The API endpoint path to check (e.g. ``/camera/snapshot``).
    force : bool, optional
        When ``True`` the entire dependency chain is bypassed and the function
        returns ``(True, None)`` immediately.  Defaults to ``False``.
            The API path to verify.
        force : bool
            If True, bypass all checks.

        Returns
        -------
    tuple[bool, str | None]
        ``(True, None)`` if the chain is satisfied, or
        ``(False, error_message)`` at the first failing link.
        tuple
            A tuple containing (is_available, error_message).
        """

        if force:
            return True, None

        now = time.time()

        if path in self._cache:
            status, timestamp = self._cache[path]
            if (now - timestamp) < self._ttl:
                return status

        result = await self._perform_check(path, force)
        self._cache[path] = (result, now)

        return result


    async def _perform_check(self, path, force):
        """
        Logic for recursive dependency resolution.

        Parameters
        ----------
        path : str
            The API path.
        force : bool
            The force flag.

        Returns
        -------
        tuple
            The check result.
        """

        dependency = ends.get(path, "depends-on", fallback=None)

        if not dependency:
@@ -41,12 +83,21 @@ async def check_dependencies(path: str, force: bool = False) -> tuple[bool, str
        parent_resource = resource_registry.get(dependency)

        if not parent_resource:
        return False, f"Dependency configuration error: {dependency} not found in registry"
            return False, f"Missing resource: {dependency}"

        # Recursive call to resolve the parent's own dependencies
        parent_ok, parent_err = await self.get_status(dependency, force)
        if not parent_ok:
            return False, parent_err

        # Check parent actual state
        data, status_code = await parent_resource.get()

        if status_code != 200 or not data.get("raw") or data.get("error"):
        return False, f"Dependency failed: parent {dependency} is OFF or in error state"
            return False, f"Link failure: {dependency}"

        return True, None


    # Recurse up the chain
    return await check_dependencies(dependency, force)
# Singleton instance
dep_checker = DependencyCache()
+2 −1
Original line number Diff line number Diff line
@@ -76,6 +76,7 @@ class AlpacaDevice(BaseDevice):
        res.raise_for_status()

        resj = res.json()

        if resj["ErrorNumber"] != 0:
            msg = f'ASCOM {resj["ErrorNumber"]}: {resj["ErrorMessage"]}'
            log.error(msg)
+13 −58
Original line number Diff line number Diff line
@@ -34,6 +34,7 @@ class PowerPDU:
                           verify=False)
                           # params=base_params,

        # log.debug(res)
        res.raise_for_status()
        resj = res.json()
        outputs = resj['Outputs']
@@ -109,6 +110,7 @@ class Switch(PowerPDU):
    @property
    def state(self):
        resj = self.get(id=self.id)
        if resj:
            try:
                res = resj[0]['State']
                self._state = True if res == 1 else False
@@ -116,59 +118,12 @@ class Switch(PowerPDU):
            except IndexError as e:
                log.error("No valid response")
                return None
            except Exception as e:
                log.error("Generic exception")
                return None
            
    @state.setter
    def state(self, s):
        switchcmd = 1 if s else 0
        res = self.put(switchcmd, id=self.id)
        self._state = self.state

#     @property
#     def all(self):
#         res = self.get()
#         return res


# class Switch(Sonoff):
#     def __init__(self, url, id):
#         super().__init__(url, id)

#     def reboot(self):

#         while self.state:
#             self.state = False

#         while not self.state:
#             self.state = True

#         return self.state

#     @property
#     def state(self):
#         res = self.get("Status", id=self.id)
#         self._state = True if res == 'On' else False
#         return self._state

#     @state.setter
#     def state(self, s):
#         switchcmd = 'On' if s else 'Off'
#         params = {"param": "switchlight", "switchcmd": switchcmd}
#         res = self.put(params, id=self.id)
#         self._state = self.state


# class Sensor(Sonoff):
#     def __init__(self, url, temp_id, hum_id):
#         super().__init__(url, temp_id)
#         self.id = temp_id  # recycle for last_update
#         self.hum_id = hum_id

#     @property
#     def temperature(self):
#         res = self.get("Temp", id=self.id)
#         return res

#     @property
#     def humidity(self):
#         res = self.get("Humidity", id=self.hum_id)
#         return res
+10 −2
Original line number Diff line number Diff line
@@ -164,6 +164,14 @@ def request_errors(func):
            this.error.append(str(e))
            return

        except requests.exceptions.JSONDecodeError as e:
            msg = f"{name}: Probably malformed JSON!"
            log.error(msg)
            log.error(e)
            this.error.append(msg)
            this.error.append(str(e))
            return

        except requests.exceptions.RequestException as e:
            msg = f"{name}: Generic request exception!"
            log.error(msg)
Loading