imip-agent

Changeset

69:8d808f219d79
2014-10-26 Paul Boddie raw files shortlog changelog graph Added the beginnings of a Web-based scheduling manager plus the markup module.
imip_manager.py (file) markup.py (file)
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/imip_manager.py	Sun Oct 26 00:45:58 2014 +0200
     1.3 @@ -0,0 +1,125 @@
     1.4 +#!/usr/bin/env python
     1.5 +
     1.6 +import cgi, os, sys
     1.7 +
     1.8 +sys.path.append("/var/lib/imip-agent")
     1.9 +
    1.10 +import markup
    1.11 +import imip_store
    1.12 +
    1.13 +getenv = os.environ.get
    1.14 +setenv = os.environ.__setitem__
    1.15 +
    1.16 +class CGIEnvironment:
    1.17 +
    1.18 +    "A CGI-compatible environment."
    1.19 +
    1.20 +    def __init__(self):
    1.21 +        self.args = None
    1.22 +        self.method = None
    1.23 +        self.path = None
    1.24 +        self.path_info = None
    1.25 +        self.user = None
    1.26 +
    1.27 +    def get_args(self):
    1.28 +        if self.args is None:
    1.29 +            if self.get_method() != "POST":
    1.30 +                setenv("QUERY_STRING", "")
    1.31 +            self.args = cgi.parse(keep_blank_values=True)
    1.32 +        return self.args
    1.33 +
    1.34 +    def get_method(self):
    1.35 +        if self.method is None:
    1.36 +            self.method = getenv("REQUEST_METHOD") or "GET"
    1.37 +        return self.method
    1.38 +
    1.39 +    def get_path(self):
    1.40 +        if self.path is None:
    1.41 +            self.path = getenv("SCRIPT_NAME") or ""
    1.42 +        return self.path
    1.43 +
    1.44 +    def get_path_info(self):
    1.45 +        if self.path_info is None:
    1.46 +            self.path_info = getenv("PATH_INFO") or ""
    1.47 +        return self.path_info
    1.48 +
    1.49 +    def get_user(self):
    1.50 +        if self.user is None:
    1.51 +            self.user = getenv("REMOTE_USER") or ""
    1.52 +        return self.user
    1.53 +
    1.54 +    def get_output(self):
    1.55 +        return sys.stdout
    1.56 +
    1.57 +    def get_url(self):
    1.58 +        path = self.get_path()
    1.59 +        path_info = self.get_path_info()
    1.60 +        return "%s%s" % (path.rstrip("/"), path_info)
    1.61 +
    1.62 +class Manager:
    1.63 +
    1.64 +    "A simple manager application."
    1.65 +
    1.66 +    def __init__(self):
    1.67 +        self.store = imip_store.FileStore()
    1.68 +        self.env = CGIEnvironment()
    1.69 +        user = self.env.get_user()
    1.70 +        self.user = user and "mailto:%s" % user or None
    1.71 +        self.out = self.env.get_output()
    1.72 +        self.page = markup.page()
    1.73 +        self.encoding = "utf-8"
    1.74 +
    1.75 +    def new_page(self, title):
    1.76 +        self.page.init(title=title, charset=self.encoding)
    1.77 +
    1.78 +    def status(self, code, message):
    1.79 +        print >>self.out, "Status:", code, message
    1.80 +
    1.81 +    def no_user(self):
    1.82 +        self.status(403, "Forbidden")
    1.83 +        self.new_page(title="Forbidden")
    1.84 +        self.page.p("You are not logged in and thus cannot access scheduling requests.")
    1.85 +
    1.86 +    def show_requests(self):
    1.87 +
    1.88 +        "Show requests for the current user."
    1.89 +
    1.90 +        # NOTE: This list could be more informative, but it is envisaged that
    1.91 +        # NOTE: the requests would be visited directly anyway.
    1.92 +
    1.93 +        self.new_page(title="Pending Requests")
    1.94 +        self.page.ul()
    1.95 +
    1.96 +        requests = self.store.get_requests(self.user)
    1.97 +        for request in requests:
    1.98 +            self.page.li()
    1.99 +            self.page.a(request, href="%s/%s" % (self.env.get_url(), request))
   1.100 +            self.page.li.close()
   1.101 +
   1.102 +        self.page.ul.close()
   1.103 +
   1.104 +    def select_action(self):
   1.105 +
   1.106 +        "Select the desired action and show the result."
   1.107 +
   1.108 +        path_info = self.env.get_path_info().rstrip("/")
   1.109 +        if not path_info:
   1.110 +            self.show_requests()
   1.111 +
   1.112 +    def show(self):
   1.113 +
   1.114 +        "Interpret a request and show an appropriate response."
   1.115 +
   1.116 +        if not self.user:
   1.117 +            self.no_user()
   1.118 +        else:
   1.119 +            self.select_action()
   1.120 +
   1.121 +        print >>self.out, "Content-Type: text/html; charset=%s" % self.encoding
   1.122 +        print >>self.out
   1.123 +        self.out.write(unicode(self.page).encode(self.encoding))
   1.124 +
   1.125 +if __name__ == "__main__":
   1.126 +    Manager().show()
   1.127 +
   1.128 +# vim: tabstop=4 expandtab shiftwidth=4
     2.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     2.2 +++ b/markup.py	Sun Oct 26 00:45:58 2014 +0200
     2.3 @@ -0,0 +1,527 @@
     2.4 +# This code is in the public domain, it comes
     2.5 +# with absolutely no warranty and you can do
     2.6 +# absolutely whatever you want with it.
     2.7 +
     2.8 +__date__ = '1 October 2012'
     2.9 +__version__ = '1.9'
    2.10 +__doc__= """
    2.11 +This is markup.py - a Python module that attempts to
    2.12 +make it easier to generate HTML/XML from a Python program
    2.13 +in an intuitive, lightweight, customizable and pythonic way.
    2.14 +
    2.15 +The code is in the public domain.
    2.16 +
    2.17 +Version: %s as of %s.
    2.18 +
    2.19 +Documentation and further info is at http://markup.sourceforge.net/
    2.20 +
    2.21 +Please send bug reports, feature requests, enhancement
    2.22 +ideas or questions to nogradi at gmail dot com.
    2.23 +
    2.24 +Installation: drop markup.py somewhere into your Python path.
    2.25 +""" % ( __version__, __date__ )
    2.26 +
    2.27 +try:
    2.28 +    basestring
    2.29 +    import string
    2.30 +except:
    2.31 +    # python 3
    2.32 +    basestring = str
    2.33 +    string = str
    2.34 +
    2.35 +# tags which are reserved python keywords will be referred 
    2.36 +# to by a leading underscore otherwise we end up with a syntax error
    2.37 +import keyword
    2.38 +
    2.39 +class element:
    2.40 +    """This class handles the addition of a new element."""
    2.41 +
    2.42 +    def __init__( self, tag, case='lower', parent=None ):
    2.43 +        self.parent = parent
    2.44 +
    2.45 +        if case == 'upper':
    2.46 +            self.tag = tag.upper( )
    2.47 +        elif case == 'lower':
    2.48 +            self.tag = tag.lower( )
    2.49 +        elif case =='given':
    2.50 +            self.tag = tag
    2.51 +        else:
    2.52 +            self.tag = tag
    2.53 +    
    2.54 +    def __call__( self, *args, **kwargs ):
    2.55 +        if len( args ) > 1:
    2.56 +            raise ArgumentError( self.tag )
    2.57 +
    2.58 +        # if class_ was defined in parent it should be added to every element
    2.59 +        if self.parent is not None and self.parent.class_ is not None:
    2.60 +            if 'class_' not in kwargs:
    2.61 +                kwargs['class_'] = self.parent.class_
    2.62 +            
    2.63 +        if self.parent is None and len( args ) == 1:
    2.64 +            x = [ self.render( self.tag, False, myarg, mydict ) for myarg, mydict in _argsdicts( args, kwargs ) ]
    2.65 +            return '\n'.join( x )
    2.66 +        elif self.parent is None and len( args ) == 0:
    2.67 +            x = [ self.render( self.tag, True, myarg, mydict ) for myarg, mydict in _argsdicts( args, kwargs ) ]
    2.68 +            return '\n'.join( x )
    2.69 +            
    2.70 +        if self.tag in self.parent.twotags:
    2.71 +            for myarg, mydict in _argsdicts( args, kwargs ):
    2.72 +                self.render( self.tag, False, myarg, mydict )
    2.73 +        elif self.tag in self.parent.onetags:
    2.74 +            if len( args ) == 0:
    2.75 +                for myarg, mydict in _argsdicts( args, kwargs ):
    2.76 +                    self.render( self.tag, True, myarg, mydict )    # here myarg is always None, because len( args ) = 0
    2.77 +            else:
    2.78 +                raise ClosingError( self.tag )
    2.79 +        elif self.parent.mode == 'strict_html' and self.tag in self.parent.deptags:
    2.80 +            raise DeprecationError( self.tag )
    2.81 +        else:
    2.82 +            raise InvalidElementError( self.tag, self.parent.mode )
    2.83 +    
    2.84 +    def render( self, tag, single, between, kwargs ):
    2.85 +        """Append the actual tags to content."""
    2.86 +
    2.87 +        out = "<%s" % tag
    2.88 +        for key, value in list( kwargs.items( ) ):
    2.89 +            if value is not None:               # when value is None that means stuff like <... checked>
    2.90 +                key = key.strip('_')            # strip this so class_ will mean class, etc.
    2.91 +                if key == 'http_equiv':         # special cases, maybe change _ to - overall?
    2.92 +                    key = 'http-equiv'
    2.93 +                elif key == 'accept_charset':
    2.94 +                    key = 'accept-charset'
    2.95 +                out = "%s %s=\"%s\"" % ( out, key, escape( value ) )
    2.96 +            else:
    2.97 +                out = "%s %s" % ( out, key )
    2.98 +        if between is not None:
    2.99 +            out = "%s>%s</%s>" % ( out, between, tag )
   2.100 +        else:
   2.101 +            if single:
   2.102 +                out = "%s />" % out
   2.103 +            else:
   2.104 +                out = "%s>" % out
   2.105 +        if self.parent is not None:
   2.106 +            self.parent.content.append( out )
   2.107 +        else:
   2.108 +            return out
   2.109 +    
   2.110 +    def close( self ):
   2.111 +        """Append a closing tag unless element has only opening tag."""
   2.112 +
   2.113 +        if self.tag in self.parent.twotags:
   2.114 +            self.parent.content.append( "</%s>" % self.tag )
   2.115 +        elif self.tag in self.parent.onetags:
   2.116 +            raise ClosingError( self.tag )
   2.117 +        elif self.parent.mode == 'strict_html' and self.tag in self.parent.deptags:
   2.118 +            raise DeprecationError( self.tag )
   2.119 +
   2.120 +    def open( self, **kwargs ):
   2.121 +        """Append an opening tag."""
   2.122 +
   2.123 +        if self.tag in self.parent.twotags or self.tag in self.parent.onetags:
   2.124 +            self.render( self.tag, False, None, kwargs )
   2.125 +        elif self.mode == 'strict_html' and self.tag in self.parent.deptags:
   2.126 +            raise DeprecationError( self.tag )
   2.127 +
   2.128 +class page:
   2.129 +    """This is our main class representing a document. Elements are added
   2.130 +    as attributes of an instance of this class."""
   2.131 +
   2.132 +    def __init__( self, mode='strict_html', case='lower', onetags=None, twotags=None, separator='\n', class_=None ):
   2.133 +        """Stuff that effects the whole document.
   2.134 +
   2.135 +        mode -- 'strict_html'   for HTML 4.01 (default)
   2.136 +                'html'          alias for 'strict_html'
   2.137 +                'loose_html'    to allow some deprecated elements
   2.138 +                'xml'           to allow arbitrary elements
   2.139 +
   2.140 +        case -- 'lower'         element names will be printed in lower case (default)
   2.141 +                'upper'         they will be printed in upper case
   2.142 +                'given'         element names will be printed as they are given
   2.143 +
   2.144 +        onetags --              list or tuple of valid elements with opening tags only
   2.145 +        twotags --              list or tuple of valid elements with both opening and closing tags
   2.146 +                                these two keyword arguments may be used to select
   2.147 +                                the set of valid elements in 'xml' mode
   2.148 +                                invalid elements will raise appropriate exceptions
   2.149 +        
   2.150 +        separator --            string to place between added elements, defaults to newline
   2.151 +        
   2.152 +        class_ --               a class that will be added to every element if defined"""
   2.153 +        
   2.154 +        valid_onetags = [ "AREA", "BASE", "BR", "COL", "FRAME", "HR", "IMG", "INPUT", "LINK", "META", "PARAM" ]
   2.155 +        valid_twotags = [ "A", "ABBR", "ACRONYM", "ADDRESS", "B", "BDO", "BIG", "BLOCKQUOTE", "BODY", "BUTTON",
   2.156 +                "CAPTION", "CITE", "CODE", "COLGROUP", "DD", "DEL", "DFN", "DIV", "DL", "DT", "EM", "FIELDSET",
   2.157 +                "FORM", "FRAMESET", "H1", "H2", "H3", "H4", "H5", "H6", "HEAD", "HTML", "I", "IFRAME", "INS",
   2.158 +                "KBD", "LABEL", "LEGEND", "LI", "MAP", "NOFRAMES", "NOSCRIPT", "OBJECT", "OL", "OPTGROUP",
   2.159 +                "OPTION", "P", "PRE", "Q", "SAMP", "SCRIPT", "SELECT", "SMALL", "SPAN", "STRONG", "STYLE",
   2.160 +                "SUB", "SUP", "TABLE", "TBODY", "TD", "TEXTAREA", "TFOOT", "TH", "THEAD", "TITLE", "TR",
   2.161 +                "TT", "UL", "VAR" ]
   2.162 +        deprecated_onetags = [ "BASEFONT", "ISINDEX" ]
   2.163 +        deprecated_twotags = [ "APPLET", "CENTER", "DIR", "FONT", "MENU", "S", "STRIKE", "U" ]
   2.164 +
   2.165 +        self.header = [ ]
   2.166 +        self.content = [ ]
   2.167 +        self.footer = [ ]
   2.168 +        self.case = case
   2.169 +        self.separator = separator
   2.170 +
   2.171 +        # init( ) sets it to True so we know that </body></html> has to be printed at the end
   2.172 +        self._full = False
   2.173 +        self.class_= class_
   2.174 +
   2.175 +        if mode == 'strict_html' or mode == 'html':
   2.176 +            self.onetags = valid_onetags
   2.177 +            self.onetags += list( map( string.lower, self.onetags ) )
   2.178 +            self.twotags = valid_twotags
   2.179 +            self.twotags += list( map( string.lower, self.twotags ) )
   2.180 +            self.deptags = deprecated_onetags + deprecated_twotags
   2.181 +            self.deptags += list( map( string.lower, self.deptags ) )
   2.182 +            self.mode = 'strict_html'
   2.183 +        elif mode == 'loose_html':
   2.184 +            self.onetags = valid_onetags + deprecated_onetags 
   2.185 +            self.onetags += list( map( string.lower, self.onetags ) )
   2.186 +            self.twotags = valid_twotags + deprecated_twotags
   2.187 +            self.twotags += list( map( string.lower, self.twotags ) )
   2.188 +            self.mode = mode
   2.189 +        elif mode == 'xml':
   2.190 +            if onetags and twotags:
   2.191 +                self.onetags = onetags
   2.192 +                self.twotags = twotags
   2.193 +            elif ( onetags and not twotags ) or ( twotags and not onetags ):
   2.194 +                raise CustomizationError( )
   2.195 +            else:
   2.196 +                self.onetags = russell( )
   2.197 +                self.twotags = russell( )
   2.198 +            self.mode = mode
   2.199 +        else:
   2.200 +            raise ModeError( mode )
   2.201 +
   2.202 +    def __getattr__( self, attr ):
   2.203 +
   2.204 +        # tags should start with double underscore
   2.205 +        if attr.startswith("__") and attr.endswith("__"):
   2.206 +            raise AttributeError( attr )
   2.207 +        # tag with single underscore should be a reserved keyword
   2.208 +        if attr.startswith( '_' ):
   2.209 +            attr = attr.lstrip( '_' ) 
   2.210 +            if attr not in keyword.kwlist:
   2.211 +                raise AttributeError( attr )
   2.212 +
   2.213 +        return element( attr, case=self.case, parent=self )
   2.214 +
   2.215 +    def __str__( self ):
   2.216 +        
   2.217 +        if self._full and ( self.mode == 'strict_html' or self.mode == 'loose_html' ):
   2.218 +            end = [ '</body>', '</html>' ]
   2.219 +        else:
   2.220 +            end = [ ]
   2.221 +
   2.222 +        return self.separator.join( self.header + self.content + self.footer + end )
   2.223 +
   2.224 +    def __call__( self, escape=False ):
   2.225 +        """Return the document as a string.
   2.226 +
   2.227 +        escape --   False   print normally
   2.228 +                    True    replace < and > by &lt; and &gt;
   2.229 +                            the default escape sequences in most browsers"""
   2.230 +
   2.231 +        if escape:
   2.232 +            return _escape( self.__str__( ) )
   2.233 +        else:
   2.234 +            return self.__str__( )
   2.235 +
   2.236 +    def add( self, text ):
   2.237 +        """This is an alias to addcontent."""
   2.238 +        self.addcontent( text )
   2.239 +
   2.240 +    def addfooter( self, text ):
   2.241 +        """Add some text to the bottom of the document"""
   2.242 +        self.footer.append( text )
   2.243 +
   2.244 +    def addheader( self, text ):
   2.245 +        """Add some text to the top of the document"""
   2.246 +        self.header.append( text )
   2.247 +
   2.248 +    def addcontent( self, text ):
   2.249 +        """Add some text to the main part of the document"""
   2.250 +        self.content.append( text )
   2.251 +
   2.252 +
   2.253 +    def init( self, lang='en', css=None, metainfo=None, title=None, header=None,
   2.254 +              footer=None, charset=None, encoding=None, doctype=None, bodyattrs=None, script=None, base=None ):
   2.255 +        """This method is used for complete documents with appropriate
   2.256 +        doctype, encoding, title, etc information. For an HTML/XML snippet
   2.257 +        omit this method.
   2.258 +
   2.259 +        lang --     language, usually a two character string, will appear
   2.260 +                    as <html lang='en'> in html mode (ignored in xml mode)
   2.261 +        
   2.262 +        css --      Cascading Style Sheet filename as a string or a list of
   2.263 +                    strings for multiple css files (ignored in xml mode)
   2.264 +
   2.265 +        metainfo -- a dictionary in the form { 'name':'content' } to be inserted
   2.266 +                    into meta element(s) as <meta name='name' content='content'>
   2.267 +                    (ignored in xml mode)
   2.268 +        
   2.269 +        base     -- set the <base href="..."> tag in <head>
   2.270 +        
   2.271 +        bodyattrs --a dictionary in the form { 'key':'value', ... } which will be added
   2.272 +                    as attributes of the <body> element as <body key='value' ... >
   2.273 +                    (ignored in xml mode)
   2.274 +
   2.275 +        script --   dictionary containing src:type pairs, <script type='text/type' src=src></script>
   2.276 +                    or a list of [ 'src1', 'src2', ... ] in which case 'javascript' is assumed for all
   2.277 +
   2.278 +        title --    the title of the document as a string to be inserted into
   2.279 +                    a title element as <title>my title</title> (ignored in xml mode)
   2.280 +
   2.281 +        header --   some text to be inserted right after the <body> element
   2.282 +                    (ignored in xml mode)
   2.283 +
   2.284 +        footer --   some text to be inserted right before the </body> element
   2.285 +                    (ignored in xml mode)
   2.286 +
   2.287 +        charset --  a string defining the character set, will be inserted into a
   2.288 +                    <meta http-equiv='Content-Type' content='text/html; charset=myset'>
   2.289 +                    element (ignored in xml mode)
   2.290 +
   2.291 +        encoding -- a string defining the encoding, will be put into to first line of
   2.292 +                    the document as <?xml version='1.0' encoding='myencoding' ?> in
   2.293 +                    xml mode (ignored in html mode)
   2.294 +
   2.295 +        doctype --  the document type string, defaults to
   2.296 +                    <!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.01 Transitional//EN'>
   2.297 +                    in html mode (ignored in xml mode)"""
   2.298 +
   2.299 +        self._full = True
   2.300 +
   2.301 +        if self.mode == 'strict_html' or self.mode == 'loose_html':
   2.302 +            if doctype is None:
   2.303 +                doctype = "<!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.01 Transitional//EN'>"
   2.304 +            self.header.append( doctype )
   2.305 +            self.html( lang=lang )
   2.306 +            self.head( )
   2.307 +            if charset is not None:
   2.308 +                self.meta( http_equiv='Content-Type', content="text/html; charset=%s" % charset )
   2.309 +            if metainfo is not None:
   2.310 +                self.metainfo( metainfo )
   2.311 +            if css is not None:
   2.312 +                self.css( css )
   2.313 +            if title is not None:
   2.314 +                self.title( title )
   2.315 +            if script is not None:
   2.316 +                self.scripts( script )
   2.317 +            if base is not None:
   2.318 +                self.base( href='%s' % base )
   2.319 +            self.head.close()
   2.320 +            if bodyattrs is not None:
   2.321 +                self.body( **bodyattrs )
   2.322 +            else:
   2.323 +                self.body( )
   2.324 +            if header is not None:
   2.325 +                self.content.append( header )
   2.326 +            if footer is not None:
   2.327 +                self.footer.append( footer )
   2.328 +
   2.329 +        elif self.mode == 'xml':
   2.330 +            if doctype is None:
   2.331 +                if encoding is not None:
   2.332 +                    doctype = "<?xml version='1.0' encoding='%s' ?>" % encoding
   2.333 +                else:
   2.334 +                    doctype = "<?xml version='1.0' ?>"
   2.335 +            self.header.append( doctype )
   2.336 +
   2.337 +    def css( self, filelist ):
   2.338 +        """This convenience function is only useful for html.
   2.339 +        It adds css stylesheet(s) to the document via the <link> element."""
   2.340 +      
   2.341 +        if isinstance( filelist, basestring ):
   2.342 +            self.link( href=filelist, rel='stylesheet', type='text/css', media='all' )
   2.343 +        else:
   2.344 +            for file in filelist:
   2.345 +                self.link( href=file, rel='stylesheet', type='text/css', media='all' )
   2.346 +
   2.347 +    def metainfo( self, mydict ):
   2.348 +        """This convenience function is only useful for html.
   2.349 +        It adds meta information via the <meta> element, the argument is
   2.350 +        a dictionary of the form { 'name':'content' }."""
   2.351 +
   2.352 +        if isinstance( mydict, dict ):
   2.353 +            for name, content in list( mydict.items( ) ):
   2.354 +                self.meta( name=name, content=content )
   2.355 +        else:
   2.356 +            raise TypeError( "Metainfo should be called with a dictionary argument of name:content pairs." )
   2.357 +
   2.358 +    def scripts( self, mydict ):
   2.359 +        """Only useful in html, mydict is dictionary of src:type pairs or a list
   2.360 +        of script sources [ 'src1', 'src2', ... ] in which case 'javascript' is assumed for type.
   2.361 +        Will be rendered as <script type='text/type' src=src></script>"""
   2.362 +
   2.363 +        if isinstance( mydict, dict ):
   2.364 +            for src, type in list( mydict.items( ) ):
   2.365 +                self.script( '', src=src, type='text/%s' % type )
   2.366 +        else:
   2.367 +            try:
   2.368 +                for src in mydict:
   2.369 +                    self.script( '', src=src, type='text/javascript' )
   2.370 +            except:
   2.371 +                raise TypeError( "Script should be given a dictionary of src:type pairs or a list of javascript src's." )
   2.372 +
   2.373 +
   2.374 +class _oneliner:
   2.375 +    """An instance of oneliner returns a string corresponding to one element.
   2.376 +    This class can be used to write 'oneliners' that return a string
   2.377 +    immediately so there is no need to instantiate the page class."""
   2.378 +    
   2.379 +    def __init__( self, case='lower' ):
   2.380 +        self.case = case
   2.381 +    
   2.382 +    def __getattr__( self, attr ):
   2.383 +        
   2.384 +        # tags should start with double underscore
   2.385 +        if attr.startswith("__") and attr.endswith("__"):
   2.386 +            raise AttributeError( attr )
   2.387 +        # tag with single underscore should be a reserved keyword
   2.388 +        if attr.startswith( '_' ):
   2.389 +            attr = attr.lstrip( '_' ) 
   2.390 +            if attr not in keyword.kwlist:
   2.391 +                raise AttributeError( attr )
   2.392 +        
   2.393 +        return element( attr, case=self.case, parent=None )
   2.394 +
   2.395 +oneliner = _oneliner( case='lower' )
   2.396 +upper_oneliner = _oneliner( case='upper' )
   2.397 +given_oneliner = _oneliner( case='given' )
   2.398 +
   2.399 +def _argsdicts( args, mydict ):
   2.400 +    """A utility generator that pads argument list and dictionary values, will only be called with len( args ) = 0, 1."""
   2.401 +    
   2.402 +    if len( args ) == 0:
   2.403 +        args = None, 
   2.404 +    elif len( args ) == 1:
   2.405 +        args = _totuple( args[0] )
   2.406 +    else:
   2.407 +        raise Exception( "We should have never gotten here." )
   2.408 +
   2.409 +    mykeys = list( mydict.keys( ) )
   2.410 +    myvalues = list( map( _totuple, list( mydict.values( ) ) ) )
   2.411 +
   2.412 +    maxlength = max( list( map( len, [ args ] + myvalues ) ) )
   2.413 +
   2.414 +    for i in range( maxlength ):
   2.415 +        thisdict = { }
   2.416 +        for key, value in zip( mykeys, myvalues ):
   2.417 +            try:
   2.418 +                thisdict[ key ] = value[i]
   2.419 +            except IndexError:
   2.420 +                thisdict[ key ] = value[-1]
   2.421 +        try:
   2.422 +            thisarg = args[i]
   2.423 +        except IndexError:
   2.424 +            thisarg = args[-1]
   2.425 +
   2.426 +        yield thisarg, thisdict
   2.427 +
   2.428 +def _totuple( x ):
   2.429 +    """Utility stuff to convert string, int, long, float, None or anything to a usable tuple."""
   2.430 +
   2.431 +    if isinstance( x, basestring ):
   2.432 +        out = x,
   2.433 +    elif isinstance( x, ( int, long, float ) ):
   2.434 +        out = str( x ),
   2.435 +    elif x is None:
   2.436 +        out = None,
   2.437 +    else:
   2.438 +        out = tuple( x )
   2.439 +
   2.440 +    return out
   2.441 +
   2.442 +def escape( text, newline=False ):
   2.443 +    """Escape special html characters."""
   2.444 +
   2.445 +    if isinstance( text, basestring ):
   2.446 +        if '&' in text:
   2.447 +            text = text.replace( '&', '&amp;' )
   2.448 +        if '>' in text:
   2.449 +            text = text.replace( '>', '&gt;' )
   2.450 +        if '<' in text:
   2.451 +            text = text.replace( '<', '&lt;' )
   2.452 +        if '\"' in text:
   2.453 +            text = text.replace( '\"', '&quot;' )
   2.454 +        if '\'' in text:
   2.455 +            text = text.replace( '\'', '&quot;' )
   2.456 +        if newline:
   2.457 +            if '\n' in text:
   2.458 +                text = text.replace( '\n', '<br>' )
   2.459 +
   2.460 +    return text
   2.461 +
   2.462 +_escape = escape
   2.463 +
   2.464 +def unescape( text ):
   2.465 +    """Inverse of escape."""
   2.466 +    
   2.467 +    if isinstance( text, basestring ):
   2.468 +        if '&amp;' in text:
   2.469 +            text = text.replace( '&amp;', '&' )
   2.470 +        if '&gt;' in text:
   2.471 +            text = text.replace( '&gt;', '>' )
   2.472 +        if '&lt;' in text:
   2.473 +            text = text.replace( '&lt;', '<' )
   2.474 +        if '&quot;' in text:
   2.475 +            text = text.replace( '&quot;', '\"' )
   2.476 +
   2.477 +    return text
   2.478 +
   2.479 +class dummy:
   2.480 +    """A dummy class for attaching attributes."""
   2.481 +    pass
   2.482 +
   2.483 +doctype = dummy( )
   2.484 +doctype.frameset = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">"""
   2.485 +doctype.strict = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">"""
   2.486 +doctype.loose = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">"""
   2.487 +
   2.488 +class russell:
   2.489 +    """A dummy class that contains anything."""
   2.490 +
   2.491 +    def __contains__( self, item ):
   2.492 +        return True
   2.493 +
   2.494 +
   2.495 +class MarkupError( Exception ):
   2.496 +    """All our exceptions subclass this."""
   2.497 +    def __str__( self ):
   2.498 +        return self.message
   2.499 +
   2.500 +class ClosingError( MarkupError ):
   2.501 +    def __init__( self, tag ):
   2.502 +        self.message = "The element '%s' does not accept non-keyword arguments (has no closing tag)." % tag
   2.503 +
   2.504 +class OpeningError( MarkupError ):
   2.505 +    def __init__( self, tag ):
   2.506 +        self.message = "The element '%s' can not be opened." % tag
   2.507 +
   2.508 +class ArgumentError( MarkupError ):
   2.509 +    def __init__( self, tag ):
   2.510 +        self.message = "The element '%s' was called with more than one non-keyword argument." % tag
   2.511 +
   2.512 +class InvalidElementError( MarkupError ):
   2.513 +    def __init__( self, tag, mode ):
   2.514 +        self.message = "The element '%s' is not valid for your mode '%s'." % ( tag, mode )
   2.515 +
   2.516 +class DeprecationError( MarkupError ):
   2.517 +    def __init__( self, tag ):
   2.518 +        self.message = "The element '%s' is deprecated, instantiate markup.page with mode='loose_html' to allow it." % tag
   2.519 +
   2.520 +class ModeError( MarkupError ):
   2.521 +    def __init__( self, mode ):
   2.522 +        self.message = "Mode '%s' is invalid, possible values: strict_html, html (alias for strict_html), loose_html, xml." % mode
   2.523 +
   2.524 +class CustomizationError( MarkupError ):
   2.525 +    def __init__( self ):
   2.526 +        self.message = "If you customize the allowed elements, you must define both types 'onetags' and 'twotags'."
   2.527 +
   2.528 +if __name__ == '__main__':
   2.529 +    import sys
   2.530 +    sys.stdout.write( __doc__ )