diff --git a/development.ini b/development.ini index e69facb..db15144 100644 --- a/development.ini +++ b/development.ini @@ -10,6 +10,7 @@ pyramid.includes = pyramid_filterwarnings pyramid_tm pyramid_jinja2 + pyramid_rpc.xmlrpc pyshop # pyramid_debugtoolbar # this plugin is not configurable @@ -30,12 +31,6 @@ sqlalchemy.echo = 0 tm.commit_veto = pyramid_tm.default_commit_veto -# waiting for https://github.com/Pylons/pyramid_xmlrpc/pull/2 -pyshop.enable_xmlrpc = 0 -xmlrpc.encoding = utf-8 -xmlrpc.allow_none = True -xmlrpc.datetime = True - # pyshop options # AuthTktAuthenticationPolicy diff --git a/development.mysql.ini b/development.mysql.ini index b48bb1e..0c37d46 100644 --- a/development.mysql.ini +++ b/development.mysql.ini @@ -10,6 +10,7 @@ pyramid.includes = pyramid_filterwarnings pyramid_tm pyramid_jinja2 + pyramid_rpc.xmlrpc pyshop # pyramid_debugtoolbar # this plugin is not configurable @@ -30,12 +31,6 @@ sqlalchemy.echo = 0 tm.commit_veto = pyramid_tm.default_commit_veto -# waiting for https://github.com/Pylons/pyramid_xmlrpc/pull/2 -pyshop.enable_xmlrpc = 0 -xmlrpc.encoding = utf-8 -xmlrpc.allow_none = True -xmlrpc.datetime = True - # pyshop options # AuthTktAuthenticationPolicy diff --git a/development.pg.ini b/development.pg.ini index d5abce3..1cd8254 100644 --- a/development.pg.ini +++ b/development.pg.ini @@ -10,7 +10,7 @@ pyramid.includes = pyramid_filterwarnings pyramid_tm pyramid_jinja2 - pyramid_xmlrpc + pyramid_rpc.xmlrpc pyshop filterwarnings.action = ignore @@ -27,12 +27,6 @@ sqlalchemy.echo = 0 tm.commit_veto = pyramid_tm.default_commit_veto -# waiting for https://github.com/Pylons/pyramid_xmlrpc/pull/2 -pyshop.enable_xmlrpc = 1 -xmlrpc.encoding = utf-8 -xmlrpc.allow_none = True -xmlrpc.datetime = True - # pyshop options # AuthTktAuthenticationPolicy diff --git a/pyshop.sample.ini b/pyshop.sample.ini index 5645b6d..377aaad 100644 --- a/pyshop.sample.ini +++ b/pyshop.sample.ini @@ -10,11 +10,8 @@ pyramid.includes = pyramid_filterwarnings pyramid_tm pyramid_jinja2 + pyramid_rpc.xmlrpc pyshop -# pyramid_debugtoolbar -# this plugin is not configurable -# waiting for a pull request on gihub -# pyramid_xmlrpc filterwarnings.action = ignore @@ -26,11 +23,6 @@ sqlalchemy.echo = 0 tm.commit_veto = pyramid_tm.default_commit_veto -# waiting for https://github.com/Pylons/pyramid_xmlrpc/pull/2 -xmlrpc.encoding = utf-8 -xmlrpc.allow_none = True -xmlrpc.datetime = True - # pyshop options # AuthTktAuthenticationPolicy diff --git a/pyshop/config.py b/pyshop/config.py index 8d5ca5d..db6e7cd 100644 --- a/pyshop/config.py +++ b/pyshop/config.py @@ -3,14 +3,11 @@ PyShop Pyramid configuration helpers. """ -from pyramid.settings import asbool from pyramid.interfaces import IBeforeRender -from pyramid.security import has_permission from pyramid.url import static_path, route_path from pyramid.httpexceptions import HTTPNotFound -# from pyramid.renderers import JSONP - from pyramid_jinja2 import renderer_factory +from pyramid_rpc.xmlrpc import XMLRPCRenderer from pyshop.helpers import pypi from pyshop.helpers.restxt import parse_rest @@ -128,8 +125,10 @@ def includeme(config): # Web Services - if asbool(settings.get('pyshop.enable_xmlrpc', 'true')): - config.add_view('pyshop.views.xmlrpc.PyPI', name='pypi') + config.add_renderer('pyshopxmlrpc', XMLRPCRenderer(allow_none=True)) + config.add_xmlrpc_endpoint( + 'api', '/pypi/xmlrpc', default_renderer='pyshopxmlrpc') + config.scan('pyshop.views.xmlrpc') # Backoffice Views diff --git a/pyshop/tests/case.py b/pyshop/tests/case.py index bd9db60..868ac6c 100644 --- a/pyshop/tests/case.py +++ b/pyshop/tests/case.py @@ -24,6 +24,7 @@ def setUp(self): def tearDown(self): transaction.commit() + class DummyRoute(object): name = 'index' @@ -45,6 +46,7 @@ def setUp(self): self.maxDiff = None authz_policy = ACLAuthorizationPolicy() self.config = testing.setUp(settings=settings) + self.config.include('pyramid_rpc.xmlrpc') self.config.include(includeme) self.session = DBSession() transaction.begin() @@ -56,8 +58,8 @@ def tearDown(self): testing.tearDown() def create_request(self, params=None, environ=None, matchdict=None, - headers=None, - path='/', cookies=None, post=None, **kw): + headers=None, + path='/', cookies=None, post=None, **kw): if params and not isinstance(params, MultiDict): mparams = MultiDict() for k, v in params.items(): @@ -66,8 +68,8 @@ def create_request(self, params=None, environ=None, matchdict=None, else: mparams.add(k, v) params = mparams - rv = DummyRequest(params, environ, headers, path, cookies, - post, matchdict=(matchdict or {}), **kw) + rv = DummyRequest(params, environ, headers, path, cookies, + post, matchdict=(matchdict or {}), **kw) return rv def assertIsRedirect(self, view): diff --git a/pyshop/tests/conf.py b/pyshop/tests/conf.py index 0c7dd13..229fee8 100644 --- a/pyshop/tests/conf.py +++ b/pyshop/tests/conf.py @@ -4,7 +4,6 @@ 'sqlalchemy.url': 'sqlite://', 'sqlalchemy.echo': False, 'sqlalchemy.pool_size': 1, - 'pyshop.enable_xmlrpc': False, 'pyshop.upload.sanitize': False, 'pyshop.mirror.sanitize': False, 'pyshop.pypi.url': 'http://localhost:65432', diff --git a/pyshop/views/xmlrpc.py b/pyshop/views/xmlrpc.py index 0cf8135..844dff2 100644 --- a/pyshop/views/xmlrpc.py +++ b/pyshop/views/xmlrpc.py @@ -7,10 +7,7 @@ """ import logging -try: - from pyramid_xmlrpc import XMLRPCView -except (ModuleNotFoundError, ImportError): - XMLRPCView = object +from pyramid_rpc.xmlrpc import xmlrpc_method from pyshop.models import DBSession, Package, Release, ReleaseFile from pyshop.helpers import pypi @@ -18,220 +15,233 @@ log = logging.getLogger(__name__) -# XXX not tested. - -class PyPI(XMLRPCView): - - def list_packages(self): - """ - Retrieve a list of the package names registered with the package index. - Returns a list of name strings. - """ - session = DBSession() - names = [p.name for p in Package.all(session, order_by=Package.name)] - return names - - def package_releases(self, package_name, show_hidden=False): - """ - Retrieve a list of the releases registered for the given package_name. - Returns a list with all version strings if show_hidden is True or - only the non-hidden ones otherwise.""" - session = DBSession() - package = Package.by_name(session, package_name) - return [rel.version for rel in package.sorted_releases] - - def package_roles(self, package_name): - """ - Retrieve a list of users and their attributes roles for a given - package_name. Role is either 'Maintainer' or 'Owner'. - """ - session = DBSession() - package = Package.by_name(session, package_name) - owners = [('Owner', o.name) for o in package.owners] - maintainers = [('Maintainer', o.name) for o in package.maintainers] - return owners + maintainers - - def user_packages(self, user): - """ - Retrieve a list of [role_name, package_name] for a given username. - Role is either 'Maintainer' or 'Owner'. - """ - session = DBSession() - owned = Package.by_owner(session, user) - maintained = Package.by_maintainer(session, user) - owned = [('Owner', p.name) for p in owned] - maintained = [('Maintainer', p.name) for p in maintained] - return owned + maintained - - def release_downloads(self, package_name, version): - """ - Retrieve a list of files and download count for a given package and - release version. - """ - session = DBSession() - release_files = ReleaseFile.by_release(session, package_name, version) - if release_files: - release_files = [(f.release.package.name, - f.filename) for f in release_files] - return release_files - - def release_urls(self, package_name, version): - """ - Retrieve a list of download URLs for the given package release. - Returns a list of dicts with the following keys: - url - packagetype ('sdist', 'bdist', etc) - filename - size - md5_digest - downloads - has_sig - python_version (required version, or 'source', or 'any') - comment_text - """ - session = DBSession() - release_files = ReleaseFile.by_release(session, package_name, version) - return [{'url': f.url, - 'packagetype': f.package_type, - 'filename': f.filename, - 'size': f.size, - 'md5_digest': f.md5_digest, - 'downloads': f.downloads, - 'has_sig': f.has_sig, - 'comment_text': f.comment_text, - 'python_version': f.python_version - } - for f in release_files] - - def release_data(self, package_name, version): - """ - Retrieve metadata describing a specific package release. - Returns a dict with keys for: - name - version - stable_version - author - author_email - maintainer - maintainer_email - home_page - license - summary - description - keywords - platform - download_url - classifiers (list of classifier strings) - requires - requires_dist - provides - provides_dist - requires_external - requires_python - obsoletes - obsoletes_dist - project_url - docs_url (URL of the packages.python.org docs - if they've been supplied) - If the release does not exist, an empty dictionary is returned. - """ - session = DBSession() - release = Release.by_version(session, package_name, version) - - if release: - result = {'name': release.package.name, - 'version': release.version, - 'stable_version': '', - 'author': release.author.name, - 'author_email': release.author.email, - 'home_page': release.home_page, - 'license': release.license, - 'summary': release.summary, - 'description': release.description, - 'keywords': release.keywords, - 'platform': release.platform, - 'download_url': release.download_url, - 'classifiers': [c.name for c in release.classifiers], - #'requires': '', - #'requires_dist': '', - #'provides': '', - #'provides_dist': '', - #'requires_external': '', - #'requires_python': '', - #'obsoletes': '', - #'obsoletes_dist': '', - 'bugtrack_url': release.bugtrack_url, - 'docs_url': release.docs_url, - } - - if release.maintainer: - result.update({'maintainer': release.maintainer.name, - 'maintainer_email': release.maintainer.email, - }) - - return dict([(key, val or '') for key, val in result.items()]) - - def search(self, spec, operator='and'): - """ - Search the package database using the indicated search spec. - - The spec may include any of the keywords described in the above list - (except 'stable_version' and 'classifiers'), - for example: {'description': 'spam'} will search description fields. - Within the spec, a field's value can be a string or a list of strings - (the values within the list are combined with an OR), - for example: {'name': ['foo', 'bar']}. - Valid keys for the spec dict are listed here. Invalid keys are ignored: - name - version - author - author_email - maintainer - maintainer_email - home_page - license - summary - description - keywords - platform - download_url - Arguments for different fields are combined using either "and" - (the default) or "or". - Example: search({'name': 'foo', 'description': 'bar'}, 'or'). - The results are returned as a list of dicts - {'name': package name, - 'version': package release version, - 'summary': package release summary} - """ - api = pypi.proxy - rv = [] - # search in proxy - for k, v in spec.items(): - rv += api.search({k: v}, True) - - # search in local - session = DBSession() - release = Release.search(session, spec, operator) - rv += [{'name': r.package.name, - 'version': r.version, - 'summary': r.summary, - # hack https://mail.python.org/pipermail/catalog-sig/2012-October/004633.html - '_pypi_ordering':'', - } for r in release] - return rv - - def browse(self, classifiers): - """ - Retrieve a list of (name, version) pairs of all releases classified - with all of the given classifiers. 'classifiers' must be a list of - Trove classifier strings. - - changelog(since) - Retrieve a list of four-tuples (name, version, timestamp, action) - since the given timestamp. All timestamps are UTC values. - The argument is a UTC integer seconds since the epoch. - """ - session = DBSession() - release = Release.by_classifiers(session, classifiers) - rv = [(r.package.name, r.version) for r in release] - return rv +@xmlrpc_method(endpoint='api') +def list_packages(request): + """ + Retrieve a list of the package names registered with the package index. + Returns a list of name strings. + """ + session = DBSession() + names = [p.name for p in Package.all(session, order_by=Package.name)] + return names + + +@xmlrpc_method(endpoint='api') +def package_releases(request, package_name, show_hidden=False): + """ + Retrieve a list of the releases registered for the given package_name. + Returns a list with all version strings if show_hidden is True or + only the non-hidden ones otherwise.""" + session = DBSession() + package = Package.by_name(session, package_name) + return [rel.version for rel in package.sorted_releases] + + +@xmlrpc_method(endpoint='api') +def package_roles(request, package_name): + """ + Retrieve a list of users and their attributes roles for a given + package_name. Role is either 'Maintainer' or 'Owner'. + """ + session = DBSession() + package = Package.by_name(session, package_name) + owners = [('Owner', o.name) for o in package.owners] + maintainers = [('Maintainer', o.name) for o in package.maintainers] + return owners + maintainers + + +@xmlrpc_method(endpoint='api') +def user_packages(request, user): + """ + Retrieve a list of [role_name, package_name] for a given username. + Role is either 'Maintainer' or 'Owner'. + """ + session = DBSession() + owned = Package.by_owner(session, user) + maintained = Package.by_maintainer(session, user) + owned = [('Owner', p.name) for p in owned] + maintained = [('Maintainer', p.name) for p in maintained] + return owned + maintained + + +@xmlrpc_method(endpoint='api') +def release_downloads(request, package_name, version): + """ + Retrieve a list of files and download count for a given package and + release version. + """ + session = DBSession() + release_files = ReleaseFile.by_release(session, package_name, version) + if release_files: + release_files = [(f.release.package.name, + f.filename) for f in release_files] + return release_files + + +@xmlrpc_method(endpoint='api') +def release_urls(request, package_name, version): + """ + Retrieve a list of download URLs for the given package release. + Returns a list of dicts with the following keys: + url + packagetype ('sdist', 'bdist', etc) + filename + size + md5_digest + downloads + has_sig + python_version (required version, or 'source', or 'any') + comment_text + """ + session = DBSession() + release_files = ReleaseFile.by_release(session, package_name, version) + return [{'url': f.url, + 'packagetype': f.package_type, + 'filename': f.filename, + 'size': f.size, + 'md5_digest': f.md5_digest, + 'downloads': f.downloads, + 'has_sig': f.has_sig, + 'comment_text': f.comment_text, + 'python_version': f.python_version + } + for f in release_files] + + +@xmlrpc_method(endpoint='api') +def release_data(request, package_name, version): + """ + Retrieve metadata describing a specific package release. + Returns a dict with keys for: + name + version + stable_version + author + author_email + maintainer + maintainer_email + home_page + license + summary + description + keywords + platform + download_url + classifiers (list of classifier strings) + requires + requires_dist + provides + provides_dist + requires_external + requires_python + obsoletes + obsoletes_dist + project_url + docs_url (URL of the packages.python.org docs + if they've been supplied) + If the release does not exist, an empty dictionary is returned. + """ + session = DBSession() + release = Release.by_version(session, package_name, version) + + if release: + result = {'name': release.package.name, + 'version': release.version, + 'stable_version': '', + 'author': release.author.name, + 'author_email': release.author.email, + 'home_page': release.home_page, + 'license': release.license, + 'summary': release.summary, + 'description': release.description, + 'keywords': release.keywords, + 'platform': release.platform, + 'download_url': release.download_url, + 'classifiers': [c.name for c in release.classifiers], + #'requires': '', + #'requires_dist': '', + #'provides': '', + #'provides_dist': '', + #'requires_external': '', + #'requires_python': '', + #'obsoletes': '', + #'obsoletes_dist': '', + 'bugtrack_url': release.bugtrack_url, + 'docs_url': release.docs_url, + } + + if release.maintainer: + result.update({'maintainer': release.maintainer.name, + 'maintainer_email': release.maintainer.email, + }) + + return dict([(key, val or '') for key, val in result.items()]) + + +@xmlrpc_method(endpoint='api') +def search(request, spec, operator='and'): + """ + Search the package database using the indicated search spec. + + The spec may include any of the keywords described in the above list + (except 'stable_version' and 'classifiers'), + for example: {'description': 'spam'} will search description fields. + Within the spec, a field's value can be a string or a list of strings + (the values within the list are combined with an OR), + for example: {'name': ['foo', 'bar']}. + Valid keys for the spec dict are listed here. Invalid keys are ignored: + name + version + author + author_email + maintainer + maintainer_email + home_page + license + summary + description + keywords + platform + download_url + Arguments for different fields are combined using either "and" + (the default) or "or". + Example: search({'name': 'foo', 'description': 'bar'}, 'or'). + The results are returned as a list of dicts + {'name': package name, + 'version': package release version, + 'summary': package release summary} + """ + api = pypi.proxy + rv = [] + # search in proxy + for k, v in spec.items(): + rv += api.search({k: v}, True) + + # search in local + session = DBSession() + release = Release.search(session, spec, operator) + rv += [{'name': r.package.name, + 'version': r.version, + 'summary': r.summary, + # hack https://mail.python.org/pipermail/catalog-sig/2012-October/004633.html + '_pypi_ordering':'', + } for r in release] + return rv + + +@xmlrpc_method(endpoint='api') +def browse(request, classifiers): + """ + Retrieve a list of (name, version) pairs of all releases classified + with all of the given classifiers. 'classifiers' must be a list of + Trove classifier strings. + + changelog(since) + Retrieve a list of four-tuples (name, version, timestamp, action) + since the given timestamp. All timestamps are UTC values. + The argument is a UTC integer seconds since the epoch. + """ + session = DBSession() + release = Release.by_classifiers(session, classifiers) + rv = [(r.package.name, r.version) for r in release] + return rv diff --git a/setup.py b/setup.py index 1a0ffea..5476cce 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ 'pyramid_filterwarnings', 'pyramid_jinja2', - 'pyramid_xmlrpc', + 'pyramid_rpc', 'pyramid_tm', 'zope.sqlalchemy',