Exemple #1
0
 async def _handle_item_request(self,
                                web_request: WebRequest) -> Dict[str, Any]:
     action = web_request.get_action()
     namespace = web_request.get_str("namespace")
     if namespace in self.forbidden_namespaces:
         raise self.server.error(
             f"Read/Write access to namespace '{namespace}'"
             " is forbidden", 403)
     key: Any
     valid_types: Tuple[type, ...]
     if action != "GET":
         if namespace in self.protected_namespaces:
             raise self.server.error(
                 f"Write access to namespace '{namespace}'"
                 " is forbidden", 403)
         key = web_request.get("key")
         valid_types = (list, str)
     else:
         key = web_request.get("key", None)
         valid_types = (list, str, type(None))
     if not isinstance(key, valid_types):
         raise self.server.error(
             "Value for argument 'key' is an invalid type: "
             f"{type(key).__name__}")
     if action == "GET":
         val = await self.get_item(namespace, key)
     elif action == "POST":
         val = web_request.get("value")
         await self.insert_item(namespace, key, val)
     elif action == "DELETE":
         val = await self.delete_item(namespace, key, drop_empty_db=True)
     return {'namespace': namespace, 'key': key, 'value': val}
Exemple #2
0
 async def _handle_job_request(self,
                               web_request: WebRequest) -> Dict[str, Any]:
     action = web_request.get_action()
     if action == "POST":
         files: Union[List[str], str] = web_request.get('filenames')
         if isinstance(files, str):
             files = [f.strip() for f in files.split(',') if f.strip()]
         # Validate that all files exist before queueing
         await self.queue_job(files)
     elif action == "DELETE":
         if web_request.get_boolean("all", False):
             await self.delete_job([], all=True)
         else:
             job_ids: Union[List[str], str] = web_request.get('job_ids')
             if isinstance(job_ids, str):
                 job_ids = [
                     f.strip() for f in job_ids.split(',') if f.strip()
                 ]
             await self.delete_job(job_ids)
     else:
         raise self.server.error(f"Invalid action: {action}")
     return {
         'queued_jobs': self._job_map_to_list(),
         'queue_state': self.queue_state
     }
Exemple #3
0
    async def _handle_job_request(self,
                                  web_request: WebRequest
                                  ) -> Dict[str, Any]:
        action = web_request.get_action()
        if action == "GET":
            job_id = web_request.get_str("uid")
            if job_id not in self.cached_job_ids:
                raise self.server.error(f"Invalid job uid: {job_id}", 404)
            job = self.history_ns[job_id]
            return {"job": self._prep_requested_job(job, job_id)}
        if action == "DELETE":
            all = web_request.get_boolean("all", False)
            if all:
                deljobs = self.cached_job_ids
                self.history_ns.clear()
                self.cached_job_ids = []
                self.next_job_id = 0
                return {'deleted_jobs': deljobs}

            job_id = web_request.get_str("uid")
            if job_id not in self.cached_job_ids:
                raise self.server.error(f"Invalid job uid: {job_id}", 404)

            self.delete_job(job_id)
            return {'deleted_jobs': [job_id]}
        raise self.server.error("Invalid Request Method")
Exemple #4
0
 async def _handle_subscription_request(
         self, web_request: WebRequest) -> Dict[str, Any]:
     topic: str = web_request.get_str("topic")
     qos: int = web_request.get_int("qos", self.qos)
     timeout: Optional[float] = web_request.get_float('timeout', None)
     resp: asyncio.Future = asyncio.Future()
     hdl: Optional[SubscriptionHandle] = None
     try:
         hdl = self.subscribe_topic(topic, resp.set_result, qos)
         self.pending_responses.append(resp)
         await asyncio.wait_for(resp, timeout)
         ret: bytes = resp.result()
     except asyncio.TimeoutError:
         raise self.server.error("MQTT Subscribe Timed Out", 504)
     finally:
         try:
             self.pending_responses.remove(resp)
         except Exception:
             pass
         if hdl is not None:
             self.unsubscribe(hdl)
     try:
         payload = json.loads(ret)
     except json.JSONDecodeError:
         payload = ret.decode()
     return {'topic': topic, 'payload': payload}
