[Zope-CVS] SVN: zf.zscp/trunk/src/zf/zscp/ Import the initial ZSCP code.

Stephan Richter srichter at cosmos.phy.tufts.edu
Wed Mar 1 19:35:41 EST 2006


Log message for revision 65707:
  Import the initial ZSCP code.
  

Changed:
  A   zf.zscp/trunk/src/zf/zscp/
  A   zf.zscp/trunk/src/zf/zscp/DEPENDENCIES.cfg
  A   zf.zscp/trunk/src/zf/zscp/README.txt
  A   zf.zscp/trunk/src/zf/zscp/TASKS,txt
  A   zf.zscp/trunk/src/zf/zscp/__init__.py
  A   zf.zscp/trunk/src/zf/zscp/certification.py
  A   zf.zscp/trunk/src/zf/zscp/certification.txt
  A   zf.zscp/trunk/src/zf/zscp/certification.xmlt
  A   zf.zscp/trunk/src/zf/zscp/contact.py
  A   zf.zscp/trunk/src/zf/zscp/fields.py
  A   zf.zscp/trunk/src/zf/zscp/fileformat.py
  A   zf.zscp/trunk/src/zf/zscp/interfaces.py
  A   zf.zscp/trunk/src/zf/zscp/package.py
  A   zf.zscp/trunk/src/zf/zscp/publication.py
  A   zf.zscp/trunk/src/zf/zscp/publication.txt
  A   zf.zscp/trunk/src/zf/zscp/release.py
  A   zf.zscp/trunk/src/zf/zscp/release.txt
  A   zf.zscp/trunk/src/zf/zscp/release.xmlt
  A   zf.zscp/trunk/src/zf/zscp/tests.py
  A   zf.zscp/trunk/src/zf/zscp/website/
  A   zf.zscp/trunk/src/zf/zscp/website/__init__.py
  A   zf.zscp/trunk/src/zf/zscp/website/interfaces.py
  A   zf.zscp/trunk/src/zf/zscp/website/zscp.py
  A   zf.zscp/trunk/src/zf/zscp/zscp.py
  A   zf.zscp/trunk/src/zf/zscp/zscp.txt

-=-
Added: zf.zscp/trunk/src/zf/zscp/DEPENDENCIES.cfg
===================================================================
--- zf.zscp/trunk/src/zf/zscp/DEPENDENCIES.cfg	2006-03-02 00:34:56 UTC (rev 65706)
+++ zf.zscp/trunk/src/zf/zscp/DEPENDENCIES.cfg	2006-03-02 00:35:40 UTC (rev 65707)
@@ -0,0 +1,3 @@
+pysvn
+zope.interface
+zope.schema

Added: zf.zscp/trunk/src/zf/zscp/README.txt
===================================================================
--- zf.zscp/trunk/src/zf/zscp/README.txt	2006-03-02 00:34:56 UTC (rev 65706)
+++ zf.zscp/trunk/src/zf/zscp/README.txt	2006-03-02 00:35:40 UTC (rev 65707)
@@ -0,0 +1,21 @@
+===================
+ZSCP Implementation
+===================
+
+This package implements the process and Web site of the ZSCP as described in
+``ProcessAndRepository.txt``.
+
+Contents
+--------
+
+``release.txt``
+  Describes the parsing and writing of release data files.
+
+``certification.txt``
+  Describes the parsing and writing of certification data files.
+
+``publication.txt``
+  Describes the parsing and writing of publication data files.
+
+``zscp.txt``
+  The actual implementation of the ZSCP for an SVN repository.


Property changes on: zf.zscp/trunk/src/zf/zscp/README.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zf.zscp/trunk/src/zf/zscp/TASKS,txt
===================================================================
--- zf.zscp/trunk/src/zf/zscp/TASKS,txt	2006-03-02 00:34:56 UTC (rev 65706)
+++ zf.zscp/trunk/src/zf/zscp/TASKS,txt	2006-03-02 00:35:40 UTC (rev 65707)
@@ -0,0 +1,16 @@
+================
+Tasks to be done
+================
+
+
+
+Automation Tools
+----------------
+
+- test coverage
+- package meta-information verification (provide hook in parser)
+- Zope coding guidline verifier
+  * check package structure
+  * check class names
+  * check metod and function names
+  * check for method documentation strings

Added: zf.zscp/trunk/src/zf/zscp/__init__.py
===================================================================
--- zf.zscp/trunk/src/zf/zscp/__init__.py	2006-03-02 00:34:56 UTC (rev 65706)
+++ zf.zscp/trunk/src/zf/zscp/__init__.py	2006-03-02 00:35:40 UTC (rev 65707)
@@ -0,0 +1 @@
+# Make a pacakge.


Property changes on: zf.zscp/trunk/src/zf/zscp/__init__.py
___________________________________________________________________
Name: svn:keywords
   + Id

Added: zf.zscp/trunk/src/zf/zscp/certification.py
===================================================================
--- zf.zscp/trunk/src/zf/zscp/certification.py	2006-03-02 00:34:56 UTC (rev 65706)
+++ zf.zscp/trunk/src/zf/zscp/certification.py	2006-03-02 00:35:40 UTC (rev 65707)
@@ -0,0 +1,56 @@
+##############################################################################
+#
+# Copyright (c) 2005 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Certification
+
+$Id$
+"""
+__docformat__ = "reStructuredText"
+import zope.interface
+import zope.schema
+
+from zf.zscp import interfaces, fileformat, contact
+
+
+class Certification(object):
+    """Certification Implementation."""
+    zope.interface.implements(interfaces.ICertification)
+
+    action = None
+    sourceLevel = None
+    targetLevel = None
+    date = None
+    certificationManger = None
+    comments = None
+
+    def __repr__(self):
+        return '<%s action=%r, source=%r, target=%r>' % (
+            self.__class__.__name__, self.action,
+            self.sourceLevel, self.targetLevel)
+
+
+_rootField = zope.schema.List(
+    value_type=zope.schema.Object(schema=interfaces.ICertification))
+
+def processXML(xml):
+    """Process the XML to create a list of certifications."""
+    handler = fileformat.XMLHandler(
+        'certifications', _rootField,
+        {interfaces.IContact: contact.Contact,
+         interfaces.ICertification: Certification})
+    return fileformat.processXML(xml, handler)
+
+def produceXML(certifications):
+    """Convert the list of certifications to XML."""
+    producer = fileformat.InfoProducer(certifications, _rootField)
+    return fileformat.produceXML(producer, 'certification.xmlt')


Property changes on: zf.zscp/trunk/src/zf/zscp/certification.py
___________________________________________________________________
Name: svn:keywords
   + Id

Added: zf.zscp/trunk/src/zf/zscp/certification.txt
===================================================================
--- zf.zscp/trunk/src/zf/zscp/certification.txt	2006-03-02 00:34:56 UTC (rev 65706)
+++ zf.zscp/trunk/src/zf/zscp/certification.txt	2006-03-02 00:35:40 UTC (rev 65707)
@@ -0,0 +1,217 @@
+===================================
+Handling Package Certification Data
+===================================
+
+The package certification data is stored in an XML file. This document
+describes how to parse and generate this file.
+
+Parsing Package Certification Data
+----------------------------------
+
+The certification file contains several certifications. The simplest case is
+that there are no certifications.
+
+  >>> import StringIO
+  >>> xml = StringIO.StringIO(u'<certifications />')
+
+  >>> from zf.zscp import certification
+  >>> certifications = certification.processXML(xml)
+  >>> certifications
+  []
+
+Note that you cannot just have any root element:
+
+  >>> xml = StringIO.StringIO(u'<faux-certifications />')
+
+  >>> from zf.zscp import certification
+  >>> certification.processXML(xml)
+  Traceback (most recent call last):
+  ...
+  ValueError: The root element must be named `certifications`
+
+When a certification is added, all required fields must be specified:
+
+  >>> xml = StringIO.StringIO(u'''
+  ... <certifications>
+  ...   <certification>
+  ...    <action>grant</action>
+  ...    <source-level>none</source-level>
+  ...    <target-level>listed</target-level>
+  ...    <date>2006-01-01</date>
+  ...    <certification-manager>
+  ...      <name>John Doe</name>
+  ...      <email>john at doe.com</email>
+  ...    </certification-manager>
+  ...  </certification>
+  ... </certifications>
+  ... ''')
+
+  >>> certifications = certification.processXML(xml)
+  >>> certifications
+  [<Certification action=u'grant', source=u'none', target=u'listed'>]
+
+All data should be available via the attributes:
+
+  >>> grant = certifications[0]
+  >>> grant.action
+  u'grant'
+  >>> grant.sourceLevel
+  u'none'
+  >>> grant.targetLevel
+  u'listed'
+  >>> grant.date
+  datetime.date(2006, 1, 1)
+  >>> grant.certificationManager
+  <Contact 'John Doe <john at doe.com>'>
+
+If a required field is not specified, then an error is raised. In the example
+below, two required fields are missing:
+
+  >>> xml = StringIO.StringIO(u'''
+  ... <certifications>
+  ...   <certification>
+  ...    <source-level>none</source-level>
+  ...    <target-level>listed</target-level>
+  ...    <certification-manager>
+  ...      <name>John Doe</name>
+  ...      <email>john at doe.com</email>
+  ...    </certification-manager>
+  ...  </certification>
+  ... </certifications>
+  ... ''')
+
+  >>> certification.processXML(xml)
+  Traceback (most recent call last):
+  ...
+  RequiredElementsMissing: Required field(s) 'action', 'date' missing in
+                           `certification` (file "<string>", line 10, column 17)
+
+And finally a full file of content:
+
+  >>> xml = StringIO.StringIO(u'''
+  ... <certifications>
+  ...   <certification>
+  ...     <action>warn</action>
+  ...     <source-level>level1</source-level>
+  ...     <target-level>level1</target-level>
+  ...     <date>2006-03-15</date>
+  ...     <certification-manager>
+  ...       <name>John Doe</name>
+  ...       <email>john at doe.com</email>
+  ...     </certification-manager>
+  ...     <comments>
+  ...       The test coverage has been around 50% for over two months.
+  ...     </comments>
+  ...   </certification>
+  ...   <certification>
+  ...     <action>grant</action>
+  ...     <source-level>none</source-level>
+  ...     <target-level>level1</target-level>
+  ...     <date>2006-01-01</date>
+  ...     <certification-manager>
+  ...       <name>John Doe</name>
+  ...       <email>john at doe.com</email>
+  ...     </certification-manager>
+  ...   </certification>
+  ... </certifications>
+  ... ''')
+
+  >>> certifications = certification.processXML(xml)
+  >>> warn = certifications[0]
+  >>> warn.action
+  u'warn'
+  >>> warn.sourceLevel
+  u'level1'
+  >>> warn.targetLevel
+  u'level1'
+  >>> warn.date
+  datetime.date(2006, 3, 15)
+  >>> warn.certificationManager
+  <Contact 'John Doe <john at doe.com>'>
+  >>> warn.comments
+  u'The test coverage has been around 50% for over two months.'
+
+  >>> grant = certifications[1]
+  >>> grant.action
+  u'grant'
+  >>> grant.sourceLevel
+  u'none'
+  >>> grant.targetLevel
+  u'level1'
+  >>> grant.date
+  datetime.date(2006, 1, 1)
+  >>> grant.certificationManager
+  <Contact 'John Doe <john at doe.com>'>
+
+
+Writing Package Certification Data
+----------------------------------
+
+The simplest step is to create a totally empty file.
+
+  >>> certifications = []
+  >>> print certification.produceXML(certifications)
+  <certifications>
+  </certifications>
+
+Now let's add a certification to the certifications having the minimum data ...
+
+  >>> import datetime
+  >>> from zf.zscp import contact
+  >>> grant = certification.Certification()
+  >>> grant.action = u'grant'
+  >>> grant.sourceLevel = u'none'
+  >>> grant.targetLevel = u'level1'
+  >>> grant.date = datetime.date(2006, 1, 1)
+  >>> grant.certificationManager = contact.Contact()
+  >>> grant.certificationManager.name = u'John Doe'
+  >>> grant.certificationManager.email = u'john at doe.com'
+
+and add it to the certifications:
+
+  >>> certifications.append(grant)
+
+We can now render the structure:
+
+  >>> print certification.produceXML(certifications)
+  <certifications>
+    <certification>
+      <action>grant</action>
+      <source-level>none</source-level>
+      <target-level>level1</target-level>
+      <date>2006-01-01</date>
+      <certification-manager>
+        <name>John Doe</name>
+        <email>john at doe.com</email>
+      </certification-manager>
+    </certification>
+  </certifications>
+
+Let's now also add the complete warning (``warn``) certification action in the
+first position to ensure correct output.
+
+  >>> certifications.insert(0, warn)
+  >>> print certification.produceXML(certifications)
+  <certifications>
+    <certification>
+      <action>warn</action>
+      <source-level>level1</source-level>
+      <target-level>level1</target-level>
+      <date>2006-03-15</date>
+      <certification-manager>
+        <name>John Doe</name>
+        <email>john at doe.com</email>
+      </certification-manager>
+      <comments>The test coverage has been around 50% for over two months.</comments>
+    </certification>
+    <certification>
+      <action>grant</action>
+      <source-level>none</source-level>
+      <target-level>level1</target-level>
+      <date>2006-01-01</date>
+      <certification-manager>
+        <name>John Doe</name>
+        <email>john at doe.com</email>
+      </certification-manager>
+    </certification>
+  </certifications>


Property changes on: zf.zscp/trunk/src/zf/zscp/certification.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zf.zscp/trunk/src/zf/zscp/certification.xmlt
===================================================================
--- zf.zscp/trunk/src/zf/zscp/certification.xmlt	2006-03-02 00:34:56 UTC (rev 65706)
+++ zf.zscp/trunk/src/zf/zscp/certification.xmlt	2006-03-02 00:35:40 UTC (rev 65707)
@@ -0,0 +1,15 @@
+<certifications>
+  <certification tal:repeat="certification options/root">
+    <action tal:content="certification/action" />
+    <source-level tal:content="certification/sourceLevel" />
+    <target-level tal:content="certification/targetLevel" />
+    <date tal:content="certification/date" />
+    <certification-manager>
+      <name tal:content="certification/certificationManager/name" />
+      <email tal:content="certification/certificationManager/email" />
+    </certification-manager>
+    <comments
+        tal:condition="certification/comments"
+        tal:content="certification/comments" />
+  </certification>
+</certifications>

Added: zf.zscp/trunk/src/zf/zscp/contact.py
===================================================================
--- zf.zscp/trunk/src/zf/zscp/contact.py	2006-03-02 00:34:56 UTC (rev 65706)
+++ zf.zscp/trunk/src/zf/zscp/contact.py	2006-03-02 00:35:40 UTC (rev 65707)
@@ -0,0 +1,32 @@
+##############################################################################
+#
+# Copyright (c) 2005 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Contact
+
+$Id$
+"""
+__docformat__ = "reStructuredText"
+import zope.interface
+
+from zf.zscp import interfaces
+
+class Contact(object):
+    """Contact Implementation"""
+    zope.interface.implements(interfaces.IContact)
+
+    name = None
+    email = None
+
+    def __repr__(self):
+        return "<%s '%s <%s>'>" % (
+            self.__class__.__name__, self.name, self.email)


