diff --git a/cyclone/app.py b/cyclone/app.py index a1e10545f4..81fd2b1219 100644 --- a/cyclone/app.py +++ b/cyclone/app.py @@ -54,14 +54,16 @@ def new_project(**kwargs): else: ext = n.rsplit(".", 1)[-1] fd = open(os.path.join(dst, mod), "w", 0644) - if ext in ("conf", "html", "py", "md", "sh"): + if ext in ("conf", "html", "py", "md", "sh") or n in ('Procfile'): fd.write(string.Template(zf.read(n)).substitute(kwargs)) else: fd.write(zf.read(n)) fd.close() # make sure we can actually run start.sh - os.chmod(os.path.join(dst, 'start.sh'), 0755) + if os.path.exists(os.path.join(dst, 'start.sh')): + os.chmod(os.path.join(dst, 'start.sh'), 0755) + if kwargs["use_git"] is True: os.chdir(kwargs["project_path"]) os.system("git init") @@ -80,6 +82,8 @@ def usage(version, target): -s --set-pkg-version Set version on package name [default: False] -t --target=PATH Set path where project is created [default: %s] -l --license=FILE Append the following license file [default: Apache 2] + -f --foreman Create a foreman based project (suited to run on heroku + and other PaaS) """ % (version, target)) sys.exit(0) @@ -92,10 +96,12 @@ def main(): license_file = None default_version, version = "0.1", None default_target, target = os.getcwd(), None + foreman = False - shortopts = "hgsp:m:v:t:l:" + shortopts = "hgsp:m:v:t:l:f" longopts = ["help", "git", "set-pkg-version", - "project=", "modname=", "version=", "target=", "license="] + "project=", "modname=", "version=", "target=", "license=", + "foreman"] try: opts, args = getopt.getopt(sys.argv[1:], shortopts, longopts) except getopt.GetoptError: @@ -125,6 +131,10 @@ def main(): elif o in ("-l", "--license"): license_file = a + + elif o in ("-f", "--foreman"): + foreman = True + if license_file is None: license = DEFAULT_LICENSE @@ -167,6 +177,11 @@ def main(): "'%s': permission denied" % target) sys.exit(1) + if foreman is False: + skel_file = "app_skel.zip" + else: + skel_file = "foreman_skel.zip" + name = "Foo Bar" email = "root@localhost" if use_git is True: @@ -185,7 +200,7 @@ def main(): skel = zipfile.ZipFile(open( os.path.join(os.path.dirname(os.path.abspath(__file__)), - "appskel.zip"), "rb")) + skel_file), "rb")) if set_pkg_version is True: project_name = "%s-%s" % (project, version) diff --git a/cyclone/foreman_skel.zip b/cyclone/foreman_skel.zip new file mode 100644 index 0000000000..d163f5fda7 Binary files /dev/null and b/cyclone/foreman_skel.zip differ diff --git a/foreman_skel/.env b/foreman_skel/.env new file mode 100644 index 0000000000..ae53c3988b --- /dev/null +++ b/foreman_skel/.env @@ -0,0 +1 @@ +PYTHONPATH=. diff --git a/foreman_skel/.gitignore b/foreman_skel/.gitignore new file mode 100644 index 0000000000..fae15247ed --- /dev/null +++ b/foreman_skel/.gitignore @@ -0,0 +1,3 @@ +*.swp +*.pyc +dropin.cache diff --git a/foreman_skel/Procfile b/foreman_skel/Procfile new file mode 100644 index 0000000000..98978c8aba --- /dev/null +++ b/foreman_skel/Procfile @@ -0,0 +1 @@ +web: python $modname/main.py foobar.conf diff --git a/foreman_skel/README.md b/foreman_skel/README.md new file mode 100644 index 0000000000..ebfa99d6c8 --- /dev/null +++ b/foreman_skel/README.md @@ -0,0 +1,120 @@ +# cyclone-based project for heroku and other PaaS based on foreman. + + This is the source code of $project_name + $name <$email> + +## About + +This file has been created automatically by cyclone-tool for $project_name. +It contains the following files: + +- ``Procman``: standard foreman file +- ``$modname.conf``: configuration file for the web server +- ``$modname/__init__.py``: information such as author and version of this package +- ``$modname/web.py``: map of url handlers and main class of the web server +- ``$modname/config.py``: configuration parser for ``$modname.conf`` +- ``$modname/views.py``: code of url handlers for the web server +- ``scripts/localefix.py``: script to fix html text before running ``xgettext`` +- ``scripts/cookie_secret.py``: script for generating new secret key for the web server + +### Running + +For development and testing: + + gem install foreman + cd $project_name + foreman start + +For production on any foreman based env: + + Follow foreman instructions, configure Procman as needed. Check the .env file and the configuration file for your app. + +For production at heroku: + + - Start a git repo + git init + git add . + git commit -m 'first' + heroku create $project_name (or whatever name you want) + git push heroku master + - check your app, make it better, create a db, etc + +### Convert this document to HTML + +Well, since this is a web server, it might be a good idea to convert this document +to HTML before getting into customization details. + +This can be done using [markdown](http://daringfireball.net/projects/markdown/). + + brew install markdown + markdown README.md > frontend/static/readme.html + +And point your browser to after this server +is running. + +## Customization + +This section is dedicated to explaining how to customize your brand new package. + +### Databases + +cyclone provides built-in support for SQLite and Redis databases. +It also supports any RDBM supported by the ``twisted.enterprise.adbapi`` module, +like MySQL or PostgreSQL. + +The default configuration file ``$modname.conf`` ships with pre-configured +settings for SQLite, Redis and MySQL. + +The code for loading all the database settings is in ``$modname/config.py``. +Feel free to comment or even remove such code, and configuration entries. It +shouldn't break the web server. + +Take a look at ``$modname/utils.py``, which is where persistent database +connections are initialized. + + +### Internationalization + +cyclone uses the standard ``gettext`` library for dealing with string +translation. + +Make sure you have the ``gettext`` package installed. If you don't, you won't +be able to translate your software. + +For installing the ``gettext`` package on Debian and Ubuntu systems, do this: + + apt-get install gettext + +For Mac OS X, I'd suggest using [HomeBrew](http://mxcl.github.com/homebrew>). +If you already use HomeBrew, run: + + brew install gettext + brew link gettext + +For generating translatable files for HTML and Python code of your software, +run this: + + cat frontend/template/*.html $modname/*.py | python scripts/localefix.py | \ + xgettext - --language=Python --from-code=utf-8 --keyword=_:1,2 -d $modname + +Then translate $modname.po, compile and copy to the appropriate locale +directory: + + (pt_BR is used as example here) + vi $modname.po + mkdir -p frontend/locale/pt_BR/LC_MESSAGES/ + msgfmt $modname.po -o frontend/locale/pt_BR/LC_MESSAGES/$modname.mo + +There are sample translations for both Spanish and Portuguese in this package, +already compiled. + + +### Cookie Secret + +The current cookie secret key in ``$modname.conf`` was generated during the +creation of this package. However, if you need a new one, you may run the +``scripts/cookie_secret.py`` script to generate a random key. + +## Credits + +- [cyclone](http://github.com/fiorix/cyclone) web server. diff --git a/foreman_skel/frontend/locale/es_ES/LC_MESSAGES/modname.mo b/foreman_skel/frontend/locale/es_ES/LC_MESSAGES/modname.mo new file mode 100644 index 0000000000..57beeb164c Binary files /dev/null and b/foreman_skel/frontend/locale/es_ES/LC_MESSAGES/modname.mo differ diff --git a/foreman_skel/frontend/locale/es_ES/LC_MESSAGES/modname.po b/foreman_skel/frontend/locale/es_ES/LC_MESSAGES/modname.po new file mode 100644 index 0000000000..f8bf12cd66 --- /dev/null +++ b/foreman_skel/frontend/locale/es_ES/LC_MESSAGES/modname.po @@ -0,0 +1,25 @@ +# cyclone-tools sample translation. +# Copyright (C) 2011 Alexandre Fiori +# This file is distributed under the same license as the cyclone package. +# Alexandre Fiori , 2011. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: 0.1\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-02-24 15:36-0300\n" +"PO-Revision-Date: 2011-02-28 15:44+-0300\n" +"Last-Translator: Alexandre Fiori \n" +"Language-Team: Alexandre Fiori \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: standard input:5 +msgid "cyclone web server" +msgstr "servidor web cyclone" + +#: standard input:8 +msgid "It works!" +msgstr "¡Funciona!" diff --git a/foreman_skel/frontend/locale/pt_BR/LC_MESSAGES/modname.mo b/foreman_skel/frontend/locale/pt_BR/LC_MESSAGES/modname.mo new file mode 100644 index 0000000000..7b0ca464e5 Binary files /dev/null and b/foreman_skel/frontend/locale/pt_BR/LC_MESSAGES/modname.mo differ diff --git a/foreman_skel/frontend/locale/pt_BR/LC_MESSAGES/modname.po b/foreman_skel/frontend/locale/pt_BR/LC_MESSAGES/modname.po new file mode 100644 index 0000000000..6f04bcec3b --- /dev/null +++ b/foreman_skel/frontend/locale/pt_BR/LC_MESSAGES/modname.po @@ -0,0 +1,25 @@ +# cyclone-tools sample translation. +# Copyright (C) 2011 Alexandre Fiori +# This file is distributed under the same license as the cyclone package. +# Alexandre Fiori , 2011. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: 0.1\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-02-24 15:36-0300\n" +"PO-Revision-Date: 2011-02-28 15:44+-0300\n" +"Last-Translator: Alexandre Fiori \n" +"Language-Team: Alexandre Fiori \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: standard input:5 +msgid "cyclone web server" +msgstr "servidor web cyclone" + +#: standard input:8 +msgid "It works!" +msgstr "Funciona!" diff --git a/foreman_skel/frontend/static/favicon.ico b/foreman_skel/frontend/static/favicon.ico new file mode 100644 index 0000000000..a84192c771 Binary files /dev/null and b/foreman_skel/frontend/static/favicon.ico differ diff --git a/foreman_skel/frontend/template/base.html b/foreman_skel/frontend/template/base.html new file mode 100644 index 0000000000..7236ee5d2a --- /dev/null +++ b/foreman_skel/frontend/template/base.html @@ -0,0 +1,26 @@ + + + + + {{_("cyclone web server")}} + + + + + + + + + + +
+ + {% block page %}{% end %} +
+ + diff --git a/foreman_skel/frontend/template/index.html b/foreman_skel/frontend/template/index.html new file mode 100644 index 0000000000..01bbbc8a04 --- /dev/null +++ b/foreman_skel/frontend/template/index.html @@ -0,0 +1,14 @@ +{% extends 'base.html' %} +{% block page %} +