Exemple #5
0
 async def _handle_update_request(self,
                                  web_request: WebRequest
                                  ) -> str:
     await self.initialized_lock.wait()
     if await self._check_klippy_printing():
         raise self.server.error("Update Refused: Klippy is printing")
     app: str = web_request.get_endpoint().split("/")[-1]
     if app == "client":
         app = web_request.get('name')
     if self.cmd_helper.is_app_updating(app):
         return f"Object {app} is currently being updated"
     updater = self.updaters.get(app, None)
     if updater is None:
         raise self.server.error(f"Updater {app} not available", 404)
     async with self.cmd_request_lock:
         self.cmd_helper.set_update_info(app, id(web_request))
         try:
             if not await self._check_need_reinstall(app):
                 await updater.update()
         except Exception as e:
             self.cmd_helper.notify_update_response(
                 f"Error updating {app}")
             self.cmd_helper.notify_update_response(
                 str(e), is_complete=True)
             raise
         finally:
             self.cmd_helper.clear_update_info()
     return "ok"
Exemple #6
0
 async def _handle_webcam_request(
         self, web_request: WebRequest) -> Dict[str, Any]:
     action = web_request.get_action()
     name = web_request.get_str("name")
     webcam_data: Dict[str, Any] = {}
     if action == "GET":
         if name not in self.webcams:
             raise self.server.error(f"Webcam {name} not found", 404)
         webcam_data = self.webcams[name].as_dict()
     elif action == "POST":
         if (name in self.webcams
                 and self.webcams[name].source == "config"):
             raise self.server.error(
                 f"Cannot overwrite webcam '{name}' sourced from "
                 "Moonraker configuration")
         webcam = WebCam.from_web_request(self.server, web_request)
         self.webcams[name] = webcam
         webcam_data = webcam.as_dict()
         await self._save_cam(webcam)
     elif action == "DELETE":
         if name not in self.webcams:
             raise self.server.error(f"Webcam {name} not found", 404)
         elif self.webcams[name].source == "config":
             raise self.server.error(
                 f"Cannot delete webcam '{name}' sourced from "
                 "Moonraker configuration")
         webcam = self.webcams.pop(name)
         webcam_data = webcam.as_dict()
         await self._delete_cam(webcam)
     if action != "GET":
         self.server.send_event("webcam:webcams_changed",
                                {"webcams": self._list_webcams()})
     return {"webcam": webcam_data}
Exemple #7
0
 async def _handle_repo_recovery(self,
                                 web_request: WebRequest
                                 ) -> str:
     await self.initialized_lock.wait()
     if await self._check_klippy_printing():
         raise self.server.error(
             "Recovery Attempt Refused: Klippy is printing")
     app: str = web_request.get_str('name')
     hard = web_request.get_boolean("hard", False)
     update_deps = web_request.get_boolean("update_deps", False)
     updater = self.updaters.get(app, None)
     if updater is None:
         raise self.server.error(f"Updater {app} not available", 404)
     elif not isinstance(updater, GitDeploy):
         raise self.server.error(f"Upater {app} is not a Git Repo Type")
     async with self.cmd_request_lock:
         self.cmd_helper.set_update_info(f"recover_{app}", id(web_request))
         try:
             await updater.recover(hard, update_deps)
         except Exception as e:
             self.cmd_helper.notify_update_response(
                 f"Error Recovering {app}")
             self.cmd_helper.notify_update_response(
                 str(e), is_complete=True)
             raise
         finally:
             self.cmd_helper.clear_update_info()
     return "ok"
 async def _request_standard(self, web_request: WebRequest) -> Any:
     rpc_method = web_request.get_endpoint()
     args = web_request.get_args()
     # Create a base klippy request
     base_request = KlippyRequest(rpc_method, args)
     self.pending_requests[base_request.id] = base_request
     self.event_loop.register_callback(self._write_request, base_request)
     return await base_request.wait()
