My portfolio site is definitely a very small site with almost no visitors at all. Written in Kohana and was previously hosted in Pagodabox, I’ve migrated it to Google AppEngine for Python 2.7. It was fun and frustrating at the same time.
Things to consider before migration
Kohana is a greate framework and Pagodabox is an awesome platform as well. However, I got very interested on Python and AppEngine platform to be specific. Therefore, I used my portfolio site as a playground. Below are the highlights of the features and functionality involved on my migration.
- Request/response model
- URI routing
- Configuration
- Templates
- Caching
- Timezones
Request/response model
What I really like about Kohana is the simplicity of its Request/Response model. You can trace from the moment the request is received by the front controller then to the point where the right controller is dispatched and response is served. Things like index.php
for front controller script, and then we have bootstrap.php
for initialization purposes. If you name your controller right, you get that controller called for the expected URL mapping.
My sample /about
controller.
class Controller_Contact extends Controller_Cached { public function action_index(){} }
With Google AppEngine for Python 2.7, we can accomplish the same thing with webapp2
framework. webapp2
is a lightweight web framework that happens to work for Google AppEngine and is compatible with the original webapp
. If we have controllers in Kohana (PHP MVC frameworks in general), webapp2 also have this handler
. A handler is always associated with a route. Unlike Kohana where there is a default route by default, in webapp2, you need to create a route whether you like it or not.
My sample /about
handler with its associated route.
# config.py webapp2.Route(r'/contact', handler='dclab.lysender.handler.contact.ContactHandler', name='contact') # dclab.lysender.handler.contact.py class ContactHandler(WebHandler): def get(self): pass
Please note that code above is for presentational purpose only and should not be treated as complete and working.
URI routing
Probably the best feature I can name for Kohana is there routing feature, where you define a URL pattern to map to a certain controller. For example, I have a route for my sprint generator page at /extra/sprint
where it also accepts a letter at the end of the URL for the selected sprint letter., ex: /extra/sprint/e
. Below is my route.
/** * Route for extra group of controllers */ Route::set('extra_sprint', 'extra/sprint(/<letter>)', array('letter' => '[a-z]')) ->defaults(array( 'controller' => 'sprint', 'action' => 'index', 'directory' => 'extra' ));
And my controller is located at controller/extra/sprint.php
. A longer route is for my worldclock page. Here is my route.
/** * Route for worldclock */ Route::set('worldclock', 'extra/tools/worldclock(/<ident1>(/<ident2>(/<ident3>)))', array('ident1' => '[-_+a-zA-Z0-9]+', 'ident2' => '[-_+a-zA-Z0-9]+', 'ident3' => '[-_+a-zA-Z0-9]+')) ->defaults(array( 'controller' => 'tools', 'action' => 'worldclock', 'directory' => 'extra' ));
My worldclock route is pretty complex. It accepts optional uri segments that maps into request parameters and can be captured on the controller level same as the sprint route.
For webapp2 and Google AppEngine, routing goes like this.
routes = [webapp2.Route(r'/', handler='dclab.lysender.handler.index.IndexHandler', name='index'), webapp2.Route(r'/extra/sprint/<sprint_letter:[a-z]>', handler='dclab.lysender.handler.extra.sprint.SprintHandler:sprint_letter', name='sprint_letter', methods=['GET']), webapp2.Route(r'/extra/tools/worldclock', handler='dclab.lysender.handler.extra.tools.worldclock.WorldclockHandler', name='tools_worldclock'), webapp2.Route(r'/extra/tools/worldclock/<ident1:[-+0-9a-zA-Z_]+>', handler='dclab.lysender.handler.extra.tools.worldclock.WorldclockHandler:specific_timezone', name='tools_worldclock_selected', methods=['GET']), webapp2.Route(r'/extra/tools/worldclock/<ident1:[-+0-9a-zA-Z_]+>/<ident2:[-+0-9a-zA-Z_]+>', handler='dclab.lysender.handler.extra.tools.worldclock.WorldclockHandler:specific_timezone', name='tools_worldclock_selected', methods=['GET'])]
Yes, its a complex beast but they are simple actually. In webapp2, there are many ways to handle this routing stuff. What I used instead is to use regex routes, then use lazy loaded modules as handlers for those routes. Normally, we import modules but this time, I used string notation so that webapp2 will lazy load them. Therefore I get the same flexibility of Kohana.
If in Kohana we are allowed to specify optiona url segments, in webapp2, we can actually repate route definitions and point to the same handler but with different URI segments. This way, we achieve the same optional parameters (worldclock route).
Configuration
Kohana configuration system allows inheritance of values, for example, you are allowed to entere a few values and expect default values to be filled by higher level precedence configuration in modules or system directories. I couldn’t find the same feature on Python in general but I guess ini
configurations does the job well. However, I choose yaml
format for my configuration. Good thinkg that AppEngine bundled this library and reading yaml
from Python is relatively easy.
Templates
In Kohana (views), we are actually free whatever strategy we choose to have this templating stuff for web pages. Common strategy is to have a base template that contains the base HTML including header and footer. The content just differs from page to page. Then we inject CSS and JS files on templates and simply loop and echo on base template. For each pages, the content is defined and is injected into the main template. Also, a view and render a view inside it.
Sample base template:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <title><?php echo ( ! empty($title)) ? $title.' :: ' : '' ?>Lysender</title> <!-- basic styles --> <?php foreach ($styles as $style => $media) echo HTML::style($style.'?v='.APP_VERSION, array('media' => $media)), "\n" ?> </head> <body> <div class="container"> <div class="row"> <div id="header" class="span12"> <?php echo $header ?> </div><!-- #header --> <div id="content" class="span12"> <?php echo $content ?> </div><!-- #content --> <div id="footer" class="span12"> <?php echo $footer ?> </div><!-- #footer --> </div><!-- .row --> </div><!-- .container --> <!-- basic scripts --> <?php foreach ($scripts as $script) echo HTML::script($script.'?v='.APP_VERSION), "\n" ?> </body> </html>
In Google AppEngine Python, there are several template engines that we can use. I choose Jinja2, a template engine very similar to Django template. It is a very different world. If in Kohana, ZF or Smarty we have a base template where we inject content views into it, in Jinja2/Django world, these base templates are inherited by child templates and contents are just overriden or filled. It took me several days to finally grasp it. Aboves PHP/Kohana template strategy, when converted to Jinja, will become like below:
Base template:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <meta http-equiv="Content-type" content="text/html; charset=utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="robots" content="all" /> {% block headdesc %}{% endblock %} {% block headkeywords %}{% endblock %} <title>{% block title %}Lysender{% endblock %}</title> {% for style in styles %} <link type="text/css" href="{{ base_url }}{{ style }}" rel="stylesheet" media="screen, projection" /> {% endfor %} </head> <body> <div class="container"> <div class="row"> <div id="header" class="span12"> {% block header %}{% endblock %} </div><!-- #header --> <div id="content" class="span12"> {% block content %}{% endblock %} </div><!-- #content --> <div id="footer" class="span12"> {% block footer %}{% endblock %} </div><!-- #footer --> </div><!-- .row --> </div><!-- .container --> {% for script in scripts %} <script type="text/javascript" src="{{ base_url }}{{ script }}"></script> {% endfor %} </body> </html>
Then here is my child template:
{% extends 'base.html' %} {% block title %}Lysender :: Contact{% endblock %} {% block headdesc %} <meta name="description" content="Contact Lysender" /> {% endblock %} {% block headkeywords %} <meta name="keywords" content="contact, lysender, info" /> {% endblock %} {% block content %} <p>This is the contact page.</p> {% endblock %}
Caching
Caching probably is a general topic and comparing frameworks/platform doesn’t make sense. In Kohana (served via Apache), I was able to do page caching. In Google AppEngine, I’m not yet able to find a good soluton for full page caching where it should not hit the interpreter but rather serve cached page directly.
I was still able to do some caching (well, data caching) to improve performance somehow but it is more of data caching. I used memcache instead. Pagodabox also offers memcache and redis but haven’t tried them since I already have full page caching.
Timezones
Perhaps the most difficult part of the migration is timezone handling between languages. I have this page so-called world clock. In PHP, it was fairly easy to handle date operations that involves timezones (although timezones must be updated every year due to its very dynamic in nature). In Python, they do it right, which means that timezones are not natively supported, which means that I have to scratch my head.
To demonstrate the simplicity of timezone handling in PHP, here is the code to get UTC timezone offset for a certain timezone. This offset is what I used to facilitate the worldclock widgets page.
$d = new DateTime('now', new DateTimeZone('US/Pacific')); var_dummp($d->getOffset());
In python, you need to install some third party library (I noticed it is updated quarterly) and handles all our timezone needs for Python. Its pretty popular and its called pytz.
pytz
is a library that brings Olson timezone database into Python. You may download it here. It works but there’s a lot of files on it that it may reach the file limit of Google AppEngine which is 1000 files. Genius people created a workaround which creates a zipped version of pytz’s zoneinfo, turning 800+ files into just 1 zip file. AppEngine limit problem solved.
Here is the guide for using pytz on Google AppEngine using zipped version. Getting the UTC timezone offset is now as easy as below:
import pytz import datetime d = datetime.datetime.now() t = pytz.timezone(tz) offset = int(t.utcoffset(d).total_seconds()) print offset
Problem solved!
Enjoy and share.