Property changes on: zf.zscp/trunk/src/zf/zscp/contact.py
___________________________________________________________________
Name: svn:keywords
   + Id

Added: zf.zscp/trunk/src/zf/zscp/fields.py
===================================================================
--- zf.zscp/trunk/src/zf/zscp/fields.py	2006-03-02 00:34:56 UTC (rev 65706)
+++ zf.zscp/trunk/src/zf/zscp/fields.py	2006-03-02 00:35:40 UTC (rev 65707)
@@ -0,0 +1,27 @@
+##############################################################################
+#
+# Copyright (c) 2005 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""XXX short summary goes here.
+
+$Id$
+"""
+__docformat__ = "reStructuredText"
+import datetime
+import zope.schema
+
+class Date(zope.schema.Date):
+    """Custom date field."""
+
+    def fromUnicode(self, str):
+        year, month, day = [int(field) for field in str.split('-')]
+        return datetime.date(year, month, day)


Property changes on: zf.zscp/trunk/src/zf/zscp/fields.py
___________________________________________________________________
Name: svn:keywords
   + Id

Added: zf.zscp/trunk/src/zf/zscp/fileformat.py
===================================================================
--- zf.zscp/trunk/src/zf/zscp/fileformat.py	2006-03-02 00:34:56 UTC (rev 65706)
+++ zf.zscp/trunk/src/zf/zscp/fileformat.py	2006-03-02 00:35:40 UTC (rev 65707)
@@ -0,0 +1,306 @@
+##############################################################################
+#
+# Copyright (c) 2005 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""A collection of tools to support the ZSCP file formats.
+
+$Id$
+"""
+__docformat__ = "reStructuredText"
+import re
+from distutils.util import rfc822_escape
+from email.Parser import HeaderParser
+from xml.sax import make_parser
+from xml.sax.xmlreader import InputSource
+from xml.sax.handler import ContentHandler
+from cStringIO import StringIO
+
+import zope.schema
+from zope.pagetemplate import pagetemplatefile
+
+def getHeaderName(name):
+    """Convert field names to RFC 2822 header names."""
+    parts = re.findall('^[a-z]+|[A-Z][a-z0-9]+', name)
+    headerName = '-'.join(part.lower() for part in parts)
+    return headerName.capitalize()
+
+def getFieldName(name):
+    """Convert XML element names to field names."""
+    name = name.lower()
+    parts = name.split('-')
+    name = ''.join([part.capitalize() for part in parts])
+    return name[0].lower() + name[1:]
+
+def rfc822_unescape(value):
+    return value.replace('\n        ', '\n')
+
+
+class XMLStructuralError(Exception):
+    """An exception that represents an error in the XML structure."""
+
+    def __init__(self, locator):
+        self.locator = locator
+
+    def __str__(self):
+        return '(file "%s", line %s, column %s)'% (
+            self.locator.getSystemId(),
+            self.locator.getLineNumber(),
+            self.locator.getColumnNumber())
+
+
+class RequiredElementsMissing(XMLStructuralError):
+    """A required field is missing."""
+
+    def __init__(self, elementName, names, locator):
+        XMLStructuralError.__init__(self, locator)
+        self.elementName = elementName
+        self.names = names
+
+    def __str__(self):
+        info = XMLStructuralError.__str__(self)
+        return 'Required field(s) %s missing in `%s` %s' % (
+            ', '.join([name.__repr__() for name in self.names]),
+            self.elementName,
+            info)
+
+
+class InvalidSubElement(XMLStructuralError):
+    """An unsuspected sub-element was found."""
+
+    def __init__(self, fieldName, locator):
+        XMLStructuralError.__init__(self, locator)
+        self.fieldName = fieldName
+
+    def __str__(self):
+        info = XMLStructuralError.__str__(self)
+        return '`%s` cannot have sub-elements %s' %(self.fieldName, info)
+
+
+class XMLHandler(ContentHandler):
+    """A SAX event handler that creates a Python object from the XML events
+    using a schema.
+    """
+
+    def __init__(self, rootElementName, rootElementField, factories):
+        self.stack = []
+        self.root = None
+        self.rootElementName = rootElementName
+        self.rootElementField = rootElementField
+        self.factories = factories
+
+    @property
+    def current(self):
+        return self.stack[-1]
+
+    def _verifyComplexElement(self, obj, iface, elemName):
+        missingFields = []
+        for name, field in zope.schema.getFields(iface).items():
+            if field.required and getattr(obj, name) == field.missing_value:
+                if field.default is not field.missing_value:
+                    setattr(obj, name, field.default)
+                else:
+                    missingFields.append(name)
+        if missingFields:
+            raise RequiredElementsMissing(
+                elemName, sorted(missingFields), self.locator)
+
+    def setDocumentLocator(self, locator):
+        self.locator = locator
+
+    def characters(self, text):
+        if isinstance(self.current[0], unicode):
+            self.current[0] += text
+
+    def startElement(self, name, attrs):
+        attrName = getFieldName(name)
+
+        if not self.stack:
+            if name != self.rootElementName:
+                raise ValueError(
+                    'The root element must be named `%s`' %self.rootElementName)
+            field = self.rootElementField
+        else:
+            currentField = self.current[1]
+            if zope.schema.interfaces.IObject.providedBy(currentField):
+                field = currentField.schema.getDescriptionFor(attrName)
+            elif zope.schema.interfaces.IList.providedBy(currentField):
+                field = currentField.value_type
+            else:
+                raise InvalidSubElement(currentField.__name__, self.locator)
+
+        if zope.schema.interfaces.IObject.providedBy(field):
+            obj = self.factories[field.schema](**dict(attrs))
+        elif zope.schema.interfaces.IList.providedBy(field):
+            obj = []
+        else:
+            obj = u''
+
+        self.stack.append([obj, field])
+
+
+    def endElement(self, name):
+        attrName = getFieldName(name)
+
+        obj, field = self.stack.pop()
+        if zope.schema.interfaces.IObject.providedBy(field):
+            self._verifyComplexElement(obj, field.schema, name)
+        elif zope.schema.interfaces.IList.providedBy(field):
+            pass
+        else:
+            obj = field.fromUnicode(obj.strip())
+
+        if not self.stack:
+            self.root = obj
+        elif zope.schema.interfaces.IList.providedBy(self.current[1]):
+            self.current[0].append(obj)
+        else:
+            setattr(self.current[0], attrName, obj)
+
+
+def processXML(file, handler):
+    """Read the releases XML string."""
+    src = InputSource(getattr(file, 'name', '<string>'))
+    src.setByteStream(file)
+    parser = make_parser()
+    parser.setContentHandler(handler)
+    parser.parse(src)
+    return handler.root
+
+
+def _listHandler(producer, obj, field):
+    return [producer.produce(entry, field.value_type)
+            for entry in obj]
+
+def _objectHandler(producer, obj, field):
+    info = {}
+    for name, field in zope.schema.getFieldsInOrder(field.schema):
+        info[name] = producer.produce(getattr(obj, name), field)
+    return info
+
+
+class InfoProducer(object):
+    """An object that produces a TAL-friendly info object from an obejct using
+    a schema.
+    """
+
+    handlers = {zope.schema.interfaces.IList: _listHandler,
+                zope.schema.interfaces.IObject: _objectHandler}
+
+    def __init__(self, rootObject, rootField):
+        self.rootObject = rootObject
+        self.rootField = rootField
+
+    def produce(self, obj, field):
+        if obj is None:
+            return None
+
+        for fieldIface, handler in self.handlers.items():
+            if fieldIface.providedBy(field):
+                return handler(self, obj, field)
+
+        return str(obj)
+
+    def __call__(self):
+        return self.produce(self.rootObject, self.rootField)
+
+
+def produceXML(producer, template):
+    return pagetemplatefile.PageTemplateFile(template)(root=producer())
+
+
+class HeaderProcessor(object):
+    """Parses RFC 2822 style headers into a Python object using a schema."""
+
+    def __init__(self, root, schema):
+        self.root = root
+        self.schema = schema
+        self.missingRequired = []
+
+    def _getHeaderName(self, name):
+        parts = re.findall('^[a-z]+|[A-Z][a-z0-9]+', name)
+        headerName = '-'.join(part.lower() for part in parts)
+        return headerName.capitalize()
+
+    def _processSingleHeader(self, name, field):
+        headerName = getHeaderName(name)
+        headers = self.msg.get_all(headerName)
+        if headers and len(headers) > 1:
+            raise ValueError("header %r can only be given once" % headerName)
+        if headers:
+            value = unicode(headers[0])
+            setattr(self.root, name,
+                    field.fromUnicode(rfc822_unescape(value)))
+        else:
+            if field.missing_value != field.default:
+                setattr(self.root, name, field.default)
+            elif field.required:
+                self.missingRequired.append(headerName)
+
+    def _processMultiHeader(self, name, field):
+        headerName = getHeaderName(name)
+        headers = self.msg.get_all(headerName)
+        if headers:
+            values = [
+                field.value_type.fromUnicode(rfc822_unescape(unicode(value)))
+                for value in headers]
+            setattr(self.root, name, values)
+        else:
+            if field.missing_value != field.default:
+                setattr(self.root, name, field.default)
+            elif field.required:
+                self.missingRequired.append(headerName)
+
+    def __call__(self, file):
+        parser = HeaderParser()
+        self.msg = parser.parse(file)
+        for name, field in zope.schema.getFieldsInOrder(self.schema):
+            if zope.schema.interfaces.IList.providedBy(field):
+                self._processMultiHeader(name, field)
+            else:
+                self._processSingleHeader(name, field)
+
+        if self.missingRequired:
+            raise ValueError('Required headers missing: %s' %
+                             ', '.join(self.missingRequired))
+
+
+class HeaderProducer(object):
+    """Produces a RFC 2822 style header string from a Python object using a
+    schema.
+    """
+
+    def __init__(self, root, schema):
+        self.root = root
+        self.schema = schema
+
+    def _produceMultiHeader(self, name, value, field):
+        for subvalue in value:
+            self._produceSingleHeader(name, subvalue, field.value_type)
+
+    def _produceSingleHeader(self, name, value, field):
+        headerName = getHeaderName(name)
+        if isinstance(value, unicode):
+            value = value.encode('utf8')
+        value = rfc822_escape(str(value))
+        self.output.write('%s: %s\n' %(headerName, value))
+
+    def __call__(self):
+        self.output = StringIO()
+        for name, field in zope.schema.getFieldsInOrder(self.schema):
+            value = getattr(self.root, name)
+            if value == field.missing_value:
+                continue
+            if zope.schema.interfaces.IList.providedBy(field):
+                self._produceMultiHeader(name, value, field)
+            else:
+                self._produceSingleHeader(name, value, field)
+        return self.output.getvalue()