Exemple #9
0
    async def _handle_jobs_list(self,
                                web_request: WebRequest
                                ) -> Dict[str, Any]:
        async with self.request_lock:
            i = 0
            count = 0
            end_num = len(self.cached_job_ids)
            jobs: List[Dict[str, Any]] = []
            start_num = 0

            before = web_request.get_float("before", -1)
            since = web_request.get_float("since", -1)
            limit = web_request.get_int("limit", 50)
            start = web_request.get_int("start", 0)
            order = web_request.get_str("order", "desc")

            if order not in ["asc", "desc"]:
                raise self.server.error(f"Invalid `order` value: {order}", 400)

            reverse_order = (order == "desc")

            # cached jobs is asc order, find lower and upper boundary
            if since != -1:
                while start_num < end_num:
                    job_id = self.cached_job_ids[start_num]
                    job: Dict[str, Any] = await self.history_ns[job_id]
                    if job['start_time'] > since:
                        break
                    start_num += 1

            if before != -1:
                while end_num > 0:
                    job_id = self.cached_job_ids[end_num-1]
                    job = await self.history_ns[job_id]
                    if job['end_time'] < before:
                        break
                    end_num -= 1

            if start_num >= end_num or end_num == 0:
                return {"count": 0, "jobs": []}

            i = start
            count = end_num - start_num

            if limit == 0:
                limit = MAX_JOBS

            while i < count and len(jobs) < limit:
                if reverse_order:
                    job_id = self.cached_job_ids[end_num - i - 1]
                else:
                    job_id = self.cached_job_ids[start_num + i]
                job = await self.history_ns[job_id]
                jobs.append(self._prep_requested_job(job, job_id))
                i += 1

            return {"count": count, "jobs": jobs}
Exemple #10
0
 async def _request_standard(self, web_request: WebRequest) -> Any:
     rpc_method = web_request.get_endpoint()
     args = web_request.get_args()
     # Create a base klippy request
     base_request = BaseRequest(rpc_method, args)
     self.pending_requests[base_request.id] = base_request
     self.ioloop.spawn_callback(self.klippy_connection.send_request,
                                base_request)
     return await base_request.wait()
Exemple #11
0
 async def _handle_dismiss_request(
     self, web_request: WebRequest
 ) -> Dict[str, Any]:
     async with self.request_lock:
         entry_id: str = web_request.get_str("entry_id")
         wake_time: Optional[int] = web_request.get_int("wake_time", None)
         await self.entry_mgr.dismiss_entry(entry_id, wake_time)
         return {
             "entry_id": entry_id
         }
Exemple #12
0
 async def make_request(self, web_request: WebRequest) -> Any:
     rpc_method = web_request.get_endpoint()
     if rpc_method == "objects/subscribe":
         return await self._request_subscripton(web_request)
     else:
         if rpc_method == "gcode/script":
             script = web_request.get_str('script', "")
             data_store: DataStore = self.lookup_component('data_store')
             data_store.store_gcode_command(script)
         return await self._request_standard(web_request)
Exemple #13
0
 async def _handle_call_agent(self, web_request: WebRequest) -> Any:
     agent = web_request.get_str("agent")
     method: str = web_request.get_str("method")
     args: Optional[Union[List, Dict[str, Any]]]
     args = web_request.get("arguments", None)
     if args is not None and not isinstance(args, (list, dict)):
         raise self.server.error(
             "The 'arguments' field must contain an object or a list")
     if agent not in self.agents:
         raise self.server.error(f"Agent {agent} not connected")
     conn = self.agents[agent]
     return await conn.call_method(method, args)
Exemple #14
0
 async def _handle_publish_request(
         self, web_request: WebRequest) -> Dict[str, Any]:
     topic: str = web_request.get_str("topic")
     payload: Any = web_request.get("payload", None)
     qos: int = web_request.get_int("qos", self.qos)
     retain: bool = web_request.get_boolean("retain", False)
     timeout: Optional[float] = web_request.get_float('timeout', None)
     try:
         await asyncio.wait_for(
             self.publish_topic(topic, payload, qos, retain), timeout)
     except asyncio.TimeoutError:
         raise self.server.error("MQTT Publish Timed Out", 504)
     return {"topic": topic}
 async def request(self, web_request: WebRequest) -> Any:
     if not self.is_connected():
         raise ServerError("Klippy Host not connected", 503)
     rpc_method = web_request.get_endpoint()
     if rpc_method == "objects/subscribe":
         return await self._request_subscripton(web_request)
     else:
         if rpc_method == "gcode/script":
             script = web_request.get_str('script', "")
             if script:
                 self.server.send_event(
                     "klippy_connection:gcode_received", script)
         return await self._request_standard(web_request)
