Better logging when connecting to resources; Ability to specify timeout when connecting to resources; Update connect_to_resource test to be a bit more generic

This commit is contained in:
Michael Shepanski 2017-04-23 22:59:22 -04:00
parent 379b8ff63b
commit be2142f7ed
4 changed files with 74 additions and 66 deletions

View file

@ -25,6 +25,7 @@ class PlexClient(PlexObject):
baseurl (str): HTTP URL to connect dirrectly to this client.
token (str): X-Plex-Token used for authenication (optional).
session (:class:`~requests.Session`): requests.Session object if you want more control (optional).
timeout (int): timeout in seconds on initial connect to client (default config.TIMEOUT).
Attributes:
TAG (str): 'Player'
@ -54,7 +55,7 @@ class PlexClient(PlexObject):
TAG = 'Player'
key = '/resources'
def __init__(self, server=None, data=None, initpath=None, baseurl=None, token=None, session=None):
def __init__(self, server=None, data=None, initpath=None, baseurl=None, token=None, session=None, timeout=None):
super(PlexClient, self).__init__(server, data, initpath)
self._baseurl = baseurl.strip('/') if baseurl else None
self._token = logfilter.add_secret(token)
@ -66,9 +67,9 @@ class PlexClient(PlexObject):
self._baseurl = CONFIG.get('auth.client_baseurl', 'http://localhost:32433')
self._token = logfilter.add_secret(CONFIG.get('auth.client_token'))
if self._baseurl and self._token:
self.connect()
self.connect(timeout=timeout)
def connect(self):
def connect(self, timeout=None):
""" Alias of reload as any subsequent requests to this client will be
made directly to the device even if the object attributes were initially
populated from a PlexServer.
@ -76,7 +77,7 @@ class PlexClient(PlexObject):
if not self.key:
raise Unsupported('Cannot reload an object not built from a URL.')
self._initpath = self.key
data = self.query(self.key)
data = self.query(self.key, timeout=timeout)
self._loadData(data[0])
return self
@ -125,16 +126,17 @@ class PlexClient(PlexObject):
raise Unsupported('Cannot use client proxy with unknown server.')
self._proxyThroughServer = value
def query(self, path, method=None, headers=None, **kwargs):
def query(self, path, method=None, headers=None, timeout=None, **kwargs):
""" Main method used to handle HTTPS requests to the Plex client. This method helps
by encoding the response to utf-8 and parsing the returned XML into and
ElementTree object. Returns None if no data exists in the response.
"""
url = self.url(path)
method = method or self._session.get
timeout = timeout or TIMEOUT
log.debug('%s %s', method.__name__.upper(), url)
headers = self._headers(**headers or {})
response = method(url, headers=headers, timeout=TIMEOUT, **kwargs)
response = method(url, headers=headers, timeout=timeout, **kwargs)
if response.status_code not in (200, 201):
codename = codes.get(response.status_code)[0]
log.warn('BadRequest (%s) %s %s' % (response.status_code, codename, response.url))

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import copy
import requests
import time
from requests.status_codes import _codes as codes
from plexapi import BASE_HEADERS, CONFIG, TIMEOUT
from plexapi import log, logfilter, utils
@ -17,6 +18,13 @@ class MyPlexAccount(PlexObject):
with your username and password. This object represents the data found Account on
the myplex.tv servers at the url https://plex.tv/users/account.
Parameters:
username (str): Your MyPlex username.
password (str): Your MyPlex password.
session (requests.Session, optional): Use your own session object if you want to
cache the http responses from PMS
timeout (int): timeout in seconds on initial connect to myplex (default config.TIMEOUT).
Attributes:
SIGNIN (str): 'https://my.plexapp.com/users/sign_in.xml'
key (str): 'https://plex.tv/users/account'
@ -51,12 +59,12 @@ class MyPlexAccount(PlexObject):
WEBHOOKS = 'https://plex.tv/api/v2/user/webhooks'
key = 'https://plex.tv/users/account'
def __init__(self, username=None, password=None, session=None):
def __init__(self, username=None, password=None, session=None, timeout=None):
self._session = session or requests.Session()
self._token = None
username = username or CONFIG.get('auth.myplex_username')
password = password or CONFIG.get('auth.myplex_password')
data = self.query(self.SIGNIN, method=self._session.post, auth=(username, password))
data = self.query(self.SIGNIN, method=self._session.post, auth=(username, password), timeout=timeout)
super(MyPlexAccount, self).__init__(self, data, self.SIGNIN)
def _loadData(self, data):
@ -108,14 +116,15 @@ class MyPlexAccount(PlexObject):
data = self.query(MyPlexDevice.key)
return [MyPlexDevice(self, elem) for elem in data]
def query(self, url, method=None, headers=None, **kwargs):
def query(self, url, method=None, headers=None, timeout=None, **kwargs):
method = method or self._session.get
delim = '&' if '?' in url else '?'
url = '%s%sX-Plex-Token=%s' % (url, delim, self._token)
timeout = timeout or TIMEOUT
log.debug('%s %s', method.__name__.upper(), url)
allheaders = BASE_HEADERS.copy()
allheaders.update(headers or {})
response = method(url, headers=allheaders, timeout=TIMEOUT, **kwargs)
response = method(url, headers=allheaders, timeout=timeout, **kwargs)
if response.status_code not in (200, 201):
codename = codes.get(response.status_code)[0]
log.warn('BadRequest (%s) %s %s' % (response.status_code, codename, response.url))
@ -283,7 +292,7 @@ class MyPlexResource(PlexObject):
self.presence = utils.cast(bool, data.attrib.get('presence'))
self.connections = self.findItems(data, ResourceConnection)
def connect(self, ssl=None):
def connect(self, ssl=None, timeout=None):
""" Returns a new :class:`~server.PlexServer` object. Often times there is more than
one address specified for a server or client. This function will prioritize local
connections before remote and HTTPS before HTTP. After trying to connect to all
@ -309,26 +318,10 @@ class MyPlexResource(PlexObject):
else: connections = https + http
# Try connecting to all known resource connections in parellel, but
# only return the first server (in order) that provides a response.
listargs = [[c] for c in connections]
results = utils.threaded(self._connect, listargs)
# At this point we have a list of result tuples containing (url, token, PlexServer)
# or (url, token, None) in the case a connection could not be
# established.
for url, token, result in results:
okerr = 'OK' if result else 'ERR'
log.info('Testing resource connection: %s?X-Plex-Token=%s %s', url, token, okerr)
results = [r[2] for r in results if r and r[2] is not None]
if not results:
raise NotFound('Unable to connect to resource: %s' % self.name)
log.info('Connecting to server: %s?X-Plex-Token=%s', results[0]._baseurl, results[0]._token)
return results[0]
def _connect(self, url, results, i):
try:
results[i] = (url, self.accessToken, PlexServer(url, self.accessToken))
except Exception as err:
log.error('%s: %s', url, err)
results[i] = (url, self.accessToken, None)
listargs = [[PlexServer, c, self.accessToken, timeout] for c in connections]
log.info('Testing %s resource connections..', len(listargs))
results = utils.threaded(_connect, listargs)
return _chooseConnection('Resource', self.name, results)
class ResourceConnection(PlexObject):
@ -409,38 +402,50 @@ class MyPlexDevice(PlexObject):
self.lastSeenAt = utils.toDatetime(data.attrib.get('lastSeenAt'))
self.connections = [connection.attrib.get('uri') for connection in data.iter('Connection')]
def connect(self):
def connect(self, timeout=None):
""" Returns a new :class:`~plexapi.client.PlexClient` object. Sometimes there is more than
one address specified for a server or client. After trying to connect to all
available addresses for this client and assuming at least one connection was
successful, the PlexClient object is built and returned.
one address specified for a server or client. After trying to connect to all available
addresses for this client and assuming at least one connection was successful, the
PlexClient object is built and returned.
Raises:
:class:`~plexapi.exceptions.NotFound`: When unable to connect to any addresses for this device.
"""
# Try connecting to all known clients in parellel, but
# only return the first server (in order) that provides a response.
listargs = [[c] for c in self.connections]
results = utils.threaded(self._connect, listargs)
# At this point we have a list of result tuples containing (url, token, PlexServer)
# or (url, token, None) in the case a connection could not be established.
for url, token, result in results:
okerr = 'OK' if result else 'ERR'
log.info('Testing device connection: %s?X-Plex-Token=%s %s', url, token, okerr)
results = [r[2] for r in results if r and r[2] is not None]
if not results:
raise NotFound('Unable to connect to client: %s' % self.name)
log.info('Connecting to client: %s?X-Plex-Token=%s', results[0]._baseurl, results[0]._token)
return results[0]
def _connect(self, url, results, i):
try:
results[i] = (url, self.token, PlexClient(baseurl=url, token=self.token))
except Exception as err:
log.error('%s: %s', url, err)
results[i] = (url, self.token, None)
listargs = [[PlexClient, c, self.token, timeout] for c in self.connections]
log.info('Testing %s device connections..', len(listargs))
results = utils.threaded(_connect, listargs)
_chooseConnection('Device', self.name, results)
def delete(self):
""" Remove this device from your account. """
key = 'https://plex.tv/devices/%s.xml' % self.id
self._server.query(key, self._server._session.delete)
def _connect(cls, url, token, timeout, results, i):
""" Connects to the specified cls with url and token. Stores the connection
information to results[i] in a threadsafe way.
"""
starttime = time.time()
try:
device = cls(baseurl=url, token=token, timeout=timeout)
runtime = int(time.time() - starttime)
results[i] = (url, token, device, runtime)
except Exception as err:
runtime = int(time.time() - starttime)
log.error('%s: %s', url, err)
results[i] = (url, token, None, runtime)
def _chooseConnection(ctype, name, results):
""" Chooses the first (best) connection from the given _connect results. """
# At this point we have a list of result tuples containing (url, token, PlexServer, runtime)
# or (url, token, None, runtime) in the case a connection could not be established.
for url, token, result, runtime in results:
okerr = 'OK' if result else 'ERR'
log.info('%s connection %s (%ss): %s?X-Plex-Token=%s', ctype, okerr, runtime, url, token)
results = [r[2] for r in results if r and r[2] is not None]
if results:
log.info('Connecting to %s: %s?X-Plex-Token=%s', ctype, results[0]._baseurl, results[0]._token)
return results[0]
raise NotFound('Unable to connect to %s: %s' % (ctype.lower(), name))