Property changes on: zf.zscp/trunk/src/zf/zscp/fileformat.py
___________________________________________________________________
Name: svn:keywords
   + Id

Added: zf.zscp/trunk/src/zf/zscp/interfaces.py
===================================================================
--- zf.zscp/trunk/src/zf/zscp/interfaces.py	2006-03-02 00:34:56 UTC (rev 65706)
+++ zf.zscp/trunk/src/zf/zscp/interfaces.py	2006-03-02 00:35:40 UTC (rev 65707)
@@ -0,0 +1,382 @@
+##############################################################################
+#
+# Copyright (c) 2005 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""ZSCP Interfaces
+
+$Id$
+"""
+__docformat__ = "reStructuredText"
+
+import zope.interface
+import zope.interface.common.mapping
+import zope.schema
+import zope.schema.vocabulary
+
+import zf.zscp.fields
+
+CERTIFICATION_LEVELS = zope.schema.vocabulary.SimpleVocabulary([
+    zope.schema.vocabulary.SimpleTerm(u'none', title=u'None'),
+    zope.schema.vocabulary.SimpleTerm(u'listed', title=u'Listed'),
+    zope.schema.vocabulary.SimpleTerm(u'level1', title=u'Level 1 Certified'),
+    zope.schema.vocabulary.SimpleTerm(u'level2', title=u'Level 2 Certified'),
+    zope.schema.vocabulary.SimpleTerm(u'level3', title=u'Level 3 Certified')
+    ])
+
+CERTIFICATION_ACTIONS = zope.schema.vocabulary.SimpleVocabulary([
+    zope.schema.vocabulary.SimpleTerm(u'grant', title=u'Grant'),
+    zope.schema.vocabulary.SimpleTerm(u'revoke', title=u'Revoke'),
+    zope.schema.vocabulary.SimpleTerm(u'warn', title=u'Warn'),
+    ])
+
+class IRepositoryInitializedEvent(zope.interface.Interface):
+    """Event fired after a repository was initialized."""
+    repository = Attribute('The initialized repository')
+
+class RepositoryInitializedEvent(object):
+    zope.interface.implements(IRepositoryInitializedEvent)
+
+    def __init__(self, repository):
+        self.repository = repository
+
+
+class IPackageEvent(zope.interface.Interface):
+    """An event that involves a package."""
+    package = Attribute('The package that was acted upon')
+
+class PackageEvent(object):
+    zope.interface.implements(IPackageEvent)
+    def __init__(self, pkg):
+        self.package = pkg
+
+
+class IPackageRegisteredEvent(IPackageEvent):
+    """An event fired after the package has been registered with the ZSCP."""
+
+class PackageRegisteredEvent(PackageEvent):
+    pass
+
+
+class IPackageUnregisteredEvent(IPackageEvent):
+    """An event fired after the package has been unregistered with the ZSCP."""
+
+class PackageUnregisteredEvent(PackageEvent):
+    pass
+
+
+class IPackageUpdateddEvent(IPackageEvent):
+    """An event fired after the package has been updated."""
+
+class PackageUpdatedEvent(PackageEvent):
+    pass
+
+
+class IContact(zope.interface.Interface):
+    """A contact"""
+
+    name = zope.schema.TextLine(
+        title=u"Contact Name",
+        description=u"The full name of the contact person.",
+        required=True)
+
+    email = zope.schema.TextLine(
+        title=u"Contact E-mail",
+        description=u"The E-mail address of the contact person.",
+        required=False)
+
+
+class IRelease(zope.interface.Interface):
+    """A release of a package."""
+
+    name = zope.schema.TextLine(
+        title=u"Name",
+        description=u"The name under which the package will be known for this "
+                    u"release.",
+        required=True)
+
+    version = zope.schema.TextLine(
+        title=u"Version",
+        description=u"This field describes the version number of the release.",
+        required=True)
+
+    codename = zope.schema.TextLine(
+        title=u"Codename",
+        description=u"The code name of the release.",
+        required=False)
+
+    date = zf.zscp.fields.Date(
+        title=u"Date",
+        description=u"The date on which the release was made.",
+        required=True)
+
+    certification = zope.schema.Choice(
+        title=u"Certification",
+        description=u"The certification level of the package at the date "
+                    u"of the release.",
+        vocabulary=CERTIFICATION_LEVELS,
+        required=True,
+        default=u'none')
+
+    package = zope.schema.URI(
+        title=u"Package",
+        description=u"The URL to the installation package file.",
+        required=True)
+
+    source = zope.schema.URI(
+        title=u"Source",
+        description=u"The URL to the repository location. It should be "
+                    u"possible to use this URL to make a checkout.",
+        required=False)
+
+    dependencies = zope.schema.List(
+        title=u"Dependencies",
+        description=u"A set of dependencies to other packages. Each "
+                    u"dependency must contain the full name of the package "
+                    u"and the version number.",
+        value_type=zope.schema.TextLine(title=u'Dependency'),
+        required=False)
+
+    announcement = zope.schema.URI(
+        title=u"Announcement",
+        description=u"A link to the announcement of the release.",
+        required=False)
+
+    releaseManager = zope.schema.Object(
+        title=u"Release Manager",
+        description=u"The release manager of the release.",
+        schema=IContact,
+        required=False)
+
+    pressContact = zope.schema.Object(
+        title=u"Press Contact",
+        description=u"The press contact of the release.",
+        schema=IContact,
+        required=False)
+
+
+class ICertification(zope.interface.Interface):
+    """A certification."""
+
+    action = zope.schema.Choice(
+        title=u"Action",
+        description=u"The action describes whether a certification was "
+                    u"granted or revoked. Upon violations (as defined "
+                    u"in section 2.8 of the ZSCP proposal), a certification "
+                    u"manager can also issue a warning.",
+        vocabulary=CERTIFICATION_ACTIONS,
+        required=True)
+
+    sourceLevel = zope.schema.Choice(
+        title=u"Source Level",
+        description=u"The original certification level before this "
+                    u"certification action was executed.",
+        vocabulary=CERTIFICATION_LEVELS,
+        required=True)
+
+    targetLevel = zope.schema.Choice(
+        title=u"Target Level",
+        description=u"The final certification level after this "
+                    u"certification action was executed.",
+        vocabulary=CERTIFICATION_LEVELS,
+        required=True)
+
+    date = zf.zscp.fields.Date(
+        title=u"Date",
+        description=u"The date on which the certification action was executed.",
+        required=True)
+
+    certificationManager = zope.schema.Object(
+        title=u"Certification Manager",
+        description=u"The certification manager that executed the "
+                    u"certification action.",
+        schema=IContact,
+        required=True)
+
+    comments = zope.schema.Text(
+        title=u"Codename",
+        description=u"This field can contain arbitrary comments about the "
+                    u"certification action.",
+        required=False)
+
+
+class IPublication(zope.interface.Interface):
+    """Publication data."""
+
+    packageName = zope.schema.Id(
+        title=u"Package Name",
+        description=u"The dotted Python path of the package.",
+        required=True)
+
+    name = zope.schema.TextLine(
+        title=u"Name",
+        description=u"The commonly used name of the package.",
+        required=True)
+
+    summary = zope.schema.TextLine(
+        title=u"Summary",
+        description=u"A short description or summary of the package. It is "
+                    u"also often interpreted as the title.",
+        required=True)
+
+    description = zope.schema.Text(
+        title=u"Description",
+        description=u"A detailed description of the package's functionality. "
+                    u"While it should contain some detail, it should not "
+                    u"duplicate the documentation of the README.txt file.",
+        required=False)
+
+    homePage = zope.schema.URI(
+        title=u"Homepage",
+        description=u"A URL to the homepage of the package.",
+        required=False)
+
+    author = zope.schema.List(
+        title=u"Author Names",
+        description=u"The names of the authors of the package.",
+        value_type=zope.schema.TextLine(title=u'Name'),
+        required=True)
+
+    authorEmail = zope.schema.List(
+        title=u"Author Emails",
+        description=u"The E-mails of the authors of the package.",
+        value_type=zope.schema.TextLine(title=u'E-mail'),
+        required=True)
+
+    license = zope.schema.List(
+        title=u"Licenses",
+        description=u"The software license(s) of the package.",
+        value_type=zope.schema.TextLine(title=u'License'),
+        required=True)
+
+    platform = zope.schema.List(
+        title=u"Supported Platforms",
+        description=u"The operating system/platform the package is known to "
+                    u"run on. This field can be specified multiple times. "
+                    u"``All`` may be used, if the package is available "
+                    u"on all platforms Python is running on, i.e. the "
+                    u"package is pure Python code.",
+        value_type=zope.schema.TextLine(title=u'Platform'),
+        required=True,
+        default=[u'All'])
+
+    classifier = zope.schema.List(
+        title=u"Classifiers",
+        description=u"A classification entry identifying the package.",
+        value_type=zope.schema.TextLine(title=u'Classifier'),
+        required=False)
+
+    developersMailinglist = zope.schema.TextLine(
+        title=u"Developers Mailinglist",
+        description=u"The E-mail address of the developers mailing list.",
+        required=False)
+
+    usersMailinglist = zope.schema.TextLine(
+        title=u"Users Mailinglist",
+        description=u"The E-mail address of the users mailing list.",
+        required=False)
+
+    issueTracker = zope.schema.URI(
+        title=u"Issue Tracker",
+        description=u"A URL to the issue tracker of the package, where new "
+                    u"issues/bugs/requests can be reported.",
+        required=False)
+
+    repositoryLocation = zope.schema.URI(
+        title=u"Source Repository",
+        description=u"The URL to the repository. The URL should be usable "
+                    u"to actually check out the package.",
+        required=False)
+
+    repositoryWebLocation = zope.schema.URI(
+        title=u"Repository Web Location",
+        description=u"The URL to the repository's browsable HTML UI.",
+        required=False)
+
+    certificationLevel = zope.schema.Choice(
+        title=u"Certification Level",
+        description=u"Describes the certification level of the package.",
+        vocabulary=CERTIFICATION_LEVELS,
+        required=False)
+
+    certificationDate = zf.zscp.fields.Date(
+        title=u"Certification Date",
+        description=u"The date at which the certification was received.",
+        required=False)
+
+    metadataVersion = zope.schema.TextLine(
+        title=u"Metadata Version",
+        description=u"This is the version number of this package meta-data.",
+        required=True)
+
+
+class IPackage(zope.interface.Interface):
+    """Package"""
+
+    name = zope.schema.Id(
+        title=u"Package Name",
+        description=u"The dotted Python path of the package.",
+        required=True)
+
+    publication = zope.schema.Object(
+        title=u"Publication Information",
+        schema=IPublication,
+        required=True)
+
+    releases = zope.schema.List(
+        title=u"Releases",
+        value_type=zope.schema.Object(schema=IRelease),
+        required=True)
+
+    certifications = zope.schema.List(
+        title=u"Certifications",
+        value_type=zope.schema.Object(schema=ICertification),
+        required=True)
+
+
+class IZSCPRepository(zope.interface.common.mapping.IEnumerableMapping):
+    """ZSCP Repository."""
+
+    svnRoot = zope.schema.URI(
+        title=u"SVN Repository Root",
+        description=u"A SVN URI that can be used to checkout the package data.",
+        required=True)
+
+    localRoot = zope.schema.BytesLine(
+        title=u"Local Repository Root",
+        description=u"The directory on the server in to which the packages "
+                    u"are checked out.",
+        required=True)
+
+    password = zope.schema.TextLine(
+        title=u"SSH Password",
+        description=u"The directory on the server in to which the packages "
+                    u"are checked out.",
+        required=False)
+
+    def initialize():
+        """Initialize the ZSCP repository."""
+
+    def register(pkg):
+        """Register a package with ZSCP."""
+
+    def unregister(pkg):
+        """Unregister a package from ZSCP"""
+
+    def update(pkg):
+        """Update the local copy of the ZSCP data."""
+
+    def fetch(all=False):
+        """Fetch all package names from the SVN repository.
+
+        If ``all`` is true, then return all packages of the repository,
+        regardless of their ZSCP status.
+        """


Property changes on: zf.zscp/trunk/src/zf/zscp/interfaces.py
___________________________________________________________________
Name: svn:keywords
   + Id

Added: zf.zscp/trunk/src/zf/zscp/package.py
===================================================================
--- zf.zscp/trunk/src/zf/zscp/package.py	2006-03-02 00:34:56 UTC (rev 65706)
+++ zf.zscp/trunk/src/zf/zscp/package.py	2006-03-02 00:35:40 UTC (rev 65707)
@@ -0,0 +1,36 @@
+##############################################################################
+#
+# Copyright (c) 2005 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Package Implementation
+
+$Id$
+"""
+__docformat__ = "reStructuredText"
+import zope.interface
+
+from zf.zscp import interfaces
+
+class Package(object):
+    """Package Implementation."""
+    zope.interface.implements(interfaces.IPackage)
+
+    name = None
+    publication = None
+    releases = None
+    certifications = None
+
+    def __init__(self, name):
+        self.name = name
+
+    def __repr__(self):
+        return '<%s %r>' %(self.__class__.__name__, self.name)