Exemple #16
0
 async def _handle_batch_power_request(
         self, web_request: WebRequest) -> Dict[str, Any]:
     args = web_request.get_args()
     ep = web_request.get_endpoint()
     if not args:
         raise self.server.error("No arguments provided")
     requested_devs = {k: self.devices.get(k, None) for k in args}
     result = {}
     req = ep.split("/")[-1]
     for name, device in requested_devs.items():
         if device is not None:
             result[name] = await self._process_request(device, req)
         else:
             result[name] = "device_not_found"
     return result
Exemple #17
0
 async def _handle_single_power_request(
         self, web_request: WebRequest) -> Dict[str, Any]:
     dev_name: str = web_request.get_str('device')
     req_action = web_request.get_action()
     if dev_name not in self.devices:
         raise self.server.error(f"No valid device named {dev_name}")
     dev = self.devices[dev_name]
     if req_action == 'GET':
         action = "status"
     elif req_action == "POST":
         action = web_request.get_str('action').lower()
         if action not in ["on", "off", "toggle"]:
             raise self.server.error(f"Invalid requested action '{action}'")
     result = await self._process_request(dev, action)
     return {dev_name: result}
Exemple #18
0
 async def _handle_batch_wled_request(
         self: WLED, web_request: WebRequest) -> Dict[str, Any]:
     args = web_request.get_args()
     ep = web_request.get_endpoint()
     if not args:
         raise self.server.error("No arguments provided")
     requested_strips = {k: self.strips.get(k, None) for k in args}
     result = {}
     req = ep.split("/")[-1]
     for name, strip in requested_strips.items():
         if strip is not None:
             result[name] = await self._process_request(strip, req, -1)
         else:
             result[name] = {"error": "strip_not_found"}
     return result
Exemple #19
0
 async def _handle_feed_request(
     self, web_request: WebRequest
 ) -> Dict[str, Any]:
     action = web_request.get_action()
     name: str = web_request.get("name")
     name = name.lower()
     changed: bool = False
     db: MoonrakerDatabase = self.server.lookup_component("database")
     result = "skipped"
     if action == "POST":
         if name not in self.subscriptions:
             feed = RssFeed(name, self.entry_mgr, self.dev_mode)
             self.subscriptions[name] = feed
             await feed.initialize()
             changed = await feed.update_entries()
             self.stored_feeds.append(name)
             db.insert_item(
                 "moonraker", "announcements.stored_feeds", self.stored_feeds
             )
             result = "added"
     elif action == "DELETE":
         if name not in self.stored_feeds:
             raise self.server.error(f"Feed '{name}' not stored")
         if name in self.configured_feeds:
             raise self.server.error(
                 f"Feed '{name}' exists in the configuration, cannot remove"
             )
         self.stored_feeds.remove(name)
         db.insert_item(
             "moonraker", "announcements.stored_feeds", self.stored_feeds
         )
         if name in self.subscriptions:
             del self.subscriptions[name]
             changed = await self.entry_mgr.prune_by_feed(name)
             logging.info(f"Removed Announcement Feed: {name}")
             result = "removed"
         else:
             raise self.server.error(f"Feed does not exist: {name}")
     if changed:
         entries = await self.entry_mgr.list_entries()
         self.eventloop.delay_callback(
             .05, self.server.send_event, "announcements:entries_updated",
             {"entries": entries}
         )
     return {
         "feed": name,
         "action": result
     }
 def _login_jwt_user(self,
                     web_request: WebRequest,
                     create: bool = False) -> Dict[str, Any]:
     username: str = web_request.get_str('username')
     password: str = web_request.get_str('password')
     user_info: Dict[str, Any]
     if username in RESERVED_USERS:
         raise self.server.error(f"Invalid Request for user {username}")
     if create:
         if username in self.users:
             raise self.server.error(f"User {username} already exists")
         salt = secrets.token_bytes(32)
         hashed_pass = hashlib.pbkdf2_hmac('sha256', password.encode(),
                                           salt, HASH_ITER).hex()
         user_info = {
             'username': username,
             'password': hashed_pass,
             'salt': salt.hex(),
             'created_on': time.time()
         }
         self.users[username] = user_info
         action = "user_created"
     else:
         if username not in self.users:
             raise self.server.error(f"Unregistered User: {username}")
         user_info = self.users[username]
         salt = bytes.fromhex(user_info['salt'])
         hashed_pass = hashlib.pbkdf2_hmac('sha256', password.encode(),
                                           salt, HASH_ITER).hex()
         action = "user_logged_in"
     if hashed_pass != user_info['password']:
         raise self.server.error("Invalid Password")
     jwt_secret_hex: Optional[str] = user_info.get('jwt_secret', None)
     if jwt_secret_hex is None:
         private_key = Signer()
         jwk_id = base64url_encode(secrets.token_bytes()).decode()
         user_info['jwt_secret'] = private_key.hex_seed().decode()
         user_info['jwk_id'] = jwk_id
         self.users[username] = user_info
         self.public_jwks[jwk_id] = self._generate_public_jwk(private_key)
     else:
         private_key = self._load_private_key(jwt_secret_hex)
         jwk_id = user_info['jwk_id']
     token = self._generate_jwt(username, jwk_id, private_key)
     refresh_token = self._generate_jwt(
         username,
         jwk_id,
         private_key,
         token_type="refresh",
         exp_time=datetime.timedelta(days=self.login_timeout))
     if create:
         IOLoop.current().call_later(.005, self.server.send_event,
                                     "authorization:user_created",
                                     {'username': username})
     return {
         'username': username,
         'token': token,
         'refresh_token': refresh_token,
         'action': action
     }
