Loading noctua/api/__init__.py +29 −2 Original line number Diff line number Diff line Loading @@ -9,7 +9,7 @@ import importlib from pathlib import Path # Third-party modules from quart import Blueprint, request, abort from quart import Blueprint, request, abort, jsonify # Custom modules from noctua import devices Loading Loading @@ -63,9 +63,36 @@ def dynamic_import(url_path): 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}") 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")): dynamic_import(section) @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. """ catalog = [] for url_path, resource_instance in resource_registry.items(): for m in ["GET", "POST", "PUT", "DELETE"]: # Recuperiamo la funzione del metodo (es. get, put, post, delete) method_fn = getattr(resource_instance, m.lower(), None) if method_fn: # Estraiamo i metadati del decoratore se presenti, altrimenti None schema = getattr(method_fn, "_input_schema", None) formatted_endpoint = f"/api/{url_path.strip('/')}/" catalog.append({ "endpoint": formatted_endpoint, "method": m, "expects": schema }) return jsonify(catalog) noctua/api/baseresource.py +19 −0 Original line number Diff line number Diff line Loading @@ -157,3 +157,22 @@ def register_error_handlers(app): @app.errorhandler(424) async def failed_dependency(e): return jsonify({"error": ["Failed Dependency"], "timestamp": datetime.utcnow().isoformat()}), 424 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. """ def decorator(func): func._input_schema = { "type": param_type, # "single", "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 } return func return decorator noctua/api/switch.py +3 −1 Original line number Diff line number Diff line Loading @@ -5,11 +5,12 @@ # Custom modules from noctua.config import constants from .baseresource import BaseResource from .baseresource import BaseResource, expects # "/dome/light" # "/telescope/lamp" # "/camera/power" class State(BaseResource): '''Manage a switch state.''' Loading @@ -19,6 +20,7 @@ class State(BaseResource): raw = await self.run_blocking(lambda: self.dev.state) return self.make_response(constants.on_off.get(raw, "N/A"), raw_data=raw) @expects(param_type="boolean") async def put(self): '''Switch on or off.''' Loading noctua/api/telescope.py +3 −1 Original line number Diff line number Diff line Loading @@ -9,7 +9,7 @@ from datetime import datetime # Custom modules from noctua.config import constants from noctua.utils.coordinates import to_hms, to_hms_dms, to_radec from .baseresource import BaseResource from .baseresource import BaseResource, expects class Cover(BaseResource): Loading Loading @@ -115,6 +115,7 @@ class CoordinatesMovement(BaseResource): class CoordinatesMovementRadec(BaseResource): '''Point the telescope in Ra, Dec.''' @expects(param_type="array", count=2, unit="hms ±dms / id", placeholder="Polaris") async def post(self): '''Set new Ra and Dec coordinates.''' Loading @@ -134,6 +135,7 @@ class CoordinatesMovementRadec(BaseResource): class CoordinatesMovementAltaz(BaseResource): '''Point the telescope in Alt, Az.''' @expects(param_type="array", count=2, unit="°", placeholder="[45, 180]") async def post(self): '''Set new Alt and Az coordinates.''' Loading noctua/utils/coordinates.py +6 −6 Original line number Diff line number Diff line Loading @@ -34,7 +34,7 @@ def to_radec(string): unit=("hourangle", "deg"), equinox=Time.now()) return [coor.ra.hourangle, coor.dec.degree] return [coor.ra.hourangle.item(), coor.dec.degree.item()] except ValueError as e: log.warning("Cannot resolve coordinates, trying with name") Loading @@ -45,7 +45,7 @@ def to_radec(string): coorstr = coor.to_string( style='hmsdms', sep=' ', precision=1, pad=True) log.info(f"Correspond to {coorstr}") return [coor.ra.hourangle, coor.dec.degree] return [coor.ra.hourangle.item(), coor.dec.degree.item()] except NameResolveError as e: log.error("Cannot resolve name") log.error(e) Loading @@ -69,7 +69,7 @@ def to_radians(string, equinox=None): unit=("hourangle", "deg"), equinox=equinox) return [coor.ra.radian, coor.dec.radian] return [coor.ra.radian.item(), coor.dec.radian.item()] except ValueError as e: log.warning("Cannot resolve coordinates, trying with name") Loading @@ -77,7 +77,7 @@ def to_radians(string, equinox=None): try: coor = SkyCoord.from_name(string) log.info("Found catalog name") return [coor.ra.radian, coor.dec.radian] return [coor.ra.radian.item(), coor.dec.radian.item()] except NameResolveError as e: log.error("Cannot resolve name") log.error(e) Loading Loading @@ -187,7 +187,7 @@ def calculate_offset(string="13 56 44 +27 28 01", header=None): log.info(f"Offset in arcsec (web): {dalt.arcsec}, {-daz.arcsec}]") log.debug(f"Offset in deg (astelos): {dalt.deg}, {-daz.deg}") return [dalt.deg, -daz.deg] return [dalt.deg.item(), -daz.deg.item()] def apply_offset(fits_file=temp_fits, box=100, model="moffat", display=False): Loading Loading @@ -243,7 +243,7 @@ def apply_offset(fits_file=temp_fits, box=100, model="moffat", display=False): wcs = WCS(header) coords = wcs.pixel_to_world(true_x, true_y) string_coords = to_hms_dms([coords.ra.deg, coords.dec.deg], join=True) string_coords = to_hms_dms([coords.ra.deg.item(), coords.dec.deg.item()], join=True) log.debug(f"True HMSDMS: {string_coords}") Loading Loading
noctua/api/__init__.py +29 −2 Original line number Diff line number Diff line Loading @@ -9,7 +9,7 @@ import importlib from pathlib import Path # Third-party modules from quart import Blueprint, request, abort from quart import Blueprint, request, abort, jsonify # Custom modules from noctua import devices Loading Loading @@ -63,9 +63,36 @@ def dynamic_import(url_path): 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}") 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")): dynamic_import(section) @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. """ catalog = [] for url_path, resource_instance in resource_registry.items(): for m in ["GET", "POST", "PUT", "DELETE"]: # Recuperiamo la funzione del metodo (es. get, put, post, delete) method_fn = getattr(resource_instance, m.lower(), None) if method_fn: # Estraiamo i metadati del decoratore se presenti, altrimenti None schema = getattr(method_fn, "_input_schema", None) formatted_endpoint = f"/api/{url_path.strip('/')}/" catalog.append({ "endpoint": formatted_endpoint, "method": m, "expects": schema }) return jsonify(catalog)
noctua/api/baseresource.py +19 −0 Original line number Diff line number Diff line Loading @@ -157,3 +157,22 @@ def register_error_handlers(app): @app.errorhandler(424) async def failed_dependency(e): return jsonify({"error": ["Failed Dependency"], "timestamp": datetime.utcnow().isoformat()}), 424 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. """ def decorator(func): func._input_schema = { "type": param_type, # "single", "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 } return func return decorator
noctua/api/switch.py +3 −1 Original line number Diff line number Diff line Loading @@ -5,11 +5,12 @@ # Custom modules from noctua.config import constants from .baseresource import BaseResource from .baseresource import BaseResource, expects # "/dome/light" # "/telescope/lamp" # "/camera/power" class State(BaseResource): '''Manage a switch state.''' Loading @@ -19,6 +20,7 @@ class State(BaseResource): raw = await self.run_blocking(lambda: self.dev.state) return self.make_response(constants.on_off.get(raw, "N/A"), raw_data=raw) @expects(param_type="boolean") async def put(self): '''Switch on or off.''' Loading
noctua/api/telescope.py +3 −1 Original line number Diff line number Diff line Loading @@ -9,7 +9,7 @@ from datetime import datetime # Custom modules from noctua.config import constants from noctua.utils.coordinates import to_hms, to_hms_dms, to_radec from .baseresource import BaseResource from .baseresource import BaseResource, expects class Cover(BaseResource): Loading Loading @@ -115,6 +115,7 @@ class CoordinatesMovement(BaseResource): class CoordinatesMovementRadec(BaseResource): '''Point the telescope in Ra, Dec.''' @expects(param_type="array", count=2, unit="hms ±dms / id", placeholder="Polaris") async def post(self): '''Set new Ra and Dec coordinates.''' Loading @@ -134,6 +135,7 @@ class CoordinatesMovementRadec(BaseResource): class CoordinatesMovementAltaz(BaseResource): '''Point the telescope in Alt, Az.''' @expects(param_type="array", count=2, unit="°", placeholder="[45, 180]") async def post(self): '''Set new Alt and Az coordinates.''' Loading
noctua/utils/coordinates.py +6 −6 Original line number Diff line number Diff line Loading @@ -34,7 +34,7 @@ def to_radec(string): unit=("hourangle", "deg"), equinox=Time.now()) return [coor.ra.hourangle, coor.dec.degree] return [coor.ra.hourangle.item(), coor.dec.degree.item()] except ValueError as e: log.warning("Cannot resolve coordinates, trying with name") Loading @@ -45,7 +45,7 @@ def to_radec(string): coorstr = coor.to_string( style='hmsdms', sep=' ', precision=1, pad=True) log.info(f"Correspond to {coorstr}") return [coor.ra.hourangle, coor.dec.degree] return [coor.ra.hourangle.item(), coor.dec.degree.item()] except NameResolveError as e: log.error("Cannot resolve name") log.error(e) Loading @@ -69,7 +69,7 @@ def to_radians(string, equinox=None): unit=("hourangle", "deg"), equinox=equinox) return [coor.ra.radian, coor.dec.radian] return [coor.ra.radian.item(), coor.dec.radian.item()] except ValueError as e: log.warning("Cannot resolve coordinates, trying with name") Loading @@ -77,7 +77,7 @@ def to_radians(string, equinox=None): try: coor = SkyCoord.from_name(string) log.info("Found catalog name") return [coor.ra.radian, coor.dec.radian] return [coor.ra.radian.item(), coor.dec.radian.item()] except NameResolveError as e: log.error("Cannot resolve name") log.error(e) Loading Loading @@ -187,7 +187,7 @@ def calculate_offset(string="13 56 44 +27 28 01", header=None): log.info(f"Offset in arcsec (web): {dalt.arcsec}, {-daz.arcsec}]") log.debug(f"Offset in deg (astelos): {dalt.deg}, {-daz.deg}") return [dalt.deg, -daz.deg] return [dalt.deg.item(), -daz.deg.item()] def apply_offset(fits_file=temp_fits, box=100, model="moffat", display=False): Loading Loading @@ -243,7 +243,7 @@ def apply_offset(fits_file=temp_fits, box=100, model="moffat", display=False): wcs = WCS(header) coords = wcs.pixel_to_world(true_x, true_y) string_coords = to_hms_dms([coords.ra.deg, coords.dec.deg], join=True) string_coords = to_hms_dms([coords.ra.deg.item(), coords.dec.deg.item()], join=True) log.debug(f"True HMSDMS: {string_coords}") Loading