Property changes on: zf.zscp/trunk/src/zf/zscp/package.py
___________________________________________________________________
Name: svn:keywords
   + Id

Added: zf.zscp/trunk/src/zf/zscp/publication.py
===================================================================
--- zf.zscp/trunk/src/zf/zscp/publication.py	2006-03-02 00:34:56 UTC (rev 65706)
+++ zf.zscp/trunk/src/zf/zscp/publication.py	2006-03-02 00:35:40 UTC (rev 65707)
@@ -0,0 +1,60 @@
+##############################################################################
+#
+# Copyright (c) 2005 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Publication
+
+$Id$
+"""
+__docformat__ = "reStructuredText"
+import zope.interface
+import zope.schema
+
+from zf.zscp import interfaces, fileformat
+
+
+class Publication(object):
+    """Publication"""
+    zope.interface.implements(interfaces.IPublication)
+
+    packageName = None
+    name = None
+    summary = None
+    description = None
+    homePage = None
+    author = None
+    authorEmail = None
+    license = None
+    platform = None
+    classifier = None
+    developersMailinglist = None
+    usersMailinglist = None
+    issueTracker = None
+    repositoryLocation = None
+    repositoryWebLocation = None
+    certificationLevel = None
+    certificationDate = None
+    metadataVersion = None
+
+    def __repr__(self):
+        return "<%s for '%s' (meta-data version %s)>" % (
+            self.__class__.__name__, self.packageName, self.metadataVersion)
+
+def process(file):
+    publication = Publication()
+    processor = fileformat.HeaderProcessor(publication, interfaces.IPublication)
+    processor(file)
+    return publication
+
+def produce(publication):
+    producer = fileformat.HeaderProducer(publication, interfaces.IPublication)
+    return producer()


Property changes on: zf.zscp/trunk/src/zf/zscp/publication.py
___________________________________________________________________
Name: svn:keywords
   + Id

Added: zf.zscp/trunk/src/zf/zscp/publication.txt
===================================================================
--- zf.zscp/trunk/src/zf/zscp/publication.txt	2006-03-02 00:34:56 UTC (rev 65706)
+++ zf.zscp/trunk/src/zf/zscp/publication.txt	2006-03-02 00:35:40 UTC (rev 65707)
@@ -0,0 +1,245 @@
+=================================
+Handling Package Publication Data
+=================================
+
+The package publication data is stored in an RFC 2822 compatible format. This
+document describes how to parse and generate this file.
+
+Parsing Package Publication Data
+--------------------------------
+
+The publication data file is a collection of simple meta-data entries. The
+file has required and optional fields. So when we simply want to process an
+emoty file, an exception is raised that lists all (missing) required fields:
+
+  >>> import StringIO
+  >>> file = StringIO.StringIO(u'')
+
+  >>> from zf.zscp import publication
+  >>> publication.process(file)
+  Traceback (most recent call last):
+  ...
+  ValueError: Required headers missing: Package-name, Name, Summary, Author,
+              Author-email, License, Metadata-version
+
+So let's now create a file that has at least those required headers:
+
+  >>> file = StringIO.StringIO(u'''\
+  ... Package-name: zope.sample
+  ... Name: Sample Package
+  ... Summary: This is the Sample Package.
+  ... Author: John Doe
+  ... Author-email: john at doe.com
+  ... License: ZPL 2.1
+  ... Metadata-version: 1.0
+  ... ''')
+
+The result of the processing step should be a publication object with all
+those attributes set.
+
+  >>> metadata = publication.process(file)
+  >>> metadata
+  <Publication for 'zope.sample' (meta-data version 1.0)>
+
+  >>> metadata.packageName
+  'zope.sample'
+  >>> metadata.name
+  u'Sample Package'
+  >>> metadata.summary
+  u'This is the Sample Package.'
+  >>> metadata.author
+  [u'John Doe']
+  >>> metadata.authorEmail
+  [u'john at doe.com']
+  >>> metadata.license
+  [u'ZPL 2.1']
+  >>> metadata.metadataVersion
+  u'1.0'
+
+As you can see, some fields are lists, so multiple headers can be specified:
+
+  >>> file = StringIO.StringIO(u'''\
+  ... Package-name: zope.sample
+  ... Name: Sample Package
+  ... Summary: This is the Sample Package.
+  ... Author: John Doe
+  ... Author-email: john at doe.com
+  ... Author: Jane Doe
+  ... Author-email: jane at doe.com
+  ... License: ZPL 2.1
+  ... Metadata-version: 1.0
+  ... ''')
+
+  >>> metadata = publication.process(file)
+  >>> metadata.author
+  [u'John Doe', u'Jane Doe']
+  >>> metadata.authorEmail
+  [u'john at doe.com', u'jane at doe.com']
+
+However, for single fields, you can only provide one value:
+
+  >>> file = StringIO.StringIO(u'''\
+  ... Package-name: zope.sample
+  ... Package-name: zope.sample2
+  ... Name: Sample Package
+  ... Summary: This is the Sample Package.
+  ... Author: John Doe
+  ... Author-email: john at doe.com
+  ... License: ZPL 2.1
+  ... Metadata-version: 1.0
+  ... ''')
+
+  >>> publication.process(file)
+  Traceback (most recent call last):
+  ...
+  ValueError: header 'Package-name' can only be given once
+
+Also, additional headers are simply ignored.
+
+  >>> file = StringIO.StringIO(u'''\
+  ... Package-name: zope.sample
+  ... Faux: Faux Data
+  ... Name: Sample Package
+  ... Summary: This is the Sample Package.
+  ... Author: John Doe
+  ... Author-email: john at doe.com
+  ... License: ZPL 2.1
+  ... Metadata-version: 1.0
+  ... ''')
+
+  >>> metadata = publication.process(file)
+  >>> metadata.faux
+  Traceback (most recent call last):
+  ...
+  AttributeError: 'Publication' object has no attribute 'faux'
+
+We also need to ensure that the values are correctly unescaped. Multi-line
+value blocks are indented for the RFC 822 header format. The unescaping
+should remove that indentation:
+
+  >>> file = StringIO.StringIO(u'''\
+  ... Package-name: zope.sample
+  ... Name: Sample Package
+  ... Summary: This is the Sample Package.
+  ... Description:
+  ...         This is a really great Sample Package. It provides several
+  ...         sample features and is still very sample simple.
+  ...         I hope you enjoy it.
+  ... Author: John Doe
+  ... Author-email: john at doe.com
+  ... License: ZPL 2.1
+  ... Metadata-version: 1.0
+  ... ''')
+
+  >>> metadata = publication.process(file)
+  >>> print metadata.description
+  This is a really great Sample Package. It provides several
+  sample features and is still very sample simple.
+  I hope you enjoy it.
+
+Finally, let's have a full example:
+
+  >>> file = StringIO.StringIO(u'''\
+  ... Package-name: zope.sample
+  ... Name: Sample Package
+  ... Summary: This is the Sample Package.
+  ... Description:
+  ...         This is a really great Sample Package. It provides several
+  ...         sample features and is still very sample simple.
+  ...         I hope you enjoy it.
+  ... Home-page: http://www.zope.org/Products/SamplePackage
+  ... Author: John Doe
+  ... Author-email: john at doe.com
+  ... License: GPL 2.0
+  ... License: ZPL 2.1
+  ... Platform: Windows
+  ... Platform: Unix
+  ... Classifier: Programming Language :: Python
+  ... Classifier: Topic :: Internet :: WWW/HTTP
+  ... Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
+  ... Classifier: Topic :: Software Development :: Libraries :: Python Modules
+  ... Developers-mailinglist: sample-dev at doe.com
+  ... Users-mailinglist: sample-users at doe.com
+  ... Issue-tracker: http://www.zope.org/trackers/sample/
+  ... Repository-location: svn://svn.zope.org/repos/main/sample
+  ... Repository-web-location: http://svn.zope.org/sample
+  ... Certification-level: level1
+  ... Certification-date: 2006-02-28
+  ... Metadata-version: 1.0
+  ... ''')
+
+  >>> metadata = publication.process(file)
+  >>> metadata.packageName
+  'zope.sample'
+  >>> metadata.name
+  u'Sample Package'
+  >>> metadata.summary
+  u'This is the Sample Package.'
+  >>> print metadata.description
+  This is a really great Sample Package. It provides several
+  sample features and is still very sample simple.
+  I hope you enjoy it.
+  >>> metadata.author
+  [u'John Doe']
+  >>> metadata.authorEmail
+  [u'john at doe.com']
+  >>> metadata.license
+  [u'GPL 2.0', u'ZPL 2.1']
+  >>> metadata.platform
+  [u'Windows', u'Unix']
+  >>> metadata.classifier
+  [u'Programming Language :: Python',
+   u'Topic :: Internet :: WWW/HTTP',
+   u'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
+   u'Topic :: Software Development :: Libraries :: Python Modules']
+  >>> metadata.developersMailinglist
+  u'sample-dev at doe.com'
+  >>> metadata.usersMailinglist
+  u'sample-users at doe.com'
+  >>> metadata.issueTracker
+  'http://www.zope.org/trackers/sample/'
+  >>> metadata.repositoryLocation
+  'svn://svn.zope.org/repos/main/sample'
+  >>> metadata.repositoryWebLocation
+  'http://svn.zope.org/sample'
+  >>> metadata.certificationLevel
+  u'level1'
+  >>> metadata.certificationDate
+  datetime.date(2006, 2, 28)
+  >>> metadata.metadataVersion
+  u'1.0'
+
+
+Writing Package Publication Data
+--------------------------------
+
+The writing of the publication data is pretty straight forward. Thus, let's
+just write out the complete publication data above again:
+
+  >>> print publication.produce(metadata)
+  Package-name: zope.sample
+  Name: Sample Package
+  Summary: This is the Sample Package.
+  Description:
+          This is a really great Sample Package. It provides several
+          sample features and is still very sample simple.
+          I hope you enjoy it.
+  Home-page: http://www.zope.org/Products/SamplePackage
+  Author: John Doe
+  Author-email: john at doe.com
+  License: GPL 2.0
+  License: ZPL 2.1
+  Platform: Windows
+  Platform: Unix
+  Classifier: Programming Language :: Python
+  Classifier: Topic :: Internet :: WWW/HTTP
+  Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
+  Classifier: Topic :: Software Development :: Libraries :: Python Modules
+  Developers-mailinglist: sample-dev at doe.com
+  Users-mailinglist: sample-users at doe.com
+  Issue-tracker: http://www.zope.org/trackers/sample/
+  Repository-location: svn://svn.zope.org/repos/main/sample
+  Repository-web-location: http://svn.zope.org/sample
+  Certification-level: level1
+  Certification-date: 2006-02-28
+  Metadata-version: 1.0


Property changes on: zf.zscp/trunk/src/zf/zscp/publication.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zf.zscp/trunk/src/zf/zscp/release.py
===================================================================
--- zf.zscp/trunk/src/zf/zscp/release.py	2006-03-02 00:34:56 UTC (rev 65706)
+++ zf.zscp/trunk/src/zf/zscp/release.py	2006-03-02 00:35:40 UTC (rev 65707)
@@ -0,0 +1,58 @@
+##############################################################################
+#
+# Copyright (c) 2005 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Release
+
+$Id$
+"""
+__docformat__ = "reStructuredText"
+import zope.interface
+import zope.schema
+
+from zf.zscp import interfaces, fileformat, contact
+
+class Release(object):
+    """Release Implementation."""
+    zope.interface.implements(interfaces.IRelease)
+
+    name = None
+    version = None
+    codename = None
+    date = None
+    certification = None
+    package = None
+    source = None
+    dependencies = None
+    announcement = None
+    releaseManager = None
+    pressContact = None
+
+    def __repr__(self):
+        return '<%s name=%r, version=%r, codename=%r>' % (
+            self.__class__.__name__, self.name, self.version, self.codename)
+
+
+_rootField = zope.schema.List(
+    value_type=zope.schema.Object(schema=interfaces.IRelease))
+
+def processXML(xml):
+    """Process the XML to create a list of releases."""
+    handler = fileformat.XMLHandler(
+        'releases', _rootField,
+        {interfaces.IContact: contact.Contact, interfaces.IRelease: Release})
+    return fileformat.processXML(xml, handler)
+
+def produceXML(releases):
+    """Convert the list of releases to XML."""
+    producer = fileformat.InfoProducer(releases, _rootField)
+    return fileformat.produceXML(producer, 'release.xmlt')