{{_("It works!")}}

+

+ English  + Español  + Português +

+

hello = {{hello}}

+

awesome = {{awesome}}

+
+ +
+{% end %} diff --git a/foreman_skel/frontend/template/post.html b/foreman_skel/frontend/template/post.html new file mode 100644 index 0000000000..f691ccb942 --- /dev/null +++ b/foreman_skel/frontend/template/post.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% block page %} +

POST example

+

This variable exists = {{fields.ip}}

+

This variable does not exist = {{fields.something}}

+

This one comes from .conf = {{fields.mysql_host}}

+{% end %} diff --git a/foreman_skel/modname.conf b/foreman_skel/modname.conf new file mode 100644 index 0000000000..706a23ede8 --- /dev/null +++ b/foreman_skel/modname.conf @@ -0,0 +1,31 @@ +[server] +debug = true +xheaders = false +xsrf_cookies = false +cookie_secret = $cookie_secret + +[frontend] +locale_path = frontend/locale +static_path = frontend/static +template_path = frontend/template + +[sqlite] +enabled = yes +database = :memory: + +[redis] +enabled = no +host = 127.0.0.1 +port = 6379 +dbid = 0 +poolsize = 10 + +[mysql] +enabled = no +host = 127.0.0.1 +port = 3306 +username = foo +password = bar +database = dummy +poolsize = 10 +debug = no diff --git a/foreman_skel/modname/__init__.py b/foreman_skel/modname/__init__.py new file mode 100644 index 0000000000..da8496656c --- /dev/null +++ b/foreman_skel/modname/__init__.py @@ -0,0 +1,6 @@ +# coding: utf-8 +# +$license + +__author__ = "$name <$email>" +__version__ = "$version" diff --git a/foreman_skel/modname/config.py b/foreman_skel/modname/config.py new file mode 100644 index 0000000000..8a3cbb3d68 --- /dev/null +++ b/foreman_skel/modname/config.py @@ -0,0 +1,71 @@ +# coding: utf-8 +# +$license + +import os +import ConfigParser +from cyclone.util import ObjectDict + + +def xget(func, section, option, default=None): + try: + return func(section, option) + except: + return default + + +def parse_config(filename): + cfg = ConfigParser.RawConfigParser() + with open(filename) as fp: + cfg.readfp(fp) + fp.close() + + settings = {'raw': cfg} + + # web server settings + settings["debug"] = xget(cfg.getboolean, "server", "debug", False) + settings["xheaders"] = xget(cfg.getboolean, "server", "xheaders", False) + settings["cookie_secret"] = cfg.get("server", "cookie_secret") + settings["xsrf_cookies"] = xget(cfg.getboolean, "server", "xsrf_cookies", + False) + + # get project's absolute path + root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) + getpath = lambda k, v: os.path.join(root, xget(cfg.get, k, v)) + + # locale, template and static directories' path + settings["locale_path"] = getpath("frontend", "locale_path") + settings["static_path"] = getpath("frontend", "static_path") + settings["template_path"] = getpath("frontend", "template_path") + + # sqlite support + if xget(cfg.getboolean, "sqlite", "enabled", False): + settings["sqlite_settings"] = ObjectDict(database=cfg.get("sqlite", + "database")) + else: + settings["sqlite_settings"] = None + + # redis support + if xget(cfg.getboolean, "redis", "enabled", False): + settings["redis_settings"] = ObjectDict( + host=cfg.get("redis", "host"), + port=cfg.getint("redis", "port"), + dbid=cfg.getint("redis", "dbid"), + poolsize=cfg.getint("redis", "poolsize")) + else: + settings["redis_settings"] = None + + # mysql support + if xget(cfg.getboolean, "mysql", "enabled", False): + settings["mysql_settings"] = ObjectDict( + host=cfg.get("mysql", "host"), + port=cfg.getint("mysql", "port"), + username=xget(cfg.get, "mysql", "username"), + password=xget(cfg.get, "mysql", "password"), + database=xget(cfg.get, "mysql", "database"), + poolsize=xget(cfg.getint, "mysql", "poolsize", 10), + debug=xget(cfg.getboolean, "mysql", "debug", False)) + else: + settings["mysql_settings"] = None + + return settings diff --git a/foreman_skel/modname/main.py b/foreman_skel/modname/main.py new file mode 100644 index 0000000000..bbd78fba8f --- /dev/null +++ b/foreman_skel/modname/main.py @@ -0,0 +1,20 @@ +import web +import sys, os +from twisted.python import log +from twisted.internet import defer, reactor + + +def main(config_file): + log.startLogging(sys.stdout) + application = web.Application(config_file) + + port = os.environ.get("PORT", 8888) + reactor.listenTCP(int(port), application) + reactor.run() + +if __name__ == "__main__": + if len(sys.argv) > 1: + main(sys.argv[1]) + else: + log.error("no config file given") + sys.exit(-1) diff --git a/foreman_skel/modname/utils.py b/foreman_skel/modname/utils.py new file mode 100644 index 0000000000..78cbad7d70 --- /dev/null +++ b/foreman_skel/modname/utils.py @@ -0,0 +1,34 @@ +# coding: utf-8 +# +$license + +import cyclone.escape +import cyclone.web + +from twisted.enterprise import adbapi + + +class TemplateFields(dict): + """Helper class to make sure our + template doesn't fail due to an invalid key""" + def __getattr__(self, name): + try: + return self[name] + except KeyError: + return None + + def __setattr__(self, name, value): + self[name] = value + + +class BaseHandler(cyclone.web.RequestHandler): + #def get_current_user(self): + # user_json = self.get_secure_cookie("user") + # if user_json: + # return cyclone.escape.json_decode(user_json) + + def get_user_locale(self): + lang = self.get_secure_cookie("lang") + if lang: + return cyclone.locale.get(lang) + diff --git a/foreman_skel/modname/views.py b/foreman_skel/modname/views.py new file mode 100644 index 0000000000..ed5bc8ee9f --- /dev/null +++ b/foreman_skel/modname/views.py @@ -0,0 +1,41 @@ +# coding: utf-8 +# +$license + +import cyclone.escape +import cyclone.locale +import cyclone.web + +from twisted.internet import defer +from twisted.python import log + +from $modname.utils import BaseHandler +from $modname.utils import TemplateFields + + +class IndexHandler(BaseHandler): + def get(self): + self.render("index.html", hello='world', awesome='bacon') + # another option would be + # fields = {'hello': 'world', 'awesome': 'bacon'} + # self.render('index.html', **fields) + + def post(self): + tpl_fields = TemplateFields() + tpl_fields['post'] = True + tpl_fields['ip'] = self.request.remote_ip + # you can also fetch your own config variables defined in + # $modname.conf using + # self.settings.raw.get('section', 'parameter') + tpl_fields['mysql_host'] = self.settings.raw.get('mysql', 'host') + self.render("post.html", fields=tpl_fields) + + +class LangHandler(BaseHandler): + def get(self, lang_code): + if lang_code in cyclone.locale.get_supported_locales(): + self.set_secure_cookie("lang", lang_code) + + self.redirect(self.request.headers.get("Referer", + self.get_argument("next", "/"))) + diff --git a/foreman_skel/modname/web.py b/foreman_skel/modname/web.py new file mode 100644 index 0000000000..a0167dd593 --- /dev/null +++ b/foreman_skel/modname/web.py @@ -0,0 +1,28 @@ +# coding: utf-8 +# +$license + +import cyclone.locale +import cyclone.web + +from $modname import views +from $modname import config + + +class Application(cyclone.web.Application): + def __init__(self, config_file): + handlers = [ + (r"/", views.IndexHandler), + (r"/lang/(.+)", views.LangHandler), + ] + + settings = config.parse_config(config_file) + + # Initialize locales + locales = settings.get("locale_path") + if locales: + cyclone.locale.load_gettext_translations(locales, "$modname") + + #settings["login_url"] = "/auth/login" + #settings["autoescape"] = None + cyclone.web.Application.__init__(self, handlers, **settings) diff --git a/foreman_skel/requirements.txt b/foreman_skel/requirements.txt new file mode 100644 index 0000000000..aa1ad6b266 --- /dev/null +++ b/foreman_skel/requirements.txt @@ -0,0 +1,2 @@ +Twisted==12.2.0 +cyclone==1.0-rc13 diff --git a/foreman_skel/scripts/cookie_secret.py b/foreman_skel/scripts/cookie_secret.py new file mode 100644 index 0000000000..58fad76569 --- /dev/null +++ b/foreman_skel/scripts/cookie_secret.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +# coding: utf-8 +# +$license + +import base64 +import uuid + +if __name__ == "__main__": + print(base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes)) diff --git a/foreman_skel/scripts/localefix.py b/foreman_skel/scripts/localefix.py new file mode 100644 index 0000000000..81de669773 --- /dev/null +++ b/foreman_skel/scripts/localefix.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +# coding: utf-8 +# +$license + +import re +import sys + +if __name__ == "__main__": + try: + filename = sys.argv[1] + assert filename != "-" + fd = open(filename) + except: + fd = sys.stdin + + line_re = re.compile(r'="([^"]+)"') + for line in fd: + line = line_re.sub(r"=\\1", line) + sys.stdout.write(line) + fd.close() diff --git a/setup.py b/setup.py index ac14b511e9..18d87207c4 100644 --- a/setup.py +++ b/setup.py @@ -51,7 +51,7 @@ setup( name="cyclone", - version="1.0-rc13", + version="1.0-rc14", author="fiorix", author_email="fiorix@gmail.com", url="http://cyclone.io/", @@ -60,7 +60,7 @@ "A facebook's Tornado on top of Twisted.", keywords="python non-blocking web server twisted facebook tornado", packages=["cyclone", "twisted.plugins"], - package_data={"cyclone": ["appskel.zip"], + package_data={"cyclone": ["appskel.zip", "foreman_skel.zip"], "twisted": ["plugins/cyclone_plugin.py"]}, **extra )