Django application development

Most developers work on their startups far away from the daylight – they keep it in complete secret till the end. I’ve decided to do something new – I have started a project which is available to preview during the whole development. It is available at urevs.com. I also share all the project info on dedicated blog: blog.urevs.com.

This project consists currently of authentication framework and a lot of backend solutions. Site allows users to login using OpenID’s, Google, Twitter, Facebook or Yahoo Accounts. Full details are available here – social bootstrap engine for Django.

I’d be very glad to hear from You, about every suggestion You have.

IPv4 CIDR to netmask in Python

I needed small function to validate IPv4 netmasks, havent found one, so I wrote my own. I’ve decided to use IPv4 CIDR notation to get corresponding netmasks. Here is the code:

def ipv4_cidr_to_netmask(bits):

    """ Convert CIDR bits to netmask """

    netmask = ''
    for i in range(4):
        if i:
            netmask += '.'
        if bits >= 8:
            netmask += '%d' % (2**8-1)
            bits -= 8
        else:
            netmask += '%d' % (256-2**(8-bits))
            bits = 0
    return netmask

Example usage is presented below. They also show how lambda mappings are useful in regular, daily use.

List all possible netmasks:

for netmask in map(lambda x: ipv4_cidr_to_netmask(x), range(0,33)):
    print netmask

0.0.0.0
128.0.0.0
192.0.0.0
224.0.0.0
240.0.0.0
248.0.0.0
252.0.0.0
254.0.0.0
255.0.0.0
255.128.0.0
255.192.0.0
255.224.0.0
255.240.0.0
255.248.0.0
255.252.0.0
255.254.0.0
255.255.0.0
255.255.128.0
255.255.192.0
255.255.224.0
255.255.240.0
255.255.248.0
255.255.252.0
255.255.254.0
255.255.255.0
255.255.255.128
255.255.255.192
255.255.255.224
255.255.255.240
255.255.255.248
255.255.255.252
255.255.255.254
255.255.255.255

Get netmask for /24:

print ipv4_cidr_to_netmask(24)

255.255.255.0

Is 255.255.254.0 valid IPv4 netmask?

print "255.255.254.0" in map(lambda x: ipv4_cidr_to_netmask(x), range(0,33))

True

Reverse search – get CIDR for 255.255.192.0 netmask

print map(lambda x: ipv4_cidr_to_netmask(x), range(0,33)).index('255.255.254.0')

23

As usual – enjoy :)

Wygodny storage z użyciem memcache w Django

Czasami zachodzi konieczność napisania na szybko aplikacji w Django która standardowo coś tam trzyma w bazie danych, coś przetwarza i wyświetla. Jeśli aplikacja ma być prosta, a w bazie mają być trzymane proste struktury danych, to czy napewno musimy tyle czasu poświęcać na dopracowanie modeli? Jasne, że nie.

Poniższy kod oferuje nam możliwość trzymania dowolnych danych w postaci klucz-wartość w bazie danych. Dodatkowo – owe dane mogą mieć dowolną postać – może być to tekst, tablica asocjacyjna czy wartość typu Boolean.

Ponadto – biblioteczka ta opiera się o opisaną w poprzednim wpisie obsługę memcache’u w Django – co sprawia, że do póki nie zmienimy wartości danych, a ich obecność w cache’u nie zdąży wygasnąć – nie będziemy w ogóle obciążać naszej bazy danych. Osobiście wykorzystuję ten kod do trzymania ustawień aplikacji. Niektóre ustawienia są typu boolean, inne to stringi, jeszcze inne – tabele. Rozwiązanie sprawdza się świetnie.

Właściwy kod

Modele – models.py:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2009 nme.pl
# Dual licensed under MIT and GPL.

from django.db import models

class Vars (models.Model):

    key = models.CharField(max_length=50)
    value = models.TextField()

Proste prawda? Nie zapomnijcie wydać komendy django-admin syncdb która utworzy odpowiednią tabelę w bazie. Teraz czas na właściwą bibliotekę – vars.py:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2009 nme.pl
# Dual licensed under MIT and GPL.

""" Simple variables (key-value) handling with memcache """

import pickle,base64

import memcache,models

from settings import conf

class __Vars:

    def get (self,key,default=None):

        """ get Vars variable value """

        if memcache.enabled:
            value = memcache.get('va-%s' % key)
            if value != None:
                return pickle.loads(base64.b64decode(str(value)))
        settings = models.Vars.objects.filter(key=key)
        if settings:
            value = settings[0].value
            if value:
                return pickle.loads(base64.b64decode(str(value)))
            else:
                return default
        else:
            return default

    def __getitem__ (self,msg):

        return self.get(key)

    def set (self,key,value):

        """ set Vars variable value """

        value = base64.b64encode(pickle.dumps(value))
        if memcache.enabled:
            memcache.set('va-%s' % key,value)
        settings = models.Vars.objects.filter(key=key)
        if settings:
            setting = settings[0]
            setting.value = value
            setting.save()
        else:
            models.Vars(key=key,value=value).save()

    def has_key (self,key):

        """ return True or False if Vars variable exists """

        settings = models.Vars.objects.filter(key=key)
        if settings:
            return True
        else:
            return False

    def delete (self,key):

        """ deletes given key from Vars and memcache """

        if memcache.enabled:
            memcache.delete('va-%s' % key)
        settings = models.Vars.objects.filter(key=key)
        for i in settings:
            i.delete()

