weblog-0.8/.hg_archival.txt0000644000000000000000000000013611006524342016065 0ustar00usergroup00000000000000repo: 00ecfb3367fecf6d8ba7a94c3f995bc789c18d1e node: 5a128b1ff7cc4bd864ae78a99bb55bfe9fca9d14 weblog-0.8/.hgignore0000644000000000000000000000006411006524342014602 0ustar00usergroup00000000000000syntax: glob *.pyc *~ .*.swp build weblog.egg-info weblog-0.8/.hgtags0000644000000000000000000000101011006524342014245 0ustar00usergroup000000000000009bafbb9dfb928172d988390ea61932b610278ea3 WEBLOG_0_1 7053f6c08fab7af1c5b76d78a9bb6e41fe6a8b5a WEBLOG_0_2 ebe752dc0a655b451babdc2acb6027a523ac8474 WEBLOG_0_3 9cc6b91a2fb95946b5443461201c8a57ad301a53 WEBLOG_0_4 85db8e1cb11890a15f38b3d161dc59962e00b135 WEBLOG_0_5 fcd5f323c67112916c0d43d776f52a96064bef53 WEBLOG_0_5 44abff8da985b456545fa393a2f634932400476b WEBLOG_0_5 a0a1dd94b8b2371f45536e90ee03074dae314f71 WEBLOG_0_6 657340b5fa4b2a1747e139809da6e576d7699290 WEBLOG_0_7 f4075497305adf1cada74fa556a227049a2ccae5 WEBLOG_0_8 weblog-0.8/COPYING0000644000000000000000000000467011006524342014041 0ustar00usergroup00000000000000Copyright (c) 2007, Henry Precheur Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. --- Also the file PyRSS2Gen.py is covered by the BSD license: (This is the BSD license, based on the template at http://www.opensource.org/licenses/bsd-license.php ) Copyright (c) 2003, Dalke Scientific Software, LLC All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the Dalke Scientific Softare, LLC, Andrew Dalke, nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. weblog-0.8/README0000644000000000000000000000074111006524342013661 0ustar00usergroup00000000000000Weblog is a web log or blog publisher. It takes structured text files as input and outputs static HTML / RSS files. Weblog aims to be simple and robust. to learn how to install and use weblog please read the text file: doc/weblog.rst If you have docutils installed you can turn it into a HTML file: $ rst2html.py weblog.py > weblog.html Jinja is needed to use Weblog (http://jinja.pocoo.org) Weblog includes PyRSS2Gen (http://www.dalkescientific.com/Python/PyRSS2Gen.html) weblog-0.8/TODO0000644000000000000000000000026411006524342013471 0ustar00usergroup00000000000000-- v0.9 --- Add link checker smart error reporting documentation tips on uploading doc on templating & styling --- v1.0 --- Stable release: - more unit tests - more docs! weblog-0.8/bin/weblog0000755000000000000000000000424511006524342014761 0ustar00usergroup00000000000000#!/usr/bin/env python # vim:set filetype=python: import os import sys import datetime import logging from shutil import copy from optparse import OptionParser, SUPPRESS_HELP from weblog import command_publish, command_date _COMMANDS = ('publish', 'date') def main(): parser = OptionParser() parser.add_option("-s", "--source-dir", dest="source_dir", help="The source directory where the blog posts and the " "file weblog.ini are located", metavar="DIR") parser.add_option("-o", "--output-dir", dest="output_dir", help="The directory where all the generated files are " "written. If it does not exist it is created.", metavar="DIR") parser.add_option('-q', '--quiet', dest='quiet', default=False, action='store_true', help='Do not output anything except critical error ' 'messages') parser.add_option('--debug', dest='debug', default=False, action='store_true', help=SUPPRESS_HELP) parser.set_usage('%%prog [option] command\n\nCommands:\n %s' % \ '\n '.join(_COMMANDS)) (options, args) = parser.parse_args() if options.debug: logging.basicConfig(level=logging.DEBUG, format='%(levelname)s %(message)s') elif options.quiet: logging.basicConfig(level=logging.CRITICAL, format='%(messages)s') else: logging.basicConfig(level=logging.INFO, format='%(message)s') if not args: logging.warning('Warning: No command specified, assuming \'publish\'.\n' ' To remove this warning, type: %s publish' % sys.argv[0]) command = 'publish' else: command = args.pop(0) if command not in _COMMANDS: parser.error('invalid command \'%s\'' % command) elif command == 'publish': command_publish(args, options) elif command == 'date': command_date(args, options) if __name__ == '__main__': main() weblog-0.8/doc/weblog.rst0000644000000000000000000002345511006524342015566 0ustar00usergroup00000000000000Weblog manual ============= :Author: Henry Prêcheur :Reviewers: Anis Kadri, Bastien Simondi, Eric Salama Abstract -------- Simple blog publisher. It reads structured text files and generates static HTML / RSS files. Weblog aims to be simple and robust. In this document *Weblog* is the name of the software. The *web log* concept is referred as the more common term *blog*. According to Wikipedia_: A *blog* (a portmanteau of *web log*) is a website where entries are written in chronological order and commonly displayed in reverse chronological order. .. _Wikipedia: http://en.wikipedia.org/wiki/Blog Pre-requirements ~~~~~~~~~~~~~~~~ - Python version 2.5+ - Jinja version 1.1+. Learn how to install Jinja at http://jinja.pocoo.org/documentation/installation. Installation ------------ Download Weblog's latest version at http://henry.precheur.org/weblog/. Extract it:: tar zxf weblog.tar.gz It can be used right away using the helper script ``weblog_run.py``. Or install it using the supplied ``setup.py`` script. Run ``python setup.py --help`` to learn how to use it. Alternatively if easy_install is present, simply type:: easy_install weblog It fetches the latest version of Weblog and installs it. Quick Start ----------- In the following examples ``weblog/`` represents Weblog's installation directory. If you downloaded the source tarball without installing Weblog; Use the helper script ``weblog_run.py`` instead of the ``weblog`` command:: $ python /path/to/weblog/weblog_run.py --help Create a new directory named ``my_blog``. The $ sign represents the shell prompt, do not type it!:: $ mkdir my_blog Copy from the Weblog installation directory the file ``weblog.ini`` into ``my_blog``:: $ cp weblog/examples/weblog.ini my_blog ``weblog.ini`` is the configuration file of the blog. Check the configuration file section for more information. Do not worry about it now, no modification is required to get the following examples working. Create a file named ``first_post.html`` in the ``my_blog`` directory:: title: First post author: Me date: 2007-08-25 Hello world! Actually all the post filenames must end with ``.html``. Go in the ``my_blog`` directory and run the Weblog using the publish command:: $ cd my_blog/ $ weblog publish It should create a directory named ``output`` containing the generated files. Look at the results by opening the file ``output/index.html`` in your web-browser. The first 3 lines of the file ``first_post.html`` define the post's parameters. These are standard :RFC:`2822` headers (the headers used in Emails). Only ``title`` is mandatory. ``date`` and ``author`` are optional. If you don't fill these fields, the author is the one specified in ``weblog.ini``, and the post's date is the post file's last modification date. The line ``Hello world!`` is the actual content of the post. Note that a blank line is required between the headers and the content. The content is an HTML block. Use the HTML syntax to format your post content. For example create a second file named ``second_post.html``:: title: Second post author: Me (again!) date: 2007-08-26 Second test post!

© 2007 Me

Regenerate the blog files:: $ weblog publish Reload the page in your browser. You should see a second post with some formating. The default post file encoding is ASCII. To use a different encoding specify it via the field ``encoding``:: title: Encoding test date: 2007-11-5 encoding: latin-1 Here you can put some ISO-8856-1 text ... Specify the default encoding in ``weblog.ini``, to avoid setting the encoding field for every file. While writing your blog post, don't bother about the ``date`` field immediately. Weblog automatically sets the date to the filename's last modification time. A good practice though is to set the date when the post gets published. By doing so the date won't get changed if the file gets copied. To set the date of a post, use the command ``date``:: $ date Mon Apr 14 00:10:44 PDT 2008 $ cat my_blog_post.html title: My blog post This is a blog post without any date. $ weblog date my_blog_post.html Setting date to 2008-04-14 00:12:22 in file my_blog_post.html $ cat my_blog_post.html title: My blog post date: 2008-04-14 00:12:22 This is a blog post without any date. $ weblog date my_blog_post 2008-5-15 Setting date to 2008-05-15 in file my_blog_post.html $ cat my_blog_post.html title: My blog post date: 2008-05-15 This is a blog post without any date. The ``date`` command accepts 3 formats as argument: - YEAR-MONTH-DAY (2008-01-31) - YEAR-MONTH-DAY HOUR:MINUTE (2008-01-31 16:45) - YEAR-MONTH-DAY HOUR:MINUTE:SECONDS (2008-01-31 16:45:14) For conciseness the ``date`` command uses aliases to specify commonly used date: - now - today (like now but only set the date, not the time) - tomorrow (now + 24 hours) - next_day (like tomorrow but only sets the date, not the time) Encoding and escaping --------------------- Weblog tries to make sure its output is always *correct*. Non-ASCII characters, are converted to HTML entities so you don't have to worry about it. The output is *never* encoded into ISO-8856-1, UTF-8 or another non-ASCII encoding. Encoding conversions are not so simple in practice. By doing only one conversion to the simplest encoding possible, a lot of problems are solved. The content of the post is not escaped. The title and the date of the post are escaped. The title ``Hello World`` is escaped. HTML tags appear, and no formating is applied to ``world``. The original text "Hello World" appears instead of "Hello *World*", It is possible to override this by specifying ``raw`` as the encoding. Using the ``raw`` encoding nothing is escaped or converted, but you must make sure all characters are ASCII characters:: title: Non-escaped title author: Me <me@my_weblog.org> encoding: raw If the ``raw`` encoding is used, all the characters must be ASCII characters. Otherwise an error is reported. How URI's are handled --------------------- Relative links (````) are rewritten in the RSS file and in some HTML files. In the RSS file ``base_url`` is prepended to the link to make sure it always points to the correct URI. Absolute links (````) are not rewritten. It should always point to the correct location regardless of the context. Note that Weblog considers ``/`` as the root directory. If ``base_url`` is ``http://example.com/``; ``test.html`` and ``/test.html`` are both rewritten to ``http://example.com/test.html``. Command line parameters ----------------------- Usage: weblog [options] Options: -h, --help show this help message and exit -s DIR, --source-dir=DIR The source directory where the blog posts and the file weblog.ini are located -o DIR, --output-dir=DIR The directory where all the generated files are written. If it does not exist it is created. -q, --quiet Do not output anything except critical error messages Configuration file ------------------ All configuration options are in the ``weblog`` section. Learn more about the format of the configuration file: http://docs.python.org/lib/module-ConfigParser.html. A sample configuration file:: [weblog] title: Blog's title url: http://example.com/ description: A sample blog. source_dir: path/to/my/posts output_dir: path/to/output/directory encoding: latin-1 author: Me Fields description ~~~~~~~~~~~~~~~~~~ title The blog's title. It appears at the top of the homepage and in the page's title. This field is mandatory. url The base URL of your blog. For example ``http://my-host.com/my-weblog/``. It is used to generate the absolute URL's to your blog. This field is mandatory. description A short description of your blog. Like "My favorite books reviews", or "Dr. Spock, publications about electronics". Note that it is possible to use multiple lines:: description: My blog about configuration files. The description is merged to a single line; ``My blog about configuration files.``. This field is mandatory. source_dir The directory containing the file ``weblog.ini``, the post files and possibly the ``templates`` directory. By default the current directory. output_dir The output directory. Generated files are put there. By default ``output``. encoding The default post file encoding. Default ``ASCII``. It is overridden by the ``encoding`` field in the post file. author The default author. It is overridden by the ``author`` field in the post file. post_per_page The number of post displayed per listing page. Default is 10. rss_post_limit The maximum number of post to be included in the RSS file. The most recent posts are the ones included. Default is 10. html_head Additional information for the ```` section. Useful to add custom CSS style sheets. Can be a string or a filename. If a file with this name exists in the source directory then it is read. Else it is considered as a string. The result is processed using Jinja. Use the variable ``top_dir`` to link to external files. It contains the path to the top directory of the blog. Examples:: html_head= html_head={{ top_dir }}my_stylesheet.css html_header Additional content located just before the blog content. Can be a string or a filename. (See html_head above) Useful to add a logo or a search box at the top. html_footer Additional content located just after the blog content. Can be a string or a filename. (See html_head above) Useful to add ... A footer! .. vim:se tw=80 sw=2 ts=2 et encoding=utf-8: weblog-0.8/examples/enconding.html0000644000000000000000000000055111006524342017450 0ustar00usergroup00000000000000title: Weblog encode le français! author: Henry Prêcheur encoding: latin-1 date: 2007-10-01

Weblog encode maintenant le texte correctement! Des caractéres tels que: È, Õ ou Ä sont maintenant bien encodés!
Français, Español & Deutsh :)

