From 28c01e1a714ba88a2f447a6f07b198f4e5114a74 Mon Sep 17 00:00:00 2001 From: Maxim Likhachev Date: Sun, 20 Oct 2019 06:55:56 +0500 Subject: [PATCH] Markup moved to lib directory --- lib/markup.py | 529 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/markup.py | 529 -------------------------------------------------------- src/vkdigest.py | 404 ------------------------------------------- 3 files changed, 529 insertions(+), 933 deletions(-) create mode 100755 lib/markup.py delete mode 100755 src/markup.py delete mode 100755 src/vkdigest.py diff --git a/lib/markup.py b/lib/markup.py new file mode 100755 index 0000000..8dc8f75 --- /dev/null +++ b/lib/markup.py @@ -0,0 +1,529 @@ +# This code is in the public domain, it comes +# with absolutely no warranty and you can do +# absolutely whatever you want with it. + +__date__ = '16 March 2015' +__version__ = '1.10' +__doc__= """ +This is markup.py - a Python module that attempts to +make it easier to generate HTML/XML from a Python program +in an intuitive, lightweight, customizable and pythonic way. +It works with both python 2 and 3. + +The code is in the public domain. + +Version: %s as of %s. + +Documentation and further info is at http://markup.sourceforge.net/ + +Please send bug reports, feature requests, enhancement +ideas or questions to nogradi at gmail dot com. + +Installation: drop markup.py somewhere into your Python path. +""" % ( __version__, __date__ ) + +try: + basestring + import string +except: + # python 3 + basestring = str + string = str + long = int + +# tags which are reserved python keywords will be referred +# to by a leading underscore otherwise we end up with a syntax error +import keyword + +class element: + """This class handles the addition of a new element.""" + + def __init__( self, tag, case='lower', parent=None ): + self.parent = parent + + if case == 'upper': + self.tag = tag.upper( ) + elif case == 'lower': + self.tag = tag.lower( ) + elif case =='given': + self.tag = tag + else: + self.tag = tag + + def __call__( self, *args, **kwargs ): + if len( args ) > 1: + raise ArgumentError( self.tag ) + + # if class_ was defined in parent it should be added to every element + if self.parent is not None and self.parent.class_ is not None: + if 'class_' not in kwargs: + kwargs['class_'] = self.parent.class_ + + if self.parent is None and len( args ) == 1: + x = [ self.render( self.tag, False, myarg, mydict ) for myarg, mydict in _argsdicts( args, kwargs ) ] + return '\n'.join( x ) + elif self.parent is None and len( args ) == 0: + x = [ self.render( self.tag, True, myarg, mydict ) for myarg, mydict in _argsdicts( args, kwargs ) ] + return '\n'.join( x ) + + if self.tag in self.parent.twotags: + for myarg, mydict in _argsdicts( args, kwargs ): + self.render( self.tag, False, myarg, mydict ) + elif self.tag in self.parent.onetags: + if len( args ) == 0: + for myarg, mydict in _argsdicts( args, kwargs ): + self.render( self.tag, True, myarg, mydict ) # here myarg is always None, because len( args ) = 0 + else: + raise ClosingError( self.tag ) + elif self.parent.mode == 'strict_html' and self.tag in self.parent.deptags: + raise DeprecationError( self.tag ) + else: + raise InvalidElementError( self.tag, self.parent.mode ) + + def render( self, tag, single, between, kwargs ): + """Append the actual tags to content.""" + + out = "<%s" % tag + for key, value in list( kwargs.items( ) ): + if value is not None: # when value is None that means stuff like <... checked> + key = key.strip('_') # strip this so class_ will mean class, etc. + if key == 'http_equiv': # special cases, maybe change _ to - overall? + key = 'http-equiv' + elif key == 'accept_charset': + key = 'accept-charset' + out = "%s %s=\"%s\"" % ( out, key, escape( value ) ) + else: + out = "%s %s" % ( out, key ) + if between is not None: + out = "%s>%s" % ( out, between, tag ) + else: + if single: + out = "%s />" % out + else: + out = "%s>" % out + if self.parent is not None: + self.parent.content.append( out ) + else: + return out + + def close( self ): + """Append a closing tag unless element has only opening tag.""" + + if self.tag in self.parent.twotags: + self.parent.content.append( "" % self.tag ) + elif self.tag in self.parent.onetags: + raise ClosingError( self.tag ) + elif self.parent.mode == 'strict_html' and self.tag in self.parent.deptags: + raise DeprecationError( self.tag ) + + def open( self, **kwargs ): + """Append an opening tag.""" + + if self.tag in self.parent.twotags or self.tag in self.parent.onetags: + self.render( self.tag, False, None, kwargs ) + elif self.mode == 'strict_html' and self.tag in self.parent.deptags: + raise DeprecationError( self.tag ) + +class page: + """This is our main class representing a document. Elements are added + as attributes of an instance of this class.""" + + def __init__( self, mode='strict_html', case='lower', onetags=None, twotags=None, separator='\n', class_=None ): + """Stuff that effects the whole document. + + mode -- 'strict_html' for HTML 4.01 (default) + 'html' alias for 'strict_html' + 'loose_html' to allow some deprecated elements + 'xml' to allow arbitrary elements + + case -- 'lower' element names will be printed in lower case (default) + 'upper' they will be printed in upper case + 'given' element names will be printed as they are given + + onetags -- list or tuple of valid elements with opening tags only + twotags -- list or tuple of valid elements with both opening and closing tags + these two keyword arguments may be used to select + the set of valid elements in 'xml' mode + invalid elements will raise appropriate exceptions + + separator -- string to place between added elements, defaults to newline + + class_ -- a class that will be added to every element if defined""" + + valid_onetags = [ "AREA", "BASE", "BR", "COL", "FRAME", "HR", "IMG", "INPUT", "LINK", "META", "PARAM" ] + valid_twotags = [ "A", "ABBR", "ACRONYM", "ADDRESS", "B", "BDO", "BIG", "BLOCKQUOTE", "BODY", "BUTTON", + "CAPTION", "CITE", "CODE", "COLGROUP", "DD", "DEL", "DFN", "DIV", "DL", "DT", "EM", "FIELDSET", + "FORM", "FRAMESET", "H1", "H2", "H3", "H4", "H5", "H6", "HEAD", "HTML", "I", "IFRAME", "INS", + "KBD", "LABEL", "LEGEND", "LI", "MAP", "NOFRAMES", "NOSCRIPT", "OBJECT", "OL", "OPTGROUP", + "OPTION", "P", "PRE", "Q", "SAMP", "SCRIPT", "SELECT", "SMALL", "SPAN", "STRONG", "STYLE", + "SUB", "SUP", "TABLE", "TBODY", "TD", "TEXTAREA", "TFOOT", "TH", "THEAD", "TITLE", "TR", + "TT", "UL", "VAR" ] + deprecated_onetags = [ "BASEFONT", "ISINDEX" ] + deprecated_twotags = [ "APPLET", "CENTER", "DIR", "FONT", "MENU", "S", "STRIKE", "U" ] + + self.header = [ ] + self.content = [ ] + self.footer = [ ] + self.case = case + self.separator = separator + + # init( ) sets it to True so we know that has to be printed at the end + self._full = False + self.class_= class_ + + if mode == 'strict_html' or mode == 'html': + self.onetags = valid_onetags + self.onetags += list( map( string.lower, self.onetags ) ) + self.twotags = valid_twotags + self.twotags += list( map( string.lower, self.twotags ) ) + self.deptags = deprecated_onetags + deprecated_twotags + self.deptags += list( map( string.lower, self.deptags ) ) + self.mode = 'strict_html' + elif mode == 'loose_html': + self.onetags = valid_onetags + deprecated_onetags + self.onetags += list( map( string.lower, self.onetags ) ) + self.twotags = valid_twotags + deprecated_twotags + self.twotags += list( map( string.lower, self.twotags ) ) + self.mode = mode + elif mode == 'xml': + if onetags and twotags: + self.onetags = onetags + self.twotags = twotags + elif ( onetags and not twotags ) or ( twotags and not onetags ): + raise CustomizationError( ) + else: + self.onetags = russell( ) + self.twotags = russell( ) + self.mode = mode + else: + raise ModeError( mode ) + + def __getattr__( self, attr ): + + # tags should start with double underscore + if attr.startswith("__") and attr.endswith("__"): + raise AttributeError( attr ) + # tag with single underscore should be a reserved keyword + if attr.startswith( '_' ): + attr = attr.lstrip( '_' ) + if attr not in keyword.kwlist: + raise AttributeError( attr ) + + return element( attr, case=self.case, parent=self ) + + def __str__( self ): + + if self._full and ( self.mode == 'strict_html' or self.mode == 'loose_html' ): + end = [ '', '' ] + else: + end = [ ] + + return self.separator.join( self.header + self.content + self.footer + end ) + + def __call__( self, escape=False ): + """Return the document as a string. + + escape -- False print normally + True replace < and > by < and > + the default escape sequences in most browsers""" + + if escape: + return _escape( self.__str__( ) ) + else: + return self.__str__( ) + + def add( self, text ): + """This is an alias to addcontent.""" + self.addcontent( text ) + + def addfooter( self, text ): + """Add some text to the bottom of the document""" + self.footer.append( text ) + + def addheader( self, text ): + """Add some text to the top of the document""" + self.header.append( text ) + + def addcontent( self, text ): + """Add some text to the main part of the document""" + self.content.append( text ) + + + def init( self, lang='en', css=None, metainfo=None, title=None, header=None, + footer=None, charset=None, encoding=None, doctype=None, bodyattrs=None, script=None, base=None ): + """This method is used for complete documents with appropriate + doctype, encoding, title, etc information. For an HTML/XML snippet + omit this method. + + lang -- language, usually a two character string, will appear + as in html mode (ignored in xml mode) + + css -- Cascading Style Sheet filename as a string or a list of + strings for multiple css files (ignored in xml mode) + + metainfo -- a dictionary in the form { 'name':'content' } to be inserted + into meta element(s) as + (ignored in xml mode) + + base -- set the tag in + + bodyattrs --a dictionary in the form { 'key':'value', ... } which will be added + as attributes of the element as + (ignored in xml mode) + + script -- dictionary containing src:type pairs, + or a list of [ 'src1', 'src2', ... ] in which case 'javascript' is assumed for all + + title -- the title of the document as a string to be inserted into + a title element as my title (ignored in xml mode) + + header -- some text to be inserted right after the element + (ignored in xml mode) + + footer -- some text to be inserted right before the element + (ignored in xml mode) + + charset -- a string defining the character set, will be inserted into a + + element (ignored in xml mode) + + encoding -- a string defining the encoding, will be put into to first line of + the document as in + xml mode (ignored in html mode) + + doctype -- the document type string, defaults to + + in html mode (ignored in xml mode)""" + + self._full = True + + if self.mode == 'strict_html' or self.mode == 'loose_html': + if doctype is None: + doctype = "" + self.header.append( doctype ) + self.html( lang=lang ) + self.head( ) + if charset is not None: + self.meta( http_equiv='Content-Type', content="text/html; charset=%s" % charset ) + if metainfo is not None: + self.metainfo( metainfo ) + if css is not None: + self.css( css ) + if title is not None: + self.title( title ) + if script is not None: + self.scripts( script ) + if base is not None: + self.base( href='%s' % base ) + self.head.close() + if bodyattrs is not None: + self.body( **bodyattrs ) + else: + self.body( ) + if header is not None: + self.content.append( header ) + if footer is not None: + self.footer.append( footer ) + + elif self.mode == 'xml': + if doctype is None: + if encoding is not None: + doctype = "" % encoding + else: + doctype = "" + self.header.append( doctype ) + + def css( self, filelist ): + """This convenience function is only useful for html. + It adds css stylesheet(s) to the document via the element.""" + + if isinstance( filelist, basestring ): + self.link( href=filelist, rel='stylesheet', type='text/css', media='all' ) + else: + for file in filelist: + self.link( href=file, rel='stylesheet', type='text/css', media='all' ) + + def metainfo( self, mydict ): + """This convenience function is only useful for html. + It adds meta information via the element, the argument is + a dictionary of the form { 'name':'content' }.""" + + if isinstance( mydict, dict ): + for name, content in list( mydict.items( ) ): + self.meta( name=name, content=content ) + else: + raise TypeError( "Metainfo should be called with a dictionary argument of name:content pairs." ) + + def scripts( self, mydict ): + """Only useful in html, mydict is dictionary of src:type pairs or a list + of script sources [ 'src1', 'src2', ... ] in which case 'javascript' is assumed for type. + Will be rendered as """ + + if isinstance( mydict, dict ): + for src, type in list( mydict.items( ) ): + self.script( '', src=src, type='text/%s' % type ) + else: + try: + for src in mydict: + self.script( '', src=src, type='text/javascript' ) + except: + raise TypeError( "Script should be given a dictionary of src:type pairs or a list of javascript src's." ) + + +class _oneliner: + """An instance of oneliner returns a string corresponding to one element. + This class can be used to write 'oneliners' that return a string + immediately so there is no need to instantiate the page class.""" + + def __init__( self, case='lower' ): + self.case = case + + def __getattr__( self, attr ): + + # tags should start with double underscore + if attr.startswith("__") and attr.endswith("__"): + raise AttributeError( attr ) + # tag with single underscore should be a reserved keyword + if attr.startswith( '_' ): + attr = attr.lstrip( '_' ) + if attr not in keyword.kwlist: + raise AttributeError( attr ) + + return element( attr, case=self.case, parent=None ) + +oneliner = _oneliner( case='lower' ) +upper_oneliner = _oneliner( case='upper' ) +given_oneliner = _oneliner( case='given' ) + +def _argsdicts( args, mydict ): + """A utility generator that pads argument list and dictionary values, will only be called with len( args ) = 0, 1.""" + + if len( args ) == 0: + args = None, + elif len( args ) == 1: + args = _totuple( args[0] ) + else: + raise Exception( "We should have never gotten here." ) + + mykeys = list( mydict.keys( ) ) + myvalues = list( map( _totuple, list( mydict.values( ) ) ) ) + + maxlength = max( list( map( len, [ args ] + myvalues ) ) ) + + for i in range( maxlength ): + thisdict = { } + for key, value in zip( mykeys, myvalues ): + try: + thisdict[ key ] = value[i] + except IndexError: + thisdict[ key ] = value[-1] + try: + thisarg = args[i] + except IndexError: + thisarg = args[-1] + + yield thisarg, thisdict + +def _totuple( x ): + """Utility stuff to convert string, int, long, float, None or anything to a usable tuple.""" + + if isinstance( x, basestring ): + out = x, + elif isinstance( x, ( int, long, float ) ): + out = str( x ), + elif x is None: + out = None, + else: + out = tuple( x ) + + return out + +def escape( text, newline=False ): + """Escape special html characters.""" + + if isinstance( text, basestring ): + if '&' in text: + text = text.replace( '&', '&' ) + if '>' in text: + text = text.replace( '>', '>' ) + if '<' in text: + text = text.replace( '<', '<' ) + if '\"' in text: + text = text.replace( '\"', '"' ) + if '\'' in text: + text = text.replace( '\'', '"' ) + if newline: + if '\n' in text: + text = text.replace( '\n', '
' ) + + return text + +_escape = escape + +def unescape( text ): + """Inverse of escape.""" + + if isinstance( text, basestring ): + if '&' in text: + text = text.replace( '&', '&' ) + if '>' in text: + text = text.replace( '>', '>' ) + if '<' in text: + text = text.replace( '<', '<' ) + if '"' in text: + text = text.replace( '"', '\"' ) + + return text + +class dummy: + """A dummy class for attaching attributes.""" + pass + +doctype = dummy( ) +doctype.frameset = """""" +doctype.strict = """""" +doctype.loose = """""" + +class russell: + """A dummy class that contains anything.""" + + def __contains__( self, item ): + return True + + +class MarkupError( Exception ): + """All our exceptions subclass this.""" + def __str__( self ): + return self.message + +class ClosingError( MarkupError ): + def __init__( self, tag ): + self.message = "The element '%s' does not accept non-keyword arguments (has no closing tag)." % tag + +class OpeningError( MarkupError ): + def __init__( self, tag ): + self.message = "The element '%s' can not be opened." % tag + +class ArgumentError( MarkupError ): + def __init__( self, tag ): + self.message = "The element '%s' was called with more than one non-keyword argument." % tag + +class InvalidElementError( MarkupError ): + def __init__( self, tag, mode ): + self.message = "The element '%s' is not valid for your mode '%s'." % ( tag, mode ) + +class DeprecationError( MarkupError ): + def __init__( self, tag ): + self.message = "The element '%s' is deprecated, instantiate markup.page with mode='loose_html' to allow it." % tag + +class ModeError( MarkupError ): + def __init__( self, mode ): + self.message = "Mode '%s' is invalid, possible values: strict_html, html (alias for strict_html), loose_html, xml." % mode + +class CustomizationError( MarkupError ): + def __init__( self ): + self.message = "If you customize the allowed elements, you must define both types 'onetags' and 'twotags'." + +if __name__ == '__main__': + import sys + sys.stdout.write( __doc__ ) diff --git a/src/markup.py b/src/markup.py deleted file mode 100755 index 8dc8f75..0000000 --- a/src/markup.py +++ /dev/null @@ -1,529 +0,0 @@ -# This code is in the public domain, it comes -# with absolutely no warranty and you can do -# absolutely whatever you want with it. - -__date__ = '16 March 2015' -__version__ = '1.10' -__doc__= """ -This is markup.py - a Python module that attempts to -make it easier to generate HTML/XML from a Python program -in an intuitive, lightweight, customizable and pythonic way. -It works with both python 2 and 3. - -The code is in the public domain. - -Version: %s as of %s. - -Documentation and further info is at http://markup.sourceforge.net/ - -Please send bug reports, feature requests, enhancement -ideas or questions to nogradi at gmail dot com. - -Installation: drop markup.py somewhere into your Python path. -""" % ( __version__, __date__ ) - -try: - basestring - import string -except: - # python 3 - basestring = str - string = str - long = int - -# tags which are reserved python keywords will be referred -# to by a leading underscore otherwise we end up with a syntax error -import keyword - -class element: - """This class handles the addition of a new element.""" - - def __init__( self, tag, case='lower', parent=None ): - self.parent = parent - - if case == 'upper': - self.tag = tag.upper( ) - elif case == 'lower': - self.tag = tag.lower( ) - elif case =='given': - self.tag = tag - else: - self.tag = tag - - def __call__( self, *args, **kwargs ): - if len( args ) > 1: - raise ArgumentError( self.tag ) - - # if class_ was defined in parent it should be added to every element - if self.parent is not None and self.parent.class_ is not None: - if 'class_' not in kwargs: - kwargs['class_'] = self.parent.class_ - - if self.parent is None and len( args ) == 1: - x = [ self.render( self.tag, False, myarg, mydict ) for myarg, mydict in _argsdicts( args, kwargs ) ] - return '\n'.join( x ) - elif self.parent is None and len( args ) == 0: - x = [ self.render( self.tag, True, myarg, mydict ) for myarg, mydict in _argsdicts( args, kwargs ) ] - return '\n'.join( x ) - - if self.tag in self.parent.twotags: - for myarg, mydict in _argsdicts( args, kwargs ): - self.render( self.tag, False, myarg, mydict ) - elif self.tag in self.parent.onetags: - if len( args ) == 0: - for myarg, mydict in _argsdicts( args, kwargs ): - self.render( self.tag, True, myarg, mydict ) # here myarg is always None, because len( args ) = 0 - else: - raise ClosingError( self.tag ) - elif self.parent.mode == 'strict_html' and self.tag in self.parent.deptags: - raise DeprecationError( self.tag ) - else: - raise InvalidElementError( self.tag, self.parent.mode ) - - def render( self, tag, single, between, kwargs ): - """Append the actual tags to content.""" - - out = "<%s" % tag - for key, value in list( kwargs.items( ) ): - if value is not None: # when value is None that means stuff like <... checked> - key = key.strip('_') # strip this so class_ will mean class, etc. - if key == 'http_equiv': # special cases, maybe change _ to - overall? - key = 'http-equiv' - elif key == 'accept_charset': - key = 'accept-charset' - out = "%s %s=\"%s\"" % ( out, key, escape( value ) ) - else: - out = "%s %s" % ( out, key ) - if between is not None: - out = "%s>%s" % ( out, between, tag ) - else: - if single: - out = "%s />" % out - else: - out = "%s>" % out - if self.parent is not None: - self.parent.content.append( out ) - else: - return out - - def close( self ): - """Append a closing tag unless element has only opening tag.""" - - if self.tag in self.parent.twotags: - self.parent.content.append( "" % self.tag ) - elif self.tag in self.parent.onetags: - raise ClosingError( self.tag ) - elif self.parent.mode == 'strict_html' and self.tag in self.parent.deptags: - raise DeprecationError( self.tag ) - - def open( self, **kwargs ): - """Append an opening tag.""" - - if self.tag in self.parent.twotags or self.tag in self.parent.onetags: - self.render( self.tag, False, None, kwargs ) - elif self.mode == 'strict_html' and self.tag in self.parent.deptags: - raise DeprecationError( self.tag ) - -class page: - """This is our main class representing a document. Elements are added - as attributes of an instance of this class.""" - - def __init__( self, mode='strict_html', case='lower', onetags=None, twotags=None, separator='\n', class_=None ): - """Stuff that effects the whole document. - - mode -- 'strict_html' for HTML 4.01 (default) - 'html' alias for 'strict_html' - 'loose_html' to allow some deprecated elements - 'xml' to allow arbitrary elements - - case -- 'lower' element names will be printed in lower case (default) - 'upper' they will be printed in upper case - 'given' element names will be printed as they are given - - onetags -- list or tuple of valid elements with opening tags only - twotags -- list or tuple of valid elements with both opening and closing tags - these two keyword arguments may be used to select - the set of valid elements in 'xml' mode - invalid elements will raise appropriate exceptions - - separator -- string to place between added elements, defaults to newline - - class_ -- a class that will be added to every element if defined""" - - valid_onetags = [ "AREA", "BASE", "BR", "COL", "FRAME", "HR", "IMG", "INPUT", "LINK", "META", "PARAM" ] - valid_twotags = [ "A", "ABBR", "ACRONYM", "ADDRESS", "B", "BDO", "BIG", "BLOCKQUOTE", "BODY", "BUTTON", - "CAPTION", "CITE", "CODE", "COLGROUP", "DD", "DEL", "DFN", "DIV", "DL", "DT", "EM", "FIELDSET", - "FORM", "FRAMESET", "H1", "H2", "H3", "H4", "H5", "H6", "HEAD", "HTML", "I", "IFRAME", "INS", - "KBD", "LABEL", "LEGEND", "LI", "MAP", "NOFRAMES", "NOSCRIPT", "OBJECT", "OL", "OPTGROUP", - "OPTION", "P", "PRE", "Q", "SAMP", "SCRIPT", "SELECT", "SMALL", "SPAN", "STRONG", "STYLE", - "SUB", "SUP", "TABLE", "TBODY", "TD", "TEXTAREA", "TFOOT", "TH", "THEAD", "TITLE", "TR", - "TT", "UL", "VAR" ] - deprecated_onetags = [ "BASEFONT", "ISINDEX" ] - deprecated_twotags = [ "APPLET", "CENTER", "DIR", "FONT", "MENU", "S", "STRIKE", "U" ] - - self.header = [ ] - self.content = [ ] - self.footer = [ ] - self.case = case - self.separator = separator - - # init( ) sets it to True so we know that has to be printed at the end - self._full = False - self.class_= class_ - - if mode == 'strict_html' or mode == 'html': - self.onetags = valid_onetags - self.onetags += list( map( string.lower, self.onetags ) ) - self.twotags = valid_twotags - self.twotags += list( map( string.lower, self.twotags ) ) - self.deptags = deprecated_onetags + deprecated_twotags - self.deptags += list( map( string.lower, self.deptags ) ) - self.mode = 'strict_html' - elif mode == 'loose_html': - self.onetags = valid_onetags + deprecated_onetags - self.onetags += list( map( string.lower, self.onetags ) ) - self.twotags = valid_twotags + deprecated_twotags - self.twotags += list( map( string.lower, self.twotags ) ) - self.mode = mode - elif mode == 'xml': - if onetags and twotags: - self.onetags = onetags - self.twotags = twotags - elif ( onetags and not twotags ) or ( twotags and not onetags ): - raise CustomizationError( ) - else: - self.onetags = russell( ) - self.twotags = russell( ) - self.mode = mode - else: - raise ModeError( mode ) - - def __getattr__( self, attr ): - - # tags should start with double underscore - if attr.startswith("__") and attr.endswith("__"): - raise AttributeError( attr ) - # tag with single underscore should be a reserved keyword - if attr.startswith( '_' ): - attr = attr.lstrip( '_' ) - if attr not in keyword.kwlist: - raise AttributeError( attr ) - - return element( attr, case=self.case, parent=self ) - - def __str__( self ): - - if self._full and ( self.mode == 'strict_html' or self.mode == 'loose_html' ): - end = [ '', '' ] - else: - end = [ ] - - return self.separator.join( self.header + self.content + self.footer + end ) - - def __call__( self, escape=False ): - """Return the document as a string. - - escape -- False print normally - True replace < and > by < and > - the default escape sequences in most browsers""" - - if escape: - return _escape( self.__str__( ) ) - else: - return self.__str__( ) - - def add( self, text ): - """This is an alias to addcontent.""" - self.addcontent( text ) - - def addfooter( self, text ): - """Add some text to the bottom of the document""" - self.footer.append( text ) - - def addheader( self, text ): - """Add some text to the top of the document""" - self.header.append( text ) - - def addcontent( self, text ): - """Add some text to the main part of the document""" - self.content.append( text ) - - - def init( self, lang='en', css=None, metainfo=None, title=None, header=None, - footer=None, charset=None, encoding=None, doctype=None, bodyattrs=None, script=None, base=None ): - """This method is used for complete documents with appropriate - doctype, encoding, title, etc information. For an HTML/XML snippet - omit this method. - - lang -- language, usually a two character string, will appear - as in html mode (ignored in xml mode) - - css -- Cascading Style Sheet filename as a string or a list of - strings for multiple css files (ignored in xml mode) - - metainfo -- a dictionary in the form { 'name':'content' } to be inserted - into meta element(s) as - (ignored in xml mode) - - base -- set the tag in - - bodyattrs --a dictionary in the form { 'key':'value', ... } which will be added - as attributes of the element as - (ignored in xml mode) - - script -- dictionary containing src:type pairs, - or a list of [ 'src1', 'src2', ... ] in which case 'javascript' is assumed for all - - title -- the title of the document as a string to be inserted into - a title element as my title (ignored in xml mode) - - header -- some text to be inserted right after the element - (ignored in xml mode) - - footer -- some text to be inserted right before the element - (ignored in xml mode) - - charset -- a string defining the character set, will be inserted into a - - element (ignored in xml mode) - - encoding -- a string defining the encoding, will be put into to first line of - the document as in - xml mode (ignored in html mode) - - doctype -- the document type string, defaults to - - in html mode (ignored in xml mode)""" - - self._full = True - - if self.mode == 'strict_html' or self.mode == 'loose_html': - if doctype is None: - doctype = "" - self.header.append( doctype ) - self.html( lang=lang ) - self.head( ) - if charset is not None: - self.meta( http_equiv='Content-Type', content="text/html; charset=%s" % charset ) - if metainfo is not None: - self.metainfo( metainfo ) - if css is not None: - self.css( css ) - if title is not None: - self.title( title ) - if script is not None: - self.scripts( script ) - if base is not None: - self.base( href='%s' % base ) - self.head.close() - if bodyattrs is not None: - self.body( **bodyattrs ) - else: - self.body( ) - if header is not None: - self.content.append( header ) - if footer is not None: - self.footer.append( footer ) - - elif self.mode == 'xml': - if doctype is None: - if encoding is not None: - doctype = "" % encoding - else: - doctype = "" - self.header.append( doctype ) - - def css( self, filelist ): - """This convenience function is only useful for html. - It adds css stylesheet(s) to the document via the element.""" - - if isinstance( filelist, basestring ): - self.link( href=filelist, rel='stylesheet', type='text/css', media='all' ) - else: - for file in filelist: - self.link( href=file, rel='stylesheet', type='text/css', media='all' ) - - def metainfo( self, mydict ): - """This convenience function is only useful for html. - It adds meta information via the element, the argument is - a dictionary of the form { 'name':'content' }.""" - - if isinstance( mydict, dict ): - for name, content in list( mydict.items( ) ): - self.meta( name=name, content=content ) - else: - raise TypeError( "Metainfo should be called with a dictionary argument of name:content pairs." ) - - def scripts( self, mydict ): - """Only useful in html, mydict is dictionary of src:type pairs or a list - of script sources [ 'src1', 'src2', ... ] in which case 'javascript' is assumed for type. - Will be rendered as """ - - if isinstance( mydict, dict ): - for src, type in list( mydict.items( ) ): - self.script( '', src=src, type='text/%s' % type ) - else: - try: - for src in mydict: - self.script( '', src=src, type='text/javascript' ) - except: - raise TypeError( "Script should be given a dictionary of src:type pairs or a list of javascript src's." ) - - -class _oneliner: - """An instance of oneliner returns a string corresponding to one element. - This class can be used to write 'oneliners' that return a string - immediately so there is no need to instantiate the page class.""" - - def __init__( self, case='lower' ): - self.case = case - - def __getattr__( self, attr ): - - # tags should start with double underscore - if attr.startswith("__") and attr.endswith("__"): - raise AttributeError( attr ) - # tag with single underscore should be a reserved keyword - if attr.startswith( '_' ): - attr = attr.lstrip( '_' ) - if attr not in keyword.kwlist: - raise AttributeError( attr ) - - return element( attr, case=self.case, parent=None ) - -oneliner = _oneliner( case='lower' ) -upper_oneliner = _oneliner( case='upper' ) -given_oneliner = _oneliner( case='given' ) - -def _argsdicts( args, mydict ): - """A utility generator that pads argument list and dictionary values, will only be called with len( args ) = 0, 1.""" - - if len( args ) == 0: - args = None, - elif len( args ) == 1: - args = _totuple( args[0] ) - else: - raise Exception( "We should have never gotten here." ) - - mykeys = list( mydict.keys( ) ) - myvalues = list( map( _totuple, list( mydict.values( ) ) ) ) - - maxlength = max( list( map( len, [ args ] + myvalues ) ) ) - - for i in range( maxlength ): - thisdict = { } - for key, value in zip( mykeys, myvalues ): - try: - thisdict[ key ] = value[i] - except IndexError: - thisdict[ key ] = value[-1] - try: - thisarg = args[i] - except IndexError: - thisarg = args[-1] - - yield thisarg, thisdict - -def _totuple( x ): - """Utility stuff to convert string, int, long, float, None or anything to a usable tuple.""" - - if isinstance( x, basestring ): - out = x, - elif isinstance( x, ( int, long, float ) ): - out = str( x ), - elif x is None: - out = None, - else: - out = tuple( x ) - - return out - -def escape( text, newline=False ): - """Escape special html characters.""" - - if isinstance( text, basestring ): - if '&' in text: - text = text.replace( '&', '&' ) - if '>' in text: - text = text.replace( '>', '>' ) - if '<' in text: - text = text.replace( '<', '<' ) - if '\"' in text: - text = text.replace( '\"', '"' ) - if '\'' in text: - text = text.replace( '\'', '"' ) - if newline: - if '\n' in text: - text = text.replace( '\n', '
' ) - - return text - -_escape = escape - -def unescape( text ): - """Inverse of escape.""" - - if isinstance( text, basestring ): - if '&' in text: - text = text.replace( '&', '&' ) - if '>' in text: - text = text.replace( '>', '>' ) - if '<' in text: - text = text.replace( '<', '<' ) - if '"' in text: - text = text.replace( '"', '\"' ) - - return text - -class dummy: - """A dummy class for attaching attributes.""" - pass - -doctype = dummy( ) -doctype.frameset = """""" -doctype.strict = """""" -doctype.loose = """""" - -class russell: - """A dummy class that contains anything.""" - - def __contains__( self, item ): - return True - - -class MarkupError( Exception ): - """All our exceptions subclass this.""" - def __str__( self ): - return self.message - -class ClosingError( MarkupError ): - def __init__( self, tag ): - self.message = "The element '%s' does not accept non-keyword arguments (has no closing tag)." % tag - -class OpeningError( MarkupError ): - def __init__( self, tag ): - self.message = "The element '%s' can not be opened." % tag - -class ArgumentError( MarkupError ): - def __init__( self, tag ): - self.message = "The element '%s' was called with more than one non-keyword argument." % tag - -class InvalidElementError( MarkupError ): - def __init__( self, tag, mode ): - self.message = "The element '%s' is not valid for your mode '%s'." % ( tag, mode ) - -class DeprecationError( MarkupError ): - def __init__( self, tag ): - self.message = "The element '%s' is deprecated, instantiate markup.page with mode='loose_html' to allow it." % tag - -class ModeError( MarkupError ): - def __init__( self, mode ): - self.message = "Mode '%s' is invalid, possible values: strict_html, html (alias for strict_html), loose_html, xml." % mode - -class CustomizationError( MarkupError ): - def __init__( self ): - self.message = "If you customize the allowed elements, you must define both types 'onetags' and 'twotags'." - -if __name__ == '__main__': - import sys - sys.stdout.write( __doc__ ) diff --git a/src/vkdigest.py b/src/vkdigest.py deleted file mode 100755 index 54ad728..0000000 --- a/src/vkdigest.py +++ /dev/null @@ -1,404 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright (C) 2019, Maxim Lihachev, - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -# -# pip install vk_api -# pip install lib/markup.py -# pip install configparser -# - -import os -import re -import sys -import json -import vk_api -import getopt -import markup -import string -import datetime -import configparser - -from markup import oneliner as _ - -from smtplib import SMTP_SSL as SMTP -from email.mime.text import MIMEText - -############################################################################ - - -def usage(): - print ''' - vkdigest — сценарий для получения сообщений из сообществ vk.com - с выводом в html и/или отправкой по электронной почте. - - Аргументы командной строки: - -h --help - вывести справку по использованию - -f --file - загрузить сообщества из файла - -u --url - открыть сообщество по адресу - -m --mail - отправить дайжест по электронной почте - -s --subj - тема сообщения, макросы: {DATE}, {URL}, {COMMENT} - -t --title - HTML-заголовок, то же самое, что и --subj - -c --cli - вывести дайжест на экран (по умолчанию) - -o --out - вывести дайжест в файл - ''' - sys.exit(2) - -############################################################################ - - -# Конфигурационный файл -CONFIG_FILE = os.path.join(os.path.dirname(__file__), os.pardir, 'conf/vkdigest.ini') - -# Вывод на экран -# -c | --cli -CLI_OUTPUT = True - -# Вывод в файл -# -f -# --file -FILE_OUTPUT = False - -# Отправка почты по умолчанию -SEND_MAIL = False - -# Путь до css-файла -CSS_FILE = os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir, 'css/vkdigest.css')) - -############################################################################ - - -def readConfig(config_file): - '''Чтение конфигурационного файла''' - global settings - settings = configparser.ConfigParser() - settings._interpolation = configparser.ExtendedInterpolation() - settings.read(config_file) - return settings - - -def opt(parameter): - '''Получение опции из файла''' - setting = parameter.split(":") - return settings.get(setting[0], setting[1]) - - -def enabled(opt): - return opt.lower() in ("yes", "true", "t", "1") - - -def timestamp_to_date(timestamp, fmt='%Y-%m-%d %H:%M:%S'): - '''Преобразование временного штампа в читаемую дату''' - return datetime.datetime.fromtimestamp(int(timestamp)).strftime(fmt) - - -def today(): - '''Текущая дата''' - return int(datetime.datetime.strptime(datetime.datetime.today().strftime('%Y-%m-%d'), '%Y-%m-%d').strftime("%s")) - - -def u(string): - return string.encode('utf-8') - -############################################################################ - -def make_title(): - '''Заголовок страницы и тема письма''' - return MAIL_SUBJECT.format(URL=opt('mail:url'), - COMMENT=opt('mail:comment'), - DATE=timestamp_to_date(today(), - '%Y-%m-%d')) - - -# Псевдоним для функции send_email -make_subject = make_title - - -def send_email(message): - '''Отправка письма с дайжестом''' - subject = make_subject() - - try: - msg = MIMEText(message, 'html') - msg['Subject'] = subject - msg['From'] = opt('mail:sender') - - conn = SMTP(opt('mail:SMTPserver')) - conn.set_debuglevel(False) - conn.login(opt('mail:username'), opt('mail:password')) - try: - conn.sendmail(opt('mail:sender'), opt('mail:destination'), msg.as_string()) - finally: - conn.quit() - - except Exception, exc: - sys.exit("mail failed; %s" % str(exc)) - -############################################################################ - -def auth(login, password): - '''Аутентификация в vk.com''' - vk_session = vk_api.VkApi(login, password) - try: - vk_session.auth() - return vk_session.get_api() - except vk_api.AuthError as error_msg: - print(error_msg) - return - - -def only_today(wall, date=None): - '''Записи за указанную дату''' - return [post for post in wall if post['date'] >= today()] - - -def get_group_url(group): - '''Адрес группы''' - return group.split(' ')[0] - - -def get_group_name(url): - '''HTTP-имя группы''' - return url.split('/')[-1] - - -def get_photos(attachments): - '''Получение ссылок на изображения''' - return [attachment['photo'] for attachment in attachments if attachment['type'] == 'photo'] - - -def get_documents(attachments): - '''Получение ссылок на документы''' - return [attachment['doc'] for attachment in attachments if attachment['type'] == 'doc'] - - -def get_links(attachments): - '''Получение ссылок''' - return [attachment['link'] for attachment in attachments if attachment['type'] == 'link'] - - -def group_info(name): - '''Информация о сообществе''' - # TODO: если репост со страницы пользователя, то вставляет название группы - return vk.groups.getById(group_ids=name)[0] - - -def wall_url(group_id, post_id): - '''Ссылка на конкретный пост''' - return "http://vk.com/wall" + str(group_id) + '_' + str(post_id) - - -def wall(name): - '''Получение записей со стены сообщества''' - id = group_info(get_group_name(name))['id'] - - content = vk.wall.get(owner_id=-id)['items'] - - if enabled(opt('digest:only_today')): - content = only_today(content) - - return content - -############################################################################ - - -def html_toc(groups): - '''Содержание дайжеста со ссылками на группы''' - HTML.h2("Сообщества:") - HTML.ul() - for group in groups: - if group.strip(): - group_name = get_group_name(get_group_url(group)) - info = group_info(group_name) - - HTML.li(_.a(u(info['name']), href='#' + u(group_name))) - HTML.ul.close() - HTML.br() - HTML.hr() - - -def groups_info(input_file): - '''Информация о группах, перечисленных в файле''' - f = open(input_file, "r") - groups = f.readlines() - f.close() - - if len(groups) > 1: - html_toc(groups) - - for group in groups: - if group.strip(): - url = get_group_url(group) - MAIL_URL = url - - if '#' in group: - MAIL_COMMENT = group.split('#')[1] - - group_info_html(url) - wall_html(url) - - -def group_info_html(name): - '''Информация о группе в формате HTML''' - group_name = get_group_name(name) - info = group_info(group_name) - - HTML.div(class_='group-info') - HTML.img(src=u(info['photo_50'])) - - if enabled(opt('digest:add_links')): - HTML.h1(_.a(u(info['name']), href="http://vk.com/" + u(info['screen_name'])), id=u(group_name)) - else: - HTML.h1(u(info['name']), id=u(group_name)) - - HTML.div.close() - HTML.br() - HTML.hr() - - -def wall_html(name): - '''Сообщения со стены сообщества в формате HTML''' - info = wall(name) - - for post in info: - if post['text'] or ('attachments' in post): - post_html(post) - # Репост - elif 'copy_history' in post: - post_html(post['copy_history'][0], True) - - -def post_html(content, repost=False): - '''Информация о посте в формате HTML''' - HTML.div(class_="post") - - if repost: - original_group = group_info(abs(content['owner_id'])) - original_name = u(original_group['name']) - original_url = wall_url(content['from_id'], content['id']) - - # Дата поста - HTML.h4() - - if enabled(opt('digest:add_links')): - HTML.add(_.a(timestamp_to_date(content['date']), href=wall_url(content['from_id'], content['id'])) + (' via ' + _.a(original_name, href=original_url) if repost else '')) - else: - HTML.add(timestamp_to_date(content['date']) + (' via ' + original_name if repost else '')) - - HTML.h4.close() - - # Текст поста - HTML.p(tuple(u(content['text']).splitlines())) - - # Документы - if 'attachments' in content: - for photo in get_photos(content['attachments']): - HTML.a(_.img(src=u(photo['photo_130'])), href=u(photo['photo_604'])) - - for document in get_documents(content['attachments']): - HTML.ul(_.li(_.a(u(document['title']), href=u(document['url'])))) - - for link in get_links(content['attachments']): - HTML.ul(_.li(_.a(u(link['title']), href=u(link['url'])))) - - HTML.br() - HTML.div.close() #
- HTML.hr() - -############################################################################ - - -# Чтение файла настроек -settings = [] -readConfig(CONFIG_FILE) - -MAIL_SUBJECT = opt('mail:subject') - -# Учётная запись vk.com -vk = auth(opt('vk:username'), opt('vk:password')) - -############################################################################ - -try: - opts, args = getopt.getopt(sys.argv[1:], 'f:u:s:t:o:mch', ['file=', 'url=', 'subj=', 'title=', 'out=', 'mail', 'cli', 'help']) -except getopt.GetoptError: - usage() - -if not opts: - usage() - -input_file = False -url = False - -CLI_OUTPUT_FLAG = False - -for option, arg in opts: - if option in ('-h', '--help'): - usage() - elif option in ('-f', '--file'): - input_file = arg - elif option in ('-u', '--url'): - url = arg - elif option in ('-s', '--subj'): - MAIL_SUBJECT = arg - elif option in ('-t', '--title'): - MAIL_SUBJECT = arg - elif option in ('-o', '--out'): - FILE_OUTPUT = arg - CLI_OUTPUT = False - elif option in ('-m', '--mail'): - SEND_MAIL = True - CLI_OUTPUT = False - elif option in ('-c', '--cli'): - CLI_OUTPUT_FLAG = True - else: - usage() - -if CLI_OUTPUT_FLAG: - CLI_OUTPUT = True - -HTML = markup.page() - -HTML.init( - title=make_title(), - lang="ru", - charset="utf-8", - css=(CSS_FILE if os.path.isfile(CSS_FILE) else "") -) - -if input_file: - if os.path.isfile(input_file): - groups_info(input_file) - else: - print "Неправильный входной файл ", input_file -elif url: - group_info_html(url) - wall_html(url) -else: - usage() - -############################################################################ - -if SEND_MAIL: - send_email(HTML()) - -if FILE_OUTPUT: - with open(FILE_OUTPUT, 'w') as outfile: - outfile.write(HTML()) - -if CLI_OUTPUT: - print HTML