
srichter at cosmos
Mar 1, 2006, 4:35 PM
Post #1 of 1
(578 views)
Permalink
|
|
SVN: zf.zscp/trunk/src/zf/zscp/ Import the initial ZSCP code.
|
|
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</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>'> + +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</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</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</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>'> + >>> 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>'> + + +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' + +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</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</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</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 + ... 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'] + >>> 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 + ... Author: Jane Doe + ... Author-email: jane [at] doe + ... 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', u'jane [at] doe'] + +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 + ... 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 + ... 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 + ... 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 + ... 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 + ... Users-mailinglist: sample-users [at] doe + ... 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'] + >>> 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' + >>> metadata.usersMailinglist + u'sample-users [at] doe' + >>> 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 + 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 + Users-mailinglist: sample-users [at] doe + 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</email> + ... </release-manager> + ... <press-contact> + ... <name>Jane Doe</name> + ... <email>jane [at] doe</email> + ... </press-contact> + ... </release> + ... </releases> + ... ''') + + >>> pointNine = release.processXML(xml)[0] + >>> pointNine.releaseManager + <Contact 'John Doe <john [at] doe>'> + >>> pointNine.pressContact + <Contact 'Jane Doe <jane [at] doe>'> + +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</email> + ... </release-manager> + ... <press-contact> + ... <name>Jane Doe</name> + ... <email>jane [at] doe</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>'> + >>> one.pressContact + <Contact 'Jane Doe <jane [at] doe>'> + + >>> 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</email> + </release-manager> + <press-contact> + <name>Jane Doe</name> + <email>jane [at] doe</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 +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</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'] + >>> 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' + >>> 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</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 _______________________________________________ Zope-CVS maillist - Zope-CVS [at] zope http://mail.zope.org/mailman/listinfo/zope-cvs Zope CVS instructions: http://dev.zope.org/CVS
|