Commit e911f051 authored by vertighel's avatar vertighel
Browse files

Fase 3 API completa: convenzioni, cancellazione dead code, guider.py allineato



- state/status/connection: verificata la convenzione decisa in fase
  devices su tutto il layer API, gia' rispettata ovunque -- nessuna
  modifica.
- Cancellati environment.py, common.py, test.py (morti, nessun
  consumer), le classi duplicate/irraggiungibili in telescope.py
  (Focuser/FocuserMovement/Rotator/Rotator Movement, gia' risolte da
  dynamic_import verso focuser.py/rotator.py, piu' il blocco
  Connection commentato), .#webcam.py e #webcam.py# (backup emacs).
- Connection duplicata 4 volte: verificato via api.ini che solo
  /dome/connection e' davvero instradata -- le copie in focuser.py/
  rotator.py/camera.py erano codice morto, cancellate invece che
  fattorizzate.
- Filter/Filters: risolto cancellando Filters (morta), insieme a
  CoolerWarmup/FrameCustom nello stesso file (stesso "confirm truly
  unused" del Phase-0 audit, confermato).
- stage.py: Movement -> PositionMovement, per coerenza con
  dome.py/telescope.py/focuser.py/rotator.py (unico non prefissato).
  api.ini aggiornato.
- guider.py: le route hand-rolled (jsonify grezzo) convertite in due
  classi BaseResource (Guider, Calibrate) con make_response,
  sequencer.py's BobRun come precedente per il wiring manuale fuori da
  api.ini/dynamic_import. Registrate in resource_registry, ora visibili
  nel catalogo /api/.
- api/__init__.py: docstring di dynamic_import espansa per documentare
  la risoluzione nome-classe-device -> nome-modulo e il re-export
  cross-modulo (guider.py per tec1/2/3).
- @expects mancanti aggiunti su webcam.py (Pointing.put) e
  sequencer.py (BobRun.put/post).
- Tradotti gli ultimi commenti/docstring italiani in baseresource.py
  e api/__init__.py.
- Verificato con un import completo di noctua.api dopo ogni blocco:
  94 route registrate, nessun errore.
- PLAN.md aggiornato, fase 3 chiusa per intero (approvata in blocco).

Co-Authored-By: default avatarClaude Sonnet 5 <noreply@anthropic.com>
parent 89242039
Loading
Loading
Loading
Loading
+73 −44
Original line number Diff line number Diff line
@@ -318,49 +318,71 @@ because a file is open for another reason; re-scope explicitly first.

### 3. API

- [ ] Apply the devices-phase `state`/`status`/`connection` naming
      convention at this layer too (decided while resolving
      astelco.py/mercury.py): `state` = bool, actionable/settable where
      it makes sense; `status` = read-only, verbose/descriptive;
      `connection` = read-only bool reachability check (matches
      `atik.py`'s existing `connection` property). Does not apply to the
      camera_state int enum (0=Idle...6=Flushing, shared across all 6
      cameras) — that's a separate, already-consistent convention, don't
      touch it. `mercury.py`'s `status``connection` rename (devices
      phase) currently has no API consumer (`/stage/status` actually
      calls `is_init`, not `.status`) — nothing to update today, but
      keep the convention in mind for any new stage/mercury route.
- [ ] Delete `environment.py`, `common.py`, `test.py` (dead, see
      above), the duplicate `telescope.py:268-327` classes, and the
      `.#webcam.py` lock file.
- [ ] Factor the 4x-duplicated `Connection` resource (`dome.py:147`,
      `focuser.py:43`, `rotator.py:43`, `camera.py:221`) into one
      shared implementation — `stage.py`'s `BaseStageResource`
      (10-21) is the precedent for how to share logic here.
- [ ] Rename `Filter`/`Filters` (camera.py:106/96) — confusingly close
      names, `Filters` (plural) is unused (candidate for deletion per
      above, confirm first).
- [ ] Standardize "is this moving" naming — currently
      `FocuserMovement`/`RotatorMovement`/`PositionMovement`/
      `CoordinatesMovement` (parent-prefixed) vs stage's bare
      `Movement` — pick one shape.
- [ ] `guider.py`'s hand-rolled routes (16-78) bypass
      `BaseResource`/`make_response` entirely, using raw `jsonify` with
      ad hoc status codes — align with the standard response envelope
      used everywhere else, or document explicitly why it's different.
- [ ] Document the `dynamic_import` re-export pattern (device class
      name → module name → cross-module re-export, as `guider.py` does
      for `tec1/2/3`'s class `Guider`) — today it's discoverable only
      by reading `api/__init__.py:38-68` + `devices.ini`, no
      docstring/README mentions it anywhere.
- [ ] Add missing `@expects` on write methods that take a payload
      without it: `focuser.py:52`, `rotator.py:52` (`Connection.put`,
      inconsistent with `dome.py`'s equivalent which has it),
      `webcam.py:22` (`Pointing.put`), `sequencer.py` (`BobRun.put/
      post`).
- [ ] Translate remaining Italian docstrings/comments
      (`baseresource.py:165-175`, `__init__.py:92-94`, `webcam.py:
      80,90,97`, plus `common.py`/`test.py` before they're deleted).
Fully done — the whole phase was approved in one pass by the user
("ok a tutto"), no reduced scope this time.

- [x] Apply the devices-phase `state`/`status`/`connection` naming
      convention at this layer too. Verified, not changed: audited
      every `.state`/`.status`/`.connection` access across `api/*.py`
      (`dome.py`, `focuser.py`, `rotator.py`, `camera.py`,
      `telescope.py`, `stage.py`, `guider.py`) and every one already
      matches the convention (`camera_state` int enum correctly left
      alone). `mercury.py`'s `status``connection` rename still has no
      API consumer, as noted before — nothing to update.
- [x] Delete `environment.py`, `common.py`, `test.py`, the duplicate
      `telescope.py:268-327` classes, and the `.#webcam.py` lock file.
      Also deleted the equally dead commented-out `Connection` class
      in `telescope.py` (248-265) and the `#webcam.py#` Emacs autosave
      cruft sitting right next to `.#webcam.py` — found while touching
      the same file for the same reason.
- [x] Factor the 4x-duplicated `Connection` resource. Turned out not
      to be 4 real duplicates: checked `api.ini` for `resource =
      Connection` and only `/dome/connection` is actually routed —
      `focuser.py`, `rotator.py`, and `camera.py`'s copies are
      unreachable dead code (no matching `api.ini` section for any of
      them), same category as `environment.py`/`common.py`/`test.py`.
      Deleted the 3 dead copies instead of factoring; `dome.py` is now
      the only implementation and there's nothing left to share it
      with.
- [x] Rename `Filter`/`Filters`. Resolved by the above: `Filters`
      (plural) was itself dead code (no route, no importer anywhere),
      deleted along with `CoolerWarmup`/`FrameCustom` (same file,
      same "confirm truly unused" note from the Phase-0 audit — all
      three confirmed and deleted). Only `Filter` (singular) remains,
      no more naming collision to resolve.
- [x] Standardize "is this moving" naming. `stage.py`'s bare
      `Movement``PositionMovement`, matching `dome.py`'s
      `PositionMovement` for the same semantic pairing (the other 4
      movement classes were already `<Concept>Movement`-shaped, this
      was the only outlier). Updated `api.ini`'s `[/stage/movement]`
      `resource =` line to match.
- [x] `guider.py`'s hand-rolled routes: aligned rather than
      documented-as-different. Converted the 4 status/start/stop/
      configure routes and `/calibrate` from plain `@guider_api.route`
      + raw `jsonify` into two `BaseResource` subclasses (`Guider`,
      `Calibrate`) using `make_response`, following `sequencer.py`'s
      `BobRun` as the precedent for a `BaseResource` manually wired to
      a blueprint route outside the `api.ini`/`dynamic_import` path
      (`guider_api.add_url_rule(...)` instead of `resource = ...` in
      `api.ini`). Also registered both into `resource_registry` in
      `api/__init__.py` (matching `BobRun`'s registration) so they now
      show up in the `/api/` catalog, which they never did before.
- [x] Document the `dynamic_import` re-export pattern — expanded its
      docstring in `api/__init__.py` to explain the device-class-name
      → module-name resolution and why `guider.py` re-exports 5
      `camera.py` classes for `tec1`/`tec2`/`tec3`.
- [x] Add missing `@expects`: `webcam.py:22` (`Pointing.put`,
      matching `telescope.py`'s altaz shape) and `sequencer.py`
      (`BobRun.put`/`post`) added. `focuser.py:52`/`rotator.py:52`
      (`Connection.put`) are moot — that class is gone (see above).
- [x] Translate remaining Italian: `baseresource.py:165-175` (docstring
      + 4 inline field comments in the same dict literal, missed by
      the original line-range note), `__init__.py:92-94`, `webcam.py:
      80,90,97`. `common.py`/`test.py` deleted, moot.

Verified with a full `import noctua.api` smoke test after each batch
of changes — no import errors, all expected routes (including
`/guider`, `/guider/calibrate`, `/stage/movement`) register correctly.

### 4. Jinja/HTML

@@ -427,4 +449,11 @@ deletion). One hardware bug remains open and tracked separately
during hardware testing on main" section above) — not a phase-1
checklist item, doesn't block moving on.

**Phase 2 (Templates) not started.**
Phase 2 (Templates) done, scoped to `snapshot.py`/`observation.py`,
`focus.py`/`focus2.py`, `fillheader.py` (decided with the user — the
rest of the phase-2 audit is deferred, marked `*(deferred)*` in the
checklist, not lost).

Phase 3 (API) done, full scope this time (no reduced scope).

**Phase 4 (Jinja/HTML) not started.**
+25 −4
Original line number Diff line number Diff line
@@ -19,7 +19,8 @@ from noctua.utils.logger import log
from .blocks import blocks_api
from .defaults import defaults_api
from .sequencer import sequencer_api, BobRun
from .guider import guider_api
from .guider import guider_api, Guider, Calibrate
from .guider_instance import guider

api_blueprint = Blueprint('api', __name__)

@@ -33,11 +34,31 @@ api_blueprint.register_blueprint(guider_api, url_prefix='/guider')
# resource_registry = {}

resource_registry['/sequencer/run'] = BobRun(seq)
resource_registry['/guider'] = Guider(guider)
resource_registry['/guider/calibrate'] = Calibrate(guider)


def dynamic_import(url_path):
    """
    Import and register resources from api.ini with debug logging of supported methods.

    Resolution is by *device class name*, not by URL path or by the
    api.ini section's own name: for a section like
    ``[/telescope/focuser]`` with ``device = foc``, the module to load
    is derived from ``devices.foc``'s Python class name
    (``Focuser`` -> ``noctua.api.focuser``), and the resource class is
    looked up as an attribute of that module (``ends.get(url_path,
    "resource")``, e.g. ``Focuser`` or ``FocuserMovement``).

    This means a device's API module is shared by every device
    instance whose class has that same name — e.g. ``tec1``/``tec2``/
    ``tec3`` are all instances of ``guider.py``'s ``Guider`` class, so
    every teccam route resolves to ``noctua.api.guider``, not a
    per-teccam module. Where that module needs resource classes that
    live elsewhere (the teccam routes reuse ``camera.py``'s
    ``FrameBinning``/``Gain``/``Loop``/``SnapshotRaw``/
    ``SnapshotState``), it re-exports them with a plain import at the
    top of the file — see ``guider.py``'s import of those five names.
    """

    try:
@@ -90,8 +111,8 @@ async def api_cameras():
@api_blueprint.route('/')
async def api_catalog():
    """
    Restituisce l'elenco JSON piatto degli endpoint, dei metodi 
    e dei relativi metadati di input dichiarati tramite il decoratore @expects.
    Return the flat JSON list of endpoints, methods, and their input
    metadata as declared through the @expects decorator.
    """
    catalog = []
    
+7 −7
Original line number Diff line number Diff line
@@ -162,17 +162,17 @@ def register_error_handlers(app):

    
def expects(param_type="single", count=1, unit=None, placeholder=None):
    """Decoratore per dichiarare i requisiti di input dei metodi di
    scrittura (PUT, POST, DELETE).  Salva un dizionario
    '_input_schema' all'interno della funzione decorata.
    """Decorator declaring the input requirements of write methods
    (PUT, POST, DELETE). Stores an '_input_schema' dict on the
    decorated function.

    """
    def decorator(func):
        func._input_schema = {
            "type": param_type,       # "string", "number" "array", "boolean" o None
            "count": count,           # numero di parametri attesi
            "unit": unit,             # unità di misura (es. "°C", "mm")
            "placeholder": placeholder # valore/i di esempio per l'interfaccia
            "type": param_type,       # "string", "number", "array", "boolean", or None
            "count": count,           # number of expected parameters
            "unit": unit,             # unit of measurement (e.g. "°C", "mm")
            "placeholder": placeholder # example value(s) for the UI
        }
        return func
    return decorator
+0 −47
Original line number Diff line number Diff line
@@ -80,29 +80,6 @@ class CoolerTemperatureSetpoint(BaseResource):
        res = await self.run_blocking(action)
        return self.make_response(res)
    
class CoolerWarmup(BaseResource):
    """Manage the warmup of the CCD."""

    async def post(self):
        """Start the warm up the CCD."""
        res = await self.run_blocking(lambda: self.dev.put('cooler', False))
        return self.make_response(res)

    async def delete(self):
        """Stop the warm up of the CCD."""
        
        return self.make_response("Warmup sequence aborted")

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

    async def get(self):
        """Retrieve the filter names."""
        
        # Constants are not blocking
        res = constants.filter_number
        return self.make_response(res)

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

@@ -136,21 +113,6 @@ class FilterMovement(BaseResource):
        res = await self.run_blocking(action)
        return self.make_response(res)
    
class FrameCustom(BaseResource):
    """Camera custom frame."""

    @expects(param_type="array", count=2, unit="px", placeholder="[45, 180]")
    async def put(self):
        """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
            return new_frame
        res = await self.run_blocking(action)
        return self.make_response(res)

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

@@ -218,15 +180,6 @@ class SnapshotState(BaseResource):
        res = constants.camera_state.get(raw, "Off")
        return self.make_response(res, raw_data=raw)

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

    async def get(self):
        '''Check if the telescope is connected to ASCOM.'''
        
        res = await self.run_blocking(lambda: self.dev.connection)
        return self.make_response(res)

class Settings(BaseResource):
    '''General camera settings.'''

noctua/api/common.py

deleted100644 → 0
+0 −60
Original line number Diff line number Diff line
# In noctua/api/common.py (o direttamente nell'init delle API)

from noctua.api import api_blueprint
from noctua.api.__init__ import ends # Il configparser caricato
from quart import request, jsonify
import asyncio

@api_blueprint.route('/all/<namespace>')
async def get_all(namespace):
    """Aggregatore intelligente con gestione della cascata"""
    
    endpoints = {}
    # 1. Filtriamo le sezioni dell'ini per il namespace e ordiniamo per get-priority
    sections = []
    for section in ends.sections():
        if section.startswith("/" + namespace):
            priority = ends[section].getint('get-priority', 999)
            sections.append((priority, section))
    
    sections.sort() # Ordina per priorità (1, 2, 3...)

    # 2. Scansione sequenziale
    for priority, section in sections:
        # Troviamo l'endpoint corrispondente nel server Quart
        # In Quart, possiamo simulare una chiamata interna per massima coerenza
        from quart import current_app
        client = current_app.test_client()
        
        response = await client.get(f"/api{section}")
        data = await response.get_json()
        
        name = section.split("/")[-1] # es. "power" o "light"
        endpoints[name] = data
        
        # 3. LOGICA A CASCATA:
        # Se un elemento con priorità ha un 'raw' False (o errore), 
        # interrompiamo la scansione per questo namespace
        if priority < 999: # Solo per elementi critici (power, connection)
            if data.get("raw") is False or data.get("error") is not None:
                # Marchiamo i successivi come "unavailable"
                break
                
    return jsonify(endpoints)



import asyncio
from datetime import datetime
from dataclasses import dataclass
from typing import Optional, Any

@dataclass
class StandardResponse:
    response: Any
    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