variables = __Vars()

Przykładowe użycie

Zastosowanie biblioteki z poziomu django-admin shell:

In [1]: from vars import variables

In [2]: if not variables.has_key('test'):
   ...:     variables.set('test','tekst')
   ...:
   ...:     

In [3]: print variables.get('test')
tekst

In [4]: variables.set('test',{'asd':1,'data':['struct','ure']})

In [5]: variables.get('test')['data'][1]
Out[5]: 'ure'

In [6]: variables.delete('test')

In [7]: variables.get('test')

In [8]: variables.has_key('test')
Out[8]: False

Jak to działa?

Składowanie dowolnego typu danych w rekordzie bazy zrealizowane jest w oparciu o dwie standardowe biblioteki pythonowe – picklebase64. Pierwsza z nich oferuje zakodowanie dowolnej struktury danych do stringa, druga natomiast – zastosowanie notacji base64 – przez co zabezpieczamy się przed ewentualnymi problemami z obsługą znaków z którymi nasz bazodanowy backend mógłby mieć ewentualnie problemy. Innymi słowy – rozwiązanie przedstawione powyżej gwarantuje nam pełną przenośność danych.

Zachęcam gorąco do korzystania :)

Memcache w Django: krok ku lepszej skalowalności

Podczas tworzenia aplikacji webowych, warto w miarę wcześnie pomyśleć o skali z jaką nasze rozwiązanie będzie miało się w przyszłości zmierzyć. Niezależnie od tego czy owa aplikacja ma pracować w jednym przedsiębiorstwie, czy próbuje zainstnieć szerzej, w sieci – w każdym przypadku może się okazać, że odniesie sukces. To z kolei sprawi, że ilość jej użytkowników urośnie… Firma może się rozwinąć, przejąć konkurencję, aplikacja webowa może się okazać takim strzałem w dziesiątkę jak nie tak dawno temu Nasza-Klasa – nigdy nie wiadomo :) W momencie gdy miałaby go odnieść, nie będzie już zbyt wiele czasu na przebudowę kodu, a napewno nie będzie go na jego całkowitą reorganizację.

Jeśli rozwiązanie jest skryptem uruchamianym od święta – może to być zwykły skrypt CGI. Jeśli jednak miałby być uruchamiany częściej – wartałoby już pomyśleć o mod_php czy mod_python… Albo jeszcze lepiej – fastcgi, a w przypadku pythona – wsgi. Kiedy użycie aplikacji rośnie nadal – frontend mnożymy na kolejne węzły stawiając przed nimi balancera robiącego za reverse proxy. A w backendzie klastrujemy… ale co na styku macierz, klaster? Ilość zapytań do bazy rośnie, io waity zaczynają rosnąć i robi się nieciekawie… Da się to rozwiązać?

Jak to robi Google

Google od jakiegoś czasu pokazuje nam jak powinny być tworzone intensywnie wykorzystywane aplikacje webowe i na jak zorganizowanym backendzie powinny pracować, aby skalowały się najefektywniej. Myślę, że warto z tej wiedzy skorzystać zanim io waity zaczną spędzać nam sen z powiek.

Miałem okazję napisać sobie kilka małych aplikacji które pracują w chmurze na platformie Google – tzw. Google App Engine (w skrócie GAE). Warto było poświęcić trochę czasu aby to rozwiązanie poznać. Łatwiej było mi dzięki temu można zrozumieć między innymi dlaczego poszczególne serwery Google nie są potężnymi serwerami rackowymi z bardzo silnymi procesorami, ale raczej „lżejszymi” serwerkami, ale za to wypełnionymi po brzegi koścmi pamięci.

Dlaczego takie lżejsze maszyny? Poza oszczędnością energii i pieniędzy są one wystarczające do obsługi pythonowych frameworków, w przeciwieństwie do tych PHP’owych.

Google skupia się na Javie i Pythonie, gdzie prawie pewne jest to, że ich rozproszony Datastore – system bazodanowy, został stworzony w oparciu o język Python. Język ten znany jest m.in z wydajności, szczególnie, jeśli osoba która go używa stosuje się do najbardziej podstawowych zasad jak nie allokowanie ogromnej ilości pamięci itp. Dlaczego więc w serwerach Google tyle pamięci? Odpowiedzią jest memcache.

Google chwali się tym, że stworzyli sobie własną implementację cache, inspirowaną oprogramowaniem memcached i zachęca, aby developerzy aplikacji App Engine’owych również korzystali z memcache’a.

Na czym polega memcache

Uproszczony graf przedstawiający działanie memcache’u:

Koncepcja działania jest prosta – zamiast odwoływać się bezpośrednio do bazy danych, odwołujemy się do memcache’a – jeśli trafiliśmy na dane (hit), wykorzystujemy je, jeśli nie (miss), dopiero wtedy odwołujemy się do bazy, jednocześnie zapisując pobrane z bazy dane w naszym cache’u. Dzięki temu prostemu zabiegowi, nasza strona, która podczas każdego odwołania do niej potrzebuje jakichś danych z bazy, jest w stanie funkcjonować praktycznie w ogóle jej o nic nie odpytując – wszystko co potrzebne będzie trzymać w pamięci podręcznej.