Exemple #21
0
 async def _do_remote_request(self, args, conn):
     return await self.server.make_request(
         WebRequest(self.callback,
                    args,
                    conn=conn,
                    ip_addr=self.request.remote_ip,
                    user=self.current_user))
Exemple #22
0
 async def _handle_apikey_request(self, web_request: WebRequest) -> str:
     action = web_request.get_action()
     if action.upper() == 'POST':
         self.api_key = uuid.uuid4().hex
         self.users[API_USER]['api_key'] = self.api_key
         self._sync_user(API_USER)
     return self.api_key
Exemple #23
0
 async def _handle_service_request(self, web_request: WebRequest) -> str:
     name: str = web_request.get('service')
     action = web_request.get_endpoint().split('/')[-1]
     if name == "moonraker":
         if action != "restart":
             raise self.server.error(
                 f"Service action '{action}' not available for moonraker")
         event_loop = self.server.get_event_loop()
         event_loop.register_callback(self.do_service_action, action, name)
     elif self.sys_provider.is_service_available(name):
         await self.do_service_action(action, name)
     else:
         if name in ALLOWED_SERVICES:
             raise self.server.error(f"Service '{name}' not installed")
         raise self.server.error(f"Service '{name}' not allowed")
     return "ok"
Exemple #24
0
 async def _handle_update_request(
     self, web_request: WebRequest
 ) -> Dict[str, Any]:
     subs: Optional[Union[str, List[str]]]
     subs = web_request.get("subscriptions", None)
     if isinstance(subs, str):
         subs = [sub.strip() for sub in subs.split(",") if sub.strip()]
     elif subs is None:
         subs = list(self.subscriptions.keys())
     for sub in subs:
         if sub not in self.subscriptions:
             raise self.server.error(f"No subscription for {sub}")
     async with self.request_lock:
         changed = False
         for sub in subs:
             ret = await self.subscriptions[sub].update_entries()
             changed |= ret
         entries = await self.entry_mgr.list_entries()
         if changed:
             self.eventloop.delay_callback(
                 .05, self.server.send_event,
                 "announcements:entries_updated",
                 {"entries": entries})
         return {
             "entries": entries,
             "modified": changed
         }
 async def _select_file(self,
                        web_request: WebRequest
                        ) -> None:
     command: str = web_request.get('command')
     rel_path: str = web_request.get('relative_path')
     root, filename = rel_path.strip("/").split("/", 1)
     fmgr: FileManager = self.server.lookup_component('file_manager')
     if command == "select":
         start_print: bool = web_request.get('print', False)
         if not start_print:
             # No-op, selecting a file has no meaning in Moonraker
             return
         if root != "gcodes":
             raise self.server.error(
                 "File must be located in the 'gcodes' root", 400)
         if not fmgr.check_file_exists(root, filename):
             raise self.server.error("File does not exist")
         try:
             ret = await self.klippy_apis.query_objects(
                 {'print_stats': None})
             pstate: str = ret['print_stats']['state']
         except self.server.error:
             pstate = "not_avail"
         started: bool = False
         if pstate not in ["printing", "paused", "not_avail"]:
             try:
                 await self.klippy_apis.start_print(filename)
             except self.server.error:
                 started = False
             else:
                 logging.debug(f"Job '{filename}' started via Octoprint API")
                 started = True
         if not started:
             if fmgr.upload_queue_enabled():
                 job_queue: JobQueue = self.server.lookup_component(
                     'job_queue')
                 await job_queue.queue_job(filename, check_exists=False)
                 # Fire the file_manager's upload_queued event for
                 # compatibility.  We assume that this endpoint is
                 # requests by Cura after a file has been uploaded.
                 self.server.send_event("file_manager:upload_queued",
                                        filename)
                 logging.debug(f"Job '{filename}' queued via Octoprint API")
             else:
                 raise self.server.error("Conflict", 409)
     else:
         raise self.server.error(f"Unsupported Command: {command}")