Property changes on: zf.zscp/trunk/src/zf/zscp/release.py
___________________________________________________________________
Name: svn:keywords
   + Id

Added: zf.zscp/trunk/src/zf/zscp/release.txt
===================================================================
--- zf.zscp/trunk/src/zf/zscp/release.txt	2006-03-02 00:34:56 UTC (rev 65706)
+++ zf.zscp/trunk/src/zf/zscp/release.txt	2006-03-02 00:35:40 UTC (rev 65707)
@@ -0,0 +1,319 @@
+=============================
+Handling Package Release Data
+=============================
+
+The package release data is stored in an XML file. This document describes how
+to parse and generate this file.
+
+Parsing Package Release Data
+----------------------------
+
+The release file contains several releases. The simplest case is that there
+are no releases.
+
+  >>> import StringIO
+  >>> xml = StringIO.StringIO(u'<releases />')
+
+  >>> from zf.zscp import release
+  >>> releases = release.processXML(xml)
+  >>> releases
+  []
+
+Note that you cannot just have any root element:
+
+  >>> xml = StringIO.StringIO(u'<faux-releases />')
+
+  >>> from zf.zscp import release
+  >>> release.processXML(xml)
+  Traceback (most recent call last):
+  ...
+  ValueError: The root element must be named `releases`
+
+When a release is added, all required fields must be specified:
+
+  >>> xml = StringIO.StringIO(u'''
+  ... <releases>
+  ...   <release>
+  ...    <name>Sample Package</name>
+  ...    <version>0.9.0</version>
+  ...    <date>2006-02-03</date>
+  ...    <certification>level1</certification>
+  ...    <package>http://www.zope.org/SamplePackage/Sample-0.9.0.tgz</package>
+  ...  </release>
+  ... </releases>
+  ... ''')
+
+  >>> releases = release.processXML(xml)
+  >>> releases
+  [<Release name=u'Sample Package', version=u'0.9.0', codename=None>]
+
+All data should be available via the attributes:
+
+  >>> pointNine = releases[0]
+  >>> pointNine.name
+  u'Sample Package'
+  >>> pointNine.version
+  u'0.9.0'
+  >>> pointNine.date
+  datetime.date(2006, 2, 3)
+  >>> pointNine.certification
+  u'level1'
+  >>> pointNine.package
+  'http://www.zope.org/SamplePackage/Sample-0.9.0.tgz'
+
+If a required field is not specified, then an error is raised. In the example
+below, two required fields are missing:
+
+  >>> xml = StringIO.StringIO(u'''
+  ... <releases>
+  ...   <release>
+  ...    <name>Sample Package</name>
+  ...    <certification>level1</certification>
+  ...    <package>http://www.zope.org/SamplePackage/Sample-0.9.0.tgz</package>
+  ...  </release>
+  ... </releases>
+  ... ''')
+
+  >>> release.processXML(xml)
+  Traceback (most recent call last):
+  ...
+  RequiredElementsMissing: Required field(s) 'date', 'version' missing in
+                           `release` (file "<string>", line 7, column 11)
+
+If a required field is missing that has a default value, the default is simply
+used:
+
+  >>> xml = StringIO.StringIO(u'''
+  ... <releases>
+  ...   <release>
+  ...    <name>Sample Package</name>
+  ...    <version>0.9.0</version>
+  ...    <date>2006-02-03</date>
+  ...    <package>http://www.zope.org/SamplePackage/Sample-0.9.0.tgz</package>
+  ...  </release>
+  ... </releases>
+  ... ''')
+
+  >>> pointNine = release.processXML(xml)[0]
+  >>> pointNine.certification
+  u'none'
+
+Finally, you cannot just add sub-elements randomly:
+
+  >>> xml = StringIO.StringIO(u'''
+  ... <releases>
+  ...   <release>
+  ...    <name>
+  ...      Sample Package
+  ...      <subname>Subname</subname>
+  ...    </name>
+  ...    <version>0.9.0</version>
+  ...    <date>2006-02-03</date>
+  ...    <package>http://www.zope.org/SamplePackage/Sample-0.9.0.tgz</package>
+  ...  </release>
+  ... </releases>
+  ... ''')
+
+  >>> release.processXML(xml)
+  Traceback (most recent call last):
+  ...
+  InvalidSubElement: `name` cannot have sub-elements
+                     (file "<string>", line 6, column 14)
+
+
+Let's now have a look at a complex element within the release. The release
+manager and press contact should be converted to contact objects and then
+being added.
+
+  >>> xml = StringIO.StringIO(u'''
+  ... <releases>
+  ...   <release>
+  ...    <name>Sample Package</name>
+  ...    <version>0.9.0</version>
+  ...    <date>2006-02-03</date>
+  ...    <package>http://www.zope.org/SamplePackage/Sample-0.9.0.tgz</package>
+  ...    <release-manager>
+  ...      <name>John Doe</name>
+  ...      <email>john at doe.com</email>
+  ...    </release-manager>
+  ...    <press-contact>
+  ...      <name>Jane Doe</name>
+  ...      <email>jane at doe.com</email>
+  ...    </press-contact>
+  ...  </release>
+  ... </releases>
+  ... ''')
+
+  >>> pointNine = release.processXML(xml)[0]
+  >>> pointNine.releaseManager
+  <Contact 'John Doe <john at doe.com>'>
+  >>> pointNine.pressContact
+  <Contact 'Jane Doe <jane at doe.com>'>
+
+Another complex element case are the dependencies. Dependencies are a list of
+strings, but are represented structurally in XML. Thus they need to be
+converted properly:
+
+  >>> xml = StringIO.StringIO(u'''
+  ... <releases>
+  ...   <release>
+  ...    <name>Sample Package</name>
+  ...    <version>0.9.0</version>
+  ...    <date>2006-02-03</date>
+  ...    <package>http://www.zope.org/SamplePackage/Sample-0.9.0.tgz</package>
+  ...    <dependencies>
+  ...      <dependency>Zope 3.3</dependency>
+  ...      <dependency>Other Software 1.1</dependency>
+  ...    </dependencies>
+  ...  </release>
+  ... </releases>
+  ... ''')
+
+  >>> pointNine = release.processXML(xml)[0]
+  >>> pointNine.dependencies
+  [u'Zope 3.3', u'Other Software 1.1']
+
+And finally a full file of content:
+
+  >>> xml = StringIO.StringIO(u'''
+  ... <releases>
+  ...   <release>
+  ...    <name>Sample Package</name>
+  ...    <version>1.0.0</version>
+  ...    <codename>CoolName</codename>
+  ...    <date>2006-02-03</date>
+  ...    <certification>listed</certification>
+  ...    <package>http://www.zope.org/SamplePackage/Sample-1.0.0.tgz</package>
+  ...    <source>svn://svn.zope.org/zf.sample/tags/1.0.0</source>
+  ...    <announcement>http://www.zope.org/SamplePackage1Released</announcement>
+  ...    <dependencies>
+  ...      <dependency>Zope 3.3</dependency>
+  ...    </dependencies>
+  ...    <release-manager>
+  ...      <name>John Doe</name>
+  ...      <email>john at doe.com</email>
+  ...    </release-manager>
+  ...    <press-contact>
+  ...      <name>Jane Doe</name>
+  ...      <email>jane at doe.com</email>
+  ...    </press-contact>
+  ...  </release>
+  ...   <release>
+  ...    <name>Sample Package</name>
+  ...    <version>0.9.0</version>
+  ...    <date>2006-01-01</date>
+  ...    <package>http://www.zope.org/SamplePackage/Sample-0.9.0.tgz</package>
+  ...  </release>
+  ... </releases>
+  ... ''')
+
+  >>> releases = release.processXML(xml)
+  >>> one = releases[0]
+  >>> one.name
+  u'Sample Package'
+  >>> one.version
+  u'1.0.0'
+  >>> one.codename
+  u'CoolName'
+  >>> one.date
+  datetime.date(2006, 2, 3)
+  >>> one.certification
+  u'listed'
+  >>> one.package
+  'http://www.zope.org/SamplePackage/Sample-1.0.0.tgz'
+  >>> one.source
+  'svn://svn.zope.org/zf.sample/tags/1.0.0'
+  >>> one.announcement
+  'http://www.zope.org/SamplePackage1Released'
+  >>> one.dependencies
+  [u'Zope 3.3']
+  >>> one.releaseManager
+  <Contact 'John Doe <john at doe.com>'>
+  >>> one.pressContact
+  <Contact 'Jane Doe <jane at doe.com>'>
+
+  >>> pointNine = releases[1]
+  >>> pointNine.name
+  u'Sample Package'
+  >>> pointNine.version
+  u'0.9.0'
+  >>> pointNine.date
+  datetime.date(2006, 1, 1)
+  >>> pointNine.certification
+  u'none'
+  >>> pointNine.package
+  'http://www.zope.org/SamplePackage/Sample-0.9.0.tgz'
+
+
+Writing Package Release Data
+----------------------------
+
+The simplest step is to create a totally empty file.
+
+  >>> releases = []
+  >>> print release.produceXML(releases)
+  <releases>
+  </releases>
+
+Now let's add a release to the releases having the minimum data ...
+
+  >>> import datetime
+  >>> pointNine = release.Release()
+  >>> pointNine.name = u'Sample Package'
+  >>> pointNine.version = u'0.9.0'
+  >>> pointNine.date = datetime.date(2006, 2, 3)
+  >>> pointNine.certification = u'level1'
+  >>> pointNine.package = 'http://www.zope.org/SamplePackage/Sample-0.9.0.tgz'
+
+and add it to the releases:
+
+  >>> releases.append(pointNine)
+
+We can now render the structure:
+
+  >>> print release.produceXML(releases)
+  <releases>
+    <release>
+      <name>Sample Package</name>
+      <version>0.9.0</version>
+      <date>2006-02-03</date>
+      <certification>level1</certification>
+      <package>http://www.zope.org/SamplePackage/Sample-0.9.0.tgz</package>
+    </release>
+  </releases>
+
+Let's now also add the complete 1.0.0 (``one``) release in the first position
+to ensure correct output.
+
+  >>> releases.insert(0, one)
+  >>> print release.produceXML(releases)
+  <releases>
+    <release>
+      <name>Sample Package</name>
+      <version>1.0.0</version>
+      <codename>CoolName</codename>
+      <date>2006-02-03</date>
+      <certification>listed</certification>
+      <package>http://www.zope.org/SamplePackage/Sample-1.0.0.tgz</package>
+      <source>svn://svn.zope.org/zf.sample/tags/1.0.0</source>
+      <announcement>http://www.zope.org/SamplePackage1Released</announcement>
+      <dependencies>
+        <dependency>Zope 3.3</dependency>
+      </dependencies>
+      <release-manager>
+        <name>John Doe</name>
+        <email>john at doe.com</email>
+      </release-manager>
+      <press-contact>
+        <name>Jane Doe</name>
+        <email>jane at doe.com</email>
+      </press-contact>
+    </release>
+    <release>
+      <name>Sample Package</name>
+      <version>0.9.0</version>
+      <date>2006-02-03</date>
+      <certification>level1</certification>
+      <package>http://www.zope.org/SamplePackage/Sample-0.9.0.tgz</package>
+    </release>
+  </releases>