The encoding of the file is ISO-8859-1 or latin-1.

weblog-0.8/examples/first_post.html0000644000000000000000000000007411006524342017700 0ustar00usergroup00000000000000title: First post author: Me date: 2007-08-25 Hello world! weblog-0.8/examples/second_post.html0000644000000000000000000000020711006524342020022 0ustar00usergroup00000000000000title: Second post date: 2007-08-26 Second test post!

The author lastname is Prêcheur

weblog-0.8/examples/utf-8.html0000644000000000000000000000016111006524342016444 0ustar00usergroup00000000000000title: Some UTF-8, ç ä é ö ó date: 2008-1-1 encoding: UTF-8 Test post with UTF-8 inside ... ç ä é ö ó weblog-0.8/examples/weblog.ini0000644000000000000000000000027011006524342016574 0ustar00usergroup00000000000000[weblog] title=Sample blog url=http://blog.sample.org description=Brief description of this sample blog. Do multiline, this way! encoding=UTF-8 author=Me weblog-0.8/setup.py0000644000000000000000000000261111006524342014511 0ustar00usergroup00000000000000try: from setuptools import setup except: from distutils.core import setup import os version = '0.8' f = open(os.path.join(os.path.dirname(__file__), 'doc', 'weblog.rst')) # The long description has to be ascii encoded ... long_description = f.read().strip().decode('utf-8').encode('ascii', 'replace') f.close() setup(name="weblog", version=version, packages=['weblog'], package_data={'weblog': ['templates/*.tmpl']}, scripts=['bin/weblog'], requires=['Jinja (>=1.1)'], install_requires=['Jinja >=1.1'], data_files=[('doc', ['doc/weblog.rst'])], # unzip the egg so we can access to documentation & templates zip_safe = False, # metadata for upload to PyPI author = 'Henry Precheur', author_email = 'henry@precheur.org', description = ('Simple blog publisher. It reads structured text ' 'files and generates static HTML / RSS files. Weblog aims ' 'to be simple and robust.'), long_description=long_description, license = "ISC", keywords = "weblog blog journal diary rss", url = "http://henry.precheur.org/weblog/", classifiers=[ 'Development Status :: 4 - Beta', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content :: News/Diary', 'Intended Audience :: End Users/Desktop', 'Programming Language :: Python', ]) weblog-0.8/test.py0000644000000000000000000001264311006524342014336 0ustar00usergroup00000000000000import os import shutil import tempfile import unittest import StringIO import email import datetime from optparse import Values from weblog import Post, PostError, jinja_environment from weblog.publish import load_post_list, generate_rss, generate_index_listing from weblog.date import command_date from weblog.publish import command_publish class TestSimpleLoad(unittest.TestCase): def test_load_post_list(self): post_list = load_post_list('test/simple/') self.assertEqual(len(post_list), 3) sorted_list = sorted(post_list) self.assertEqual(sorted_list[0].title, 'post1') self.assertEqual(sorted_list[1].title, 'post2') self.assertEqual(sorted_list[2].title, 'post3') def test_load_post_list_encoding_failure(self): Post.DEFAULT_ENCODING = 'ascii' self.assertRaises(PostError, load_post_list, 'test/encoding/') def test_load_post_list_encoding(self): Post.DEFAULT_ENCODING = 'UTF-8' post_list = load_post_list('test/encoding/') self.assertEqual(len(post_list), 2) sorted_list = sorted(post_list) self.assertEqual(sorted_list[0].title, 'UTF-8 post ÖÉÈÄ ...') self.assertEqual(sorted_list[0].content, 'Öéèä\n') self.assertEqual(sorted_list[1].title, 'latin post ÖÉÈÄ ...') self.assertEqual(sorted_list[1].content, 'Öéèä\n') class TestGeneration(unittest.TestCase): env = jinja_environment(os.path.dirname(__file__)) def setUp(self): self.tempdir = tempfile.mkdtemp() def tearDown(self): shutil.rmtree(self.tempdir) def test_generate_listing_empty(self): generate_index_listing(10, self.tempdir, self.env.get_template('index.html.tmpl'), list(), dict(title='test', url='http://test.net', description='test')) def test_generate_listing(self): post1 = '''title: post1 date: 2008-02-04 post 1''' post2 = '''title: post2 date: 2008-01-18 author: test@test.com post 2''' post_list = [Post(StringIO.StringIO(post1)), Post(StringIO.StringIO(post2))] generate_index_listing(10, self.tempdir, self.env.get_template('index.html.tmpl'), post_list, dict(title='test', url='http://test.net', description='test')) def test_generate_rss(self): post1 = '''title: post1 date: 2008-02-04 post 1''' post2 = '''title: post2 date: 2008-01-18 author: test@test.com post 2
''' post_list = [Post(StringIO.StringIO(post1)), Post(StringIO.StringIO(post2))] generate_rss(post_list, os.path.join(self.tempdir, 'rss.xml'), dict(title='test', url='http://test.net', description='test')) def test_generate_rss_empty(self): generate_rss(list(), os.path.join(self.tempdir, 'rss.xml'), dict(title='test', url='http://test.net', description='test')) def test_date(self): filename = os.path.join(self.tempdir, 'set_date.html') # First test a message without any date defined def file_without_date(): file(filename, 'w').write('title: Some title\n\nSome content') file_without_date() command_date([filename, '2008-1-1'], None) message = email.message_from_file(file(filename)) self.assert_('date' in message) self.assertEqual(message['date'], str(datetime.date(2008, 1, 1))) # Then test a file which has already a date def file_with_date(): file(filename, 'w').write('title: Some title\ndate: 2008-12-31\n' '\nSome content') file_with_date() command_date([filename, '2008-1-1'], None) message = email.message_from_file(file(filename)) self.assert_('date' in message) self.assertEqual(message['date'], str(datetime.date(2008, 1, 1))) # Test aliases for alias in ('now', 'today', 'tomorrow', 'next_day'): file_without_date() command_date([filename, alias], None) message = email.message_from_file(file(filename)) self.assert_('date' in message) def _test_publish(self, dirname): options = Values(dict(source_dir=os.path.join(os.path.dirname(__file__), 'test', dirname), output_dir=self.tempdir, debug=False)) command_publish(None, options) def test_publish_empty(self): self._test_publish('empty') def test_publish_encoding(self): self._test_publish('encoding') def test_publish_full_uri(self): self._test_publish('full_uri') def test_publish_simple(self): self._test_publish('simple') if __name__ == '__main__': unittest.main() weblog-0.8/test/empty/weblog.ini0000644000000000000000000000014411006524342017073 0ustar00usergroup00000000000000[weblog] title=Test blog url=http://blog.test.org description=Test blog author=test weblog-0.8/test/encoding/latin-1.html0000644000000000000000000000010411006524342017672 0ustar00usergroup00000000000000title: latin post ÖÉÈÄ ... date: 2008-02-04 encoding: latin-1 Öéèä weblog-0.8/test/encoding/utf-8.html0000644000000000000000000000007211006524342017374 0ustar00usergroup00000000000000title: UTF-8 post ÖÉÈÄ ... date: 2008-02-03 Öéèä weblog-0.8/test/encoding/weblog.ini0000644000000000000000000000016311006524342017524 0ustar00usergroup00000000000000[weblog] title=Test blog url=http://blog.test.org description=Test blog author=test encoding=utf-8 weblog-0.8/test/full_uri/utf-8.html0000644000000000000000000000025011006524342017425 0ustar00usergroup00000000000000title: UTF-8 post ÖÉÈÄ ... date: 2008-02-03 Öéèä äyÔÀ Weblog weblog-0.8/test/full_uri/weblog.ini0000644000000000000000000000016311006524342017557 0ustar00usergroup00000000000000[weblog] title=Test blog url=http://blog.test.org description=Test blog author=test encoding=utf-8 weblog-0.8/test/simple/post1.html0000644000000000000000000000004511006524342017202 0ustar00usergroup00000000000000title: post1 date: 2007-01-01 post1 weblog-0.8/test/simple/post2.html0000644000000000000000000000004411006524342017202 0ustar00usergroup00000000000000title: post2 date: 2007-6-15 post2 weblog-0.8/test/simple/post3.html0000644000000000000000000000004511006524342017204 0ustar00usergroup00000000000000title: post3 date: 2007-12-31 post3 weblog-0.8/test/simple/weblog.ini0000644000000000000000000000014411006524342017226 0ustar00usergroup00000000000000[weblog] title=Test blog url=http://blog.test.org description=Test blog author=test weblog-0.8/weblog/PyRSS2Gen.py0000644000000000000000000003432411006524342016332 0ustar00usergroup00000000000000"""PyRSS2Gen - A Python library for generating RSS 2.0 feeds.""" __name__ = "PyRSS2Gen" __version__ = (1, 0, 0) __author__ = "Andrew Dalke " _generator_name = __name__ + "-" + ".".join(map(str, __version__)) import datetime # Could make this the base class; will need to add 'publish' class WriteXmlMixin: def write_xml(self, outfile, encoding = "iso-8859-1"): from xml.sax import saxutils handler = saxutils.XMLGenerator(outfile, encoding) handler.startDocument() self.publish(handler) handler.endDocument() def to_xml(self, encoding = "iso-8859-1"): try: import cStringIO as StringIO except ImportError: import StringIO f = StringIO.StringIO() self.write_xml(f, encoding) return f.getvalue() def _element(handler, name, obj, d = {}): if isinstance(obj, basestring) or obj is None: # special-case handling to make the API easier # to use for the common case. handler.startElement(name, d) if obj is not None: handler.characters(obj) handler.endElement(name) else: # It better know how to emit the correct XML. obj.publish(handler) def _opt_element(handler, name, obj): if obj is None: return _element(handler, name, obj) def _format_date(dt): """convert a datetime into an RFC 822 formatted date Input date must be in GMT. """ # Looks like: # Sat, 07 Sep 2002 00:00:01 GMT # Can't use strftime because that's locale dependent # # Isn't there a standard way to do this for Python? The # rfc822 and email.Utils modules assume a timestamp. The # following is based on the rfc822 module. if isinstance(dt, datetime.datetime): hour = dt.hour minute = dt.minute second = dt.second elif isinstance(dt, datetime.date): hour = minute = second = 0 return "%s, %02d %s %04d %02d:%02d:%02d GMT" % ( ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"][dt.weekday()], dt.day, ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][dt.month-1], dt.year, hour, minute, second) ## # A couple simple wrapper objects for the fields which # take a simple value other than a string. class IntElement: """implements the 'publish' API for integers Takes the tag name and the integer value to publish. (Could be used for anything which uses str() to be published to text for XML.) """ element_attrs = {} def __init__(self, name, val): self.name = name self.val = val def publish(self, handler): handler.startElement(self.name, self.element_attrs) handler.characters(str(self.val)) handler.endElement(self.name) class DateElement: """implements the 'publish' API for a datetime.date Takes the tag name and the datetime to publish. Converts the datetime to RFC 2822 timestamp (4-digit year). """ def __init__(self, name, dt): self.name = name self.dt = dt def publish(self, handler): _element(handler, self.name, _format_date(self.dt)) #### class Category: """Publish a category element""" def __init__(self, category, domain = None): self.category = category self.domain = domain def publish(self, handler): d = {} if self.domain is not None: d["domain"] = self.domain _element(handler, "category", self.category, d) class Cloud: """Publish a cloud""" def __init__(self, domain, port, path, registerProcedure, protocol): self.domain = domain self.port = port self.path = path self.registerProcedure = registerProcedure self.protocol = protocol def publish(self, handler): _element(handler, "cloud", None, { "domain": self.domain, "port": str(self.port), "path": self.path, "registerProcedure": self.registerProcedure, "protocol": self.protocol}) class Image: """Publish a channel Image""" element_attrs = {} def __init__(self, url, title, link, width = None, height = None, description = None): self.url = url self.title = title self.link = link self.width = width self.height = height self.description = description def publish(self, handler): handler.startElement("image", self.element_attrs) _element(handler, "url", self.url) _element(handler, "title", self.title) _element(handler, "link", self.link) width = self.width if isinstance(width, int): width = IntElement("width", width) _opt_element(handler, "width", width) height = self.height if isinstance(height, int): height = IntElement("height", height) _opt_element(handler, "height", height) _opt_element(handler, "description", self.description) handler.endElement("image") class Guid: """Publish a guid Defaults to being a permalink, which is the assumption if it's omitted. Hence strings are always permalinks. """ def __init__(self, guid, isPermaLink = 1): self.guid = guid self.isPermaLink = isPermaLink def publish(self, handler): d = {} if self.isPermaLink: d["isPermaLink"] = "true" else: d["isPermaLink"] = "false" _element(handler, "guid", self.guid, d) class TextInput: """Publish a textInput Apparently this is rarely used. """ element_attrs = {} def __init__(self, title, description, name, link): self.title = title self.description = description self.name = name self.link = link def publish(self, handler): handler.startElement("textInput", self.element_attrs) _element(handler, "title", self.title) _element(handler, "description", self.description) _element(handler, "name", self.name) _element(handler, "link", self.link) handler.endElement("textInput") class Enclosure: """Publish an enclosure""" def __init__(self, url, length, type): self.url = url self.length = length self.type = type def publish(self, handler): _element(handler, "enclosure", None, {"url": self.url, "length": str(self.length), "type": self.type, }) class Source: """Publish the item's original source, used by aggregators""" def __init__(self, name, url): self.name = name self.url = url def publish(self, handler): _element(handler, "source", self.name, {"url": self.url}) class SkipHours: """Publish the skipHours This takes a list of hours, as integers. """ element_attrs = {} def __init__(self, hours): self.hours = hours def publish(self, handler): if self.hours: handler.startElement("skipHours", self.element_attrs) for hour in self.hours: _element(handler, "hour", str(hour)) handler.endElement("skipHours") class SkipDays: """Publish the skipDays This takes a list of days as strings. """ element_attrs = {} def __init__(self, days): self.days = days def publish(self, handler): if self.days: handler.startElement("skipDays", self.element_attrs) for day in self.days: _element(handler, "day", day) handler.endElement("skipDays") class RSS2(WriteXmlMixin): """The main RSS class. Stores the channel attributes, with the "category" elements under ".categories" and the RSS items under ".items". """ rss_attrs = {"version": "2.0"} element_attrs = {} def __init__(self, title, link, description, language = None, copyright = None, managingEditor = None, webMaster = None, pubDate = None, # a datetime, *in* *GMT* lastBuildDate = None, # a datetime categories = None, # list of strings or Category generator = _generator_name, docs = "http://blogs.law.harvard.edu/tech/rss", cloud = None, # a Cloud ttl = None, # integer number of minutes image = None, # an Image rating = None, # a string; I don't know how it's used textInput = None, # a TextInput skipHours = None, # a SkipHours with a list of integers skipDays = None, # a SkipDays with a list of strings items = None, # list of RSSItems ): self.title = title self.link = link self.description = description self.language = language self.copyright = copyright self.managingEditor = managingEditor self.webMaster = webMaster self.pubDate = pubDate self.lastBuildDate = lastBuildDate if categories is None: categories = [] self.categories = categories self.generator = generator self.docs = docs self.cloud = cloud self.ttl = ttl self.image = image self.rating = rating self.textInput = textInput self.skipHours = skipHours self.skipDays = skipDays if items is None: items = [] self.items = items def publish(self, handler): handler.startElement("rss", self.rss_attrs) handler.startElement("channel", self.element_attrs) _element(handler, "title", self.title) _element(handler, "link", self.link) _element(handler, "description", self.description) self.publish_extensions(handler) _opt_element(handler, "language", self.language) _opt_element(handler, "copyright", self.copyright) _opt_element(handler, "managingEditor", self.managingEditor) _opt_element(handler, "webMaster", self.webMaster) pubDate = self.pubDate if isinstance(pubDate, (datetime.datetime, datetime.date)): pubDate = DateElement("pubDate", pubDate) _opt_element(handler, "pubDate", pubDate) lastBuildDate = self.lastBuildDate if isinstance(lastBuildDate, (datetime.datetime, datetime.date)): lastBuildDate = DateElement("lastBuildDate", lastBuildDate) _opt_element(handler, "lastBuildDate", lastBuildDate) for category in self.categories: if isinstance(category, basestring): category = Category(category) category.publish(handler) _opt_element(handler, "generator", self.generator) _opt_element(handler, "docs", self.docs) if self.cloud is not None: self.cloud.publish(handler) ttl = self.ttl if isinstance(self.ttl, int): ttl = IntElement("ttl", ttl) _opt_element(handler, "tt", ttl) if self.image is not None: self.image.publish(handler) _opt_element(handler, "rating", self.rating) if self.textInput is not None: self.textInput.publish(handler) if self.skipHours is not None: self.skipHours.publish(handler) if self.skipDays is not None: self.skipDays.publish(handler) for item in self.items: item.publish(handler) handler.endElement("channel") handler.endElement("rss") def publish_extensions(self, handler): # Derived classes can hook into this to insert # output after the three required fields. pass class RSSItem(WriteXmlMixin): """Publish an RSS Item""" element_attrs = {} def __init__(self, title = None, # string link = None, # url as string description = None, # string author = None, # email address as string categories = None, # list of string or Category comments = None, # url as string enclosure = None, # an Enclosure guid = None, # a unique string pubDate = None, # a datetime source = None, # a Source ): if title is None and description is None: raise TypeError( "must define at least one of 'title' or 'description'") self.title = title self.link = link self.description = description self.author = author if categories is None: categories = [] self.categories = categories self.comments = comments self.enclosure = enclosure self.guid = guid self.pubDate = pubDate self.source = source # It sure does get tedious typing these names three times... def publish(self, handler): handler.startElement("item", self.element_attrs) _opt_element(handler, "title", self.title) _opt_element(handler, "link", self.link) self.publish_extensions(handler) _opt_element(handler, "description", self.description) _opt_element(handler, "author", self.author) for category in self.categories: if isinstance(category, basestring): category = Category(category) category.publish(handler) _opt_element(handler, "comments", self.comments) if self.enclosure is not None: self.enclosure.publish(handler) _opt_element(handler, "guid", self.guid) pubDate = self.pubDate if isinstance(pubDate, (datetime.datetime, datetime.date)): pubDate = DateElement("pubDate", pubDate) _opt_element(handler, "pubDate", pubDate) if self.source is not None: self.source.publish(handler) handler.endElement("item") def publish_extensions(self, handler): # Derived classes can hook into this to insert # output after the title and link elements pass weblog-0.8/weblog/__init__.py0000644000000000000000000000130111006524342016362 0ustar00usergroup00000000000000from utils import load_configuration from post import Post, PostError from jinja_environment import jinja_environment from html_full_uri import html_full_uri from publish import command_publish from date import command_date import listing __all__ = ('Post', 'PostError', 'listing', 'jinja_environment', 'load_configuration', 'html_full_uri', 'command_publish', 'command_date') if __name__ == '__main__': import doctest import utils import post import listing import html_full_uri import date doctest.testmod(utils) doctest.testmod(post) doctest.testmod(listing) doctest.testmod(html_full_uri) doctest.testmod(date) doctest.testmod() weblog-0.8/weblog/date.py0000644000000000000000000000403211006524342015544 0ustar00usergroup00000000000000import sys import logging import datetime from post import Post def command_date(args, options): ''' Execute the 'date' command, which set the date to the specified filename. The command need at least one parameter. The remaining parameters are the date to be set in the file. >>> command_date(None, None) # doctest: +ELLIPSIS Traceback (most recent call last): ... SystemExit: No file specified: ... >>> command_date(['/dev/null', '2008-1000-10'], None) Traceback (most recent call last): ... SystemExit: Unable to parse date '2008-1000-10' (Use YYYY-MM-DD [[HH:MM]:SS] format) ''' if not args: sys.exit(StandardError('No file specified:\n %s date filename [date]' % sys.argv[0])) filename = args.pop(0) if args: if len(args) == 1 and args[0] == 'today': date = datetime.date.today() elif len(args) == 1 and args[0] == 'next_day': date = datetime.date.today() + datetime.timedelta(days=1) elif len(args) == 1 and args[0] == 'tomorrow': date = datetime.datetime.now() + datetime.timedelta(days=1) elif len(args) == 1 and args[0] == 'now': date = datetime.datetime.now() else: try: date = Post.parse_date(' '.join(args)) except ValueError, e: sys.exit(e) else: date = datetime.datetime.now() def _date_string(date): if isinstance(date, datetime.datetime): return date.strftime('%Y-%m-%d %H:%M:%S') else: return str(date) logging.info('Setting date to %s in file %s', _date_string(date), filename) import email try: post_file = email.message_from_file(file(filename)) if 'date' in post_file: post_file.replace_header('date', _date_string(date)) else: post_file.add_header('date', _date_string(date)) file(filename, 'w').write(post_file.as_string()) except IOError, e: sys.exit(e) weblog-0.8/weblog/html_full_uri.py0000644000000000000000000001072211006524342017477 0ustar00usergroup00000000000000import re _scheme_regex = re.compile(r'\w+://') def external_uri(uri): ''' Returns True if ``uri`` refers to an external resource. >>> external_uri('http://www.google.ca/') True >>> external_uri('mailto://me@example.com') True >>> external_uri('/pic.jpg') False >>> external_uri('') False ''' if _scheme_regex.match(uri): return True else: return False from HTMLParser import HTMLParser from cStringIO import StringIO class FullUrlHtmlParser(HTMLParser): ''' Parse an HTML document and transform relative URI to absolute URI. Prepending ``base_uri`` to them. >>> p = FullUrlHtmlParser('http://www.example.com') >>> p.feed('') >>> print p.buffer.getvalue() A more complex example:: >>> p.reset() >>> p.feed(r""" ... ... foo ... ... some random text. ... bar ... »~ ... ... ... More ..........""") >>> print p.buffer.getvalue() #doctest: +NORMALIZE_WHITESPACE foo some random text. bar »~ More .......... ''' def __init__(self, base_uri): HTMLParser.__init__(self) self.buffer = StringIO() self.base_uri = base_uri.rstrip('/') def reset(self): HTMLParser.reset(self) if hasattr(self, 'buffer'): del self.buffer self.buffer = StringIO() @staticmethod def html_attrs(attrs): return ' '.join('%s=\'%s\'' % (k, v) for k,v in attrs.iteritems()) def make_full_url(self, attr, attrs): if attr in attrs and not external_uri(attrs[attr]): attrs[attr] = '/'.join((self.base_uri, attrs[attr].lstrip('/'))) def check_and_rewrite_tag(self, tag, attrs, endtag=''): if attrs: attrs = dict(attrs) if tag == 'a': self.make_full_url('href', attrs) elif tag == 'img': self.make_full_url('src', attrs) elif tag == 'object': self.make_full_url('data', attrs) self.make_full_url('codebase', attrs) elif tag == 'script': self.make_full_url('src', attrs) self.buffer.write('<%s %s%s>' % (tag, self.html_attrs(attrs), endtag)) else: self.buffer.write('<%s%s>' % (tag, endtag)) def handle_starttag(self, tag, attrs): self.check_and_rewrite_tag(tag, attrs) def handle_startendtag(self, tag, attrs): self.check_and_rewrite_tag(tag, attrs, endtag='/') def handle_endtag(self, tag): self.buffer.write('' % tag) def handle_data(self, data): self.buffer.write(data) def handle_charref(self, name): self.buffer.write('&#%s;' % name) def handle_entityref(self, name): self.buffer.write('&%s;' % name) def handle_comment(self, comment): self.buffer.write('' % comment) def handle_decl(self, decl): self.buffer.write('' % decl) def handle_pi(self, pi): self.buffer.write('' % pi) def html_full_uri(base_url, text): ''' Appends ``base_uri`` to relative uri's in the HTML document ``text``. Example with ``base_uri=http://example.com``:: '' becomes '' '' becomes '' but ' is not changed since it is an *absolute* URI. >>> html_full_uri('http://example.com', '') "" >>> html_full_uri('http://example.com', '') "" ''' p = FullUrlHtmlParser(base_url) p.feed(text) return p.buffer.getvalue() if __name__ == '__main__': import doctest doctest.testmod() weblog-0.8/weblog/jinja_environment.py0000644000000000000000000000331411006524342020350 0ustar00usergroup00000000000000import os import sys try: from jinja import Environment, FileSystemLoader, PackageLoader, ChoiceLoader except ImportError: sys.exit('Please install Jinja (http://jinja.pocoo.org/) to use Weblog') def jinja_environment(source_dir): """ Build the Jinja environment. Setup all template loaders. """ TEMPLATE_DIR = 'templates' fs_loader = FileSystemLoader(os.path.join(source_dir, TEMPLATE_DIR)) fs_app_loader = FileSystemLoader(os.path.join(sys.path[0], 'weblog', TEMPLATE_DIR)) # if setuptools is present use the loader else fake it. try: import pkg_resources except ImportError: pkg_loader = FileSystemLoader(os.path.join(os.path.dirname(__file__), TEMPLATE_DIR)) else: pkg_loader = PackageLoader('weblog', TEMPLATE_DIR) choice_loader = ChoiceLoader([fs_loader, fs_app_loader, pkg_loader]) env = Environment(loader=choice_loader, trim_blocks=True) def do_renderstring(): def wrapped(env, context, value): ''' Render the passed string. It is similar to the tag rendertemplate, except it uses the passed string as the template. Example: The template 'Hello {{ string_template|renderstring }}!'; Called with the following context: dict(string_template='{{ foo }} world', foo='crazy') Renders to: 'Hello crazy world!' ''' if value: return env.from_string(value).render(context.to_dict()) return wrapped env.filters['renderstring'] = do_renderstring return env weblog-0.8/weblog/listing.py0000644000000000000000000001122611006524342016303 0ustar00usergroup00000000000000from os.path import join class Page(object): ''' Page contains a `string key` named title used to compare against other `Page`s and strings. It is used for the pagination. The item can be whatever you want. >>> page_list = sorted([Page('2', 2), Page('3', None), Page('1', '1')]) >>> page_list [, , ] >>> for i in page_list: ... print i.title, i.item 1 1 2 2 3 None >>> '2' in page_list True >>> '5' in page_list False >>> page_list.index('2') 1 ''' def __init__(self, title, item): ''' >>> Page('foo', 'bar') ''' super(Page, self).__init__() self.title = title self.item = item def filename(self): return self.title + '.html' def url(self): ''' >>> Page('foo', 'bar').url() 'foo.html' ''' return self.filename() def __repr__(self): return '<%s(%r, %r)>' % \ (self.__class__.__name__, self.title, self.item) def __cmp__(self, other): ''' >>> Page('1', '') > '2' False >>> Page('foo', 'string') == 'foo' True >>> Page('2', '') > Page('4', '') False >>> Page('bar', '') == Page('bar', '') True >>> Page('1', '') > 0 # base object address comparison True ''' if isinstance(other, Page): return cmp(self.title, other.title) elif isinstance(other, self.title.__class__): return cmp(self.title, other) else: return cmp(id(self), id(object)) class PageIndex(Page): ''' Special case to have a page that returns 'index.html' as filename. Used for the first page of the listing. ''' def filename(self): return 'index.html' def slice_list(full_list, limit): ''' Return an iterable containing the given list sliced in sub-lists of size `limit`. >>> list(slice_list(range(10), 3)) [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]] >>> list(slice_list([], 10)) [] >>> list(slice_list([1, 2, 3], 5)) [[1, 2, 3]] >>> list(slice_list([1, 2, 3], 3)) [[1, 2, 3]] ''' it = iter(full_list) result = list() try: while True: for c in xrange(limit): result.append(it.next()) yield result result = list() except StopIteration: if result: yield result raise StopIteration def slice_list_groupby(full_list, func): ''' >>> slice_list_groupby(range(10), lambda x: 'even' if x % 2 == 0 else 'odd') {'even': [0, 2, 4, 6, 8], 'odd': [1, 3, 5, 7, 9]} >>> slice_list_groupby([1, 2, 3], lambda x: x) {1: [1], 2: [2], 3: [3]} >>> slice_list_groupby([dict(key='1'), dict(key='2'), dict(key='3')], ... lambda x: x['key']) {'1': [{'key': '1'}], '3': [{'key': '3'}], '2': [{'key': '2'}]} ''' d = dict() for i in full_list: key = func(i) if key in d: d[key].append(i) else: d[key] = [i] return d def generate_list(output_dir, template, page_list, template_params): for page in page_list: filename = join(output_dir, page.filename()) output_file = file(filename, 'w') output_file.write(template.render(post_list=page.item, page=page, pages=page_list, **template_params)) output_file.close() def generate_index_listing(limit, output_dir, template, post_list, template_params): ''' Generate a listing containing at most `limit` post per page. ''' # list() needed since pages is subscripted later pages = list(slice_list(post_list, limit)) if pages: pages = [PageIndex('0', pages[0])] + \ [Page(str(k + 1), v) for k, v in enumerate(pages[1:])] else: # If there is no page generates an 'empty' page pages = [PageIndex('0', None)] generate_list(output_dir, template, pages, template_params) def generate_monthly_listing(output_dir, template, post_list, template_params): pages = slice_list_groupby(post_list, lambda x: str(x.date.year) + '-' + str(x.date.month)) pages = list(Page(str(k), pages[k]) for k in (sorted(pages.keys()))) generate_list(output_dir, template, pages, template_params) if __name__ == '__main__': import doctest doctest.testmod() weblog-0.8/weblog/post.py0000644000000000000000000002075611006524342015627 0ustar00usergroup00000000000000import email import codecs import logging from os import stat from datetime import datetime, date from urllib import quote from cgi import escape from utils import encode, escape_and_encode class PostError(Exception): ''' Error in post file ''' def __init__(self, message, filename, line=None): super(PostError, self).__init__(self, message) self.message = message self.filename = filename self.line = line def __repr__(self): return '%s(%r, %r, %r)' % (self.__class__.__name__, self.exception, self.filename, self.line) def __str__(self): if self.line is None: return 'Error in post file %s: %s' % (self.filename, self.message) else: return 'Error in post file %s line %d: %s' % (self.filename, self.line, self.message) class Post(object): DEFAULT_ENCODING = 'ascii' DEFAULT_AUTHOR = 'unknown author' def __init__(self, f): ''' >>> file_content = """title: test ... date: 2008-1-1 ... author: test author ... encoding: utf-8 ... ... test.""" >>> from StringIO import StringIO >>> p = Post(StringIO(file_content)) >>> p >>> p.title == 'test' True >>> import datetime >>> p.date == datetime.date(2008, 1, 1) True >>> p.content == 'test.' True >>> p.encoding == 'utf-8' True >>> p.author == 'test author' True >>> Post(StringIO('title: no payload\\ndate: 2008-1-1')) >>> Post(StringIO('title: no date')) Traceback (most recent call last): ... PostError: Error in post file : No date defined >>> Post(StringIO("""title: bad encoding ... date: 2008-1-1 ... encoding: bad-encoding""")) Traceback (most recent call last): ... PostError: Error in post file : unknown encoding: \ bad-encoding >>> Post(StringIO("""title: bad date ... date: 200008-101-10""")) Traceback (most recent call last): ... PostError: Error in post file : Unable to parse \ date '200008-101-10' (Use YYYY-MM-DD [[HH:MM]:SS] format) ''' if isinstance(f, (str, basestring)): self._filename = f input_file = open(f) else: self._filename = None input_file = f post_file = email.message_from_file(input_file) self.__dict__.update((key.lower(), value) for (key, value) in post_file.items()) if not hasattr(self, 'encoding'): self.encoding = self.DEFAULT_ENCODING if self.encoding.lower() != 'raw': try: codecs.lookup(self.encoding) except LookupError, e: raise PostError(e.message, self.get_filename()) if not hasattr(self, 'author'): self.author = self.DEFAULT_AUTHOR # Handle the date. If no date was specified use the file's modification # time. if not hasattr(self, 'date'): # Get the date from file's mtime and issue a warning if not self._filename: raise PostError('No date defined', self.get_filename()) else: self.date = datetime.\ fromtimestamp(stat(self._filename).st_mtime) logging.warning('No date defined in \'%s\', using the ' \ 'file\'s last modification time instead.' % \ self._filename) else: try: self.date = self.parse_date(self.date) except ValueError, e: raise PostError(e.message, self.get_filename()) try: self.ascii_title = self.title.decode('ascii' if self.encoding == 'raw' else self.encoding).\ encode('ascii', 'replace') self.title = escape_and_encode(self.title, self.encoding) except UnicodeDecodeError, e: raise PostError('Bad encoding in title', self.get_filename()) try: self.author = escape_and_encode(self.author, self.encoding) except UnicodeDecodeError, e: raise PostError('Bad encoding in author', self.get_filename()) try: self.content = encode(post_file.get_payload(), self.encoding) except UnicodeDecodeError, e: # find error line number for line_number, line in enumerate(post_file.as_string().\ splitlines()): try: line.decode('ascii' if self.encoding == 'raw' else self.encoding) except UnicodeDecodeError, e: break # line_number starts at 0, real line number == line_number + 1 raise PostError('Bad encoding in content line %d, %s' % \ (line_number + 1, e), self.get_filename()) # FIXME prefix & suffix param or members of the class ? def url(self, prefix=''): ''' >>> file_content = """title: test ... date: 2008-1-1 ... ... test""" >>> from StringIO import StringIO >>> Post(StringIO(file_content)).url() '2008/1/1/test.html' >>> Post(StringIO(file_content)).url('prefix/') 'prefix/2008/1/1/test.html' >>> file_content = """title: Weird @!% filename ... date: 2008-1-1 ... ... test""" >>> Post(StringIO(file_content)).url() '2008/1/1/Weird%20%40%21%25%20filename.html' ''' return '%s%d/%d/%d/%s.html' % \ (prefix, self.date.year, self.date.month, self.date.day, quote(self.ascii_title)) _DATE_FORMAT_LIST = ('%Y-%m-%d', '%y-%m-%d') _DATETIME_FORMAT_LIST = \ tuple('%s %%H:%%M' % f for f in _DATE_FORMAT_LIST) + \ tuple('%s %%H:%%M:%%S' % f for f in _DATE_FORMAT_LIST) @staticmethod def parse_date(date_): """ >>> Post.parse_date('2006-1-1') datetime.date(2006, 1, 1) >>> Post.parse_date('2007-12-31') datetime.date(2007, 12, 31) >>> Post.parse_date('2008-4-05 12:35') datetime.datetime(2008, 4, 5, 12, 35) >>> Post.parse_date('10000-1-1') Traceback (most recent call last): ... ValueError: Unable to parse date '10000-1-1' (Use YYYY-MM-DD [[HH:MM]:SS] format) >>> Post.parse_date(2007) Traceback (most recent call last): ... TypeError: strptime() argument 1 must be string, not int """ for date_format in Post._DATE_FORMAT_LIST: try: return datetime.strptime(date_, date_format).date() except ValueError: continue for date_format in Post._DATETIME_FORMAT_LIST: try: return datetime.strptime(date_, date_format) except ValueError: continue raise ValueError('Unable to parse date %r\n' '(Use YYYY-MM-DD [[HH:MM]:SS] format)' % (date_)) def get_filename(self): if not self._filename: return '' else: return self._filename def __cmp__(self, other): ''' >>> file1 = """title: 1 ... date: 2008-1-1 ... ... test""" >>> file2 = """title: 2 ... date: 2007-12-31""" >>> from StringIO import StringIO >>> Post(StringIO(file1)) > Post(StringIO(file2)) True >>> Post(StringIO(file1)) == Post(StringIO(file2)) False >>> Post(StringIO(file1)) == Post(StringIO(file1)) True >>> l = [Post(StringIO(file2)), Post(StringIO(file1))] >>> l.index(Post(StringIO(file1))) 1 ''' return cmp(str(self.date) + str(self.title), str(other.date) + str(self.title)) def __hash__(self): return hash(str(self.date) + self.title) def __repr__(self): return '<%s(%r, %r)>' % (self.__class__.__name__, self.title, self.date) if __name__ == '__main__': import doctest doctest.testmod() weblog-0.8/weblog/publish.py0000644000000000000000000001315311006524342016301 0ustar00usergroup00000000000000import os import datetime import logging from sys import exit from shutil import copy import jinja_environment from PyRSS2Gen import RSS2, RSSItem from html_full_uri import html_full_uri from post import Post, PostError from utils import load_configuration from listing import generate_index_listing def load_post_list(path): ''' List and load all the files ending with '.html' in the passed directory. Returns a list containing ``Post`` objects created using the loaded files. ''' post_list = set() for filename in os.listdir(path): if filename.endswith('.html'): logging.debug('Loading \'%s\'', filename) p = Post(os.path.join(path, filename)) if p in post_list: logging.debug('%r is duplicated', p) for duplicated_post in post_list: if duplicated_post == p: break raise IOError('"%s", there is already a post ' 'with this title and date ("%s")' % \ (filename, duplicated_post.get_filename())) else: post_list.add(p) else: logging.debug('Ignoring \'%s\'', filename) return post_list def generate_post_html(post_list, output_dir, post_tmpl, params): for post in post_list: logging.debug('Generating HTML file for %r', post) dir = os.path.join(output_dir, str(post.date.year), str(post.date.month), str(post.date.day)) if not os.path.exists(dir): logging.debug('Creating \'%s\'', dir) os.makedirs(dir) elif not os.path.isdir(dir): raise IOError('\'%s\' already exists and is not a directory' % dir) filename = os.path.join(dir, post.ascii_title + '.html') output = file(filename, 'w') top_dir = '../../../' output.write(post_tmpl.render(title=post.title, date=post.date, author=post.author, content=html_full_uri(top_dir, post.content), top_dir=top_dir, **dict(((k, v) for k, v in params.iteritems() if k != 'title')))) def generate_rss(post_list, filename, params): def make_rss_item(post): return RSSItem(title=post.title, link=post.url(prefix=params['url']), description=html_full_uri(params['url'], post.content), guid=post.url(prefix=params['url']), pubDate=post.date) rss = RSS2(title = params['title'], link = params['url'], description = params['description'], lastBuildDate = datetime.datetime.now(), items=(make_rss_item(post) for post in post_list)) rss.write_xml(open(filename, "w")) def command_publish(args, options): source_dir = options.source_dir output_dir = options.output_dir # hard-coded configuration file. Might be a good idea to make it # customizable. CONFIG_FILE = 'weblog.ini' try: config = load_configuration(CONFIG_FILE, source_dir or '.') except IOError, e: logging.error('Error while loading configuration file \'%s\'' % \ CONFIG_FILE) exit(e) source_dir = source_dir or config.get('source_dir', '.') output_dir = output_dir or config.get('output_dir', 'output') # add the default author & encoding constant to the post class if 'encoding' in config: Post.DEFAULT_ENCODING = config['encoding'] author = config.get('author', None) if author: Post.DEFAULT_AUTHOR = author if not os.path.exists(output_dir): os.mkdir(output_dir) env = jinja_environment.jinja_environment(source_dir) try: post_list = list(reversed(sorted(load_post_list(source_dir)))) except (IOError, PostError), e: logging.error('Error while loading post files.') exit(e) def generate_all(): params = dict(title=config['title'], description=config['description'], url=config['url'], html_head=config.get('html_head'), html_header=config.get('html_header'), html_footer=config.get('html_footer')) # generate the main index page logging.debug('Generating HTML listings') index_template = env.get_template('index.html.tmpl') generate_index_listing(config['post_per_page'], output_dir, index_template, post_list, params) logging.debug('Generating HTML posts files') post_tmpl = env.get_template('post.html.tmpl') generate_post_html(post_list, output_dir, post_tmpl, params) generate_rss(post_list[:config['rss_post_limit']], os.path.join(output_dir, 'rss.xml'), params) for f in config.get('extra_files', '').split(): copy(os.path.join(source_dir, f), output_dir) if options.debug: generate_all() else: try: generate_all() except IOError, e: logging.error('Error while generating files ...') exit(e) else: logging.info('Successfully generated weblog.') weblog-0.8/weblog/templates/base.html.tmpl0000644000000000000000000000114211006524342021025 0ustar00usergroup00000000000000 {% block title %}{{ title }}{% endblock %} {{ html_head|renderstring }} {% block extrahead %} {% endblock %} {{ html_header|renderstring }}
{% block content %}{% endblock %}
{{ html_footer|renderstring }} {# vim:set ft=htmljinja: #} weblog-0.8/weblog/templates/index.html.tmpl0000644000000000000000000000173611006524342021233 0ustar00usergroup00000000000000{% extends 'base.html.tmpl' %} {% block content %}

{{ title }}

{% if description %}

{{ description }}

{% endif %} {% for post in post_list %}

{{ post.title }}

{{ post.date }}, by {{ post.author | urlize }}

{{ post.content }}
{% endfor %} {% if pages|length > 1 %} {% endif %} {% endblock %} {# vim:set ft=htmljinja: #} weblog-0.8/weblog/templates/post.html.tmpl0000644000000000000000000000053711006524342021107 0ustar00usergroup00000000000000{% extends 'base.html.tmpl' %} {% block content %}

{{ title }}

{{ date }}, by {{ author | urlize }}

{{ content }}

back to the blog

{% endblock %} {# vim:set ft=htmljinja: #} weblog-0.8/weblog/utils.py0000644000000000000000000001053511006524342015774 0ustar00usergroup00000000000000import os import sys from cgi import escape from ConfigParser import SafeConfigParser, NoOptionError def encode(text, encoding, errors='xmlcharrefreplace'): ''' >>> encode('foo & bar', 'ascii') 'foo & bar' >>> encode('\\xdcTF-8 ?', 'raw') Traceback (most recent call last): ... UnicodeDecodeError: 'ascii' codec can't decode byte 0xdc in position 0: \ ordinal not in range(128) >>> encode('\\xdcTF-8 ?', 'latin-1') 'ÜTF-8 ?' >>> encode(u'\\xdcTF-8 ?', 'UTF-8') 'ÜTF-8 ?' ''' if encoding.lower() == 'raw': return text.encode('ascii') elif isinstance(text, unicode): return text.encode('ascii', errors) else: return text.decode(encoding).encode('ascii', errors) def escape_and_encode(text, encoding, errors='xmlcharrefreplace'): ''' Escapes '&', '<' and '>' to HTML-safe sequences and encode the text to the specified encoding. >>> escape_and_encode('<>&', 'ascii') '<>&' >>> escape_and_encode(u'\\xdcTF-8', 'utf-8') 'ÜTF-8' >>> escape_and_encode('\\xdcTF-8', 'latin-1') 'ÜTF-8' >>> escape_and_encode('\\xdcTF-8 &<>', 'raw') Traceback (most recent call last): ... UnicodeDecodeError: 'ascii' codec can't decode byte 0xdc in position 0: \ ordinal not in range(128) >>> escape_and_encode('UTF-8 &<>', 'raw') 'UTF-8 &<>' ''' if encoding.lower() == 'raw': return text.encode('ascii') else: return encode(escape(text), encoding, errors) def load_if_filename(source_dir, f): ''' If ``f`` is a filename. Read it and returns the content. Else return ``f``. If ``bool(f)`` is false returns ``None``. # Assumes that there is no file named 'This is not a file' in the current # directory ;-) >>> load_if_filename('.', 'This is not a file') 'This is not a file' >>> load_if_filename('.', '') >>> load_if_filename('.', list()) >>> load_if_filename('.', None) ''' if not f: return full = os.path.join(source_dir, f) if os.path.exists(full): return file(full).read() else: return f def load_configuration(config_file, source_dir=None): ''' Read the file ``config_file`` and sanitise it. Returns a dictionnary containing the parameters from the [weblog] section. >>> from StringIO import StringIO >>> config_file = StringIO(""" ... [weblog] ... title = Test title ... url = http://example.com ... description = Example blog""") >>> load_configuration(config_file) #doctest: +NORMALIZE_WHITESPACE {'url': 'http://example.com/', 'rss_post_limit': 10, 'description': 'Example blog', 'post_per_page': 10, 'title': 'Test title'} ''' config = SafeConfigParser() if isinstance(config_file, basestring): config_file = os.path.join(source_dir or '', config_file) if not os.path.exists(config_file): raise IOError('Unable to find configuration file %s' % config_file) config.read(config_file) else: config.readfp(config_file) config_dict = dict(config.items('weblog')) try: config_dict['title'] = encode(config_dict['title'], config_dict.get('encoding', 'ascii')) blog_base_url = config_dict['url'] if blog_base_url and not blog_base_url.endswith('/'): blog_base_url += '/' config_dict['url'] = blog_base_url def _load_if_filename(key): if key in config_dict: config_dict[key] = load_if_filename(source_dir, config_dict[key]) _load_if_filename('html_head') _load_if_filename('html_header') _load_if_filename('html_footer') def config_set_int(key, default): try: config_dict[key] = int(config_dict.get(key, default)) except ValueError, e: sys.exit('In config file \'%s\'\n' '%s is not an integer: %s' % \ (config_file, key, e)) config_set_int('post_per_page', 10) config_set_int('rss_post_limit', 10) except KeyError, e: sys.exit('Unable to find %s in configuration file \'%s\'' % \ (e, CONFIG_FILE)) else: return config_dict if __name__ == '__main__': import doctest doctest.testmod() weblog-0.8/weblog_run.py0000755000000000000000000000037511006524342015524 0ustar00usergroup00000000000000#!/usr/bin/env python import imp from os import path filename = path.join(path.dirname(__file__), 'bin', 'weblog') module = imp.load_module('weblog_executable', file(filename), filename, ('', 'r', imp.PY_SOURCE)) module.main()