New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refactor the webservice code #753
Conversation
1bf7eb3
to
1641d8c
Compare
@samj1912 Are you planning a parallel PR to change any plugins which use webservices? |
@Sophist-UK Yeah, I will be porting the plugins completely to be Picard 2.0 compatible. |
picard/webservice/__init__.py
Outdated
@@ -150,6 +79,8 @@ def __init__(self, parent=None): | |||
} | |||
self._init_queues() | |||
self._init_timers() | |||
self.format_parser = None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the default parser should be a function that raises an exception (because if it's called, you've passed parse_format=True
to some function on an object that's not configured for that).
(Nitpicking:) I also find the name format
in this context not good, it actually refers to the either the content type or (in the case of the parse_format
argument) the body of the response. Maybe just parser
and parse_response
or parse_response_body
are better?
picard/webservice/__init__.py
Outdated
|
||
|
||
class XmlWebService(QtCore.QObject): | ||
class WebService(QtCore.QObject): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it a good idea to provide multiple WebService implementations? Each one would have its own set of queues, which I do find a bit weird...
picard/webservice/api_helpers.py
Outdated
queryargs=queryargs) | ||
|
||
|
||
class MBAPIHelper(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not make this one and the acoustid one simply inherit from APIHelper?
@mineo changes done |
picard/webservice/__init__.py
Outdated
response_parser = self.response_parser(parse_response_format) | ||
try: | ||
document = response_parser(reply) | ||
except Exception as e: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should have a specific exception class here (ExceptionResponseParserUnknown?)
picard/webservice/__init__.py
Outdated
if name in cls.PARSERS: | ||
return cls.PARSERS[name]['parser'] | ||
else: | ||
log.error('Parser of type %s not found', name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Raise ExceptionResponseParserUnknown here
picard/webservice/__init__.py
Outdated
from picard import (PICARD_APP_NAME, | ||
PICARD_ORG_NAME, | ||
PICARD_VERSION_STR, | ||
config, | ||
log) | ||
from picard.const import (ACOUSTID_KEY, | ||
ACOUSTID_HOST, | ||
from picard.const import (ACOUSTID_HOST, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Those are now only used for request delays (REQUEST_DELAY
dict below), i think it can be moved to API helpers as well. Not sure exactly how though ;)
picard/webservice/api_helpers.py
Outdated
CAA_HOST, | ||
CAA_PORT) | ||
|
||
from picard.webservice import PICARD_VERSION_STR, CLIENT_STRING, REQUEST_DELAY |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PICARD_VERSION_STR
should be imported from picard
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
@@ -60,79 +54,16 @@ | |||
PICARD_APP_NAME, | |||
PICARD_VERSION_STR))) | |||
|
|||
DEFAULT_RESPONSE_PARSER = "xml" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have to admit that the name "Parser" always suggests to me that the thing in question itself is able to parse something.
response_parser = self.response_parser(parse_response_format) | ||
try: | ||
document = response_parser(reply) | ||
except UnknownResponseParserError as e: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't it the call to self.response_parser
above that would raise this exception?
|
||
class UnknownResponseParserError(Exception): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would make sense to have this exception carry at least information about which parser is unknown.
if xml: | ||
request.setRawHeader(b"Accept", b"application/xml") | ||
if parse_response_format: | ||
request.setRawHeader(b"Accept", self.response_header(parse_response_format).encode('utf-8')) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For both the Accept and Content-Type headers it's more common to not refer to a format, but MIME or media types (see https://tools.ietf.org/html/rfc7231#section-3.1.1).
|
||
@classmethod | ||
def add_parser(cls, name, header, parser): | ||
cls.PARSERS[name] = {'header': header, 'parser': parser} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For other cases where there's only a small set of dict keys (some in script.py and searchdialog.py) we're using collections.namedtuple.
cls.PARSERS[name] = {'header': header, 'parser': parser} | ||
|
||
@classmethod | ||
def response_header(cls, name): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See above somewhere, this returns the mime type, not a complete header.
host, port = ACOUSTID_HOST, ACOUSTID_PORT | ||
body = self._encode_acoustid_args(args, format='json') | ||
return self.post(host, port, '/v2/submit', body, handler, priority=True, important=False, mblogin=False) | ||
log.error('Parser of type %s not found', name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will implicitly return None if the parser is not found (unlike response_parser
, which raises an exception). Since this method is called earlier during the lifetime of an HTTP request, wouldn't it make sense to signal that error here already with something more than a log message?
'<metadata xmlns="http://musicbrainz.org/ns/mmd-2.0#">%s</metadata>' % data) | ||
|
||
|
||
class APIHelper(object): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Deriving from object is not necessary in Python 3 anymore (but I fail to find the documentation about it everytime I look for it)
import unittest | ||
from picard import config | ||
from picard.webservice import WebService | ||
from mock import patch, MagicMock |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit surprised the test passes on travis, the mock module included with Python 3 is only available as unittest.mock
?
mock_add_task.assert_called() | ||
self.assertIn(host, mock_add_task.call_args[0]) | ||
self.assertIn(port, mock_add_task.call_args[0]) | ||
self.assertIn("'GET'", repr(mock_add_task.call_args)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm 100% sure there's a better way to test this than looking at the repr
of call_args :-) self.assertIn("GET", mock_add_task.call_args[0][0].args)
will do it, for example.
No description provided.