View file

@ -31,6 +31,7 @@ class PlexServer(PlexObject):
token (str): Required Plex authentication token to access the server.
session (requests.Session, optional): Use your own session object if you want to
cache the http responses from PMS
timeout (int): timeout in seconds on initial connect to server (default config.TIMEOUT).
Attributes:
allowCameraUpload (bool): True if server allows camera upload.
@ -90,13 +91,14 @@ class PlexServer(PlexObject):
"""
key = '/'
def __init__(self, baseurl=None, token=None, session=None):
def __init__(self, baseurl=None, token=None, session=None, timeout=None):
self._baseurl = baseurl or CONFIG.get('auth.server_baseurl', 'http://localhost:32400')
self._token = logfilter.add_secret(token or CONFIG.get('auth.server_token'))
self._session = session or requests.Session()
self._library = None # cached library
self._settings = None # cached settings
super(PlexServer, self).__init__(self, self.query(self.key), self.key)
data = self.query(self.key, timeout=timeout)
super(PlexServer, self).__init__(self, data, self.key)
def _loadData(self, data):
""" Load attribute values from Plex XML response. """
@ -260,17 +262,17 @@ class PlexServer(PlexObject):
"""
return self.fetchItem('/playlists', title=title)
def query(self, key, method=None, headers=None, **kwargs):
def query(self, key, method=None, headers=None, timeout=None, **kwargs):
""" Main method used to handle HTTPS requests to the Plex server. This method helps
by encoding the response to utf-8 and parsing the returned XML into and
ElementTree object. Returns None if no data exists in the response.
"""
#url = key if key.startswith('http') else self.url(key)
url = self.url(key)
method = method or self._session.get
timeout = timeout or TIMEOUT
log.debug('%s %s', method.__name__.upper(), url)
headers = self._headers(**headers or {})
response = method(url, headers=headers, timeout=TIMEOUT, **kwargs)
response = method(url, headers=headers, timeout=timeout, **kwargs)
if response.status_code not in (200, 201):
codename = codes.get(response.status_code)[0]
log.warn('BadRequest (%s) %s %s' % (response.status_code, codename, response.url))

View file

@ -34,13 +34,12 @@ def test_myplex_resources(account):
assert resources, 'No resources found for account: %s' % account.name
def test_myplex_connect_to_resource(account):
def test_myplex_connect_to_resource(plex, account):
servername = plex.friendlyName
for resource in account.resources():
if resource.name == 'PMS_API_TEST_SERVER':
if resource.name == servername:
break
server = resource.connect()
assert 'Ohno' in server.url('Ohno')
assert server
assert resource.connect(timeout=10)
def test_myplex_devices(account):