Zastosowanie i dokumentacja

Aby wykorzystać memcache w Google App Engine, polecam zapoznanie się z dokumentacją na stronach Google.

A co jeśli chcielibyśmy wykorzystać memcache w Django? Naturalnie – nic nie stoi na przeszkodzie – Django posiada coś takiego jak cache framework, który na dodatek jest bardzo dobrze udokumentowany.

Django samo z siebie jest przystosowane do wykorzystania cache’u przy obsłudze widoków czy template’ów, ale również oferuje dostęp niskopoziomowy – niemalże analogiczny do tego z Google App Engine… Niemalże, bo jakoś wersja Google bardziej przypadła mi do gustu. Dlatego przygotowałem sobie prosty interfejs, który wydaje mi się być odrobinę bardziej logiczny. Kod oczywiście załączam. Mam nadzieję, że komuś się przyda.

Konfiguracja systemu, projektu Django oraz moja niskopoziomowa obsługa

Zakładając, że pracujemy na systemie GNU Debian/Ubuntu oraz Django jest już w systemie jakąś drogą zainstalowane (osobiście w tym akurat przypadku polecam instalację z oryginalnych źródeł nad paczką Django z dystrybucji), doinstalowujemy obługę memcached, włączamy ją i restartujemy demona memcached:

sudo -i
aptitude install memcached python-memcache
sed -i 's/no/yes/' /etc/default/memcached
service memcached restart
exit

teraz w naszym projekcie Django włączamy obsługę cache:

w pliku settings.py:

CACHE_BACKEND = 'memcached://127.0.0.1:11211/'

jeśli chcemy wykorzystać moją małą biblioteczkę poniżej, do settings.py musimy dodać też konfigurację memcache:

class conf:
    class memcache:
        disabled = False
        timeout = 600

Moja niskopoziomowa obsługa memcache.py:

#!/usr/bin/python
# -*- coding: utf-8 -*-

""" cache low-level backend based on memcached """

from django.core.cache import cache

from settings import conf

def get(key, default=None, timeout=conf.memcache.timeout):

    """ returns value for given key from cache, and refreshes it
    in memcache with automatic conversion from unicode
    to str - fix to:
    __import__() argument 1 must be string without null bytes, not str """

    value = cache.get(key, default)

    if value is not default:
        cache.set(key, value, timeout)

    if type(value) is unicode:
        value = str(value)

    return value

def set(key, value, timeout=conf.memcache.timeout):

    """ sets a value for given key in cache """

    cache.set(key, value, timeout)

def delete(key):

    """ remove given key from cache """

    cache.delete(key)

# in the name of syntatic sugar ;) 

disabled = bool(conf.memcache.disabled)
enabled = not disabled

A przykład zastosowania pojawi się jutro albo pojutrze – w nowym wpisie.

Python – Demonologia

artwork by ~laurD on DeviantART

Python jest świetnym językiem do całej palety zastosowań. Służy on pomocą zarówno administratorom systemów, osobom które potrzebują napisać prosty parser, a nawet twórcom aplikacji webowych – dzięki takim frameworkom jak Django. W poniższym tekscie chciałbym pokazać jak łatwo jest stworzyć usługę systemową (ang. daemon) w języku Python, która będzie wykonywała dla nas określoną czynność – jako przykład przedstawię fakedns – bardzo prostego daemona DNS, który na dowolne zapytanie o adres IP będzie zwracał zawsze ten sam.

Dlaczego Python? Co z innymi językami?

Nie lepiej mieć aplikację którą można skompilować? Absolwenci kierunków informatycznych jeszcze kilka lat temu dowiadywali się na studiach, że najbardziej zaawansowanym językiem do wszelakich zastosowań jest Java. Mieli praktyczne doświadczenie z tworzenia aplikacji w Javie… i zaczęli pisać. Kod który chciałbym tu przedstawić pisałem dwa lata temu. Zanim się za niego zabrałem, zacząłem od szukania już gotowego rozwiązania za pomocą Google. Znalazłem, nawet kilka. Jedno w Javie. O zgrozo. Ja wiem, że się da. Wiem, że Java jest naprawde zaawansowanym językiem. Gotowa aplikacja wymaga jednak dużego nadkładu. Trzeba przygotować środowisko, przygotować konkretną ilość pamięci… i czekać kiedy aplikacja się wyłoży. Takie mam przynajmniej doświadczenia z aplikacjami w Javie, które pisali programiści zaraz po studiach, pracując w profesjonalnej firmie software’owej.

A inne języki? Natualnie. Jest C. Świetny język, ale czy mamy wystarczająco dużo czasu aby w nim pisać nawet małe aplikacje? .NET ? Wolne żarty. Nie dość, że pod Linuxem jest bardzo grubymi nićmi przyszywany, to i nie mamy pewności, czy Microsoft nie powie pewnego dnia hola, jedynie na ich, komercyjnej platformie. Naddatek potrzebnej mocy obliczeniowej, pamięci, wymaganej przestrzeni dyskowej… i ta świadomość, że nie jest to dopracowane i pewnie nigdy nie będzie – niczym Samba próbująca naśladować również kalekie i dziurawe, Microsoftowe domeny.

