[saem-devel] [PATCH 2 of 2 oaipmh V2] [views] switch to pyramid views

Philippe Pepiot philippe.pepiot at logilab.fr
Wed Feb 22 16:26:10 CET 2017


# HG changeset patch
# User Philippe Pepiot <philippe.pepiot at logilab.fr>
# Date 1487773765 -3600
#      Wed Feb 22 15:29:25 2017 +0100
# Node ID 693a9fa19c80c7497c4fb0a161da689591b028d8
# Parent  b990e45e6bd34cf6c2c442b2f77a893726b96ea5
# Available At https://hg.logilab.org/review/cubes/oaipmh
#              hg pull https://hg.logilab.org/review/cubes/oaipmh -r 693a9fa19c80
# Tested at https://jenkins.logilab.org/job/cubicweb-oaipmh/10/
[views] switch to pyramid views

Use pyramid views instead of cubicweb views.

Use a single predicate to match verb and handle errors in errors(), if errors()
don't return None verb_content() is not called.

Depend on cubicweb[pyramid] >= 3.24.5

Related to extranet #11855076

diff --git a/__init__.py b/__init__.py
--- a/__init__.py
+++ b/__init__.py
@@ -90,3 +90,7 @@ class ResumptionToken(object):
         """Return string encoding state of the resumption token."""
         if self:
             return base64.urlsafe_b64encode(str(self))