Property changes on: zf.zscp/trunk/src/zf/zscp/release.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zf.zscp/trunk/src/zf/zscp/release.xmlt
===================================================================
--- zf.zscp/trunk/src/zf/zscp/release.xmlt	2006-03-02 00:34:56 UTC (rev 65706)
+++ zf.zscp/trunk/src/zf/zscp/release.xmlt	2006-03-02 00:35:40 UTC (rev 65707)
@@ -0,0 +1,33 @@
+<releases>
+  <release tal:repeat="release options/root">
+    <name tal:content="release/name" />
+    <version tal:content="release/version" />
+    <codename
+        tal:condition="release/codename"
+        tal:content="release/codename" />
+    <date tal:content="release/date" />
+    <certification tal:content="release/certification" />
+    <package tal:content="release/package" />
+    <source
+        tal:condition="release/source"
+        tal:content="release/source" />
+    <announcement
+        tal:condition="release/announcement"
+        tal:content="release/announcement" />
+    <dependencies
+        tal:condition="release/dependencies"
+        tal:repeat="dependency release/dependencies">
+      <dependency tal:content="dependency" />
+    </dependencies>
+    <release-manager
+        tal:condition="release/releaseManager">
+      <name tal:content="release/releaseManager/name" />
+      <email tal:content="release/releaseManager/email" />
+    </release-manager>
+    <press-contact
+        tal:condition="release/pressContact">
+      <name tal:content="release/pressContact/name" />
+      <email tal:content="release/pressContact/email" />
+    </press-contact>
+  </release>
+</releases>

