Big update to 0.1.0. Improved error handling, syncing, the works... 0.1.0
authorAndrew Flegg <andrew@bleb.org>
Tue, 20 Oct 2009 20:04:20 +0000 (21:04 +0100)
committerAndrew Flegg <andrew@bleb.org>
Tue, 20 Oct 2009 20:04:20 +0000 (21:04 +0100)
package/debian/changelog
package/src/contacts.py
package/src/gui.py
package/src/hermes.py
package/src/names.py
package/src/trans.py [new file with mode: 0644]

index a3cef84..1720051 100644 (file)
@@ -1,3 +1,17 @@
+hermes (0.1.0) unstable; urgency=low
+
+  * Improve error handling, including opening accounts dialogue
+    for failed Twitter auth MB#5352 (reported by Zach Goldberg)
+  * Use Python `trans' module to match differing accents (reported
+    by Valério Valério)
+  * Add ability to create "birthday-only" contacts from Facebook
+    (suggested by Jussi Mäkinen)
+  * Pull down homepages from Facebook, and extract URLs.
+  * Clarify accounts dialogue & Facebook authentication (suggested
+    by Keith Varty)
+
+ -- Andrew Flegg <andrew@bleb.org>  Sun, 18 Oct 2009 09:36:36 +0100
+
 hermes (0.0.6) unstable; urgency=low
 
   * New icons from Tim Samoff.
index 4963191..38b4624 100644 (file)
@@ -5,6 +5,7 @@ import Image
 import ImageOps
 import StringIO
 import datetime
+import re
 from pygobject import *
 from ctypes import *
 
@@ -103,10 +104,21 @@ class ContactStore:
 
     
   # -----------------------------------------------------------------------
-  def add_url(self, contact, url, unique = ''):
+  def add_url(self, contact, str, unique = ''):
     """Add a new URL to the set of URLs for the given contact."""
 
-    unique = unique or url
+    urls = re.findall('(?:(?:ftp|https?):\/\/|\\bwww\.|\\bftp\.)[,\w\.\-\/@:%?&=%+#~_$\*]+[\w=\/&=+#]', str, re.I | re.S)
+    updated = False
+    for url in urls:
+      updated = self._add_url(contact, url, unique or url) or updated
+
+    return updated
+
+
+  # -----------------------------------------------------------------------
+  def _add_url(self, contact, url, unique):
+    """Do the work of adding a unique URL to a contact."""
+
     url_attr = None
     ai = GList.new(ebook.e_contact_get_attributes(hash(contact), E_CONTACT_HOMEPAGE_URL))
     while ai.has_next():
@@ -117,13 +129,13 @@ class ContactStore:
       elif existing.find(unique) > -1:
         url_attr = attr
         break
-    
+      
     if not url_attr:
       ai.add()
       url_attr = EVCardAttribute()
       url_attr.group = ''
       url_attr.name = 'URL'
-      
+    
     val = GList()
     print "Setting URL for [%s] to [%s]" % (contact.get_name(), url)
     val.set(create_string_buffer(url))
index b82a71c..c283ff1 100755 (executable)
@@ -40,7 +40,8 @@ class HermesGUI:
       try:
         fb2c = Hermes(self,
                       twitter = (self.get_use_twitter() and self.get_twitter_credentials()) or None,
-                      facebook = self.get_use_facebook())
+                      facebook = self.get_use_facebook(),
+                      empty = self.get_create_empty())
         fb2c.load_friends()
         fb2c.sync_contacts(resync = force)
         gobject.idle_add(self.open_summary, fb2c)
@@ -114,47 +115,81 @@ class HermesGUI:
     dialog = gtk.Dialog('Accounts', self.window)
     dialog.add_button('Save', gtk.RESPONSE_OK)
 
-    use_facebook = hildon.CheckButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
-    use_facebook.set_label('Use Facebook')
+    #pa = hildon.PannableArea()
+    #dialog.vbox.add(pa)
+    content = dialog.vbox 
+    #content = gtk.VBox()
+    #pa.add(content)
+    #pa.set_size_request(600, 380)
+
+    use_facebook = self.new_checkbox('Use Facebook', content)
     use_facebook.set_active(self.get_use_facebook())
-    dialog.vbox.add(use_facebook)
 
-    use_twitter = hildon.CheckButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
-    use_twitter.set_label('Use Twitter')
+    indent = self.new_indent(content)
+    fb_msg = self.add_linked(indent, use_facebook, gtk.Label('Note: authentication via web page'))
+    fb_msg.set_property('justify', gtk.JUSTIFY_LEFT)
+    
+    fb_empty = self.add_linked(indent, use_facebook, self.new_checkbox('Create birthday-only contacts'))
+    fb_empty.set_active(self.get_create_empty())
+
+    use_twitter = self.new_checkbox('Use Twitter', content)
     use_twitter.set_active(self.get_use_twitter())
-    dialog.vbox.add(use_twitter)
 
-    tw_indent = gtk.HBox()
-    tw_user = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
-    tw_user.set_property('hildon-input-mode', gtk.HILDON_GTK_INPUT_MODE_FULL)
-    tw_user.set_placeholder("Twitter username")
-    tw_user.set_property('is-focus', False)
+    indent = self.new_indent(content)
+    tw_user = self.add_linked(indent, use_twitter, self.new_input('Twitter username'))
     tw_user.set_text(self.get_twitter_credentials()[0])
-    self.sync_edit(use_twitter, tw_user)
-    use_twitter.connect('toggled', self.sync_edit, tw_user)
-    tw_indent.pack_start(tw_user, padding=48)
-    dialog.vbox.add(tw_indent)
-
-    tw_indent = gtk.HBox()
-    tw_pass = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
-    tw_pass.set_placeholder("Twitter password")
+
+    tw_pass = self.add_linked(indent, use_twitter, self.new_input('Twitter password'))
     tw_pass.set_property('hildon-input-mode', gtk.HILDON_GTK_INPUT_MODE_FULL |
                                               gtk.HILDON_GTK_INPUT_MODE_INVISIBLE)
     tw_pass.set_text(self.get_twitter_credentials()[1])
-    self.sync_edit(use_twitter, tw_pass)
-    use_twitter.connect('toggled', self.sync_edit, tw_pass)
-    tw_indent.pack_start(tw_pass, padding=48)
-    dialog.vbox.add(tw_indent)
 
     dialog.show_all()
     result = dialog.run()
     dialog.hide()
     if result == gtk.RESPONSE_OK:
       self.set_use_facebook(use_facebook.get_active())
+      self.set_create_empty(fb_empty.get_active())
       self.set_use_twitter(use_twitter.get_active(), tw_user.get_text(), tw_pass.get_text())
 
     return result
-    
+  # -----------------------------------------------------------------------
+  def new_checkbox(self, label, box = None):
+    checkbox = hildon.CheckButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
+    checkbox.set_label(label)
+    if box:
+      box.add(checkbox)
+    return checkbox
+
+
+  # -----------------------------------------------------------------------
+  def new_indent(self, box):
+    outer = gtk.HBox()
+    indent = gtk.VBox()
+    outer.pack_start(indent, padding=48)
+    box.add(outer)
+    return indent
+
+  # -----------------------------------------------------------------------
+  def new_input(self, text, box = None):
+    input = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
+    input.set_property('hildon-input-mode', gtk.HILDON_GTK_INPUT_MODE_FULL)
+    input.set_placeholder(text)
+    input.set_property('is-focus', False)
+    if box:
+      box.add(input)
+    return input
+
+
+  # -----------------------------------------------------------------------
+  def add_linked(self, box, ctrl, to_add):
+    box.add(to_add)
+    self.sync_edit(ctrl, to_add)
+    ctrl.connect('toggled', self.sync_edit, to_add)
+    return to_add
+  
     
   # -----------------------------------------------------------------------
   def sync_edit(self, use_twitter, edit):
@@ -278,6 +313,13 @@ class HermesGUI:
   def set_use_facebook(self, value):
     self.gc.set_bool("/apps/maemo/hermes/use_facebook", value)
 
+  def get_create_empty(self):
+    return self.gc.get_bool("/apps/maemo/hermes/create_empty")
+
+
+  def set_create_empty(self, value):
+    self.gc.set_bool("/apps/maemo/hermes/create_empty", value)
+
 
   def get_use_twitter(self):
     return self.gc.get_bool("/apps/maemo/hermes/use_twitter")
index ffed489..e7ec2fc 100644 (file)
@@ -2,7 +2,7 @@ import os.path
 import evolution
 from facebook import Facebook, FacebookError
 import twitter
-import unicodedata
+import trans
 import gnome.gconf
 from contacts import ContactStore
 import names
@@ -26,7 +26,7 @@ class Hermes:
 
 
   # -----------------------------------------------------------------------
-  def __init__(self, callback, twitter = None, facebook = False):
+  def __init__(self, callback, twitter = None, facebook = False, empty = False):
     """Constructor. Passed a callback which must implement three informational
        methods:
        
@@ -45,12 +45,16 @@ class Hermes:
                    
          facebook - boolean indicating if Facebook should be used. Defaults to
                     False.
+
+         empty - boolean indicating if 'empty' contacts consisting of a profile
+                 URL and birthday should be created.
                           """
 
     self.gc       = gnome.gconf.client_get_default()
     self.callback = callback
     self.twitter  = twitter
     self.facebook = facebook
+    self.create_empty = empty
 
     # -- Check the environment is going to work...
     #
@@ -117,11 +121,14 @@ class Hermes:
         self.do_fb_login()
 
       # Get the list of friends...
-      attrs = ['uid', 'name', 'pic_big', 'birthday_date', 'profile_url']
+      attrs = ['uid', 'name', 'pic_big', 'birthday_date', 'profile_url', 'first_name', 'last_name', 'website']
       for friend in self.fb.users.getInfo(self.fb.friends.get(), attrs):
-        friend['name'] = unicodedata.normalize('NFKD', unicode(friend['name']))
+        key = unicode(friend['name']).encode('trans')
+        self.friends[key] = friend
         friend['pic']  = friend[attrs[2]]
-        self.friends[friend['name']] = friend
+        if friend['website']:
+          friend['homepage'] = friend['website']
+
         if not friend['pic']:
           self.blocked_pictures.append(friend)
           
@@ -132,7 +139,7 @@ class Hermes:
       api = twitter.Api(username=user, password=passwd)
       users = api.GetFriends()
       for friend in api.GetFriends():
-        self.friends[friend.name] = {'name': unicodedata.normalize('NFKD', unicode(friend.name)), 'pic': friend.profile_image_url, 'birthday_date': None, 'twitter_url': 'http://twitter.com/%s' % (friend.screen_name), 'homepage' : friend.url}
+        self.friends[friend.name] = {'name': unicode(friend.name).encode('trans'), 'pic': friend.profile_image_url, 'birthday_date': None, 'twitter_url': 'http://twitter.com/%s' % (friend.screen_name), 'homepage' : friend.url}
   
     # TODO What if the user has *no* contacts?
 
@@ -147,7 +154,7 @@ class Hermes:
     print "+++ Syncing contacts..."
     addresses = evolution.ebook.open_addressbook('default')
     print "+++ Addressbook opened..."
-    store = ContactStore(addresses)
+    self.store = ContactStore(addresses)
     print "+++ Contact store created..."
     self.updated = []
     self.unmatched = []
@@ -162,29 +169,9 @@ class Hermes:
       found = False
       for name in names.variants(contact.get_name()):
         if name in self.friends:
-          friend = self.friends[name]
+          updated = self.update_contact(contact, self.friends[name], resync)
           found = True
-          updated = False
-      
-          if friend['pic'] and (resync or contact.get_property('photo') is None):
-            updated = store.set_photo(contact, friend['pic']) or updated
-        
-          if friend['birthday_date'] and (resync or contact.get_property('birth-date') is None):
-            date_str = friend['birthday_date'].split('/')
-            date_str.append('0')
-            updated = store.set_birthday(contact, int(date_str[1]),
-                                                  int(date_str[0]),
-                                                  int(date_str[2])) or updated
-
-          if 'profile_url' in friend and friend['profile_url']:
-            updated = store.add_url(contact, friend['profile_url'], unique='facebook.com') or updated
-            
-          if 'twitter_url' in friend and friend['twitter_url']:
-            updated = store.add_url(contact, friend['twitter_url'], unique='twitter.com') or updated
-            
-          if 'homepage' in friend and friend['homepage']:
-            updated = store.add_url(contact, friend['homepage']) or updated
-    
+   
           if updated:
             self.updated.append(contact)
             addresses.commit_contact(contact)
@@ -197,5 +184,57 @@ class Hermes:
       else:
         self.unmatched.append(contact)
 
-    store.close()
+    # -- Create 'empty' contacts with birthdays...
+    #
+    if self.create_empty:
+      for name in self.friends:
+        friend = self.friends[name]
+        if 'contact' in friend or 'birthday_date' not in friend or not friend['birthday_date']:
+          continue
+
+        contact = evolution.ebook.EContact()
+        contact.props.full_name = friend['name']
+        contact.props.given_name = friend['first_name']
+        contact.props.family_name = friend['last_name']
+
+        self.update_contact(contact, friend)
+   
+        addresses.add_contact(contact)
+        self.updated.append(contact)
+        addresses.commit_contact(contact)
+
+        print "Created [%s]" % (contact.get_name())
+        self.matched.append(contact)
+
+    self.store.close()
+
+
+  # -----------------------------------------------------------------------
+  def update_contact(self, contact, friend, resync = False):
+    """Update the given contact with information from the 'friend'
+       dictionary."""
+
+    updated = False
+    friend['contact'] = contact
+      
+    if friend['pic'] and (resync or contact.get_property('photo') is None):
+      updated = self.store.set_photo(contact, friend['pic']) or updated
+        
+    if friend['birthday_date'] and (resync or contact.get_property('birth-date') is None):
+      date_str = friend['birthday_date'].split('/')
+      date_str.append('0')
+      updated = self.store.set_birthday(contact, int(date_str[1]),
+                                                 int(date_str[0]),
+                                                 int(date_str[2])) or updated
+
+    if 'profile_url' in friend and friend['profile_url']:
+      updated = self.store.add_url(contact, friend['profile_url'], unique='facebook.com') or updated
+            
+    if 'twitter_url' in friend and friend['twitter_url']:
+      updated = self.store.add_url(contact, friend['twitter_url'], unique='twitter.com') or updated
+            
+    if 'homepage' in friend and friend['homepage']:
+      updated = self.store.add_url(contact, friend['homepage']) or updated
+
+    return updated 
 
index d560841..0835e8e 100644 (file)
@@ -1,4 +1,4 @@
-import unicodedata
+import trans
 
 """Utilities for determing name variants.
 
@@ -34,7 +34,7 @@ def variants(name):
     if (name is None):
       return result
     
-    name = unicodedata.normalize('NFKD', unicode(name))
+    name = unicode(name).encode('trans')
     result.add(name)
     bits = name.split(' ')
     for bit in bits:
diff --git a/package/src/trans.py b/package/src/trans.py
new file mode 100644 (file)
index 0000000..e130142
--- /dev/null
@@ -0,0 +1,278 @@
+# coding: utf-8
+
+ur"""
+
+The **trans** module
+====================
+
+This module translates national characters into similar sounding
+latin characters (transliteration).
+At the moment, Greek, Turkish, Russian, Ukrainian, Czech, Polish,
+Latvian alphabets are supported (it covers 99% of needs).
+
+.. contents::
+
+Simple usage
+------------
+It's very easy to use
+~~~~~~~~~~~~~~~~~~~~~
+  >>> # coding: utf-8
+  >>> import trans
+  >>> u'Hello World!'.encode('trans')
+  u'Hello World!'
+  >>> u'Привет, Мир!'.encode('trans')
+  u'Privet, Mir!'
+
+
+Work only with unicode strings
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+  >>> 'Hello World!'.encode('trans')
+  Traceback (most recent call last):
+      ...
+  TypeError: trans codec support only unicode string, <type 'str'> given.
+
+This is readability
+~~~~~~~~~~~~~~~~~~~
+  >>> s = u'''\
+  ...    -- Раскудрить твою через коромысло в бога душу мать
+  ...             триста тысяч раз едрену вошь тебе в крыло
+  ...             и кактус в глотку! -- взревел разъяренный Никодим.
+  ...    -- Аминь, -- робко добавил из склепа папа Пий.
+  ...                 (c) Г. Л. Олди, "Сказки дедушки вампира".'''
+  >>> 
+  >>> print s.encode('trans')
+     -- Raskudrit tvoyu cherez koromyslo v boga dushu mat
+              trista tysyach raz edrenu vosh tebe v krylo
+              i kaktus v glotku! -- vzrevel razyarennyy Nikodim.
+     -- Amin, -- robko dobavil iz sklepa papa Piy.
+                  (c) G. L. Oldi, "Skazki dedushki vampira".
+
+Table "**id**"
+~~~~~~~~~~~~~~
+Use the table "id", leaving only the Latin characters, digits and underscores:
+
+  >>> print u'1 2 3 4 5 \n6 7 8 9 0'.encode('trans')
+  1 2 3 4 5 
+  6 7 8 9 0
+  >>> print u'1 2 3 4 5 \n6 7 8 9 0'.encode('trans/id')
+  1_2_3_4_5__6_7_8_9_0
+  >>> s.encode('trans/id')[-42:-1]
+  u'_c__G__L__Oldi___Skazki_dedushki_vampira_'
+
+
+Define user tables
+------------------
+Simple variant
+~~~~~~~~~~~~~~
+  >>> u'1 2 3 4 5 6 7 8 9 0'.encode('trans/my')
+  Traceback (most recent call last):
+      ...
+  ValueError: Table "my" not found in tables!
+  >>> trans.tables['my'] = {u'1': u'A', u'2': u'B'}; 
+  >>> u'1 2 3 4 5 6 7 8 9 0'.encode('trans/my')
+  u'A_B________________'
+  >>> 
+
+A little harder
+~~~~~~~~~~~~~~~
+Table can consist of two parts - the map of diphthongs and map of characters.
+First are processed diphthongs, by simple replacement on the substring.
+Then according to the map of characters, replacing each character of string
+by it's mapping. If character is absent in characters map, checked key None,
+if not, then is used the default character u'_'.
+
+  >>> diphthongs = {u'11': u'AA', u'22': u'BB'}
+  >>> characters = {u'a': u'z', u'b': u'y', u'c': u'x',
+  ...               u'A': u'A', u'B': u'B', None: u'-'}
+  >>> trans.tables['test'] = (diphthongs, characters)
+  >>> u'11abc22cbaCC'.encode('trans/test')
+  u'AAzyxBBxyz--'
+
+**The characters created by processing of diphthongs are also processed
+by the map of the symbols:**
+
+  >>> diphthongs = {u'11': u'AA', u'22': u'BB'}
+  >>> characters = {u'a': u'z', u'b': u'y', u'c': u'x', None: u'-'}
+  >>> trans.tables['test'] = (diphthongs, characters)
+  >>> u'11abc22cbaCC'.encode('trans/test')
+  u'--zyx--xyz--'
+
+Without the diphthongs
+~~~~~~~~~~~~~~~~~~~~~~
+These two tables are equivalent:
+
+  >>> characters = {u'a': u'z', u'b': u'y', u'c': u'x', None: u'-'}
+  >>> trans.tables['t1'] = characters
+  >>> trans.tables['t2'] = ({}, characters)
+  >>> u'11abc22cbaCC'.encode('trans/t1') == u'11abc22cbaCC'.encode('trans/t2')
+  True
+
+
+Finally
+-------
++ *Special thanks to Yuri Yurevich aka j2a for the kick in the right direction.*
+    - http://www.python.su/forum/viewtopic.php?pid=28965
+    - http://code.djangoproject.com/browser/django/trunk/django/contrib/admin/media/js/urlify.js
++ *I please forgiveness for my bad English. I promise to be corrected.*
+
+"""
+
+__version__ = '1.1a7'
+__author__ = 'Zelenyak Aleksandr aka ZZZ <zzz.sochi@gmail.com>'
+
+latin = {
+    u'à': u'a',  u'á': u'a',  u'â': u'a', u'ã': u'a', u'ä': u'a', u'å': u'a',
+    u'æ': u'ae', u'ç': u'c',  u'è': u'e', u'é': u'e', u'ê': u'e', u'ë': u'e',
+    u'ì': u'i',  u'í': u'i',  u'î': u'i', u'ï': u'i', u'ð': u'd', u'ñ': u'n',
+    u'ò': u'o',  u'ó': u'o',  u'ô': u'o', u'õ': u'o', u'ö': u'o', u'ő': u'o',
+    u'ø': u'o',  u'ù': u'u',  u'ú': u'u', u'û': u'u', u'ü': u'u', u'ű': u'u',
+    u'ý': u'y',  u'þ': u'th', u'ÿ': u'y',
+
+    u'À': u'A',  u'Á': u'A',  u'Â': u'A', u'Ã': u'A', u'Ä': u'A', u'Å': u'A',
+    u'Æ': u'AE', u'Ç': u'C',  u'È': u'E', u'É': u'E', u'Ê': u'E', u'Ë': u'E',
+    u'Ì': u'I',  u'Í': u'I',  u'Î': u'I', u'Ï': u'I', u'Ð': u'D', u'Ñ': u'N',
+    u'Ò': u'O',  u'Ó': u'O',  u'Ô': u'O', u'Õ': u'O', u'Ö': u'O', u'Ő': u'O',
+    u'Ø': u'O',  u'Ù': u'U',  u'Ú': u'U', u'Û': u'U', u'Ü': u'U', u'Ű': u'U',
+    u'Ý': u'Y',  u'Þ': u'TH', u'ß': u'ss'
+}
+
+greek = {
+    u'α': u'a', u'β': u'b', u'γ': u'g', u'δ': u'd', u'ε': u'e',  u'ζ': u'z',
+    u'η': u'h', u'θ': u'8', u'ι': u'i', u'κ': u'k', u'λ': u'l',  u'μ': u'm',
+    u'ν': u'n', u'ξ': u'3', u'ο': u'o', u'π': u'p', u'ρ': u'r',  u'σ': u's',
+    u'τ': u't', u'υ': u'y', u'φ': u'f', u'χ': u'x', u'ψ': u'ps', u'ω': u'w',
+    u'ά': u'a', u'έ': u'e', u'ί': u'i', u'ό': u'o', u'ύ': u'y',  u'ή': u'h',
+    u'ώ': u'w', u'ς': u's', u'ϊ': u'i', u'ΰ': u'y', u'ϋ': u'y',  u'ΐ': u'i',
+
+    u'Α': u'A', u'Β': u'B', u'Γ': u'G', u'Δ': u'D', u'Ε': u'E',  u'Ζ': u'Z',
+    u'Η': u'H', u'Θ': u'8', u'Ι': u'I', u'Κ': u'K', u'Λ': u'L',  u'Μ': u'M',
+    u'Ν': u'N', u'Ξ': u'3', u'Ο': u'O', u'Π': u'P', u'Ρ': u'R',  u'Σ': u'S',
+    u'Τ': u'T', u'Υ': u'Y', u'Φ': u'F', u'Χ': u'X', u'Ψ': u'PS', u'Ω': u'W',
+    u'Ά': u'A', u'Έ': u'E', u'Ί': u'I', u'Ό': u'O', u'Ύ': u'Y',  u'Ή': u'H',
+    u'Ώ': u'W', u'Ϊ': u'I', u'Ϋ': u'Y'
+}
+
+turkish = {
+    u'ş': u's', u'Ş': u'S', u'ı': u'i', u'İ': u'I', u'ç': u'c', u'Ç': u'C',
+    u'ü': u'u', u'Ü': u'U', u'ö': u'o', u'Ö': u'O', u'ğ': u'g', u'Ğ': u'G'
+}
+
+russian = (
+    {u'юй': u'yuy', u'ей': u'yay',
+     u'Юй': u'Yuy', u'Ей': u'Yay'},
+    {
+    u'а': u'a',  u'б': u'b',  u'в': u'v',  u'г': u'g', u'д': u'd', u'е': u'e',
+    u'ё': u'yo', u'ж': u'zh', u'з': u'z',  u'и': u'i', u'й': u'y', u'к': u'k',
+    u'л': u'l',  u'м': u'm',  u'н': u'n',  u'о': u'o', u'п': u'p', u'р': u'r',
+    u'с': u's',  u'т': u't',  u'у': u'u',  u'ф': u'f', u'х': u'h', u'ц': u'c',
+    u'ч': u'ch', u'ш': u'sh', u'щ': u'sh', u'ъ': u'',  u'ы': u'y', u'ь': u'',
+    u'э': u'e',  u'ю': u'yu', u'я': u'ya',
+
+    u'А': u'A',  u'Б': u'B',  u'В': u'V',  u'Г': u'G', u'Д': u'D', u'Е': u'E',
+    u'Ё': u'Yo', u'Ж': u'Zh', u'З': u'Z',  u'И': u'I', u'Й': u'Y', u'К': u'K',
+    u'Л': u'L',  u'М': u'M',  u'Н': u'N',  u'О': u'O', u'П': u'P', u'Р': u'R',
+    u'С': u'S',  u'Т': u'T',  u'У': u'U',  u'Ф': u'F', u'Х': u'H', u'Ц': u'C',
+    u'Ч': u'Ch', u'Ш': u'Sh', u'Щ': u'Sh', u'Ъ': u'',  u'Ы': u'Y', u'Ь': u'',
+    u'Э': u'E',  u'Ю': u'Yu', u'Я': u'Ya'
+})
+
+ukrainian = (russian[0].copy(), {
+    u'Є': u'Ye', u'І': u'I', u'Ї': u'Yi', u'Ґ': u'G',
+    u'є': u'ye', u'і': u'i', u'ї': u'yi', u'ґ': u'g'
+})
+ukrainian[1].update(russian[1])
+
+czech = {
+    u'č': u'c', u'ď': u'd', u'ě': u'e', u'ň': u'n', u'ř': u'r', u'š': u's',
+    u'ť': u't', u'ů': u'u', u'ž': u'z',
+    u'Č': u'C', u'Ď': u'D', u'Ě': u'E', u'Ň': u'N', u'Ř': u'R', u'Š': u'S',
+    u'Ť': u'T', u'Ů': u'U', u'Ž': u'Z'
+}
+
+polish = {
+    u'ą': u'a', u'ć': u'c', u'ę': u'e', u'ł': u'l', u'ń': u'n', u'ó': u'o',
+    u'ś': u's', u'ź': u'z', u'ż': u'z',
+    u'Ą': u'A', u'Ć': u'C', u'Ę': u'e', u'Ł': u'L', u'Ń': u'N', u'Ó': u'o',
+    u'Ś': u'S', u'Ź': u'Z', u'Ż': u'Z'
+}
+
+latvian = {
+    u'ā': u'a', u'č': u'c', u'ē': u'e', u'ģ': u'g', u'ī': u'i', u'ķ': u'k',
+    u'ļ': u'l', u'ņ': u'n', u'š': u's', u'ū': u'u', u'ž': u'z',
+    u'Ā': u'A', u'Č': u'C', u'Ē': u'E', u'Ģ': u'G', u'Ī': u'i', u'Ķ': u'k',
+    u'Ļ': u'L', u'Ņ': u'N', u'Š': u'S', u'Ū': u'u', u'Ž': u'Z'
+}
+
+
+
+ascii_str = u'''_0123456789
+abcdefghijklmnopqrstuvwxyz
+ABCDEFGHIJKLMNOPQRSTUVWXYZ
+!"#$%&'()*+,_-./:;<=>?@[\\]^`{|}~ \t\n\r\x0b\x0c'''
+
+ascii = ({}, dict(zip(ascii_str, ascii_str)))
+for t in [latin, greek, turkish, russian, ukrainian, czech, polish, latvian]:
+    if isinstance(t, dict):
+        t = ({}, t)
+    ascii[0].update(t[0])
+    ascii[1].update(t[1])
+ascii[1][None] = u'_'
+del t
+
+
+id = (ascii[0].copy(), ascii[1].copy())
+for c in '''!"#$%&'()*+,_-./:;<=>?@[\\]^`{|}~ \t\n\r\x0b\x0c''':
+    del id[1][c]
+
+
+def trans(input, table=ascii):
+    '''Translate unicode string, using 'table'.
+       Table may be tuple (diphthongs, other) or dict (other).'''
+    if not isinstance(input, unicode):
+        raise TypeError, 'trans codec support only unicode string, %r given.' \
+                                                        % type(input)
+    if isinstance(table, dict):
+        table = ({}, table)
+
+    first = input
+    for diphthong, value in table[0].items():
+        first = first.replace(diphthong, value)
+
+    second = u''
+    for char in first:
+        second += table[1].get(char, table[1].get(None, u'_'))
+
+    return second, len(second)
+
+tables = {'ascii': ascii, 'text': ascii, 'id': id}
+
+import codecs
+
+def encode(input, errors='strict', table_name='ascii'):
+    try:
+        table = tables[table_name]
+    except KeyError:
+        raise ValueError, 'Table "%s" not found in tables!' % table_name
+    else:
+        return trans(input, table)
+
+def no_decode(input, errors='strict'):
+    raise TypeError("trans codec does not support decode.")
+
+def trans_codec(enc):
+    if enc == 'trans':
+        return codecs.CodecInfo(encode, no_decode)
+#    else:
+    try:
+        enc_name, table_name = enc.split('/', 1)
+    except ValueError:
+        return None
+    if enc_name != 'trans':
+        return None
+    if table_name not in tables:
+        raise ValueError, 'Table "%s" not found in tables!' % table_name
+
+    return codecs.CodecInfo(lambda i, e='strict': encode(i, e, table_name),
+                            no_decode)
+
+codecs.register(trans_codec)