A co ze skryptowymi? Mnie na myśl przychodzą trzy alternatywy – Lua, Ruby i skrypt w bashu.

Lua – niesamowicie przejrzysty język. Piekielnie szybki, realizowany przez wręcz mikroskopijną bibliotekę. Na jego niekorzyść trzeba jednak zaliczyć wręcz niemal konieczność liczenia się z faktem, że jeśli rozwiązanie które potrzebujemy stworzyć, będzie wymagało biblioteki – będziemy zmuszeni długo jej szukać lub przygotować samemu jej obsługę za pomocą języka C.

Ruby – chyba najbliższy Pythonowi. Dlaczego nie. Bibliotek jest coraz więcej, jest wydajny. Miałem okazję jakiś czas temu nawet coś napisać w tym języku (a dokładnie – przepisać mały framework z Pythona), ale… jakoś w Pythonie pisze mi się lepiej. Co ważne – Ruby jest głęboko zakorzeniony w Perlu. Programistom mówi się jak powinni pisać kod, jednak zawsze mogą robić w to w stylu Perlowym – pisać szybko i wydajnie, jednak przeczytanie i przeanalizowanie go po jakimś czasie staje się piekłem. Kod w Pythonie jest de facto zawsze taki sam, nie zależnie kto by go pisał – czytanie go jest proste i przyjemne.

No i jeszcze uzupełnie o język skryptowy – Bash. Można w nim naprawdę dużo zrobić. Warto jednak zastanowić się na ile aplikacja będzie zaawansowana i czy zależy nam na wydajności. Podam przykład. Stworzyłem kiedyś skrypt w bashu generujący w oparciu o kilka plików wejściowych reguły dla firewalla, reguły shapera oraz treść dhcpd.conf. Skrypt wewnątrz wywoływał po kilka tysięcy razy awk/sed’a itp. Na maszynie jednoprocesorowej (Pentium 4 1.4 GHz), która pełniła funkcję routera dla sieci 100 Mbit/s, przy mniejszym obciążeniu – skrypt wykonywał się kilka minut. Przy większym – godzinę. Za każdym razem uruchamiając awk, spawnował odrębnego basha. Cóż. Przepisałem rozwiązanie na język Lua. Skrypt niezależnie od obciążenia maszyny wykonywał się w 0.02 sekundy. Jeśli byłby to Python czy Ruby – efekt byłby zapewne zbliżony do Lua.

Tworzenie usługi systemowej dla Linux/Unix

OK, zaczynamy. Usługa systemowa powinna przede wszystkim:
- wiedzieć jak pisać do sysloga
- zrzucić uprawnienia root’a
- odkleić się od stdin i stdout i przejść w tło
Jeśli taka usługa dodatkowo potrzebuje obsługiwać jakiś systemowy port TCP/UDP (o numerze niższym niż 1024) – zanim zrzuci ona prawa roota, powinna otworzyć sobie określonego socketa.

Nie będę tłumaczył poszczególnych fragmentu kodu ponieważ uważam, że jest on wystarczająco przejżysty.

fakedns.py

#!/usr/bin/python -u
# -*- coding: utf-8 -*-
" Fake DNS server: Always answer with the same, defined as conf.destip reply, listen at conf.listen "

class conf:
    listen='ADRES_IP_NA_KTÓRYM_APLIKACJA_MA_NASŁUCHIWAĆ'
    destip='ADRES_IP_NA_KTÓRY_APLIKACJA_BĘDZIE_KIEROWAĆ'

import os,sys,pwd,grp
import logging,logging.handlers
import socket

log=logging.getLogger('fakedns')
logging.basicConfig()
logging.root.setLevel(level=logging.INFO)
#logging.root.setLevel(level=logging.DEBUG)
hdlr=logging.handlers.SysLogHandler('/dev/log')
log.addHandler(hdlr)

def drop_privileges(uid_name='nobody',gid_name='nogroup'):
    starting_uid=os.getuid()
    starting_gid=os.getgid()
    starting_uid_name=pwd.getpwuid(starting_uid)[0]
    log.info('fakedns: drop_privileges: started as %s/%s'%(pwd.getpwuid(starting_uid)[0],grp.getgrgid(starting_gid)[0]))
    if os.getuid()!=0:
        log.info("fakedns: drop_privileges: already running as '%s'"%starting_uid_name)
        return
    if starting_uid==0:
        # Get the uid/gid from the name
        running_uid = pwd.getpwnam(uid_name)[2]
        running_gid = grp.getgrnam(gid_name)[2]
        # Try setting the new uid/gid
        try:
            os.setgid(running_gid)
        except OSError,e:
            log.error('Could not set effective group id: %s'%e)
        try:
            os.setuid(running_uid)
        except OSError,e:
            log.error('Could not set effective user id: %s'%e)
        # Ensure a very convervative umask
        new_umask=077
        old_umask=os.umask(new_umask)
        log.info('fakedns: drop_privileges: Old umask: %s, new umask: %s'%(oct(old_umask),oct(new_umask)))
    final_uid=os.getuid()
    final_gid=os.getgid()
    log.info('fakedns: drop_privileges: running as %s/%s'%(pwd.getpwuid(final_uid)[0],grp.getgrgid(final_gid)[0]))