Added: zf.zscp/trunk/src/zf/zscp/tests.py
===================================================================
--- zf.zscp/trunk/src/zf/zscp/tests.py	2006-03-02 00:34:56 UTC (rev 65706)
+++ zf.zscp/trunk/src/zf/zscp/tests.py	2006-03-02 00:35:40 UTC (rev 65707)
@@ -0,0 +1,208 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Viewlet tests
+
+$Id$
+"""
+__docformat__ = 'restructuredtext'
+import os
+import unittest
+import StringIO
+from zope.testing import doctest
+from zope.testing.doctestunit import DocFileSuite, pprint
+
+class Directory(dict):
+
+    def checkout(self, target, svnPath):
+        os.mkdir(target)
+        # Write a local .svn file that stores the target data.
+        svnFile = file(os.path.join(target, '.svn'), 'w')
+        svnFile.write(svnPath)
+        svnFile.close()
+        # Now recurse through the sub directories
+        for name, value in self.items():
+            path = os.path.join(target, name)
+            value.checkout(path, os.path.join(svnPath, name))
+
+    def checkin(self, localPath):
+        for name, value in self.items():
+            path = os.path.join(localPath, name)
+            value.checkin(path)
+
+    def update(self, localPath):
+        if not os.path.exists(localPath):
+            os.mkdir(localPath)
+        for name, value in self.items():
+            fullPath = os.path.join(localPath, name)
+            value.update(fullPath)
+
+
+class File(StringIO.StringIO):
+
+    def checkout(self, target, svnPath):
+        f = file(target, 'w')
+        f.write(self.getvalue())
+        f.close()
+
+    def checkin(self, localPath):
+        f = file(localPath, 'r')
+        self.__init__(f.read())
+        f.close()
+
+    def update(self, localPath):
+        f = file(localPath, 'w')
+        f.write(self.getvalue())
+        f.close()
+
+
+class SVNTestClient(object):
+
+    root = None
+    dir = None
+    adds = []
+
+    def _getSVNPath(self, svnDir):
+        local_path = svnDir.replace(self.root, '')
+        obj = self.dir
+        for segment in local_path.split('/'):
+            if segment == '':
+                continue
+            obj = obj[segment]
+        return obj
+
+    def checkout(self, svnDir, localDir):
+        obj = self._getSVNPath(svnDir)
+        obj.checkout(localDir, svnDir)
+
+    def checkin(self, localDir, message):
+        while self.adds:
+            dir = self.adds.pop()
+            baseDir, name = os.path.split(dir)
+            svnPath = file(os.path.join(baseDir, '.svn'), 'r').read()
+            obj = self._getSVNPath(svnPath)
+            if os.path.isdir(dir):
+                obj[name] = Directory()
+            else:
+                f = file(dir)
+                obj[name] = File(f.read())
+                f.close()
+
+        svnPath = file(os.path.join(localDir, '.svn'), 'r').read()
+        self._getSVNPath(svnPath).checkin(localDir)
+
+    def update(self, localDir):
+        svnPath = file(os.path.join(localDir, '.svn'), 'r').read()
+        obj = self._getSVNPath(svnPath)
+        obj.update(localDir)
+
+    def ls(self, svnDir):
+        obj = self._getSVNPath(svnDir)
+        return [{'name': svnDir + '/' + name} for name in obj]
+
+    def mkdir(self, svnDir, message):
+        svnDir, name = os.path.split(svnDir)
+        obj = self._getSVNPath(svnDir)
+        obj[name] = Directory()
+
+    def add(self, files):
+        if isinstance(files, list):
+            self.adds += files
+        else:
+            self.adds.append(files)
+
+    def remove(self, svnPath):
+        svnDir, name = os.path.split(svnPath)
+        obj = self._getSVNPath(svnDir)
+        del obj[name]
+
+
+ZSCP_cfg = '''\
+publication PUBLICATION.cfg
+certifications CERTIFICATIONS.xml
+releases RELEASES.xml
+'''
+
+PUBLICATION_cfg = '''\
+Package-name: zope.sample
+Name: Sample Package
+Summary: This is the Sample Package.
+Author: John Doe
+Author-email: john at doe.com
+License: ZPL 2.1
+Metadata-version: 1.0
+'''
+
+RELEASES_xml = '''\
+<releases>
+  <release>
+   <name>Sample Package</name>
+   <version>0.9.0</version>
+   <date>2006-02-03</date>
+   <certification>level1</certification>
+   <package>http://www.zope.org/SamplePackage/Sample-0.9.0.tgz</package>
+ </release>
+</releases>
+'''
+
+CERTIFICATIONS_xml = '''\
+<certifications>
+  <certification>
+    <action>grant</action>
+    <source-level>none</source-level>
+    <target-level>listed</target-level>
+    <date>2006-01-01</date>
+    <certification-manager>
+      <name>John Doe</name>
+      <email>john at doe.com</email>
+    </certification-manager>
+  </certification>
+</certifications>
+'''
+
+def zscpSetUp(test):
+    client = SVNTestClient()
+
+    client.dir = Directory()
+    client.dir['zope.sample'] = Directory()
+    client.dir['zope.sample']['zscp'] = Directory()
+    client.dir['zope.sample']['zscp']['ZSCP.cfg'] = File(ZSCP_cfg)
+    client.dir['zope.sample']['zscp']['PUBLICATION.cfg'] = File(PUBLICATION_cfg)
+    client.dir['zope.sample']['zscp']['RELEASES.xml'] = File(RELEASES_xml)
+    client.dir['zope.sample']['zscp']['CERTIFICATIONS.xml'] = File(
+        CERTIFICATIONS_xml)
+    client.dir['zope.sample1'] = Directory()
+    client.dir['zope.sample2'] = Directory()
+
+    test.globs['svnClient'] = client
+
+def test_suite():
+    return unittest.TestSuite((
+        DocFileSuite('release.txt',
+                     optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,
+                     ),
+        DocFileSuite('certification.txt',
+                     optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,
+                     ),
+        DocFileSuite('publication.txt',
+                     optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,
+                     ),
+        DocFileSuite('zscp.txt',
+                     setUp=zscpSetUp,
+                     globs={'pprint': pprint},
+                     optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,
+                     ),
+        ))
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')


Property changes on: zf.zscp/trunk/src/zf/zscp/tests.py
___________________________________________________________________
Name: svn:keywords
   + Id

Added: zf.zscp/trunk/src/zf/zscp/website/__init__.py
===================================================================
--- zf.zscp/trunk/src/zf/zscp/website/__init__.py	2006-03-02 00:34:56 UTC (rev 65706)
+++ zf.zscp/trunk/src/zf/zscp/website/__init__.py	2006-03-02 00:35:40 UTC (rev 65707)
@@ -0,0 +1 @@
+# Make a pacakge.


Property changes on: zf.zscp/trunk/src/zf/zscp/website/__init__.py
___________________________________________________________________
Name: svn:keywords
   + Id

Added: zf.zscp/trunk/src/zf/zscp/website/interfaces.py
===================================================================
--- zf.zscp/trunk/src/zf/zscp/website/interfaces.py	2006-03-02 00:34:56 UTC (rev 65706)
+++ zf.zscp/trunk/src/zf/zscp/website/interfaces.py	2006-03-02 00:35:40 UTC (rev 65707)
@@ -0,0 +1,25 @@
+##############################################################################
+#
+# Copyright (c) 2005 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""ZSCP Web Site Interfaces
+
+$Id$
+"""
+__docformat__ = "reStructuredText"
+from zope.app import folder
+
+class IZSCPSite(folder.interfaces.IFolder):
+    """The root object for the ZSCP site.
+
+    The site mainly contains ZSCP repository objects.
+    """


Property changes on: zf.zscp/trunk/src/zf/zscp/website/interfaces.py
___________________________________________________________________
Name: svn:keywords
   + Id

Added: zf.zscp/trunk/src/zf/zscp/website/zscp.py
===================================================================
--- zf.zscp/trunk/src/zf/zscp/website/zscp.py	2006-03-02 00:34:56 UTC (rev 65706)
+++ zf.zscp/trunk/src/zf/zscp/website/zscp.py	2006-03-02 00:35:40 UTC (rev 65707)
@@ -0,0 +1,29 @@
+##############################################################################
+#
+# Copyright (c) 2005 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""ZSCP Web Site
+
+$Id$
+"""
+__docformat__ = "reStructuredText"
+import zope.interface
+from zope.app import folder
+
+from zf.zscp.website import interfaces
+
+
+class ZSCPSite(folder.folder.Folder):
+    zope.interface.implements(interface.IZSCPSite)
+
+
+


Property changes on: zf.zscp/trunk/src/zf/zscp/website/zscp.py
___________________________________________________________________
Name: svn:keywords
   + Id

Added: zf.zscp/trunk/src/zf/zscp/zscp.py
===================================================================
--- zf.zscp/trunk/src/zf/zscp/zscp.py	2006-03-02 00:34:56 UTC (rev 65706)
+++ zf.zscp/trunk/src/zf/zscp/zscp.py	2006-03-02 00:35:40 UTC (rev 65707)
@@ -0,0 +1,216 @@
+##############################################################################
+#
+# Copyright (c) 2005 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""ZSCP Data Management
+
+$Id$
+"""
+__docformat__ = "reStructuredText"
+import os
+import os.path
+import pysvn
+import zope.event
+import zope.interface
+
+from zf.zscp import interfaces, package, publication, release, certification
+
+
+class ZSCPRepository(object):
+    """A ZSCP-compliant repository."""
+    zope.interface.implements(interfaces.IZSCPRepository)
+
+    def __init__(self, svnRoot, localRoot, password):
+        self.svnRoot = svnRoot
+        self.localRoot = localRoot
+        self.password = password
+
+    def _getClient(self):
+        """Get an SVN client."""
+        client = pysvn.Client()
+
+        def ssl_password(realm, may_save):
+            return True, self.password, True
+        client.callback_ssl_client_cert_password_prompt = ssl_password
+
+        return client
+
+    def initialize(self):
+        """See interfaces.IZSCPRepository"""
+        client = self._getClient()
+        # for each package that is already part of the ZSCP, make sure to
+        # check out the ZSCP data.
+        for name in self.fetch():
+            full_url = self.svnRoot + '/' + name + '/zscp'
+            local_path = os.path.join(self.localRoot, name)
+            client.checkout(full_url, local_path)
+        # Send out an event notification
+        zope.event.notify(interfaces.RepositoryInitializedEvent(self))
+
+    def register(self, pkg):
+        """See interfaces.IZSCPRepository"""
+        client = self._getClient()
+        full_url = self.svnRoot + '/' + pkg.name + '/zscp'
+        local_path = os.path.join(self.localRoot, pkg.name)
+        # Create a directory for zscp remotely
+        client.mkdir(full_url, 'Create a directory for the ZSCP process.')
+        # Check out the directory
+        client.checkout(full_url, local_path)
+        # Create a ZSCP.cfg file
+        zscp = {'publication': 'PUBLICATION.cfg',
+                'releases': 'RELEASES.xml',
+                'certifications': 'CERTIFICATIONS.xml'}
+        zscp_file = file(os.path.join(local_path, 'ZSCP.cfg'), 'w')
+        zscp_file.write(produce(zscp))
+        zscp_file.close()
+        # Now update that data
+        self.update(pkg)
+        # Add all files to the repository and check them in
+        client.add([os.path.join(local_path, 'ZSCP.cfg'),
+                    os.path.join(local_path, 'PUBLICATION.cfg'),
+                    os.path.join(local_path, 'RELEASES.xml'),
+                    os.path.join(local_path, 'CERTIFICATIONS.xml')])
+        client.checkin(local_path, 'Initial addition of package data.')
+        # Send out an event notification
+        zope.event.notify(interfaces.PackageRegisteredEvent(pkg))
+
+    def unregister(self, pkg):
+        """See interfaces.IZSCPRepository"""
+        client = self._getClient()
+        full_url = self.svnRoot + '/' + pkg.name + '/zscp'
+        # Remove the directory
+        client.remove(full_url)
+        # Remove local checkout
+        # LAME! Simulate recursive delete!
+        def remove(arg, dirname, fnames):
+            for fname in fnames:
+                path = os.path.join(dirname, fname)
+                if os.path.isfile(path):
+                    os.remove(path)
+
+        os.path.walk(os.path.join(self.localRoot, pkg.name), remove, None)
+        os.removedirs(os.path.join(self.localRoot, pkg.name))
+        # Send out an event notification
+        zope.event.notify(interfaces.PackageUnregisteredEvent(pkg))
+
+    def update(self, pkg):
+        """See interfaces.IZSCPRepository"""
+        client = self._getClient()
+        local_path = os.path.join(self.localRoot, pkg.name)
+        # Do checkout update
+        client.update(local_path)
+        # Load the ZSCP configuration
+        zscp_path = os.path.join(local_path, 'ZSCP.cfg')
+        zscp = process(file(zscp_path))
+        # Update publication
+        pub_file = file(os.path.join(local_path, zscp['publication']), 'w')
+        pub_file.write(publication.produce(pkg.publication))
+        pub_file.close()
+        # Update releases
+        rel_file = file(os.path.join(local_path, zscp['releases']), 'w')
+        rel_file.write(release.produceXML(pkg.releases))
+        rel_file.close()
+        # Update certifications
+        cert_file = file(os.path.join(local_path, zscp['certifications']), 'w')
+        cert_file.write(certification.produceXML(pkg.certifications))
+        cert_file.close()
+        # Commit the changes
+        client.checkin(local_path, 'Update of package data.')
+        # Send out an event notification
+        zope.event.notify(interfaces.PackageUpdatedEvent(pkg))
+
+    def fetch(self, all=False):
+        """See interfaces.IZSCPRepository"""
+        names = []
+        client = self._getClient()
+        for entry in client.ls(self.svnRoot):
+            name = os.path.split(entry['name'])[-1]
+            if all is True:
+                names.append(name)
+            else:
+                path = entry['name'] + '/zscp'
+                if path in [e['name'] for e in client.ls(entry['name'])]:
+                    names.append(name)
+        return names
+
+    def __getitem__(self, key):
+        """See zope.interface.common.mapping.IItemMapping"""
+        if key not in os.listdir(self.localRoot):
+            raise KeyError, key
+
+        local_path = os.path.join(self.localRoot, key)
+        # Load the ZSCP configuration
+        zscp_path = os.path.join(local_path, 'ZSCP.cfg')
+        zscp = process(file(zscp_path))
+        # Create the package
+        pkg = package.Package(key)
+        # Add publication
+        pub_file = file(os.path.join(local_path, zscp['publication']), 'r')
+        pkg.publication = publication.process(pub_file)
+        pub_file.close()
+        # Add releases
+        rel_file = file(os.path.join(local_path, zscp['releases']), 'r')
+        pkg.releases = release.processXML(rel_file)
+        rel_file.close()
+        # Add certifications
+        cert_file = file(os.path.join(local_path, zscp['certifications']), 'r')
+        pkg.certifications = certification.processXML(cert_file)
+        cert_file.close()
+
+        return pkg
+
+    def keys(self):
+        """See zope.interface.common.mapping.IEnumerableMapping"""
+        return os.listdir(self.localRoot)
+
+    def get(self, key, default=None):
+        """See zope.interface.common.mapping.IReadMapping"""
+        try:
+            return self[key]
+        except KeyError:
+            return default
+
+    def __contains__(self, key):
+        """See zope.interface.common.mapping.IReadMapping"""
+        return key in self.keys()
+
+    def __iter__(self):
+        """See zope.interface.common.mapping.IEnumerableMapping"""
+        return iter(keys)
+
+    def values(self):
+        """See zope.interface.common.mapping.IEnumerableMapping"""
+        return [self[name] for name in self.keys()]
+
+    def items(self):
+        """See zope.interface.common.mapping.IEnumerableMapping"""
+        return [(name, self[name]) for name in self.keys()]
+
+    def __len__(self):
+        """See zope.interface.common.mapping.IEnumerableMapping"""
+        return len(self.keys())
+
+
+def process(file):
+    """Process the ZSCP.cfg file content."""
+    config = {}
+    for line in file.readlines():
+        line = line.strip()
+        if line.startswith('#') or not line:
+            continue
+        key, value = line.split(' ')
+        config[key] = value
+    return config
+
+def produce(zscp):
+    """Produce the ZSCP.cfg file content."""
+    return '\n'.join(['%s %s' %(key, value) for key, value in zscp.items()])