Exemple #26
0
    async def _handle_single_wled_request(
            self: WLED, web_request: WebRequest) -> Dict[str, Any]:
        strip_name: str = web_request.get_str('strip')
        preset: int = web_request.get_int('preset', -1)

        req_action = web_request.get_action()
        if strip_name not in self.strips:
            raise self.server.error(f"No valid strip named {strip_name}")
        strip = self.strips[strip_name]
        if req_action == 'GET':
            return {strip_name: strip.get_strip_info()}
        elif req_action == "POST":
            action = web_request.get_str('action').lower()
            if action not in ["on", "off", "toggle"]:
                raise self.server.error(f"Invalid requested action '{action}'")
            result = await self._process_request(strip, action, preset)
        return {strip_name: result}
Exemple #27
0
 async def _handle_klippy_identified(self) -> None:
     if self.status_objs:
         args = {'objects': self.status_objs}
         try:
             await self.server.make_request(
                 WebRequest("objects/subscribe", args, conn=self))
         except self.server.error:
             pass
Exemple #28
0
 async def _handle_gcode_store_request(
         self, web_request: WebRequest) -> Dict[str, List[Dict[str, Any]]]:
     count = web_request.get_int("count", None)
     if count is not None:
         gc_responses = list(self.gcode_queue)[-count:]
     else:
         gc_responses = list(self.gcode_queue)
     return {'gcode_store': gc_responses}
Exemple #29
0
 async def _handle_service_request(self, web_request: WebRequest) -> str:
     name: str = web_request.get('service')
     action = web_request.get_endpoint().split('/')[-1]
     if name == "moonraker":
         if action != "restart":
             raise self.server.error(
                 f"Service action '{action}' not available for moonraker")
         IOLoop.current().spawn_callback(self.do_service_action, action,
                                         name)
     elif name in self.available_services:
         await self.do_service_action(action, name)
     else:
         if name in ALLOWED_SERVICES and \
                 name not in self.available_services:
             raise self.server.error(f"Service '{name}' not installed")
         raise self.server.error(f"Service '{name}' not allowed")
     return "ok"
Exemple #30
0
    async def _request_subscripton(self,
                                   web_request: WebRequest) -> Dict[str, Any]:
        args = web_request.get_args()
        conn = web_request.get_connection()

        # Build the subscription request from a superset of all client
        # subscriptions
        sub = args.get('objects', {})
        if conn is None:
            raise self.error(
                "No connection associated with subscription request")
        self.subscriptions[conn] = sub
        all_subs: Dict[str, Any] = {}
        # request superset of all client subscriptions
        for sub in self.subscriptions.values():
            for obj, items in sub.items():
                if obj in all_subs:
                    pi = all_subs[obj]
                    if items is None or pi is None:
                        all_subs[obj] = None
                    else:
                        uitems = list(set(pi) | set(items))
                        all_subs[obj] = uitems
                else:
                    all_subs[obj] = items
        args['objects'] = all_subs
        args['response_template'] = {'method': "process_status_update"}

        result = await self._request_standard(web_request)

        # prune the status response
        pruned_status = {}
        all_status = result['status']
        sub = self.subscriptions.get(conn, {})
        for obj, fields in all_status.items():
            if obj in sub:
                valid_fields = sub[obj]
                if valid_fields is None:
                    pruned_status[obj] = fields
                else:
                    pruned_status[obj] = {
                        k: v
                        for k, v in fields.items() if k in valid_fields
                    }
        result['status'] = pruned_status
        return result