19 from requests
import HTTPError, RequestException
22 from threading
import Lock
44 """ Generic class to wrap web APIs """ 53 config = Configuration.get([DEFAULT_CONFIG,
57 config_server = config.get(
"server")
58 self.
url = config_server.get(
"url")
59 self.
version = config_server.get(
"version")
66 return self.
send(params)
70 if not self.identity.has_refresh():
71 self.
identity = IdentityManager.load()
73 if self.identity.refresh
and self.identity.is_expired():
74 self.
identity = IdentityManager.load()
76 if self.identity.is_expired():
80 LOG.debug(
'Refreshing token')
81 if identity_lock.acquire(blocking=
False):
86 "Authorization":
"Bearer " + self.identity.refresh,
87 "Device": self.identity.uuid
90 IdentityManager.save(data, lock=
False)
91 LOG.debug(
'Saved credentials')
92 except HTTPError
as e:
93 if e.response.status_code == 401:
94 LOG.error(
'Could not refresh token, invalid refresh code.')
99 identity_lock.release()
102 LOG.debug(
'Refresh is already in progress, waiting until done')
105 self.
identity = IdentityManager.load(lock=
False)
106 LOG.debug(
'new credentials loaded')
108 def send(self, params, no_refresh=False):
109 """ Send request to mycroft backend. 110 The method handles Etags and will return a cached response value 111 if nothing has changed on the remote. 114 params (dict): request parameters 115 no_refresh (bool): optional parameter to disable refreshs of token 118 Requests response object. 120 query_data = frozenset(params.get(
'query', {}).items())
121 params_key = (params.get(
'path'), query_data)
122 etag = self.params_to_etag.get(params_key)
124 method = params.get(
"method",
"GET")
134 headers[
'If-None-Match'] = etag
136 response = requests.request(
137 method, url, headers=headers, params=query,
138 data=data, json=json_body, timeout=(3.05, 15)
140 if response.status_code == 304:
143 elif 'ETag' in response.headers:
144 etag = response.headers[
'ETag'].strip(
'"')
152 """ Parse response and extract data from response. 154 Will try to refresh the access token if it's expired. 157 response (requests Response object): Response to parse 158 no_refresh (bool): Disable refreshing of the token 160 data fetched from server 164 if 200 <= response.status_code < 300:
166 elif (
not no_refresh
and response.status_code == 401
and not 167 response.url.endswith(
"auth/token")
and 168 self.identity.is_expired()):
171 raise HTTPError(data, response=response)
175 return response.json()
180 headers = params.get(
"headers", {})
183 params[
"headers"] = headers
187 if not headers.__contains__(
"Content-Type"):
188 headers[
"Content-Type"] =
"application/json" 191 if not headers.__contains__(
"Authorization"):
192 headers[
"Authorization"] =
"Bearer " + self.identity.access
195 return params.get(
"data")
198 json = params.get(
"json")
199 if json
and params[
"headers"][
"Content-Type"] ==
"application/json":
200 for k, v
in json.items():
203 params[
"json"] = json
207 return params.get(
"query")
210 path = params.get(
"path",
"")
211 params[
"path"] = self.
path + path
212 return params[
"path"]
215 path = params.get(
"path",
"")
216 version = params.get(
"version", self.
version)
217 return self.
url +
"/" + version +
"/" + path
221 """ Web API wrapper for obtaining device-level information """ 222 _skill_settings_lock = Lock()
223 _skill_settings =
None 226 super(DeviceApi, self).
__init__(
"device")
229 IdentityManager.update()
231 "path":
"/code?state=" + state
235 version = VersionManager.get()
240 config = Configuration.get([SYSTEM_CONFIG,
243 if "enclosure" in config:
244 platform = config.get(
"enclosure").
get(
"platform",
"unknown")
245 platform_build = config.get(
"enclosure").
get(
"platform_build",
"")
250 "json": {
"state": state,
252 "coreVersion": version.get(
"coreVersion"),
253 "platform": platform,
254 "platform_build": platform_build,
255 "enclosureVersion": version.get(
"enclosureVersion")}
259 version = VersionManager.get()
264 config = Configuration.get([SYSTEM_CONFIG,
267 if "enclosure" in config:
268 platform = config.get(
"enclosure").
get(
"platform",
"unknown")
269 platform_build = config.get(
"enclosure").
get(
"platform_build",
"")
273 "path":
"/" + self.identity.uuid,
274 "json": {
"coreVersion": version.get(
"coreVersion"),
275 "platform": platform,
276 "platform_build": platform_build,
277 "enclosureVersion": version.get(
"enclosureVersion")}
283 "path":
"/" + self.identity.uuid +
"/message",
284 "json": {
"title": title,
"body": body,
"sender": sender}
290 "path":
"/" + self.identity.uuid +
"/metric/" + name,
295 """ Retrieve all device information from the web backend """ 297 "path":
"/" + self.identity.uuid
301 """ Retrieve device settings information from the web backend 304 str: JSON string with user configuration information. 307 "path":
"/" + self.identity.uuid +
"/setting" 311 """ Retrieve device location information from the web backend 314 str: JSON string with user location. 317 "path":
"/" + self.identity.uuid +
"/location" 322 Get information about type of subscrition this unit is connected 325 Returns: dictionary with subscription information 328 'path':
'/' + self.identity.uuid +
'/subscription'})
333 status of subscription. True if device is connected to a paying 344 archs = {
'x86_64':
'x86_64',
'armv7l':
'arm',
'aarch64':
'arm'}
347 path =
'/' + self.identity.uuid +
'/voice?arch=' + arch
348 return self.
request({
'path': path})[
'link']
352 Get Oauth token for dev_credential dev_cred. 355 dev_cred: development credentials identifier 358 json string containing token and additional information 362 "path":
"/" + self.identity.uuid +
"/token/" + str(dev_cred)
366 """ Fetch all skill settings. """ 367 with DeviceApi._skill_settings_lock:
368 if (DeviceApi._skill_settings
is None or 369 time.monotonic() > DeviceApi._skill_settings[0] + 30):
370 DeviceApi._skill_settings = (
374 "path":
"/" + self.identity.uuid +
"/skill" 377 return DeviceApi._skill_settings[1]
380 """ Upload skill metadata. 383 settings_meta (dict): settings_meta typecasted to suite the backend 387 "path":
"/" + self.identity.uuid +
"/skill",
388 "json": settings_meta
392 """ Delete the current skill metadata from backend 394 TODO: Real implementation when method exists on backend 396 uuid (str): unique id of the skill 399 LOG.debug(
"Deleting remote metadata for {}".format(skill_gid))
402 "path": (
"/" + self.identity.uuid +
"/skill" +
403 "/{}".format(skill_gid))
405 except Exception
as e:
406 LOG.error(
"{} cannot delete metadata because this".format(e))
409 """ Upload skills.json file. This file contains a manifest of installed 410 and failed installations for use with the Marketplace. 413 data: dictionary with skills data from msm 415 if not isinstance(data, dict):
416 raise ValueError(
'data must be of type dict')
420 if 'blacklist' in data:
421 to_send[
'blacklist'] = data[
'blacklist']
423 LOG.warning(
'skills manifest lacks blacklist entry')
424 to_send[
'blacklist'] = []
428 skills = {s[
'name']: s
for s
in data[
'skills']}
429 to_send[
'skills'] = [skills[key]
for key
in skills]
431 LOG.warning(
'skills manifest lacks skills entry')
432 to_send[
'skills'] = []
435 for s
in to_send[
'skills']:
436 s[
'skill_gid'] = s.get(
'skill_gid',
'').replace(
437 '@|',
'@{}|'.format(self.identity.uuid))
441 "path":
"/" + self.identity.uuid +
"/skillJson",
447 """ Web API wrapper for performing Speech to Text (STT) """ 452 def stt(self, audio, language, limit):
453 """ Web API wrapper for performing Speech to Text (STT) 456 audio (bytes): The recorded audio, as in a FLAC file 457 language (str): A BCP-47 language code, e.g. "en-US" 458 limit (int): Maximum minutes to transcribe(?) 461 str: JSON structure with transcription results 466 "headers": {
"Content-Type":
"audio/x-flac"},
467 "query": {
"lang": language,
"limit": limit},
473 """ Determine if this device has ever been paired with a web backend 476 bool: True if ever paired with backend (not factory reset) 480 id = IdentityManager.load()
481 return id.uuid
is not None and id.uuid !=
"" 485 """ Determine if this device is actively paired with a web backend 487 Determines if the installation of Mycroft has been paired by the user 488 with the backend system, and if that pairing is still active. 491 bool: True if paired with backend 503 _paired_cache = api.identity.uuid
is not None and \
504 api.identity.uuid !=
"" 506 except HTTPError
as e:
507 if e.response.status_code == 401:
509 except Exception
as e:
510 LOG.warning(
'Could not get device info: ' + repr(e))
def get_subscription(self)
def build_url(self, params)
def stt(self, audio, language, limit)
def build_headers(self, params)
def get_subscriber_voice_url(self, voice=None)
def upload_skills_data(self, data)
def delete_skill_metadata(self, uuid)
def is_paired(ignore_errors=True)
def get_code(self, state)
def add_content_type(self, headers)
def get_data(self, response)
def build_json(self, params)
dictionary params_to_etag
def build_query(self, params)
def send_email(self, title, body, sender)
def request(self, params)
def build_path(self, params)
def build_data(self, params)
dictionary etag_to_response
def get_response(self, response, no_refresh=False)
def activate(self, state, token)
def get_skill_settings(self)
def report_metric(self, name, data)
def add_authorization(self, headers)
def send(self, params, no_refresh=False)
def upload_skill_metadata(self, settings_meta)
def get_oauth_token(self, dev_cred)