Property changes on: zf.zscp/trunk/src/zf/zscp/zscp.py
___________________________________________________________________
Name: svn:keywords
   + Id

Added: zf.zscp/trunk/src/zf/zscp/zscp.txt
===================================================================
--- zf.zscp/trunk/src/zf/zscp/zscp.txt	2006-03-02 00:34:56 UTC (rev 65706)
+++ zf.zscp/trunk/src/zf/zscp/zscp.txt	2006-03-02 00:35:40 UTC (rev 65707)
@@ -0,0 +1,203 @@
+=====================
+A ZSCP Implementation
+=====================
+
+The zscp module,
+
+  >>> from zf.zscp import zscp
+
+implements the process for an SVN repository. It is controoled by the ZSCP
+repository object:
+
+  >>> import tempfile
+  >>> localRoot = tempfile.mkdtemp()
+  >>> repos = zscp.ZSCPRepository(
+  ...   'svn+ssh://svn.zope.org/repos/main', localRoot, 'mypass')
+
+The first argument of the constructor is the SVN URL to the repository, the
+second is the directory that will be used to checkout the ``zscp`` directories
+and the final argument is the passphrase for the SSL authentication.
+
+For the purpose of this test, we register a stub pysvn client:
+
+  >>> svnClient.root = 'svn+ssh://svn.zope.org/repos/main'
+  >>> repos._getClient = lambda : svnClient
+
+The initial test SVN repository looks as follows::
+
+  root/
+    zope.sample/
+      zscp/
+        ZSCP.cfg
+        PUBLICATION.cfg
+        RELEASES.xml
+        CERTIFICATIONS.xml
+    zope.sample1/
+    zope.sample2/
+
+With the setup all done, let's see how the ZSCP is realized. The first step is
+to initialize the repository, which means that all available package ``zscp``
+directories are checked out from the repository. In the local copy, those
+``zscp`` directories will be known by their package name:
+
+  >>> repos.initialize()
+
+  >>> import os
+  >>> os.listdir(localRoot)
+  ['zope.sample']
+
+Since only the ``zope.sample`` package has a ``zscp`` directory, it is the
+only one checked out. You can also use the ``fetch()`` method to get a list of
+all ZSCP packages in the repository:
+
+  >>> repos.fetch()
+  ['zope.sample']
+
+When you pass ``all=True`` to the method, then all packages in the repository
+will be returned:
+
+  >>> sorted(repos.fetch(all=True))
+  ['zope.sample', 'zope.sample1', 'zope.sample2']
+
+Once the repository is initialized, you can use the mapping interface to
+discover the content:
+
+  >>> len(repos)
+  1
+
+  >>> 'zope.sample' in repos
+  True
+  >>> 'zope.sample1' in repos
+  False
+
+  >>> repos['zope.sample']
+  <Package 'zope.sample'>
+  >>> repos.get('zope.sample')
+  <Package 'zope.sample'>
+  >>> repos.get('zope.sample1') is None
+  True
+
+  >>> repos.keys()
+  ['zope.sample']
+  >>> repos.items()
+  [('zope.sample', <Package 'zope.sample'>)]
+  >>> repos.values()
+  [<Package 'zope.sample'>]
+
+Now we would like to register a new package with the ZSCP process. This is
+done by first creating a package ...
+
+  >>> from zf.zscp import package, publication
+  >>> sample1 = package.Package('zope.sample1')
+
+  >>> sample1.publication = publication.Publication()
+  >>> sample1.publication.packageName = 'zope.sample1'
+  >>> sample1.publication.name = u'Sample Package 1'
+  >>> sample1.publication.summary = u'This is the Sample Package 1.'
+  >>> sample1.publication.author = [u'Jane Doe']
+  >>> sample1.publication.authorEmail = [u'jane at doe.com']
+  >>> sample1.publication.license = [u'GPL 2.0']
+  >>> sample1.publication.metadataVersion = u'1.0'
+
+  >>> sample1.certifications = []
+  >>> sample1.releases = []
+
+and then registering it:
+
+  >>> repos.register(sample1)
+
+Let's make sure all the data was really stored in the SVN repository:
+
+  >>> sorted(repos.items())
+  [('zope.sample', <Package 'zope.sample'>),
+   ('zope.sample1', <Package 'zope.sample1'>)]
+
+  >>> svnClient.dir['zope.sample1']['zscp'].keys()
+  ['PUBLICATION.cfg', 'CERTIFICATIONS.xml', 'RELEASES.xml', 'ZSCP.cfg']
+
+At the beginning there are no certifications:
+
+  >>> sample1.certifications
+  []
+  >>> print svnClient.dir['zope.sample1']['zscp']['CERTIFICATIONS.xml'].read()
+  <certifications>
+  </certifications>
+
+Let's now add a certification:
+
+  >>> import datetime
+  >>> from zf.zscp import certification, contact
+  >>> listed = certification.Certification()
+  >>> listed.action = u'grant'
+  >>> listed.sourceLevel = u'none'
+  >>> listed.targetLevel = u'listed'
+  >>> listed.date = datetime.date(2006, 1, 1)
+  >>> listed.certificationManager = contact.Contact()
+  >>> listed.certificationManager.name = 'John Doe'
+  >>> listed.certificationManager.email = 'john at doe.com'
+  >>> sample1.certifications.append(listed)
+
+To update the checkout and the repository, we can do the following:
+
+  >>> repos.update(sample1)
+
+So now we should have an entry:
+
+  >>> sample1.certifications
+  [<Certification action=u'grant', source=u'none', target=u'listed'>]
+  >>> svnClient.dir['zope.sample1']['zscp']['CERTIFICATIONS.xml'].seek(0)
+  >>> print svnClient.dir['zope.sample1']['zscp']['CERTIFICATIONS.xml'].read()
+  <certifications>
+    <certification>
+      <action>grant</action>
+      <source-level>none</source-level>
+      <target-level>listed</target-level>
+      <date>2006-01-01</date>
+      <certification-manager>
+        <name>John Doe</name>
+        <email>john at doe.com</email>
+      </certification-manager>
+    </certification>
+  </certifications>
+
+In case a project wants to be removed from the ZSCP process, you simply
+unregister it:
+
+  >>> repos.unregister(sample1)
+
+It should be gone in the SVN repository and the local checkout:
+
+
+  >>> print svnClient.dir['zope.sample1'].keys()
+  []
+  >>> os.listdir(localRoot)
+  ['zope.sample']
+
+And that's it.
+
+
+Parsing and writing the ``ZSCP.cfg`` file
+-----------------------------------------
+
+It is necessary to parse the ``ZSCP.cfg`` file in order to determine the
+locations of the other data files.
+
+  >>> import StringIO
+  >>> config = StringIO.StringIO(u'''
+  ...    publication PUBLICATION.cfg
+  ...    releases RELEASES.xml
+  ...    certifications CERTIFICATIONS.xml
+  ... ''')
+
+  >>> zscp_data = zscp.process(config)
+  >>> pprint(zscp_data)
+  {u'certifications': u'CERTIFICATIONS.xml',
+   u'publication': u'PUBLICATION.cfg',
+   u'releases': u'RELEASES.xml'}
+
+On the other hand, we also need to be able create the contents of the file:
+
+  >>> print zscp.produce(zscp_data)
+  certifications CERTIFICATIONS.xml
+  publication PUBLICATION.cfg
+  releases RELEASES.xml


Property changes on: zf.zscp/trunk/src/zf/zscp/zscp.txt
___________________________________________________________________
Name: svn:eol-style
   + native



More information about the Zope-CVS mailing list