Loading src/api/handlers/restconf_handler.py +35 −8 Changes for src/api/handlers/restconf_handler.py: 35 added lines, 8 removed lines. Original line number Diff line number Diff line Loading @@ -829,19 +829,33 @@ class RestconfHandler: # RESTCONF Telemetry Operations: Subscriptions # ------------------------------------------------------------------------- def get_subscriptions(self, client_id: str, slice_id: str | None = None) -> tuple[dict[str, Any], int]: """Retrieve telemetry subscriptions for a client.""" def get_subscriptions( self, client_id: str, slice_id: str | None = None, from_ts: float | None = None, to_ts: float | None = None, limit: int | None = None, ) -> tuple[dict[str, Any], int]: """Retrieve telemetry subscriptions for a client, with optional history filters.""" try: if slice_id is not None: return self._get_single_client_subscription(client_id, slice_id) return self._get_all_client_subscriptions(client_id) return self._get_single_client_subscription(client_id, slice_id, from_ts, to_ts, limit) return self._get_all_client_subscriptions(client_id, from_ts, to_ts, limit) except ValueError as exc: return send_response(False, code=404, message=str(exc)) except Exception as exc: return send_response(False, code=500, message=str(exc)) def _get_single_client_subscription(self, client_id: str, slice_id: str) -> tuple[dict[str, Any], int]: """Get a single subscription with telemetry data.""" def _get_single_client_subscription( self, client_id: str, slice_id: str, from_ts: float | None = None, to_ts: float | None = None, limit: int | None = None, ) -> tuple[dict[str, Any], int]: """Get a single subscription with the latest or historic telemetry.""" try: get_sub_fn = _dep("get_subscription", _real_get_subscription) subscription = get_sub_fn(client_id, slice_id) Loading @@ -851,16 +865,29 @@ class RestconfHandler: if not subscription: raise ValueError(f"Client '{client_id}' has no subscription for slice '{slice_id}'") if from_ts is not None or to_ts is not None or limit is not None: telemetry, _ = self._get_telemetry_history(slice_id, from_ts, to_ts, limit) else: telemetry, _ = self.get_telemetry(slice_id) return {**subscription, "telemetry": telemetry}, 200 def _get_all_client_subscriptions(self, client_id: str) -> tuple[dict[str, Any], int]: """Get all subscriptions for a client with telemetry data.""" def _get_all_client_subscriptions( self, client_id: str, from_ts: float | None = None, to_ts: float | None = None, limit: int | None = None, ) -> tuple[dict[str, Any], int]: """Get all subscriptions for a client with the latest or historic telemetry.""" get_cli_subs_fn = _dep("get_client_subscriptions", _real_get_client_subscriptions) subscriptions = get_cli_subs_fn(client_id) use_history = from_ts is not None or to_ts is not None or limit is not None result = [] for sub in subscriptions: sub_slice_id = sub["slice_id"] if use_history: telemetry, _ = self._get_telemetry_history(sub_slice_id, from_ts, to_ts, limit) else: telemetry, _ = self.get_telemetry(sub_slice_id) result.append({**sub, "telemetry": telemetry}) Loading swagger/restconf_namespace.py +60 −39 Changes for swagger/restconf_namespace.py: 60 added lines, 39 removed lines. Original line number Diff line number Diff line Loading @@ -325,6 +325,39 @@ class SliceNetworkTopology(Resource): # ============================================================================= TELEMETRY_HISTORY_PARAMS = { "from": "Optional. Inclusive lower bound as Unix epoch seconds (e.g. 1758700000.5). Returns stored history.", "to": "Optional. Inclusive upper bound as Unix epoch seconds. Returns stored history.", "limit": f"Optional. Return only the N most recent stored samples per slice (1-{MAX_TELEMETRY_HISTORY}).", } def _parse_telemetry_history_args() -> dict[str, Any]: """Parse the optional 'from', 'to' and 'limit' query parameters. Raises: ValueError: If a parameter is not numeric or 'limit' is out of range. """ args: dict[str, Any] = {"from_ts": None, "to_ts": None, "limit": None} for param, key in (("from", "from_ts"), ("to", "to_ts")): raw = request.args.get(param) if raw is not None: try: args[key] = float(raw) except ValueError: raise ValueError(f"Query parameter '{param}' must be a Unix epoch timestamp in seconds") from None raw_limit = request.args.get("limit") if raw_limit is not None: try: args["limit"] = int(raw_limit) except ValueError: raise ValueError("Query parameter 'limit' must be an integer") from None if not 1 <= args["limit"] <= MAX_TELEMETRY_HISTORY: raise ValueError(f"Query parameter 'limit' must be between 1 and {MAX_TELEMETRY_HISTORY}") return args @restconf_ns.route("/operations/telemetry/client") class ClientsList(Resource): """Resource for telemetry client collection operations.""" Loading Loading @@ -386,15 +419,24 @@ class Subscriptions(Resource): """Resource for client subscription collection operations.""" @restconf_ns.doc( description="Retrieves all telemetry subscriptions of a given client.", params={"client_id": "Client ID."}, description=( "Retrieves all telemetry subscriptions of a given client. " "Without filters returns the latest telemetry per subscription; " "with 'from', 'to' or 'limit' returns stored history instead." ), params={"client_id": "Client ID.", **TELEMETRY_HISTORY_PARAMS}, ) @restconf_ns.response(200, "Subscriptions successfully retrieved") @restconf_ns.response(400, "Invalid query parameters") @restconf_ns.response(500, "Internal server error") def get(self, client_id: str) -> Any: """Retrieve all subscriptions for a client.""" """Retrieve all subscriptions for a client, with latest or historic telemetry.""" try: history_args = _parse_telemetry_history_args() except ValueError as exc: return send_response(False, code=400, message=str(exc)) handler = RestconfHandler(NSController(controller_type="RESTCONF")) return current_app.ensure_sync(handler.get_subscriptions)(client_id) return current_app.ensure_sync(handler.get_subscriptions)(client_id, **history_args) @restconf_ns.doc( description="Creates a new telemetry subscription for a given client.", Loading Loading @@ -438,15 +480,25 @@ class SliceSubscriptions(Resource): """Resource for specific client-slice subscription operations.""" @restconf_ns.doc( description="Retrieves a specific telemetry subscription of a given client.", params={"client_id": "Client ID.", "slice_id": "Network Slice ID."}, description=( "Retrieves a specific telemetry subscription of a given client. " "Without filters returns the latest telemetry; " "with 'from', 'to' or 'limit' returns stored history instead." ), params={"client_id": "Client ID.", "slice_id": "Network Slice ID.", **TELEMETRY_HISTORY_PARAMS}, ) @restconf_ns.response(200, "Subscription successfully retrieved") @restconf_ns.response(400, "Invalid query parameters") @restconf_ns.response(404, "Subscription not found") @restconf_ns.response(500, "Internal server error") def get(self, client_id: str, slice_id: str) -> Any: """Retrieve a specific subscription.""" """Retrieve a specific subscription, with latest or historic telemetry.""" try: history_args = _parse_telemetry_history_args() except ValueError as exc: return send_response(False, code=400, message=str(exc)) handler = RestconfHandler(NSController(controller_type="RESTCONF")) return current_app.ensure_sync(handler.get_subscriptions)(client_id, slice_id) return current_app.ensure_sync(handler.get_subscriptions)(client_id, slice_id, **history_args) @restconf_ns.doc( description="Updates a telemetry subscription for a given client.", Loading Loading @@ -509,37 +561,6 @@ class SliceSubscriptionsStream(Resource): ) TELEMETRY_HISTORY_PARAMS = { "from": "Optional. Inclusive lower bound as Unix epoch seconds (e.g. 1758700000.5). Returns stored history.", "to": "Optional. Inclusive upper bound as Unix epoch seconds. Returns stored history.", "limit": f"Optional. Return only the N most recent stored samples per slice (1-{MAX_TELEMETRY_HISTORY}).", } def _parse_telemetry_history_args() -> dict[str, Any]: """Parse the optional 'from', 'to' and 'limit' query parameters. Raises: ValueError: If a parameter is not numeric or 'limit' is out of range. """ args: dict[str, Any] = {"from_ts": None, "to_ts": None, "limit": None} for param, key in (("from", "from_ts"), ("to", "to_ts")): raw = request.args.get(param) if raw is not None: try: args[key] = float(raw) except ValueError: raise ValueError(f"Query parameter '{param}' must be a Unix epoch timestamp in seconds") from None raw_limit = request.args.get("limit") if raw_limit is not None: try: args["limit"] = int(raw_limit) except ValueError: raise ValueError("Query parameter 'limit' must be an integer") from None if not 1 <= args["limit"] <= MAX_TELEMETRY_HISTORY: raise ValueError(f"Query parameter 'limit' must be between 1 and {MAX_TELEMETRY_HISTORY}") return args @restconf_ns.route("/operations/telemetry/slice") Loading Loading
src/api/handlers/restconf_handler.py +35 −8 Changes for src/api/handlers/restconf_handler.py: 35 added lines, 8 removed lines. Original line number Diff line number Diff line Loading @@ -829,19 +829,33 @@ class RestconfHandler: # RESTCONF Telemetry Operations: Subscriptions # ------------------------------------------------------------------------- def get_subscriptions(self, client_id: str, slice_id: str | None = None) -> tuple[dict[str, Any], int]: """Retrieve telemetry subscriptions for a client.""" def get_subscriptions( self, client_id: str, slice_id: str | None = None, from_ts: float | None = None, to_ts: float | None = None, limit: int | None = None, ) -> tuple[dict[str, Any], int]: """Retrieve telemetry subscriptions for a client, with optional history filters.""" try: if slice_id is not None: return self._get_single_client_subscription(client_id, slice_id) return self._get_all_client_subscriptions(client_id) return self._get_single_client_subscription(client_id, slice_id, from_ts, to_ts, limit) return self._get_all_client_subscriptions(client_id, from_ts, to_ts, limit) except ValueError as exc: return send_response(False, code=404, message=str(exc)) except Exception as exc: return send_response(False, code=500, message=str(exc)) def _get_single_client_subscription(self, client_id: str, slice_id: str) -> tuple[dict[str, Any], int]: """Get a single subscription with telemetry data.""" def _get_single_client_subscription( self, client_id: str, slice_id: str, from_ts: float | None = None, to_ts: float | None = None, limit: int | None = None, ) -> tuple[dict[str, Any], int]: """Get a single subscription with the latest or historic telemetry.""" try: get_sub_fn = _dep("get_subscription", _real_get_subscription) subscription = get_sub_fn(client_id, slice_id) Loading @@ -851,16 +865,29 @@ class RestconfHandler: if not subscription: raise ValueError(f"Client '{client_id}' has no subscription for slice '{slice_id}'") if from_ts is not None or to_ts is not None or limit is not None: telemetry, _ = self._get_telemetry_history(slice_id, from_ts, to_ts, limit) else: telemetry, _ = self.get_telemetry(slice_id) return {**subscription, "telemetry": telemetry}, 200 def _get_all_client_subscriptions(self, client_id: str) -> tuple[dict[str, Any], int]: """Get all subscriptions for a client with telemetry data.""" def _get_all_client_subscriptions( self, client_id: str, from_ts: float | None = None, to_ts: float | None = None, limit: int | None = None, ) -> tuple[dict[str, Any], int]: """Get all subscriptions for a client with the latest or historic telemetry.""" get_cli_subs_fn = _dep("get_client_subscriptions", _real_get_client_subscriptions) subscriptions = get_cli_subs_fn(client_id) use_history = from_ts is not None or to_ts is not None or limit is not None result = [] for sub in subscriptions: sub_slice_id = sub["slice_id"] if use_history: telemetry, _ = self._get_telemetry_history(sub_slice_id, from_ts, to_ts, limit) else: telemetry, _ = self.get_telemetry(sub_slice_id) result.append({**sub, "telemetry": telemetry}) Loading
swagger/restconf_namespace.py +60 −39 Changes for swagger/restconf_namespace.py: 60 added lines, 39 removed lines. Original line number Diff line number Diff line Loading @@ -325,6 +325,39 @@ class SliceNetworkTopology(Resource): # ============================================================================= TELEMETRY_HISTORY_PARAMS = { "from": "Optional. Inclusive lower bound as Unix epoch seconds (e.g. 1758700000.5). Returns stored history.", "to": "Optional. Inclusive upper bound as Unix epoch seconds. Returns stored history.", "limit": f"Optional. Return only the N most recent stored samples per slice (1-{MAX_TELEMETRY_HISTORY}).", } def _parse_telemetry_history_args() -> dict[str, Any]: """Parse the optional 'from', 'to' and 'limit' query parameters. Raises: ValueError: If a parameter is not numeric or 'limit' is out of range. """ args: dict[str, Any] = {"from_ts": None, "to_ts": None, "limit": None} for param, key in (("from", "from_ts"), ("to", "to_ts")): raw = request.args.get(param) if raw is not None: try: args[key] = float(raw) except ValueError: raise ValueError(f"Query parameter '{param}' must be a Unix epoch timestamp in seconds") from None raw_limit = request.args.get("limit") if raw_limit is not None: try: args["limit"] = int(raw_limit) except ValueError: raise ValueError("Query parameter 'limit' must be an integer") from None if not 1 <= args["limit"] <= MAX_TELEMETRY_HISTORY: raise ValueError(f"Query parameter 'limit' must be between 1 and {MAX_TELEMETRY_HISTORY}") return args @restconf_ns.route("/operations/telemetry/client") class ClientsList(Resource): """Resource for telemetry client collection operations.""" Loading Loading @@ -386,15 +419,24 @@ class Subscriptions(Resource): """Resource for client subscription collection operations.""" @restconf_ns.doc( description="Retrieves all telemetry subscriptions of a given client.", params={"client_id": "Client ID."}, description=( "Retrieves all telemetry subscriptions of a given client. " "Without filters returns the latest telemetry per subscription; " "with 'from', 'to' or 'limit' returns stored history instead." ), params={"client_id": "Client ID.", **TELEMETRY_HISTORY_PARAMS}, ) @restconf_ns.response(200, "Subscriptions successfully retrieved") @restconf_ns.response(400, "Invalid query parameters") @restconf_ns.response(500, "Internal server error") def get(self, client_id: str) -> Any: """Retrieve all subscriptions for a client.""" """Retrieve all subscriptions for a client, with latest or historic telemetry.""" try: history_args = _parse_telemetry_history_args() except ValueError as exc: return send_response(False, code=400, message=str(exc)) handler = RestconfHandler(NSController(controller_type="RESTCONF")) return current_app.ensure_sync(handler.get_subscriptions)(client_id) return current_app.ensure_sync(handler.get_subscriptions)(client_id, **history_args) @restconf_ns.doc( description="Creates a new telemetry subscription for a given client.", Loading Loading @@ -438,15 +480,25 @@ class SliceSubscriptions(Resource): """Resource for specific client-slice subscription operations.""" @restconf_ns.doc( description="Retrieves a specific telemetry subscription of a given client.", params={"client_id": "Client ID.", "slice_id": "Network Slice ID."}, description=( "Retrieves a specific telemetry subscription of a given client. " "Without filters returns the latest telemetry; " "with 'from', 'to' or 'limit' returns stored history instead." ), params={"client_id": "Client ID.", "slice_id": "Network Slice ID.", **TELEMETRY_HISTORY_PARAMS}, ) @restconf_ns.response(200, "Subscription successfully retrieved") @restconf_ns.response(400, "Invalid query parameters") @restconf_ns.response(404, "Subscription not found") @restconf_ns.response(500, "Internal server error") def get(self, client_id: str, slice_id: str) -> Any: """Retrieve a specific subscription.""" """Retrieve a specific subscription, with latest or historic telemetry.""" try: history_args = _parse_telemetry_history_args() except ValueError as exc: return send_response(False, code=400, message=str(exc)) handler = RestconfHandler(NSController(controller_type="RESTCONF")) return current_app.ensure_sync(handler.get_subscriptions)(client_id, slice_id) return current_app.ensure_sync(handler.get_subscriptions)(client_id, slice_id, **history_args) @restconf_ns.doc( description="Updates a telemetry subscription for a given client.", Loading Loading @@ -509,37 +561,6 @@ class SliceSubscriptionsStream(Resource): ) TELEMETRY_HISTORY_PARAMS = { "from": "Optional. Inclusive lower bound as Unix epoch seconds (e.g. 1758700000.5). Returns stored history.", "to": "Optional. Inclusive upper bound as Unix epoch seconds. Returns stored history.", "limit": f"Optional. Return only the N most recent stored samples per slice (1-{MAX_TELEMETRY_HISTORY}).", } def _parse_telemetry_history_args() -> dict[str, Any]: """Parse the optional 'from', 'to' and 'limit' query parameters. Raises: ValueError: If a parameter is not numeric or 'limit' is out of range. """ args: dict[str, Any] = {"from_ts": None, "to_ts": None, "limit": None} for param, key in (("from", "from_ts"), ("to", "to_ts")): raw = request.args.get(param) if raw is not None: try: args[key] = float(raw) except ValueError: raise ValueError(f"Query parameter '{param}' must be a Unix epoch timestamp in seconds") from None raw_limit = request.args.get("limit") if raw_limit is not None: try: args["limit"] = int(raw_limit) except ValueError: raise ValueError("Query parameter 'limit' must be an integer") from None if not 1 <= args["limit"] <= MAX_TELEMETRY_HISTORY: raise ValueError(f"Query parameter 'limit' must be between 1 and {MAX_TELEMETRY_HISTORY}") return args @restconf_ns.route("/operations/telemetry/slice") Loading