def become_daemon(home='.',stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
    log.info('fakedns: deamonize')
    # perform first fork
    try:
        if os.fork()>0:
            sys.exit(0)
    except OSError,e:
        log.info("fakedns: 1st fork failed: %s"%e.strerror)
        sys.exit(1)
    os.setsid()
    os.chdir(home)
    os.umask(0)
    # second fork
    try:
        if os.fork()>0:
            sys.exit(0)
    except OSError,e:
        log.info("fakedns: 2nd fork failed: %s"%e.strerror)
        sys.exit(1)
    i=open(stdin,'r')
    o=open(stdout,'a+')
    e=open(stderr,'a+',0)
    os.dup2(i.fileno(),sys.stdin.fileno())
    os.dup2(o.fileno(),sys.stdout.fileno())
    os.dup2(e.fileno(),sys.stderr.fileno())

class DNSQuery:
    def __init__(self, data):
        self.data=data
        self.dominio=''

        tipo = (ord(data[2]) >> 3) & 15     # Opcode bits
        if tipo == 0: # Standard query
            ini=12
            lon=ord(data[ini])
            while lon != 0:
                self.dominio+=data[ini+1:ini+lon+1]+'.'
                ini+=lon+1
                lon=ord(data[ini])

    def respuesta(self, ip):
        packet=''
        if self.dominio:
            packet+=self.data[:2] + "\x81\x80"
            # Questions and Answers Counts
            packet+=self.data[4:6] + self.data[4:6] + '\x00\x00\x00\x00'
            # Original Domain Name Question
            packet+=self.data[12:]
            # Pointer to domain name
            packet+='\xc0\x0c'
            # Response type, ttl and resource data length -> 4 bytes
            packet+='\x00\x01\x00\x01\x00\x00\x00\x3c\x00\x04'
            # 4bytes of IP
            packet+=str.join('',map(lambda x: chr(int(x)), ip.split('.')))
        return packet

if __name__ == '__main__':
    udps = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    try:
        udps.bind((conf.listen,53))
    except socket.error, (val,e):
        log.info("fakedns: %s"%e)
        sys.exit(1)
    log.info('fakedns: dom.query. 60 IN A %s' % conf.destip)
    drop_privileges()
    become_daemon()
    try:
        while 1:
            data, addr = udps.recvfrom(1024)
            #log.info(addr)
            p=DNSQuery(data)
            udps.sendto(p.respuesta(conf.destip), addr)
            #log.info('%s -> %s' % (p.dominio, conf.destip))
            log.info('fakedns: %s -> %s'%(p.dominio,addr[0]))
    except KeyboardInterrupt:
        log.info('fakedns: done.' % (p.dominio, conf.destip))
        udps.close()
    except:
        log.info('fakedns: crashed serving %s (%s)'%(addr, data))

Powiadomienia SMS mBank mobile – drugie podejście

Dwa miesiące temu przedstawiłem dwa sposoby w jaki można sobie uruchomić powiadomienia SMS od aplikacji lub o przychodzących mailach. Oba miały pewne wady więc zdecydowałem się powrócić do tematu raz jeszcze.

Pierwszym rozwiązaniem, było wykorzystanie +48xxxxxxxxx@text.plusgsm.pl
Rozwiązanie dobre, bo dostajemy natychmiast powiadomienie na komórkę. Dodatkowo większość skrzynek pocztowych ma możliwość ustawienia automatycznej kopii na drugi adres – więc w dosłownie chwilę możemy sobie ustawić automatyczne powiadomienie o mailach.
Rozwiązanie to ma jednak swoją wadę – dostajemy pełną treść, która nie zmieści się w jednym SMS’ie jeśli w mailu jest jakakolwiek treść poza samym tematem.

Drugim zasugerowanym przeze mnie rozwiązaniem było wykorzystanie możliwości wysyłania powiadomień SMS z Google Calendar. To rozwiązanie również jest dobre. Możemy wykorzystać konto w Google Apps, dzięki czemu adres powiadomień jest bardziej uniwersalny, ale ma też swoje wady – chodzi mianowicie o dzienny limit powiadomień SMS z kalendarza Google. Jak się przekonałem, taki limit jest. Dokładnej liczby nie znam, ale w przybliżeniu to około 25 SMS’ów.

Rozwiązanie optymalne?

Postanowiłem zatem poszukać czegoś po środku. Wykorzystałem obsługę skrzynki Google do pobierania wiadomości, a następnie jedynie nadawcę i temat przekazuje do usługi text.plusgsm.pl. Wydaje mi się, że rozwiązanie jest optymalne. Oto kod:

#!/usr/bin/env python
# -- encoding: utf8 --

__author__ = 'mw AT nme.pl'
__version__ = '1.4'
__date__ = 'pią, 25 wrz 2009, 13:22:28 CEST'

class setup:
    verbose = False
    class google:
        login = 'KONTO@GOOGLE'
        password = 'HASŁO'
    class notify:
        recipient="+48xxxxxxxxx@text.plusgsm.pl"

import poplib
import email
import email.Header
#import sms
import smtplib
import re

class Fwd:

    def __init__(self,address,subject):

        mesg = "From: %s\nTo: %s\nSubject: %s\n\n" % (address, setup.notify.recipient, subject)
        server = smtplib.SMTP('smsb.plusgsm.pl')
        if setup.verbose:
            server.set_debuglevel(1)
        server.sendmail (address, setup.notify.recipient, mesg)
        server.quit()

class Mailbox:

    def __init__(self):

        self.server = poplib.POP3_SSL('pop.gmail.com', 995)
        self.server.user(setup.google.login)
        self.server.pass_(setup.google.password)
        self.server.set_debuglevel(0)

        # fetch
        count = self.server.stat()[0]
        if setup.verbose:
                print count
        if count:
            for i in range(1,count+1):
                (header, msg, octets) = self.server.retr (i)
                mail = email.message_from_string('\n'.join(msg))
                address = email.Header.decode_header(mail['From'])[1]
                if address[1]:
                    address = address[0].decode(address[1],'ignore')
                else:
                    address = address[0]
                address = re.sub('>\s*$','',re.sub('^\s*< ','',address))
                subject = email.Header.decode_header(mail['Subject'])[0]
                if subject[1]:
                    subject = subject[0].decode(subject[1],'ignore')
                else:
                    subject = subject[0]
                #print subject
                #sms.parse(subject.split())
                Fwd(address,subject)
                #self.server.dele (i)

        # commit
        self.server.quit()

mbox = Mailbox()

Aby te powiadomienia działały, na koncie shellowym umieszczamy skrypt, nadajemy mu prawa do uruchomienia, przechodzimy do edycji crona:

chmod +x pop2smsV2.py
crontab -e

dodajemy zadanie do crona, aby był uruchamiany co minutę:

* * * * * /home/NAZWA/bin/pop2smsV2.py >/dev/null 2>&1

Podsumowanie

Teraz to rozwiązanie najbardziej przypomina mi to które miałem w Erze. Trzeba niestety użyć do tego shella z cronem. OVH odpada z dwóch powodów – brak crona oraz brak dostępu do sieci z poziomu shella. Może i by się dało uruchomić to jako CGI z ich webowym cronem, ale niestety mija się to trochę z celem, ponieważ cron OVH może być uruchamiany najczęściej co godzinę.

Przewagą tego rozwiązania nad powiadomieniem z Ery jest brak limitu skrzynki 12 MB. Limitem jest ponad 7 GB co nie stanowi już problemu. Jeśli nie chcemy zostawiać na skrzynce śladu – możemy usunąć komentarz z linii self.server.dele (i) – co spowoduje kasowanie przesłanych jako SMS wiadomości.

Zaletą obsługi powiadomień za pomocą crona jest to, że możemy ustalić sztywne godziny w których skrypt ma działać – możemy ustawić, że powiadomienia mogą przychodzić pomiędzy 8:00, a 16:00 oraz 18:00 – 22:00 (trzeba się wtedy liczyć z faktem, że o ósmej rano otrzymamy wszystkie SMS’y z nocy). Jeśli chcemy mieć powiadomienia aktywne w godzinach 8:30 – 22:30, robimy to następującymi regułami:

30-59   8 * * * /home/nme/sms-notify/pop2smsV2.py >/dev/null 2>&1
*    9-21 * * * /home/nme/sms-notify/pop2smsV2.py >/dev/null 2>&1
0-30   22 * * * /home/nme/sms-notify/pop2smsV2.py >/dev/null 2>&1

Dodatkowo – dzięki temu rozwiązaniu mamy adres do powiadomień, znacznie łatwiejszy do zapamiętania – bo w postaci np. powiadomienia@nasza-domena.pl :)