+
+
+def includeme(config):
+    config.include('.views')
diff --git a/__pkginfo__.py b/__pkginfo__.py
--- a/__pkginfo__.py
+++ b/__pkginfo__.py
@@ -19,7 +19,7 @@ description = 'OAI-PMH server for CubicW
 web = 'http://www.cubicweb.org/project/%s' % distname
 
 __depends__ = {
-    'cubicweb': '>= 3.22.1',
+    'cubicweb[pyramid]': '>= 3.24.5',
     'six': '>= 1.4.0',
     'python-dateutil': None,
     'isodate': None,
diff --git a/cubicweb-oaipmh.spec b/cubicweb-oaipmh.spec
--- a/cubicweb-oaipmh.spec
+++ b/cubicweb-oaipmh.spec
@@ -20,7 +20,7 @@ BuildArch:      noarch
 BuildRoot:      %{_tmppath}/%{name}-%{version}-%{release}-buildroot
 
 BuildRequires:  %{python} %{python}-setuptools
-Requires:       cubicweb >= 3.22.1
+Requires:       cubicweb >= 3.24.5
 Requires:       %{python}-six >= 1.4.0
 Requires:       python-isodate
 Requires:       python-dateutil
diff --git a/debian/control b/debian/control
--- a/debian/control
+++ b/debian/control
@@ -11,7 +11,7 @@ X-Python-Version: >= 2.6
 Package: cubicweb-oaipmh
 Architecture: all
 Depends:
- cubicweb-common (>= 3.22.1),
+ python-cubicweb (>= 3.24.5),
  python-six (>= 1.4.0),
  python-isodate,
  python-dateutil,
diff --git a/test/test_oaipmh.py b/test/test_oaipmh.py
--- a/test/test_oaipmh.py
+++ b/test/test_oaipmh.py
@@ -30,6 +30,7 @@ from six import text_type
 from logilab.common import tempattr
 
 from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb.pyramid.test import PyramidCWTest
 
 from cubes.oaipmh import utcnow, MetadataFormat, ResumptionToken
 from cubes.oaipmh.entities import OAIComponent, NoRecordsMatch
@@ -261,7 +262,16 @@ def xmlpp(string):
     print(etree.tostring(etree.fromstring(string), pretty_print=True))
 
 
-class OAIPMHViewsTC(CubicWebTC, OAITestMixin):
+class OAIPMHViewsTC(PyramidCWTest, OAITestMixin):
+    settings = {
+        # to get clean traceback in tests (instead of in an html error page)
+        'cubicweb.bwcompat': False,
+        # avoid noise in test output (UserWarning: !! WARNING WARNING !! you
+        # should stop this instance)
+        'cubicweb.session.secret': 'x',
+        'cubicweb.auth.authtkt.session.secret': 'x',
+        'cubicweb.auth.authtkt.persistent.secret': 'x',
+    }
 
     _validate_xml = True
     _debug_xml = True
@@ -278,11 +288,12 @@ class OAIPMHViewsTC(CubicWebTC, OAITestM
         xmlschema.assertValid(root)
 
     def oai_request(self, req, **formparams):
-        req.form.update(formparams)
-        out = self.app_handle_request(req, 'oai')
+        response = self.webapp.get('/oai', formparams)
+        self.assertEqual(response.headers['Content-Type'], 'text/xml; charset=UTF-8')
         if self._validate_xml:
-            self.assertXmlValid(out, self.datapath('OAI-PMH.xsd'), debug=self._debug_xml)
-        return out
+            self.assertXmlValid(response.body, self.datapath('OAI-PMH.xsd'),
+                                debug=self._debug_xml)
+        return response.body
 
     def test_xml_attributes_and_namespaces(self):
         """Check XML attributes and namespace declaration of the response."""
@@ -334,7 +345,7 @@ class OAIPMHViewsTC(CubicWebTC, OAITestM
         with self.admin_access.web_request() as req:
             result = self.oai_request(req, verb='Identify')
         self.assertIn('<repositoryName>unset title</repositoryName>', result)
-        self.assertIn('<baseURL>http://testing.fr/cubicweb/oai</baseURL>', result)
+        self.assertIn('<baseURL>https://localhost.local/oai</baseURL>', result)
         self.assertIn('<protocolVersion>2.0</protocolVersion>', result)
         self.assertIn('<adminEmail>oai-admin at example.org</adminEmail>', result)
         self.assertIn('<deletedRecord>no', result)
@@ -423,10 +434,10 @@ class OAIPMHViewsTC(CubicWebTC, OAITestM
 
         def check_request(expected_identifiers, token=None):
             with self.admin_access.web_request() as req:
+                kwargs = {'resumptionToken': token} if token is not None else {}
                 result = self.oai_request(
                     req, verb='ListIdentifiers', set='agent',
-                    metadataPrefix='oai_dc',
-                    resumptionToken=token)
+                    metadataPrefix='oai_dc', **kwargs)
                 # Ensure there are as many <identifier> tag than expected items.
                 self.assertEqual(
                     result.count('<identifier>'), len(expected_identifiers))
diff --git a/tox.ini b/tox.ini
--- a/tox.ini
+++ b/tox.ini
@@ -5,6 +5,7 @@ envlist = py27,flake8
 sitepackages = true
 deps =
     pytest
+    webtest
 commands =
   {envpython} -m pytest {posargs:{toxinidir}/test}
 
diff --git a/views.py b/views.py
--- a/views.py
+++ b/views.py
@@ -13,7 +13,7 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with this program. If not, see <http://www.gnu.org/licenses/>.
-"""cubicweb-oaipmh views for OAI-PMH export
+"""cubicweb-oaipmh pyramid views for OAI-PMH export
 
 Set hierarchy specification
 ---------------------------
@@ -51,11 +51,8 @@ from lxml import etree
 from lxml.builder import E, ElementMaker
 import pytz
 
-from logilab.common.registry import objectify_predicate
-
-from cubicweb.predicates import ExpectedValuePredicate, match_form_params
-from cubicweb.view import View
-from cubicweb.web.views import urlrewrite
+from pyramid.response import Response
+from pyramid.view import view_config
 
 from cubes.oaipmh import utcnow, OAIError, ResumptionToken
 
@@ -74,20 +71,6 @@ def utcparse(timestr):
         return date.astimezone(pytz.utc)
 
 
-class match_verb(ExpectedValuePredicate):
-    """Predicate checking `verb` request form parameter presence and value.
-
-    Return 2 in case of match, for precedence over
-    ``match_form_params('verb')``.
-    """
-
-    def __call__(self, cls, req, rset=None, **kwargs):
-        verb = req.form.get('verb')
-        if verb is None:
-            return 0
-        return 2 * int(verb in self.expected)
-
-
 def filter_none(mapping):
     """Return a dict from `mapping` with None values filtered out."""
     out = {}
@@ -132,16 +115,15 @@ class OAIRequest(object):
 
     @classmethod
     def from_request(cls, baseurl, request):
-        form = request.form
         return cls(
             baseurl,
-            setspec=form.get('set'),
-            verb=form.get('verb'),
-            identifier=form.get('identifier'),
-            from_date=form.get('from'),
-            until_date=form.get('until'),
-            resumption_token=form.get('resumptionToken'),
-            metadata_prefix=form.get('metadataPrefix'),
+            setspec=request.params.get('set'),
+            verb=request.params.get('verb'),
+            identifier=request.params.get('identifier'),
+            from_date=request.params.get('from'),
+            until_date=request.params.get('until'),
+            resumption_token=request.params.get('resumptionToken'),
+            metadata_prefix=request.params.get('metadataPrefix'),
         )
 
     def __init__(self, baseurl, verb=None, setspec=None, identifier=None,
@@ -347,19 +329,10 @@ class OAIRecord(object):
         return E.record(*elems)
 
 
-class OAIPMHRewriter(urlrewrite.SimpleReqRewriter):
-    rules = [('/oai', dict(vid='oai'))]
-
-
-class OAIView(View):
+class OAIView(object):
     """Base class for any OAI view, subclasses should either implement
     `errors` or `verb_content` methods.
     """
-    __regid__ = 'oai'
-    __abstract__ = True
-    templatable = False
-    content_type = 'text/xml'
-    binary = True
 
     @staticmethod
     def verb_content():
@@ -368,52 +341,52 @@ class OAIView(View):
     @staticmethod
     def errors():
         """Return the errors of the OAI-PMH request."""
-        return {}
+        return
 
-    def __init__(self, *args, **kwargs):
-        super(OAIView, self).__init__(*args, **kwargs)
+    def __init__(self, request):
+        self.request = request
+        self._cw = request.cw_request
+        baseurl = request.cw_cnx.build_url('oai')
         self.oai_request = OAIRequest.from_request(
-            self._cw.build_url('oai'), self._cw)
+            baseurl, request)
 
-    def call(self):
+    def __call__(self):
         encoding = self._cw.encoding
         assert encoding == 'UTF-8', 'unexpected encoding {0}'.format(encoding)
-        self.w('<?xml version="1.0" encoding="%s"?>\n' % encoding)
+        content = '<?xml version="1.0" encoding="%s"?>\n' % encoding
         oai_response = OAIResponse(self.oai_request)
         # combine errors coming from view selection with those of request
         # processing.
-        errors = self.errors()
+        errors = self.errors() or {}
+        verb_content = self.verb_content() if not errors else None
         errors.update(self.oai_request.errors)
-        response_elem = oai_response.to_xml(self.verb_content(), errors=errors)
-        self.w(etree.tostring(response_elem, encoding='utf-8'))
+        response_elem = oai_response.to_xml(verb_content, errors=errors)
+        content += etree.tostring(response_elem, encoding='utf-8')
+        return Response(content, content_type='text/xml')
 
 
-class OAIBaseView(OAIView):
-    """Base view for OAI-PMH request with no "verb" specified.
+ at view_config(route_name='oai')
+class OAIErrorView(OAIView):
+    """Error view for OAI-PMH request with no or bad "verb" specified.
 
     `verb` request parameter is necessary in our implementation.
     """
 
     def errors(self):
-        return {'badVerb': 'no verb specified'}
+        verb = self.request.params.get('verb')
+        if verb:
+            return {'badVerb': 'illegal verb "{0}"'.format(self.oai_request.verb)}
+        else:
+            return {'badVerb': 'no verb specified'}
 
 
-class OAIWithVerbView(OAIView):
-    """Base view for OAI-PMH request with a "verb" specified.
-
-    This view generated an error as the implementation relies on explicit view
-    to handle supported verbs.
-    """
-    __select__ = match_form_params('verb')
+ at view_config(route_name='oai', verb='Identify')
+class OAIIdentifyView(OAIView):
+    """View handling verb="Identify" requests."""
 
     def errors(self):
-        """Return the errors of the OAI-PMH request."""
-        return {'badVerb': 'illegal verb "{0}"'.format(self.oai_request.verb)}
-
-
-class OAIIdentifyView(OAIView):
-    """View handling verb="Identify" requests."""
-    __select__ = match_verb('Identify')
+        if set(self.request.params) - set(['verb', 'vid']):
+            return {'badArgument': 'Identify accepts no argument'}
 
     def verb_content(self):
         oai = self._cw.vreg['components'].select('oai', self._cw)
@@ -435,51 +408,28 @@ class OAIIdentifyView(OAIView):
         yield E('granularity', 'YYYY-MM-DDThh:mm:ssZ')
 
 
- at objectify_predicate
-def no_params_in_form(cls, req, **kwargs):
-    """Return 1 if req.form only has "verb" (and "vid") parameters."""
-    extra = set(req.form) - set(['verb', 'vid'])
-    return 0 if extra else 1
-
+ at view_config(route_name='oai', verb='ListMetadataFormats')
+class OAIListMetadatFormatsByIdentifierView(OAIView):
+    """View handling verb="ListMetadataFormats" requests."""
 
-class OAIIdentifyBadArgumentsView(OAIView):
-    """View handling verb="Identify" requests but with bad arguments"""
-    __select__ = match_verb('Identify') & ~no_params_in_form()
-
-    def errors(self):
-        return {
-            'badArgument': 'Identify accepts no argument'}
+    def verb_content(self):
+        identifier = self.request.params.get('identifier')
+        if identifier:
+            rset = self.oai_request.rset_from_identifier(self._cw)
+            if not rset:
+                raise IdDoesNotExist(self.oai_request.identifier)
+            for record in oai_records(rset):
+                for fmt in record.metadata_formats():
+                    yield fmt
+        else:
+            oai = self._cw.vreg['components'].select('oai', self._cw)
+            for prefix, fmt in oai.metadata_formats():
+                yield xml_metadataformat(prefix, fmt)
 
 
-class OAIListMetadatFormatsView(OAIView):
-    """View handling verb="ListMetadataFormats" requests for the whole
-    repository.
-    """
-    __select__ = match_verb('ListMetadataFormats')
-
-    def verb_content(self):
-        oai = self._cw.vreg['components'].select('oai', self._cw)
-        for prefix, fmt in oai.metadata_formats():
-            yield xml_metadataformat(prefix, fmt)
-
-
-class OAIListMetadatFormatsByIdentifierView(OAIView):
-    """View handling verb="ListMetadataFormats" requests."""
-    __select__ = (match_verb('ListMetadataFormats')
-                  & match_form_params('identifier'))
-
-    def verb_content(self):
-        rset = self.oai_request.rset_from_identifier(self._cw)
-        if not rset:
-            raise IdDoesNotExist(self.oai_request.identifier)
-        for record in oai_records(rset):
-            for fmt in record.metadata_formats():
-                yield fmt
-
-
+ at view_config(route_name='oai', verb='ListSets')
 class OAIListSetsView(OAIView):
     """View handling verb="ListSets" requests."""
-    __select__ = match_verb('ListSets')
 
     @staticmethod
     def build_set(spec, name=u''):
@@ -492,22 +442,14 @@ class OAIListSetsView(OAIView):
             yield self.build_set(spec, name=self._cw._(description))
 
 
-class OAIListIdentifiersView(OAIView):
-    """View handling verb="ListIdentifiers" requests.
-
-    This view returns an error as it handles cases where no "set" selection is
-    specified.
-    """
-    __select__ = match_verb('ListIdentifiers')
+ at view_config(route_name='oai', verb='ListIdentifiers')
+class OAIListIdentifiersWithSetView(OAIView):
+    """View handling verb="ListIdentifiers" requests with "set" selection."""
 
     def errors(self):
-        return {
-            'badArgument': 'ListIdentifiers verb requires a "metadataPrefix" restriction'}
-
-
-class OAIListIdentifiersWithSetView(OAIView):
-    """View handling verb="ListIdentifiers" requests with "set" selection."""
-    __select__ = match_verb('ListIdentifiers') & match_form_params('metadataPrefix')
+        if 'metadataPrefix' not in self.request.params:
+            return {'badArgument': ('ListIdentifiers verb requires a "metadataPrefix" '
+                                    'restriction')}
 
     def verb_content(self):
         rset, token = self.oai_request.rset(self._cw)
@@ -518,20 +460,15 @@ class OAIListIdentifiersWithSetView(OAIV
             yield new_token
 
 
-class OAIListRecordsErrorView(OAIView):
-    """View handling verb="ListRecords" requests when arguments are missing."""
-    __select__ = match_verb('ListRecords')
+ at view_config(route_name='oai', verb='ListRecords')
+class OAIListRecordsView(OAIView):
+    """View handling verb="ListRecords"."""
 
     def errors(self):
-        return {
-            'badArgument': 'ListRecords verb requires a "metadataPrefix" restriction'}
-
-
-class OAIListRecordsView(OAIView):
-    """View handling verb="ListRecords"."""
-    __select__ = (match_verb('ListRecords')
-                  & (match_form_params('metadataPrefix')
-                     | match_form_params('resumptionToken')))
+        params = set(self.request.params) & set({'metadataPrefix', 'resumptionToken'})
+        if not params:
+            return {'badArgument': ('ListRecords verb requires a "metadataPrefix" '
+                                    'restriction')}
 
     def verb_content(self):
         rset, token = self.oai_request.rset(self._cw)
@@ -542,38 +479,19 @@ class OAIListRecordsView(OAIView):
             yield new_token
 
 
-class OAIGetRecordErrorView(OAIView):
-    """View handling verb="GetRecord" requests when arguments are missing."""
-    __select__ = match_verb('GetRecord')
-
-    def errors(self):
-        return {'badArgument': ('GetRecord verb requires "identifier" and '
-                                '"metadataPrefix" arguments')}
-
-
-class OAIGetRecordMissingIdentifierView(OAIView):
-    """View handling verb="GetRecord" requests when "identifier" is missing.
-    """
-    __select__ = match_verb('GetRecord') & match_form_params('metadataPrefix')
+ at view_config(route_name='oai', verb='GetRecord')
+class OAIGetRecordView(OAIView):
+    """View handling verb="GetRecord" with proper arguments."""
 
     def errors(self):
-        return {'badArgument': 'GetRecord verb requires "identifier" restriction'}
-
-
-class OAIGetRecordMissingMetadataPrefixView(OAIView):
-    """View handling verb="GetRecord" requests when metadataPrefix argument is
-    missing.
-    """
-    __select__ = match_verb('GetRecord') & match_form_params('identifier')
-
-    def errors(self):
-        return {'badArgument': 'GetRecord verb requires "metadataPrefix" restriction'}
-
-
-class OAIGetRecordView(OAIView):
-    """View handling verb="GetRecord" with proper arguments."""
-    __select__ = (match_verb('GetRecord')
-                  & match_form_params('identifier', 'metadataPrefix'))
+        params = set(self.request.params) & set({'identifier', 'metadataPrefix'})
+        if not params:
+            return {'badArgument': ('GetRecord verb requires "identifier" and '
+                                    '"metadataPrefix" arguments')}
+        elif params == set({'identifier'}):
+            return {'badArgument': 'GetRecord verb requires "metadataPrefix" restriction'}
+        elif params == set({'metadataPrefix'}):
+            return {'badArgument': 'GetRecord verb requires "identifier" restriction'}
 
     def verb_content(self):
         rset = self.oai_request.rset_from_identifier(self._cw)
@@ -583,3 +501,23 @@ class OAIGetRecordView(OAIView):
                 break
         else:
             raise IdDoesNotExist(self.oai_request.identifier)
+
+
+class VerbPredicate(object):
+
+    def __init__(self, val, config):
+        self.val = val
+
+    def text(self):
+        return 'verb = %s' % (self.val,)
+
+    phash = text
+
+    def __call__(self, context, request):
+        return request.params.get('verb') == self.val
+
+
+def includeme(config):
+    config.add_route('oai', '/oai')
+    config.add_view_predicate('verb', VerbPredicate)
+    config.scan(__name__)



More information about the saem-devel mailing list