Podgląd strony do pliku jpg?

Poruszenie w Internecie tematyką wprowadzania cenzury, o której ostatnio pisałem, podsunęło mi pewien pomysł, który mam nadzieję niebawem Wam przedstawię. W między czasie uznałem, że w projekcie przyda mi się funkcjonalność zautomatyzowanego robienia podglądów (thumbnail) stron WWW do obrazków. Miałem przy tym całkiem dużo frajdy i mam nadzieję, że efekty mojej pracy komuś się to przydadzą.

Serwisów które przygotowują podgląd stron jest w sieci niewiele, a takich, które za darmo udostępniają wygodne API, nie mają śmiesznie małych limitów przygotowań podglądów nie znalazłem. Napisałem sobie takie narzędzie sam. Jeśli ktoś chce wykorzystać ten kod – proszę bardzo – proszę tylko o informację gdzie jest on dostępny oraz liczę na darmowy dostęp ;)

Twoja własna mini-przeglądarka

Jak szybko zrobić sobie przeglądarkę z obsługą Flasha, JavaScriptu i tych wszystkich urozmaiceń? Wystarczyło mi kilkanaście minut :) Zdecydowałem się na użycie GTK (domyślnego zestawu kontrolek dla m.in. Gnome) oraz Webkit’a – silnika do renderowania wykorzystywanego w takich przeglądarkach jak Safari (Mac OS), Google Chrome (Windows) czy Midori (Linux):

#!/usr/bin/env python

url = 'http://www.nme.pl'
size = [800, 600]

import gtk,webkit
window = gtk.Window(gtk.WINDOW_TOPLEVEL)
window.set_default_size(size[0], size[1])
window.connect('destroy', gtk.main_quit)
preview = webkit.WebView()
preview.open(url)
scrolled = gtk.ScrolledWindow()
scrolled.add(preview)
window.add(scrolled)
window.show_all()
gtk.main()

Właściwy podgląd strony

Kod trzeba było trochę rozbudować. Konfiguracja jest w klasie conf. Uruchamiając kod w pierwszym parametrze należy podać adres strony (nie zapominając o nazwie protokołu, np. http://www.nme.pl).

w katalogu w którym znajduje się skrypt trzeba założyć podkatalog thumb, w którym zapisywane będą obrazki jpg.

shot.py:

#!/usr/bin/env python

__author__ = 'mw AT nme.pl'
__version__ = '1.0'
__date__ = 'wto, 25 sie 2009, 09:15:48 CEST'

import pygtk
pygtk.require('2.0')
import gtk,gtk.gdk,gobject
import webkit
import hashlib
import os,sys

class conf:
    show_scrollbars = True
    show_progress = True
    script_timeout_secs = 15
    size = [1024, 768]

class Snapshot:

    def load_progress_changed(self, view, progress):
        print '%d%%' % progress

    def load_finished(self, view, frame):
        print 'loading finished'
        w = gtk.gdk.get_default_root_window()
        wp = self.window.get_position()
        ws = self.window.get_size()
        sz = w.get_size()
        pb = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB,False,8,ws[0],ws[1])
        pb = pb.get_from_drawable(w,w.get_colormap(),wp[0],wp[1],0,0,ws[0],ws[1])
        if (pb != None):
            pb.save('thumbs/'+self.file+'.jpg','jpeg',{'quality':'95'})
        gtk.main_quit()

    def __init__(self, url=None):

        self.url = url
        self.file = hashlib.sha512(url).hexdigest()
        self.overtime = False
        self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
        self.window.set_default_size(conf.size[0], conf.size[1])
        self.window.set_decorated(False)
        self.window.connect('destroy', gtk.main_quit)
        self.preview = webkit.WebView()
        self.preview.open(self.url)
        if conf.show_progress:
            self.preview.connect('load-progress-changed',self.load_progress_changed)
        self.preview.connect('load-finished',self.load_finished)
        self.scrolled = gtk.ScrolledWindow()
        if conf.show_scrollbars:
            self.scrolled.props.hscrollbar_policy = gtk.POLICY_AUTOMATIC
            self.scrolled.props.vscrollbar_policy = gtk.POLICY_AUTOMATIC
        else:
            self.scrolled.props.hscrollbar_policy = gtk.POLICY_NEVER
            self.scrolled.props.vscrollbar_policy = gtk.POLICY_NEVER
        self.scrolled.add(self.preview)
        self.window.add(self.scrolled)
        self.window.show_all()
        gobject.timeout_add(conf.script_timeout_secs*1000, self.timeout)
        gtk.main()

    def timeout(self):
        gtk.main_quit()
        sys.exit(1)

if len(sys.argv)==2:
    Snapshot(url=sys.argv[1])
else:
    print >> sys.stderr, "Usage: %s url\n" % sys.argv[0]
    sys.exit(1)

Wrapper do generowania właściwego poglądu

Skrypt działa, ale wymaga działania X’ów, coś nam jeszcze wyświetla i zapisuje obrazek w skali 1:1. Da się to zrobić w tle. Potrzebujemy do tego dwóch narzędzi – convert z pakietu imagemagick oraz pakietu xvfbVirtual Framebuffer ‘fake’ X server. Mając już te narzędzia – całą robotę zrobi za nas skrypt o nazwie snap:

#!/bin/bash

state=0
plik=`python -c "import hashlib; print hashlib.sha512('$1').hexdigest()+'.jpg'"`
[ -e "thumbs/$plik" ] || {
	xvfb-run -a --server-args='-screen 0 1024x768x16' python shot.py $1
}
state=$?
[ "$state" -eq "0" ] && {
	[ -e "thumbs/$plik" ] && {
		convert -scale 310x -quality 75 thumbs/$plik thumbs/$plik
	} || {
		exit 1
	}
} || {
	exit 1
}

skryptowi nadajemy prawa do jego uruchamiania:

chmod +x snap

i uruchamiamy:

./snap http://www.nme.pl

w katalogu thumb odnajdujemy gotowego jpg’a (jego nazwa jest hashem sha512 URL’a). Wygląda to tak:

www.nme.pl-thumb

Powiązane, teksty w języku angielskim:

  1. Podobne rozwiązanie w oparciu o Qt, warto obejrzeć.
  2. Przykładowy opis jak można uruchomić serwis do generowania podglądów w oparciu o Django z nawiązaniem do gotowych już rozwiązań.

Oskryptowanie Gmaila

Google nie udostępniło do Gmaila tak wygodnego API jak dla Google Calendar, którego przykład zastosowania umieściłem w poprzednim wpisie. Zastanawiając się jednak chwilę – mamy przecież dostęp do protokołu POP, więc jakieś API się znajdzie ;)

Kontynuując myśl z poprzedniego tekstu – jeśli mam już gotowy skrypt który potrafi wysłać powiadomienie SMS o konkretnej treści, można zrobić skrypt, który jako SMS wyśle np. temat z maila. Et voila:

Wymagania: Python 2.4+, skrypt z sms.py z poprzedniego wpisu.

#!/usr/bin/env python

__author__ = 'mw AT nme.pl'
__version__ = '1.2'
__date__ = 'pią lip 31 13:17:45 CEST 2009'

class setup:
    class google:
        login = 'KONTO@GOOGLE'
        password = 'HASŁO'

import poplib
import email
import email.Header
import sms

class Mailbox:

    def __init__(self):

        self.server = poplib.POP3_SSL('pop.gmail.com', 995)
        self.server.user(setup.google.login)
        self.server.pass_(setup.google.password)
        self.server.set_debuglevel(0)

        # fetch
        count = self.server.stat()[0]
        print count
        if count:
            for i in range(1,count+1):
                (header, msg, octets) = self.server.retr (i)
                mail = email.message_from_string('\n'.join(msg))
                subject = email.Header.decode_header(mail['Subject'])[0]
                if subject[1]:
                    subject = subject[0].decode(subject[1],'ignore')
                else:
                    subject = subject[0]
                #print subject
                sms.parse(subject.split())
                self.server.dele (i)

        # commit
        self.server.quit()

mbox = Mailbox()

Bramka SMS od Google

Oficjalnie Google takiej usługi przynajmniej w Polsce nie udostępnia. Można to jednak osiągnąć w sposób pośredni, który chciałbym dzisiaj przedstawić. Jest bardzo prawdopodobne, że już niebawem, stosowanie takich rozwiązań będzie zbędne, bo ceny usług transmisji danych tanieją, a telefony swoją funkcjonalnością przypominają coraz bardziej PDA.

Spośród aplikacji Google, powiadomienia SMS na tą chwilę możemy otrzymywać jedynie Google Calendar. Dzięki bardzo dobrej polityce tej firmy, do większości ich aplikacji dostępne jest API, które sprawia, że ich integracja ze swoimi rozwiązaniami jest znacznie prostsza. Calendar API jest świetnie udokumentowane, wystarczyła chwila i narzędzie do wysyłania SMS’ów było gotowe.

Do pełni szczęścia potrzebne jest konto w Google, a na nim Gmail i kalendarz. Posiadając własną domenę oraz uruchomione Google Apps, można założyć sobie dedykowane konto np. powiadomienia@domena.pl co jest znacznie elastyczniejszym rozwiązaniem.

Wymagania: Python 2.4+, Python Client Library

#!/usr/bin/python

__author__ = 'mw AT nme.pl'
__version__ = '1.1'
__date__ = 'pią lip 31 10:42:26 CEST 2009'

class setup:
    class google:
        login = 'KONTO@GOOGLE'
        password = 'HASŁO'
    retries = 3

from elementtree import ElementTree
import gdata.calendar.service
import gdata.service
import atom.service
import gdata.calendar
import atom
import getopt
import sys
import string
import time

class CalendarNotify:

    def __init__(self):

        self.cal_client = gdata.calendar.service.CalendarService()
        self.cal_client.email = setup.google.login
        self.cal_client.password = setup.google.password
        self.cal_client.source = 'api'
        self.cal_client.ProgrammaticLogin()

    def reminder(self,event,minutes):

        for a_when in event.when:
            if len(a_when.reminder) > 0:
                a_when.reminder[0].minutes = minutes
            else:
                a_when.reminder.append(gdata.calendar.Reminder(minutes=minutes))
        self.cal_client.UpdateEvent(event.GetEditLink().href, event)

    def event(self,title,where=None):

        try:
            # add event
            event = gdata.calendar.CalendarEventEntry()
            event.title = atom.Title(text=title)
            #event.content = atom.Content(text=content)
            if where:
                event.where.append(gdata.calendar.Where(value_string=where))
            start_time = time.strftime('%Y-%m-%dT%H:%M:%S.000Z', time.gmtime(time.time() + 180))
            end_time = time.strftime('%Y-%m-%dT%H:%M:%S.000Z', time.gmtime(time.time() + 180 + 60))
            event.when.append(gdata.calendar.When(start_time=start_time, end_time=end_time))
            new_event = self.cal_client.InsertEvent(event, '/calendar/feeds/default/private/full')
            # add reminder
            self.reminder(new_event,1)
        except:
            return False
        return True

def parse(args):

    where = None
    if len(args):
        if args[0][0] == '@':
            where = args.pop(0)

    count = 0
    while count < setup.retries:
        if cal.event(' '.join(args),where):
            break
        count += 1

try:
    cal = CalendarNotify()
except:
    print >>sys.stderr, 'Unable to login to Google Calendar!'
    exit(1)

if __name__ == '__main__':

    args = sys.argv
    name = args.pop(0)

    if not len(args):
        print >>sys.stderr, 'Usage: %s [@where] message\n' % name
        exit(1)

    parse(args)

Uruchamiając skrypt można podać opcjonalny parametr @lokalizacja, a następnie podajemy treść powiadomienia – przykładowa komenda:

./sms.py @firma awaria bazy danych