No, byłem. Co do konferencji jako takiej to wolę się nie wypowiadać, ale wnioskuję, że nie byłem jej targetem — był może jeden wykład, na którym dowiedziałem się czegoś nowego i jeden, który obejrzałem dla przyjemności. Aspekt społeczny w pewnym stopniu został zaspokojony przez bratanie się z kolegami z Allegro.pl. Infrastrukturę (dostęp do sieci, wyżywienie, zakwaterowanie, napoje, przekąski) pominę milczeniem. W końcu też w niedzielę dałem spicz i odniosłem wrażenie, że pomimo pewnych problemów technicznych się podobał. Czyli: pod względem merytorycznym niejaki sukces.
Dla uzupełnienia:
-
prezentacja do obejrzenia/ściągnięcia jest na SlideShare
-
kod w takiej wersji, jaka była prezentowana, jest na BitBucket
Jeżeli czas pozwoli, to skrobnę też jakiś artykuł na podstawie tego, co zostało powiedziane.
Megiteam.pl w swoich dokumentach pomocy proponuje jeden sposób na oddzielenie swojego środowiska Pythona od systemowego, a także na ustalenie wersji Pythona, z jaką ma być uruchamiana aplikacja. Metodą prób i błędów doszedłem do innego, jak mi się wydaje także lepszego, sposobu na osiągnięcie tego celu.
Virtualenv na pomoc
Virtualenv to obecnie najpopularniejszy sposób na wirtualizację środowiska uruchomieniowego aplikacji. Mocno korciło mnie, by go wykorzystać do oddzielenia środowiska aplikacji od Pythona dostarczanego przez Megiteam.pl i w końcu się udało. Potrzebny będzie do tego plik virtualenv.py z dystrybucji źródłowej virtualenv. Kopiujemy go sobie w jakieś poręczne miejsce, np. do ~/.. Potem już idzie normalnie:
~$ mkdir v
~$ python2.5 virtualenv.py v/chrzczone
New python executable in v/chrzczone/bin/python2.5
Also creating executable in v/chrzczone/bin/python
Installing setuptools................done.
~$ cd v/chrzczone/
~/v/chrzczone$ bin/easy_install -U -Z psycopg2 \
Django pytz Babel BabelDjango \
django-registration django-tagging-ng \
django-pagination
Po kilku minutach mamy już swoje środowisko w katalogu $HOME/v/chrzczone. Pozostaje tylko ustawić aplikację tak, żeby wiedziała kto tu rządzi. Do tego służy plik .environment w katalogu aplikacji. Oto jak on wygląda w mojej przykładowej aplikacji o nazwie "chrzczone":
PATH=$HOME/v/chrzczone/bin:/usr/local/python2.5/bin:$PATH
PYTHONPATH=$HOME/v/chrzczone/lib/python2.5/site-packages:/usr/local/python2.5/lib/python2.5/site-packages
Wszystko w tym momencie powinno działać, ponieważ pliki .environment są dołączane kaskadowo i zmienne zdefiniowane w nich nadpisują zdefiniowane wcześniej. Ale jest również lepsza (mniej podatna na błędy) metoda osiągnięcia tego samego — żeby wszystko zagrało wystarczy, że plik .environment będzie zawierał tylko jedną linijkę:
source $HOME/v/chrzczone/bin/activate
(dzięki, Eluś, masz u mnie piwo w Ustroniu).
Ale po co to wszystko?!
A choćby po to, żeby łatwiej można było tym zarządzać, np. przy robieniu deploymentu przy użyciu Pavera. Virtualenv obecnie staje się standardową metodą zapewniania rozdzielności środowisk uruchomieniowych Pythona, dającym tyle izolacji, ile się chce. Jest wygodne i bardzo upraszcza obsługę takiego zwirtualizowanego środowiska, zwalniając z konieczności ręcznego ustawiania zmiennych środowiskowych — wystarczy zaimportować w powłoce skrypt aktywujący środowisko i wszystko jest na swoim miejscu. A jeżeli używa się virtualenvwrapper to rzeczy proste stają się jeszcze prostsze (workon, mmm...).
Uwaga, to już nieaktualne!
Będę miał 2 wolne miejsca w samochodzie z Wołomina/Warszawy do Ustronia na wyjazd na PyCon PL 2009, a o ile nie rozbiję się w drodze na konferencję to także z powrotem. W związku z tym, że jest to Escort kombi, to ta propozycja raczej nie jest skierowana do koszykarzy, chyba że mają odpinane nogi... Szczegółów organizacyjnych jeszcze nie mam, ale najprawdopodobniej wyjazd z Warszawy będzie w piątek wcześnie rano (około 7), a w drogę powrotną będę wyruszał w niedzielę tuż po śniadaniu (10?).
Kontakt Jabberem lub mailem na jarek.zgoda na GMailu. Jak sądzę, na stronie samochodowej swatki (dostępna dla potwierdzonych uczestników konferencji) także będzie możliwe skontaktowanie się ze mną, ogłoszenie tam umieszczę jak tylko organizatorzy potwierdzą mój udział.
Ponownie brudzę sobie ręce w aplikacji zrobionej przy użyciu alternative stack (Werkzeug + Jinja2 + SQLAlchemy + WTForms), trochę przy okazji przygotowań do mojego nadchodzącego wystąpienia na PyconPL 2009. I ponownie wszystko szło gładko, dopóki nie musiałem zabrać do autentykacji uwierzytelniana (nie autoryzacji).
Krąży po mieście plotka, że najlepiej do tego użyć AuthKit lub repoze.who. Jak w każdej plotce, w tej także może być ziarno prawdy, więc kilka dni temu postanowiłem to sprawdzić. Już początki nie były zachęcające (każda z tych bibliotek doinstalowuje w zależnościach połowę Pylons), a potem było jeszcze gorzej.
AuthKit okazał się tak mocno związany z Pylons, że nawet nie ma własnej dokumentacji, tylko jakieś rozdziały w Pylons Book. Jedyne, co nadawało się do użycia bez Pylons, to kilka przykładów w repozytorium. Spróbowałem pokonać niechęć i zapoznać się bliżej z tą biblioteką, ale dałem za wygraną po godzinie zastanawiania się, jak do tego bydlaka podejść. Może jakoś niedługo przyjdzie mi do głowy idea, jak mógłbym z tego skorzystać bez Pylons (a może ktoś to opisze...).
Dokumentacja do repoze.who okazała się dużo lepsza, ale tym razem przerosła mnie idea tej biblioteki. Przeczytałem wprowadzenie i zagotowało mi się pod kopułą — jak to w ogóle działa? I najważniejsze, jak to podłączyć do istniejącego kodu? Jestem pewien, że dokumentacja dokładnie opisuje każdy aspekt działania tej biblioteki, ale ja tego zwyczajnie nie zrozumiałem. Może dlatego, że przez cały czas przed oczami miałem mój use case, który obejmuje jedynie mały fragmencik tego, do czego ta biblioteka została stworzona.
Na dzień 10 lipca 2009 roku konkluzja jest taka, że albo przysiądę fałdów i spróbuję użyć jednej z tych dwóch bibliotek, albo pójdę na skróty i użyję RPX (czy czegoś w tym stylu). Co biorąc pod uwagę obecny trend w branży nie wydaje się być takim głupim rozwiązaniem. Niestety, pomijając całkowite niedopasowanie tego rozwiązania do docelowej grupy użytkowników mojej aplikacji...
Jeżeli ktoś jeszcze się zastanawia, czy wybrać się na PyconPL do Ustronia w październiku, to może przekonam go tym, że będę tam trzymał spicz na temat przygód Adama Słodowego w krainie ramówek webowych — rozłożę na czynniki pierwsze jakąś ramówkę (pewnie będzie to najbliższe mi Django) i spróbuję złożyć coś podobnego używając zamienników.
Dawno nie dawałem żadnego występu publicznego (od ostatniego WarPY w październiku minie 2 lata...), więc trzeba przygotować lepszy show. :)
Wiele było już głosów, że setuptools jest pełne błędów i nie powinno być używane. W mojej praktyce na te błędy nie trafiałem, więc poprzestawałem na niechęci, a drobne wpadki powodowały jedynie wzrost przychylności wobec antagonistów setuptools. Tym razem w ciągu ostatnich dni zostałem doświadczony dwukrotnie przez poważne niedoróbki w setuptools, w tym jedna ma znaczenie krytyczne (nie instaluje się część zawartości pakietu):
Setuptools musi odejść!
Nie nie chodzi o żadne feedy, tylko o tzw. resident set aplikacji w Django. Obserwuję tę wartość od dłuższego czasu dla tej małej aplikacyjki i z pewnym niepokojem obserwuję jak rośnie, od ~15MB pod koniec 2007 roku przez ~17MB w okolicach wydania 1.0 do ~19MB z wersją 1.1-beta1 (przy wręcz zmniejszającym się feature secie aplikacji). A mój niepokój bierze się z mojej zadziwiająco dobrej pamięci, jak na mój podeszły wiek.
Kiedyś, w zamierzchłych czasach (okolice Slackware 9.0, czyli początek 2003 roku), była sobie fajna, mała przeglądarka WWW, która nazywała się Phoenix. Wyrosła z potrzeby istnienia po prostu przeglądarki i w swoich bebechach była Mozillą (tak się kiedyś nazywała przeglądarka wyrosła z Netscape Communicatora) bez wszystkiego tego, co nie służyło przeglądaniu stron WWW. Była szybka, miała małe wymagania i wszyscy ją pokochali od razu. Pokochali ją tak bardzo, że Mozilla Foundation postanowiła skupić na niej swoje wysiłki developerskie. W szybkim tempie (od wersji 0.4 Oceano z listopada 2002 do wersji 0.8 Royal Oak z lutego 2004) program zbliżył się apetytem na zasoby do swojego rodzica i przestał być postrzegany jako lekki i szybki. Po 7 latach istnienia Mozilla Firefox wciąż bohatersko zwalcza bloat, który był głównym powodem jego wypączkowania z projektu Mozilla Suite.
Na ile ostatnie develpmenta w Django przypominają to, co stało się z Phoeniksem? Na tyle, że nagle wszyscy pokochali Django i zaczęło ono obrastać w rzeczy, które już nie mieszczą się w legendarnych 80% (i django.contrib.gis to naprawdę mały pikuś w tym zestawie). Wygląda na to, że zadowalanie coraz większej rzeszy użytkowników daje w wyniku coraz większe zapotrzebowanie na zasoby...
Rekapitulując, ja też uważam, że martwię się na zapas. Sytuacja sama w sobie nie wygląda jeszcze na niepokojącą, niepokojący jedynie może być ten trend. Czy doprowadzi do tego, że Django przejdzie na ciemną stronę mocy, to się dopiero okaże.
Zmiany się szykują... Wspominałem o planach przepisania silnika tego bloga przy użyciu narzędziówki, ale im dłużej nad tym siedzę, tym bardziej jestem przekonany, że nie ma to wielkiego sensu — zbyt wiele rzeczy wymagałoby ręcznej ingerencji, patchowania komponentów i rwania włosów z głowy, bo coś nie działa, jak na przykład odkrycie z wczoraj:
-
nie działa Beaker z powodu błędu w kodzie backendów sesyjnych wykorzystujących bazę danych i memcached (pliki działają OK, ale ich nie chcę);
-
w związku z tym, że nie działa Beaker, nie działa też AuthKit, więc nici z ułatwień autentykacji (np. po OpenID).
Oczywiście, byłbym w stanie naprawić Beaker'a, powstaje jedynie pytanie, czy to się w ogóle opłaca? Alternatywą jest przejrzenie i odchudzenie istniejącej aplikacji w Django, a przy okazji zoptymalizowanie jej nieco, wykorzystując doświadczenie, jakiego nabrałem w ciągu tych kilkunastu miesięcy. To też mogłoby być nienajgorsze rozwiązanie, a na pewno szybsze. Będę żałował Jinja2, ale jak mnie żal przyciśnie, to podłączę sobie ten silnik do Django, żeby stonować trochę ten żal...
Poważnie się zastanawiam i jeszcze nie podjąłem żadnej decyzji — na razie kod wykorzystujący narzędziówkę poszedł do oddzielnego brancha w repozytorium, a w trunku pojawił się kod wersji działającej obecnie, ale nie ma to jakiegoś symbolicznego znaczenia (tak sobie to tłumaczę). Uch, jak ja lubię mieć takie dylematy... :)
Jestem po kilku tygodniach dłubania aplikacji webowej przy użyciu zestawu narzędzi (Werkzeug, SQLAlchemy lub datastore, Jinja2, WTForms) zamiast Django. Zawsze uważałem Django za kawał świetnego softu, ale dopiero teraz doceniam to, na ile ramówka aplikacyjna ułatwia życie programisty, załatwiając masę rzeczy z kategorii boilerplate przy użyciu kawałka swojej magii.
Narzędziówka jest pozbawiona zupełnie magii. To surowy zestaw narzędzi (niektóre z nich, jak SQLAlchemy mają własną magię wewnętrzną do załatwiania swoich spraw) i żeby zagrały ze sobą w zgodnej orkiestrze i razem umożliwiły napisanie dobrego kodu aplikacji, trzeba napisać sporo rzeczy: procesory żądań (w Django nazywa się to middleware), procesory kontekstu, podsystem konfiguracyjny, skróty ułatwiające życie (jak render_to_response() czy funkcja do odwracania URL-i), podsystem autoryzacji. W Django to wszystko już jest, gotowe i zainstalowane, a użycie jest tylko kwestią podłączenia w konfiguracji lub nawet zaimportowania odpowiedniego modułu — i za sprawą magicznej różdżki masz sitemapę, masz feed RSS/Atom, masz bufor stron i masę innych drobiazgów, na napisanie których poświęciłbyś trochę czasu, bo trzeba to mieć, pomimo tego, że te wszystkie rzeczy nie są corem aplikacji. Łatwo jest to wszystko docenić: działające zręby bloga w Django (tego bloga) miałem po około 2 godzinach roboty (wpisy, etykiety, archiwum, feed z wpisów i sitemapa), a powtórzenie tego samego wyczynu z użyciem zestawu narzędzi zajęło mi ponad 6 godzin pracy, z czego co najmniej 3 spędziłem na użeraniu się z duperelami. Warto było, bo moja wiedza na temat działania aplikacji w środowisku WSGI jest teraz o wiele większa. I lepiej rozumiem bebechy Django.
Z drugiej strony nie można powiedzieć, żeby ta wyprawa miała jedynie edukacyjną wartość — dorobiłem się sporej ilości kodu narzędziowego, który może mi się kiedyś przydać. Zobaczyłem też, jak wydajna i oszczędna w wykorzystaniu zasobów może być mała aplikacja, jeżeli się ją rozsądnie napisze, dla porównania aplikacja tego bloga żre na megiteam.pl ~20MB pamięci rezydentnej, a moja reimplementacja przy użyciu zestawu narzędzi jedynie ~12MB — różnica jest kolosalna (nie wspominam już o tym, że jest o wiele szybsza, prawdopodobnie dzięki Jinja2). Przyszłością tego bloga jest właśnie reimplementacja przy użyciu zestawu narzędzi, głównie w celach oszczędnościowych.
Napisawszy tyle wypadałoby przejść do jakiejś konkluzji... Jak dla mnie to każdy kto robi w webie przy użyciu pythonowych ramówek (nie tylko Django, to się tyczy również Pylons i TurboGears w takim samym stopniu), powinien zrobić sobie taką krótką wycieczkę do źródeł. Będzie mógł wtedy dostrzec zarówno mocne jak i słabe strony używanej przez siebie ramówki, a przede wszystkim zrozumie, jak jej wszystkie komponenty współgrają ze sobą, tworząc całość, która tak bardzo ułatwia pracę.
A to dlatego, że moje przemyślenia na temat technologii przeniosłem do technologicznej tuby tego serwisu, którą w ramach edukacji implementuję na Google AppEngine. Aplikacja, choć wciąż w trakcie rozwoju, powoli zyskuje kolejne funkcje (a ja przy okazji uczę się nowych rzeczy).
Dzisiejsze ogłoszenie zasad płatności za Google AppEngine wywołało burzę w małej szklaneczce, jaką jest środowisko ludzi robiących aplikacje na AppEngine:
-
część (wygląda mi to na większość) cieszy się z tego, że może płacić, w tym także z tego, że za 3 miesiące będzie płacić za to, co do tej pory miała za darmo;
-
część (mniejszość) nazywa to po imieniu: vendor lock-in (najpierw skuś, potem zmień zasady i zmuś do płacenia za to, co do tej pory było za darmo).
Wyliczenia przeprowadzane tu i ówdzie (głównie na liście AppEngine) wskazują, że nowe limity będą odczuwalne przez wszystkie co najmniej przeciętnie popularne serwisy, a koszt może być znaczący.
Wysiłek włożony w zrobienie aplikacji, którą można wyjąć z GAE i włożyć na normalny serwer może się opłacić. A już na pewno można przestać myśleć o przenoszeniu aplikacji z normalnej platformy na GAE — to się po prostu nie opłaci.
Aplikacja, która jest moją piaskownicą na Google AppEngine z upływem czasu rozrosła się trochę (procesory kontekstu, middleware, takie tam...) i okazało się, że każdy request w przeglądarce logów świeci na żółto, to znaczy że według Google jego obsługa zjadła nadmierną ilość zasobów i powinien zostać w jakiś sposób zoptymalizowany. No i faktycznie, powinien — obsługa każdego z żądań do aplikacji zjadała ~1200 ms CPU (łącznie kod + storage). Coś było ewidentnie nie halo, więc musiałem podjąć pewne kroki zaradcze:
-
użycie googlowego cache gdzie się tylko da (porzucając tradycyjne pojęcie o tym, gdzie ma to sens);
-
optymalizacja sposobu dostępu do danych w datastore (wybieranie encji używając klucza/kluczy, a nie budując Query);
-
jak dla mnie najważniejsze: optymalizacja importów.
Zanim przejdę do omówienia poszczególnych optymalizacji... Warto było nad tym popracować, bo średni czas obsługi żądania spadł do ~120 ms CPU (z grubsza: 10x wzrost wydajności). Pomimo tego, co ja uznałem za najważniejsze, to nie optymalizacja importów dała największy zysk, a zmniejszenie ilości odczytów z datastore (dzięki użyciu memcache i wybierania danych przy użyciu kluczy). Co uważam za dobre w tym wszystkim to to, że AppEngine wymusza przemyślane zaplanowanie aplikacji i optymalizację każdego aspektu działania kodu (i to od samego początku). To nie jest zabawka dla niecierpliwych chłopców-pehapowców. ;)
Memcache gdzie się da
Memcache API było jedną z pierwszych rzeczy, jakie Google dodało do AppEngine po jego uruchomieniu w kwietniu 2008 roku. Jego użycie w aplikacji jest nie tyle optymalizacją, co po prostu koniecznością (szczególnie w świetle zapowiedzianego na koniec maja 2009 obniżenia limitów). O ile w zwykłych aplikacjach w cache umieszcza się rzeczy, które są albo kosztowne do wyliczenia, albo niemal statyczne, o tyle na AppEngine buforować trzeba niemal wszystko, bo pomimo twierdzeń googlarzy, że odczyty z datastore są tanie, to ta taniość jest względna (chyba względem kosztu zapisów) — a koszt obsługi żądania jest liczony jako suma kosztu wykonania kodu jako całości, wraz z kosztem pobrania danych (w limitach te wartości są liczone oddzielnie).
Na co zwrócić uwagę na początku? Na drobne rzeczy: profil użytkownika, listy ostatnio dodanych/popularnych obiektów, to, co pojawia się w kontekście w wyniku działania procesorów lub jest dodawane do obiektu request przez middleware. Po tych rzeczach można zająć się resztą, czyli każdą instancją modelu (i każdą wyliczoną wartością), która pojawia się w aplikacji. Czasem trzeba będzie podjąć decyzję, czy warto aktualizować pokazywane dane w czasie rzeczywistym, czy może da się przełknąć mały poślizg rzędu 15 minut... Bo obiekty wyjęte z cache nie zawsze zachowują się tak, jak byśmy tego oczekiwali (przynajmniej na razie).
Dostęp do danych
Tym, co bywa najtrudniejsze do przełknięcia przy robieniu aplikacji na AppEngine jest zupełnie inny model storage — nierelacyjny, bez złączeń i nastawiony na zupełnie inne użycie, niż bazy danych ogólnego stosowania, jak MySQL czy PostgreSQL (nie ujmując nic relacyjnym bazom danych). Oczywiście, można udawać, że się tego nie zauważa i próbować symulować relacyjność, ale efektem tego będzie obniżona wydajność. Z tego co zauważyłem w różnych artykułach i podpowiedziach tu i ówdzie, najważniejsze podpowiedzi można streścić w kilku punktach:
-
storage świetnie sprawdza się jako wielka tablica asocjacyjna, dostęp do danych przy użyciu kluczy jest najbardziej wydajny;
-
klucze są jedynymi unikalnymi atrybutami obiektów, można to wykorzystać do kilku celów;
-
przechowywanie listy kluczy (np. w atrybucie typu
db.ListProperty) jest równie wygodne jak złączenie wiele-do-wiele, a w większości wypadków wygodniejsze;
-
atrybuty indeksowane (
db.StringProperty) są bardziej kosztowne w aktualizacji niż nieindeksowane (db.TextProperty), warto wziąć to pod uwagę przy projektowaniu modelu.
Uwaga na importy
Czym się różni kod uruchamiany na AppEngine od kodu uruchamianego w zwykłym środowisku Pythona? Niczym, oprócz tego, że proces, który obsługuje żądanie żyje dokładnie tyle, ile trwa obsługa żądania. A to oznacza, że wszystkie moduły konieczne do wykonania kodu przy każdym żądaniu muszą zostać zaimportowane, co jest sporym obciążeniem. Aby trochę poprawić sytuację, AppEngine buforuje zaimportowane moduły przez jakiś czas (liczony raczej w sekundach niż w godzinach). Nie są buforowane moduły, które zostały zaimportowane przy użyciu funkcji __import__(), co oznacza tylko jedno: wszystkie wynalazki typu klass = import_string('mypackage.mymodule.MyClass') trzeba odłożyć na półkę. Nie zawsze da się tego całkiem uniknąć, ale w takich przypadkach trzeba przygotować swój kod na to, że może otrzymać obiekt wykonywalny lub ciąg znaków i zminimalizować ilość miejsc, gdzie wykonywany jest niebuforowany import.
Dużo tego, ale nikt nie mówił, że będzie lekko. :)
Google zapowiedziało, że za trzy miesiące zmniejszy darmowe limity na AppEngine, a od 24 lutego można sobie zwiększyć limity, dopłacając (drobne bo drobne, ale zawsze) parę dolarków. Moje aplikacje są w takim stadium, że tak naprawdę mnie to nie będzie dotyczyło, ale zmniejszenie limitów przy jednoczesnym wprowadzeniu opłat jak dla mnie coś oznacza — Google przygotowuje się na przetrwanie kryzysu w IT ograniczając wydatki. Może to być także oznaką tego, że uznali AppEngine za produkt na tyle dojrzały, że można za niego brać pieniądze (choć według mnie jeszcze sporo do tego brakuje). Redukcja limitów jest spora, bo w przypadku ilości przesłanych danych to jest 90% (z 10GB do 1GB/24h), a w przypadku obciążenia CPU 86% (z 46 godzin do 6.5 godziny/24h). Na blog czy inny maleńki serwis to może i wystarczy, ale nie daj Boże żeby ktoś się tym poważnie zainteresował, bo limit się wyczerpie w pół godziny. Biorąc pod uwagę dodatkowo fakt, że Google nie powiadamia o zbliżającym się wyczerpaniu limitów (np. po przekroczeniu 80% któregokolwiek z nich), można się nagle obudzić z ręką w nocniku. Nie zmienia to oczywiście faktu, że tą platformą nadal warto się interesować i nawet w wersji płatnej wciąż jest ciekawą propozycją do budowania aplikacji, choć już nie tak atrakcyjną.
Tak czy inaczej, chwilowo nie zamierzam się tym przejmować, choć trudno przewidzieć, jak sytuacja będzie się przedstawiała za trzy miesiące. Na razie staram się robić moje aplikacje w ten sposób, żeby przeniesienie ich na normalny hosting nie wymagało przepisywania całości (co kosztuje więcej zachodu, niż mogłoby się wydawać, ale o tym innym razem).
Obiecałem, że będzie o procesorach kontekstu jak w Django (a może nawet trochę lepszych), więc proszę.
Procesory kontekstu
Procesor kontekstu wg. Django to kod wykonywalny, który przyjmując obiekt request zwraca słownik, który następnie jest dodawany do kontekstu przed renderowaniem szablonu. W ten sposób można zapewnić sobie dostępność pewnych danych w kontekście bez potrzeby pamiętania o tym, by te dane tam umieścić. W Django procesory kontekstu są wywoływane wtedy, gdy używa się klasy RequestContext. Pół biedy, gdy chodzi o mój kod — już ja o to zadbam, żeby tam był użyty RequestContext, ale problemem może być kod, którego autor o tym nie pomyślał.
Używając Jinja2 i Werkzeug nie możemy liczyć na tak silną integrację systemu szablonów z modelem żądania i odpowiedzi (założenia obydwu komponentów właśnie taką integrację wykluczają). Na szczęście zarówno Jinja2 jak i Werkzeug dostarczają nam wszystkiego czego potrzeba, żeby zaimplementować sobie taką funkcjonalność. Niestety, nie będzie tak prosto jak w przypadku middleware.
Werkzeug posiada coś, co jest nazywane local proxy — jest to wariant thread local storage, specjalnie przystosowany do potrzeb aplikacji webowych. Zazwyczaj umieszcza się w nim kod aplikacji WSGI (klasy lub funkcji), ale nie ma przeszkód, żeby umieścić tam cokolwiek innego, np. obiekt request:
def __call__(self, environ, start_response):
local.application = self
local.request = request = Request(environ)
Tak umieszczony obiekt request będzie następnie dostępny w dowolnej innej części aplikacji w trakcie jej czasu życia (czyli przetwarzania żądania i produkowania odpowiedzi). Teraz w okolicy mojego kodu renderującego szablon mogę zrobić taki myk:
def build_globals():
jinja_globals = {}
for item in getattr(settings, 'CONTEXT_PROCESSORS', []):
try:
processor = import_string(item)
jinja_globals.update(processor(local.request))
except (ImportError, AttributeError):
pass
return jinja_globals
jinja_env = Environment(loader=FileSystemLoader(settings.TEMPLATE_DIRS))
jinja_env.globals.update(build_globals())
Chodzi o to, że Jinja2 posiada tzw. globalne zasoby — można je zmodyfikować po utworzeniu środowiska a przed wyrenderowaniem pierwszego szablonu. Od teraz w każdym szablonie będzie dostępne to, co znajduje się w moim słowniku jinja_env.globals. A co się tam może znaleźć? Na przykład to:
def messages(request):
messages = request.environ['beaker.session'].get('messages', [])
request.environ['beaker.session']['messages'] = []
request.environ['beaker.session'].save()
return {
'MESSAGES': messages,
}
Czemu jest to lepsze rozwiązanie niż w Django? Ponieważ nie trzeba pamiętać o tym, żeby kontekst był obiektem klasy RequestContext. Dzięki buforowaniu środowiska przez Jinja2, narzut jest minimalny, a prostota rozwiązania kusząca. Czyżby więc było to rozwiązanie idealne? W moim przypadku nie. Trzeba bardzo uważać na to, by zaimportować ten kod w odpowiednim momencie (dla uproszczenia umieściłem go w przestrzeni globalnej). Ale działa i robi dokładnie to, co chciałem.
Ten kod nie powstałby, gdyby nie Ali Afshar (tak, ten od PIDA), który podał rozwiązanie tego problemu na StackOverflow.
Żartuję, Django jest na tyle dobre, żeby nie musieć go robić samemu. Chodzi mi raczej o sytuację, kiedy chce się mieć tyle Django, ile trzeba i ani trochę więcej (na przykład dlatego, że z całego oryginalnego Django wykorzystuje się tylko parę komponentów, bo na więcej nie pozwala AppEngine). Z pomocą przychodzi Werkzeug, Beaker i Jinja2 i parę innych bibliotek. Oprócz oczywistego zysku, jakim jest odchudzenie narzędziówki o kilka cennych setek plików, można zyskać bezcenną wiedzę, jak działa Django (im więcej wiem, tym większym podziwem darzę core devs, bo Django okazuje się jeszcze lepsze, niż wygląda na pierwszy rzut oka).
W paru kolejnych odcinkach opiszę rzeczy, które uznawałem za bardzo użyteczne w Django i zaimplementowałem sobie jako uzupełnienie narzędziówki (inna rzecz, że niełatwo jest się wyrzec niektórych przyzwyczajeń).
Middleware
Nie chodzi mi o middleware w rozumieniu WSGI, lecz takie, jak w Django — metody zwykłych klas, które są wywoływane z ustalonymi argumentami w określonych momentach przetwarzania żądania i odpowiedzi. Niespodzianka, Werkzeug ma już coś takiego! Nazywa się to Processor i klasa, która znajduje się w module werkzeug.contrib.kickstart pokazuje, jak powinien wyglądać kompletny interfejs takiej klasy i sygnatury jej metod. Leniwi mogą sobie z tej klasy po prostu odziedziczyć...
Pozostaje jedna rzecz, czyli podłączenie tego cuda do naszej aplikacji WSGI. Poniżej fragment mojej metody __call__() z klasy reprezentującej aplikację WSGI. Podłączam w niej tylko jeden rodzaj middleware (są jeszcze 3 inne możliwe).
for item in getattr(settings, 'MIDDLEWARES', []):
try:
klass = import_string(item)
middleware = klass()
response = middleware.process_request(request)
except (ImportError, AttributeError):
pass
if response is not None:
break
if response is None:
local.url_adapter = adapter = url_map.bind_to_environ(environ)
try:
endpoint, values = adapter.match()
handler = import_string(endpoint)
response = handler(request, **values)
except HTTPException, e:
response = e
Inny sposób na podłączenie do kodu aplikacji WSGI można znaleźć na wiki Werkzeug — jak zwykle pełna wolność wyboru metody (oczywiście, działa dobrze w każdym przypadku).
W następnym odcinku
A w następnym odcinku okaże się, jak zrobić procesory kontekstu (context processors) takie jak w Django, a nawet lepsze. Tym razem w użyciu będzie zarówno Werkzeug jak i Jinja.
Od dziś na AppEngine zniesiono jeden z najbardziej denerwujących limitów (bo zazwyczaj nie można było go kontrolować) - limit tzw. High CPU requests (czyli pików CPU). Do tej pory limit wynosił 2 na minutę, co łatwo było przekroczyć szczególnie gdy występowały jakieś lokalne błędy, np. z dostępem do datastore. Podobne problemy występują okresowo na platformie Google i jak do tej pory dotyczą szczególnie żądań HTTP przychodzących z Europy.
Z innych udogodnień: dopuszczalny czas odpowiedzi wzrósł z 10 sekund do 30 i dopuszczony został rozmiar odpowiedzi i upload zasobów większych niż dotychczasowe 1MB (obecnie limit wynosi 10MB).
Idzie ku lepszemu, ale jak dla mnie to wciąż mało. :)
Od nowej wersji (wydanej właśnie dzisiaj) możliwe jest wreszcie używanie operatorów IN i =! w metodzie filter(), właściwa dokumentacja została zaktualizowana. Co prawda pod spodem wykonywane jest kilka zapytań do datastore (co wpływa na zużycie limitów), ale wygoda jest o wiele większa. Fajnie.
W końcu się przemogłem i postanowiłem w celach edukacyjnych przemóc się i spróbować zrobić jakąś aplikację na Google AppEngine. Ponieważ chciałem mieć coś więcej, niż tylko naukę odpalania aplikacji pod Django na nowym środowisku, wybrałem sobie narzędziówkę z kosmosu: Werkzeug, Jinja2 i WTForms. I jak do tej pory psioczyłem na AppEngine, to teraz spodobało mi się to środowisko. Co prawda pod względem dołączonych baterii moja nowa narzędziówka jest bardzo uboga w porównaniu do wszystkiego tego, co przychodzi z Django, to jednak dzięki temu zorientowałem się, jak dużo rzeczy z Django tak naprawdę jest mi zupełnie niepotrzebne. Okazało się też, jak prosto można zaimplementować te rzeczy, które są mi potrzebne lub ułatwiają życie.
Więcej na ten temat wkrótce.
Z początkiem roku wiele osób robi sobie plany, czym zamierza się zająć w nadchodzącym okresie, więc i ja sobie usiadłem. Zacząłem się zastanawiać, z czym też warto byłoby się bliżej zapoznać i - biorąc pod uwagę realne możliwości - też zmontowałem sobie taką listę:
Mało? Mało, ale raczej na więcej nie znajdę czasu, który mógłbym określić jako wolny — jedynym rozwiązaniem problemu brakującego czasu wydaje mi się użycie tego w sposób bardziej lub mniej potajemny w pracy, z Werkzeugiem powinno się udać. Jeżeli zostanie mi trochę czasu, to rezerwowym tradycyjnie jest JavaScript w zakresie wykraczającym poza jQuery + pluginy.
Recently I had to make a widget for Django admin to display movies (H.264 encoded, MPEG-4 and FLV). We already have a widget for displaying the movie inline within the admin change form, but I had to create a widget that opens separate entity (lightbox-like overlay or popup browser window). For a start I took the popup browser — this seemed easier because one of the requirements was that the movie player will not load with the page (I am not that good in javascript, mind you...). What it takes to have a widget that shows some content in separate browser window?
-
you can open window by manually appropriate javascript code, but I prefer to use some ready-made plugin for jQuery, like jquery-popupwindow;
-
you'll need Django view (and template) that displays actual page with embedded player;
-
to embed the player (which is Flash object) you'd need swfobject and the player itself, like JW Player (it plays both mp4 and flv movies — but watch out, it's free only for strictly non-commercial use);
-
a Django's forms widget that will have something that can be clicked to open new window.
Now for the fun part: actual code. The widget code is a mish-mash of my previous experiences in writing custom Django widgets:
class AdminPopupMovieWidget(forms.FileInput):
class Media:
js = (
'js/jquery.js',
'js/jquery.popupwindow.js',
)
def render(self, name, value, attrs=None):
output = []
if value and hasattr(value, 'url'):
width = getattr(value, 'width', 600)
height = getattr(value, 'height', 500)
ctx = {
'url': value.url,
'href': '%s?url=%s' % (reverse('movie-show'), value.url),
'class': 'popupwindow',
'rel': 'width:%(width)d,height:%(height)d' % locals(),
}
output.append(u'<a href="%(href)s" class="%(class)s" rel="%(rel)s">%(url)s</a>' % ctx)
output.append(u'<br />')
output.append(super(AdminPopupMovieWidget, self).render(name, value, attrs))
output.append('''<script type="text/javascript">
$(".popupwindow").popupwindow();
</script>''')
return mark_safe(u''.join(output))
The code above produces a widget that has a clickable link with the title of movie URL. The anchor is linked to the URL of the view that produces HTML to display, like the code below:
@require_GET
def view_movie(request):
movie_url = request.GET.get('url')
if not movie_url:
raise Http404
ctx = {
'url': movie_url,
}
return render_to_response('movie/view.html', ctx,
context_instance=RequestContext(request))
The only variable that is passed in the context is the URL of the movie, but you are free to include as much as you'd like. Of course, nothing forces you to use urls, you can call the view with a PK to the movie-holding object, but this will require changes to the above view code.
But that's not all. You have to register your model in Django admin application. If you just register it with default modelform, the instances will display themselves with default admin widget. Two more classes are required:
class MovieAdminModelForm(forms.ModelForm):
movie = forms.FileField(widget=widgets.AdminPopupMovieWidget)
class Meta:
model = Movie
class MovieAdmin(admin.ModelAdmin):
form = MovieAdminModelForm
Is this all? Yes, I think.
Dosłownie, od wydania Pythona 3.0 dzielą nas już tylko godziny.
To ja lecę wstawić szampana do lodówki.
Chętnie spotkałbym się z tymi programistami Delphi, którzy robią (zawodowo lub OMC zawodowo) w Pythonie. Właśnie nie dawno zobaczyłem posta Fiedzi na django-users, parę innych nazwisk też mi się wydaje znajomych...
Dlaczego właśnie Python? Często sam zadaję sobie to pytanie i próbuję dać autorytatywną odpowiedź, biorąc pod uwagę mnogość języków, które znam i których używałem (zawodowo i hobbistycznie) w ciągu moich ponad 10 lat praktyki. W ostatnich dniach pojawiło się kilka głosów, które dają pewne sugestie co do możliwej odpowiedzi:
Jestem coraz bliżej odpowiedzi na moje pytanie.
To uczucie nieporównywalne z niczym: budujesz aplikację przez kilka tygodni lub miesięcu i przez cały czas zastanawiasz się czy ktoś będzie tego używał?. W takiej niepewności trwasz dopóki nie zauważysz, że jednak ktoś zdecydował się jej użyć, a wtedy... No i wtedy właśnie przychodzi ten strzał adrenaliny, który mogę porównać tylko z paroma rzeczami (niestety, nie mogę napisać, co to za rzeczy, bo każda z nich jest albo szkodliwa albo zakazana przez prawo — tę stronę przecież czytują dzieci...). Dziś rano spojrzałem w panel administracyjny jednego z moich prywatnych serwisów i poczułem właśnie TO.
Pomimo trzymania aplikacji w niemal całkowitej tajemnicy do czasu ukończenia prac (wiedziała o niej tylko moja żona i Google, o ile ktoś wiedział czego ma szukać), ktoś zajrzał na wMiastoWzięci.pl i dodał wydarzenie (choćby z tego powodu warte uwiecznienia). Nie będę tego ukrywał dłużej, to mój serwis. Wciąż jeszcze jest szlifowany, wciąż nie wszystko w nim działa, ale jak widać komuś to nie przeszkadzało. Co nie działa:
-
zgłaszanie obiektów (wydarzenia, zdjęcia, recenzje) do moderacji
-
komentarze do wydarzeń i miejsc
-
przeglądanie wielu etykiet na raz
Dla spragnionych informacji technicznych: serwis działa na Django-trunk, baza to MySQL 5.0 (z backendem MyISAM), deployment na FastCGI/lighttpd.
A skoro już poszło w świat, to skończył się luźny czas developmentu w modelu langsam, langsam i trzeba w szybkim tempie dorobić brakujące funkcje serwisu. ;)
Pomimo tego, że robię w Django od paru lat, wciąż zdarzają mi się odkrycia na miarę archimedejskiej eureki. Dziś właśnie zauważyłem, że skrót render_to_response może przyjmować jako pierwszy argument nie tylko nazwę szablonu, ale także listę nazw szablonów. Dzięki temu wiekopomnemu odkryciu kod, który wyglądał dość marnie:
templates = [
'flagging/%s_flagging_form.html' % obj._meta.module_name,
'flagging/flagging_form.html',
]
template = loader.select_template(templates)
return HttpResponse(template.render(ctx))
teraz może wyglądać trochę ładniej:
templates = [
'flagging/%s_flagging_form.html' % obj._meta.module_name,
'flagging/flagging_form.html',
]
return render_to_response(templates, ctx)
Świadomie używam określeń ładnie i brzydko, bo chodzi tu jedynie o estetykę (obydwa warianty robią dokładnie to samo pod spodem).
Na swoje usprawiedliwienie mogę powiedzieć tylko tyle, że takie zachowanie render_to_response jest obecnie nieudokumentowane (ale zgłosiłem odpowiedniego patcha).
I'm pleased to announce my first (hopefully) reusable app for Django: django-confirmation. The idea for this app came from my personal need to handle confirming object's creation on one of my sites. I found few apps performing similar tasks, but both are targeting single classes of objects, while I needed more generic approach (I hate this word...) — there are several classes of objects that need to be confirmed separately but using the same mechanism.
The use case for this app is as follows:
-
non-registered/not-logged-in user creates an object;
-
application sends an email asking user to click (within configurable period of time) on provided link to make the object "active";
-
user clicks a link and makes the object "active" or
-
key expires after specified amount of time.
The implementation is based on what I found in other apps, specially django-registration and django-email-confirmation, but with added support for confirming generic Django model instances (as long as they can have different activity states, that is).
Django jest fantastyczne m.in. w tym, że istnieje masa podłączalnych aplikacji, które w sposób wystarczająco generyczny realizują często spotykane zadania, jak np. django-registration do rejestrowania użytkowników, czy django-tagging do etykietowania obiektów. Część z tych aplikacji jest naprawdę wysokiej jakości, w dodatku łatwo konfigurowalnych i możliwych do dopasowania w dość szerokim zakresie. Są jednak takie, które dobrze się zapowiadają od dłuższego czasu, jednak na tym się kończy. Co bardziej zaskakujące, wszystkie te potencjalnie użyteczne aplikacje pochodzą z projektu Pinax.
W jednym z projektów potrzebuję dodać funkcję powiadamiania użytkowników o wystąpieniu pewnych zdarzeń. Jest aplikacja django-notification, ale w stanie obecnym nie nadaje się do wykorzystania poza Pinaxem (lub w sposób jakkolwiek różny od tego, jak to jest robione w Pinaksie). Zgłoszone są problemy z tym związane, jednak zostały odłożone na lepsze czasy. Będę musiał wziąć kod django-notification i wzorując się na nim napisać swoją własną aplikacyjkę, bo na te lepsze czasy się nie doczekam.
Inny przykład. Niedawno potrzebowałem dodać do pewnego projektu możliwość tworzenia obiektów przez niezalogowanych użytkowników — wymagane jest wtedy podanie adresu email i potwierdzenie dodania obiektu przez standardowe kliknięcie w przesłany link. Znalazłem aplikację django-email-confirmation, która robi coś podobnego, ale nie nadaje się do potwierdzania jakichkolwiek innych obiektów, niż jej własne (a ja potrzebowałem zastosować ten mechanizm do 3-4 różnych klas obiektów). Trzeba było napisać własną, na szczęście okazało się to dość proste, przyjmując django-email-confirmation za wzór.
I to samo mam za każdym razem, gdy trafiam na aplikację, która mogłaby mi się przydać, a pochodzi z Pinaxa — bez zakodowania własnego rozwiązania bazującego na tym kodzie się nie obejdzie. Rozumiem, że Pinax jest rozwijany w normalny dla OS/FS sposób, to znaczy w czasie wolnym i nikt nie bierze za to pieniędzy, ale skoro tak to wygląda, to jaki jest sens wydzielać aplikacje jako niby reużywalne, skoro tak naprawdę nie dają się użyć nigdzie poza aplikacjami typu Pinaxa i ich reużywalność to głęboki mit?
Poszukuję współpracownika do odtworzenia pewnej kultowej gry planszowej w realiach interwebu (Django 1.x + jQuery), może być na AppEngine, ale nie musi. Projekt może być Open Source, ale nie musi (bo w sumie kogo to obchodzi...). Wskazany wiek > 30 lat (młodsi raczej nie będą mieli odpowiedniego szacunku dla tej gry ;)). Gra jest turową strategią, więc łatwo nie będzie.
Kontakt tam gdzie zawsze - Jarek kropka Zgoda na usłudze pocztowej Google®. Proszę, tylko poważne propozycje...
[edit]: kilka osób mnie już o to pytało, nie chodzi o grę Revival (nawet nie wiem, co to takiego).
Piotr Maliński przemyśliwał nad IDE, więc miałem okazję porównać moje potrzeby w tym samym względzie. Są większe. :)
Do jego listy dopisuję rozszerzalność w postaci definiowania akcji zdarzeń — niech takie IDE ma zdefiniowane zdarzenia typu file-open, file-save i niech mi da możliwość (choćby ograniczoną) podpinania swoich akcji na te wydarzenia. To, co dla mnie było największym hitem PIDA to ciągłe sprawdzanie poprawności syntaktycznej modułu po każdym zapisie (i dodatkowo kilka innych rzeczy, jak np. aktualizacja drzewa definicji w kodzie). Wszystkie (no, prawie...) edytory, z którymi pracowałem, mają możliwość uruchamiania zewnętrznych narzędzi, niektóre nawet pozwalają przypiąć do nich skróty klawiszowe, ale żaden nie dawał możliwości automatycznego uruchamiania tych narzędzi (Komodo Edit pozwala uruchomić makro w odpowiedzi na pewne zdarzenia, ale to nie to samo...). Oczywiście, nauczyłem się bez tego żyć, wyrobiłem sobie też nawyk częstego pociskania Shift-Ctrl-V w TextMate, ale to namiastka.
Przynajmniej Pythoniarze powinni się trzymać z daleka od tej bazy, bo jej adapter jest obciążony kilkoma bardzo, ale to bardzo poważnymi błędami (abstrahując zupełnie od problemów, jakie ta baza ma sama ze sobą):
Moim zdaniem kicha. Pierwszy daje się obejść (kosztem nieprzenośnego kodu), ale drugi zupełnie mnie zabija, zwłaszcza, że płacę m.in. za zużycie pamięci...
Czas przyszedł, żeby pochwalić się środowiskiem developerskim. Na linuksie było różowo, na Macu już tak różowo nie jest, ale wydaje mi się, że doszedłem wreszcie do stanu, że infrastruktura nie przeszkadza mi w programowaniu, a środowisko z czasem mi się zintegrowało. Więc jak wygląda to środowisko programisty aplikacji webowych?
The missing editor. Vim jest bardzo dobry jako edytor. Jest tak dobry, że nie ma lepszych, ale to tylko edytor. TextMate to coś po środku między Vimem a Eclipse — to jeszcze nie środowisko zintegrowane, ale już dużo więcej niż edytor, głównie za sprawą bundles, czyli dodatkowych funkcji związanych z trybem edycyjnym. Na linuksie kiedyś podobnie bogate w funkcje było Kate, ale już nie jest, odkąd jakaś mądra głowa postanowiła wyrzucić z niego funkcję projektu. Do podobnego poziomu obecnie próbuje doszlusować PIDA, ale idzie to bardzo opornie (z tego co wiem, to z powodu braku siły roboczej — taka mała podpowiedź dla tych, co chcieliby się wykazać w jakimś projekcie). Dla łyżki dziegciu — PIDA ma hook do wykonywania automatycznych akcji przy zapisie bufora, za to TextMate ma okno wyboru pliku do otwarcia z inteligentnym podpowiadaniem (pod Cmd+T).
Nie zna życia, kto nie służył w marynarce, a nie zasmakował programowania w Pythonie ktoś, kto nie potrzebował kiedyś mieć zainstalowanych dwóch różnych wersji tej samej biblioteki. Lub wręcz z podwórka Django: 0.96.x, 1.0.x i trunk równolegle. Zamiast robić dziwne myki z PYTHONPATH, tworzy się izolowane środowisko z oddzielnym interpreterem i oddzielną biblioteką. Żyć, nie umierać i tworzyć izolowane środowiska. Wygoda polega przede wszystkim na tym, że utworzenie takiego środowiska jest szybkie i nie wymaga szczególnych zabiegów.
Tylko dla niego trzymam Firefoksa na Macu. Debugger JavaScriptu jest wielki. Można go określić jako missing plugin for Safari.
Nie ma aplikacji webowych bez baz danych, a dzięki ORM można nie babrać się z ustawianiem MySQL czy innego PostgreSQL tylko po to, żeby sobie podevelopować. Dopóki aplikacja porusza się w obrębie tego, na co pozwala ORM, to nie ma powodu, żeby używać innej bazy danych, bo po prostu od strony storage nie widać różnic. Niezwykle bardzo cenię sobie to, że start z aplikacją jest tak prosty i później naprawdę nie ma dla mnie różnicy, na jakiej bazie stoi moja aplikacja (tak długo, jak długo mogę zrobić django-admin.py dumpdata --format=xml). SQLite to fajna, bezobsługowa baza danych, która ma wszystko to, co jest potrzebne w momencie developmentu.
Rzeczy, których jeszcze nie używam, ale pewnie będę używał w niedalekiej przyszłości
Pozostałe rzeczy mają znaczenie drugorzędne (albo nawet trzeciorzędne), bo są albo specyficzne dla moich upodobań (subversion, MacPorts), albo specyficzne dla moich upodobań (Django), albo dla odmiany specyficzne dla moich upodobań (jQuery). Jestem w stanie robić programy w każdym środowisku, ale z moich ostatnich doświadczeń wynika, że jedyny warunek jest taki, żeby to było środowisko przynajmniej trochę uniksawe. :)
Jak w Django odczytać dane z pliku?
Once again, I think you're confusing ideas here; Django is simply a set of Python libraries you use to write code, in Python, for web applications. The code in your Django applications is Python code. Not some sort of special separate "Django code", but just plain old ordinary everyday Python code doing the sorts of things plain old ordinary everyday Python code does: importing things from libraries and using them.
Once you get over that conceptual problem, I think you'll have a much easier time of it. (z #)
Skąd się ludziom bierze wizja, że Django to coś nie z tej ziemi? Po PHP? Railsach? Springu? Pamiętam z dawnych czasów, że pytanie "jak odczytać dane z pliku tekstowego?" było jednym z najczęściej zadawanych na pl.comp.lang.php. Czy to stąd? A może stąd, że pisząc aplikację w RoR tak naprawdę nie ma się wiele kontaktu z językiem Ruby, a samo Ruby cierpi z powodu ubogiej biblioteki (zarówno standardowej, jak i 3rd party code)? Czy może chodzi o to, że robiąc w Springu + Hibernate tak naprawdę dłubie się w plikach XML, a kod w Javie (o ile w ogóle jakiś się pisze) jest powtarzalny aż do bólu tyłka?
Dużo złośliwych pytań i nie ukrywam, że mocno tendencyjnych. Nie oczekuję odpowiedzi. :)
Yesterday on #django-newbie IRC channel somebody asked, if it is possible to create syndication feed that gets items filtered by some parameter, that does not come from the URL, but comes from the request instead. Barely remembering my previous work with feeds I replied that there has to be some way to achieve such effect by wrapping feed view in custom view and manipulate call parameters. After long thinking I came to following code (tested and working):
Here's a snippet of the basic urlconf module before applying any changes:
feeds = {
'label': feeds.LatestEntriesByUser,
}
urlpatterns = patterns('django.contrib.syndication.views',
(r'^feeds/(?P<url>.*)/$', 'feed', {'feed_dict': feeds}),
)
Using this urlconf, everything that is requested from the /feeds url, is being served by default syndication framework's view, but for the sake of completeness I'll wrap the default view with my own, customized view function. To achieve this, the above urlconf line must be changed to:
(r'^feeds/(?P<url>.*)/$', myapp.views.feeds, {'feed_dict': feeds}),
Then comes my view function in myapp.views:
from django.contrib.syndication.views import feed as orig_feed
def feeds(request, url, feed_dict):
username = request.user.username
url = '%s/%s' % (url, username)
return orig_feed(request, url, feed_dict)
As shown above, I'm modifying the url parameter to contain an extra bit: the username I got from request. In the case of the feed 'articles' and username 'joe', the url that gets passed to built-in view will be /feeds/articles/joe/. So how do I handle this extra bit in my feed class?
Not surprisingly, this case is described in Django documentation (did I say the docs for Django are awesome?) - the feeds reference has a chapter on advanced feeds that covers the exactly identical case. Following this example, I'll add get_object method to my LatestEntriesByUser class:
def get_object(self, bits):
return Entry.objects.get(user__username=bits[0])
Obvious? No. Easy? Yes. Documented? Yes!
Robiąc ostanio w Django trafiłem na pewien problem, którego nie jestem w stanie rozwiązać (przynajmniej elegancko) do dziś.
Załóżmy, że mamy klasę, reprezentującą formularz. Ma ona sobie jakieś pola, powiedzmy:
class MyForm(forms.Form):
name = forms.CharField(label='name')
description = forms.CharField(label='description', widget=forms.Textarea)
class Media:
js = ('js/jquery.js', )
Jak widać, będziemy używać jakiegoś JavaScriptu. Problem polega teraz na tym, że chcę dodać trochę dynamiki do wyrenderowanego formularza — na przykład przy polu name dodać link, po którego kliknięciu pojawi się wiersz z polem description, a sam wiersz z polem description ma być początkowo ukryty, na przykład przy użyciu reguły CSS display: none;. Niestety, o ile można dodać definicję atrybutów do widgetu, to, jak się okazuje, nie ma możliwości określenia dodatkowych atrybutów dla całego pola (co wydaje się logiczne, ponieważ pole jest pewnym abstraktem i nie ma własnej warstwy prezentacji). Takich atrybutów, które na przykład mogłyby określić klasę CSS dla elementu obejmującego zarówno label, jak i widget, czy może nawet coś, co mogłoby być dodatkowym tekstem, wyświetlanym oprócz etykiety i widgetu.
Próbowałem paru podejść i żadne nie było zadowalające — albo powodowało, że cały dokument się nie walidował (dodawanie własnego atrybutu do widgetu i następnie wyświetlanie całego wiersza z klasą, będącą wartością tego atrybutu), albo było potwornie brzydkie (słownik z konfiguracją dodatowych atrybutów dla każdego z pól, któremu chcę coś dodać lub przedefiniowanie wszystkich wbudowanych klas pól, żeby akceptowały dodatkowe argumenty). Inne sposoby, których nie próbowałem, ale też przychodziły mi do głowy, były o wiele bardziej hackerskie, np. klasa domieszkowa do wstrzyknięcia w klasy pól wbudowanych przy użyciu jakiegoś rodzaju monkey patchingu. Nie wiem, może i by to zadziałało, ale moje upodobanie do prostych rozwiązań mocno by ucierpiało.
Efekt jest taki, że robię to nieelegancko i wciąż szukam inspiracji, żeby wymyślić sposób zrobienia tego ładnie. Każda sugestia mile widziana.
Przyglądając się WebOb i dołączonym do niego przykładom można wreszcie zobaczyć, jak dużą rzeczą są ramówki webowe. O ile WebOb dostarcza podstawy do zbudowania własnej ramówki, to porównanie tej podstawy z gotowym frameworkiem, np. Django, uświadamia, jak wiele rzeczy trzeba napisać, żeby wszystko razem zagrało. Oczywiście, zazwyczaj używa się tylko pewnej części ramówki i wydaje się, że napisanie własnej spowoduje, że będzie szybsza, lżejsza i w ogóle lepsza, bo przecież pozbawiona kodu (a więc i funkcji), którego się i tak nie używa. Ale to, co pozostaje, to i tak ogromna ilość kodu i ogromna ilość funkcji, które trzeba zaimplementować. I to zaimplementować poprawnie, przewidując sytuacje niezwykłe (dziwnego klienta, któremu się coś pieprzy z HTTP i czegoś-tam zapomina wysłać albo nie rozumie) i awaryjne.
Ja w każdym razie dziękuję, zostanę przy cudzej robocie. :)
W celach zupełnie edukacyjnych postanowiłem zrobić jakąś aplikacyjkę na Google AppEngine, na początek w Django 1.0 (na próby ujarzmienia WebOb czas przyjdzie później).
Napisałem kawałek kodu, zdeployowałem, co mogę powiedzieć po tych paru godzinach... Django musi zostać mocno okrojone, żeby zadziałało poprawnie. Ograniczenie do 1000 plików na aplikację oznacza, że z samego Django trzeba wywalić wszystko, co niepotrzebne lub nieużywalne, bo samo Django liczy sobie około 900 plików i na aplikację nie zostanie wiele miejsca. Przydaje się AppEngine Helper for Django, dzięki temu jest sporo prościej. django.forms można używać, ale ostrożnie i w razie potrzeby podpierając się google.appengine.ext.db.djangoforms (oryginalne ModelChoiceField nie działa). Modele przypominają modele Django, używa się ich podobnie, ale API nie jest takie samo, więc trzeba uważać. Jeszcze nie rozgryzłem autoryzacji, ale nie wygląda jakoś tragicznie.
W sumie wygląda nieźle, ale praca z takim okrojonym Django jest dość stresująca. It's a challenge, baby. :)
Przybywa dokumentacji w moich django-tutors — skończyłem (a przynajmniej tak mi się wydaje) odcinki o uploadowaniu plików do ścieżek określanych statycznie i dynamicznie, właśnie kończę odcinek o upload handlers. Na napisanie czeka jeszcze odcinek o storage backends, a potem... A potem to się zobaczy.
Padła propozycja, żeby napisać coś o uploadowaniu wielu plików na raz przy użyciu jakiegoś flashowego uploadera, ale nie wiem, czy taki dość specyficzny temat mi odpowiada. A może ktoś zechciałby napisać taki artykuł? Projekt django-tutors jest otwarty, kto chce może dostarczyć co chce, to niekoniecznie musi być komplet (dokument + przykładowy kod), jeżeli czegoś będzie brakowało, to w razie potrzeby się dopisze. Wymaganie właściwie jest tylko jedno, to musi działać z fabrycznym Django 1.0 (czyli bez przeróbek w kodzie ramówki).
Jeżeli ktoś chce dołożyć swoje 3 grosze do projektu, to proszę się zgłosić mailem na moje konto na gmailu (jarek.zgoda i tak dalej), albo odezwać się na jabberze, też na moje konto na gmailu.
Moje niedawne przejścia z uploadem plików popchnęły mnie do tego, żeby spisać moje doświadczenia w postaci samouczków. Na początek poszedł właśnie upload plików, jak do tej pory napisałem o uploadzie do statycznej ścieżki.
django-tutors
Wszelka pomoc mile widziana — fragmenty szkoleniowego kodu, samouczki, itp. Jeżeli ktoś chciałby zredagować jakiś artykuł, to oczywiście zapraszam. Język: polski. Na razie wyłącznie.
Od kilku tygodni mamy w Django nową ramówkę do komentarzy. Kod jest pokłosiem projektu Google Summer of Code™ i trafił do repozytorium Django właściwie bez poważnego przejrzenia, w wielkim pośpiechu, już po wydaniu którejś bety. Resultatem jest fura błędów i niedoróbek, które znalazły się w finalnym wydaniu Django. Nie chodzi bynajmniej o to, że ktoś to marnie napisał — dla każdego projektu programistycznego normalne jest to, że pierwsza wersja służy temu, by poprawić podstawowe błędy i doszlifować ją w trakcie używania. Nowa ramówka nie miała szansy sprawdzić się w boju, pod czujnym okiem dziesiątek czy setek użytkowników.
Próbowałem doprowadzić do tego, by zmiana ramówki komentarzy była niedostrzegalna dla moich czytelników. Niestety, okazało się, że w obecnym stanie ramówki jest to po prostu niemożliwe bez napisania... własnej ramówki. Nie wszystko daje się owrapować i nie wszystko można podmienić, nawet przy bardzo dynamicznej naturze Pythona. Dlatego wspomniane prace konserwatorskie zostają odłożone na bliżej nieokreśloną przyszłość, gdy ramówka otrzyma wreszcie wymaganą konfigurowalność. W sumie, patrząc na to z innej strony, skoro działa, to po co poprawiać? ;)
Wydane
jarek:~/install/django$ svn log | grep zgoda | wc -l
17
Niewiele, ale duma mnie rozpiera — dokonaliśmy tego!
Dziś ma zostać wydane Django 1.0.
Będzie?
Nie będzie?
To jest głównie a note to self, żeby mieć to pod ręką w razie potrzeby.
Zakładając, że mam funkcję i model:
def get_userphoto_upload_path(instance, filename):
return 'users/%s/photos/%s' % (str(instance.user_id), filename)
class UserPhoto(models.Model):
user = models.ForeignKey(User, verbose_name=_('user'), related_name='photos')
photo = models.ImageField(_('photo'), upload_to=get_userphoto_upload_path)
caption = models.CharField(_('caption'), max_length=200, null=True, blank=True)
w klasie formularza metoda save() powinna zawierać kod:
def save(self, user):
user_photo = UserPhoto(user=user)
if 'caption' in self.cleaned_data:
user_photo.caption = self.cleaned_data['caption']
data = self.cleaned_data['photo']
user_photo.photo.save(data.name, data)
return user_photo
Zawsze zapominam, jak powinno się to robić. Może teraz będzie mi łatwiej trafić na ślad własnych dokonań. ;)
Tytuł cokolwiek na wyrost, jak sądzę, ale mam powody do dumy. W spolszczeniu Django nie brakuje już żadnych tekstów, jedynie dwa (oba dotyczące GIS) powinny zostać przejrzane przez kogoś, kto się na tym temacie zna. Biorąc pod uwagę nadchodzący string-freeze, mogę się spodziewać, że w takim stanie dotrwamy do finalnego wydania.
Jam-ci to sprawił. Tymi recami!
Dziś w pracy miałem robotę głównie poszukiwawczą — było kilka rzeczy, których nie wiadomo było jak zrobić, choć wiadomo było, że się da. Na przykład na początek: jak w adminie Django wyświetlić obrazek na formularzu edycji obiektu, zamiast zwykłego linku do niego, jak to jest domyślnie? Na szczęście wiedziałem jak się to robi, więc nie miałem zbyt trudnego zadania na początek.
W nowym Django (czyli beta-1 lub nowszym) w adminie jest to o tyle proste, że dla każdego indywidualnego pola formularza można przypisać odpowiedni widget, czyli reprezentację w HTML. Wystarczyło utworzyć sobie odpowiednią klasę dla takiego widgetu:
class AdminImageWidget(forms.FileInput):
"""
Widget do wyświetlania obrazka w adminie (change form)
"""
def render(self, name, value, attrs=None):
output = []
file_name = str(value)
if value and hasattr(value, 'url'):
output.append('<img src="%(url)s" alt="image" /><br />' % {'url': value.url})
output.append('%s ' % _('Change:'))
output.append(super(AdminImageWidget, self).render(name, value, attrs))
return mark_safe(u''.join(output))
i następnie użyć jej w klasie administracyjnej dla modelu:
class MovieImageAdmin(admin.ModelAdmin):
list_display = ('movie', 'width', 'height', 'shot_at')
search_fields = ['movie__title']
save_on_top = True
list_filter = ('movie', 'width')
def formfield_for_dbfield(self, db_field, **kwargs):
if db_field.name == 'image':
kwargs['widget'] = AdminImageWidget
return db_field.formfield(**kwargs)
Metoda formfield_for_dbfield jest wywoływana dla każdego pola formularza i w tym przypadku sprawdzeniu podlega nazwa pola, ale można równie dobrze sprawdzić jego typ lub inny atrybut, aby dopasować właściwy widget. W ten sposób można także dostosować wyświetlanie plików video czy reprezentację dla plików dźwiękowych.
No dobrze, to było proste i nie zajęło mi całego dnia. Ale następna robota nie była już taka oczywista. Kolega chciał, żeby główne okno aplikacji administracyjnej (to, co jest pod / w adminie) dostało kilka dodatkowych zmiennych w kontekście. W starszym Django (sprzed newforms-admin) nie byłoby to możliwe bez patchowania samego Django (co nie jest kłopotliwe, jeżeli używa się Virtualenv, ale zawsze jest uciążliwe), ale teraz dostaliśmy wszystkie potrzebne narzędzia.
Jak wiadomo, za sterowanie aplikacją administracyjną odpowiada obiekt klasy AdminSite, który jest o tyle fajny, że jest najzwyklejszym obiektem najzwyklejszej klasy. Klasy, którą oczywiście można nadpisać. Jak to może wyglądać? Na przykład tak:
class AdminSite(admin.AdminSite):
def index(self, request, extra_context=None):
if extra_context is None:
extra_context = {}
ctx = {
'my_item': u'my_value',
}
extra_context.update(ctx)
return super(AdminSite, self).index(request, extra_context)
Nieprzyjemy aspekt używania obiektu site własnej klasy AdminSite jest taki, że nie działa funkcja autodiscover() i wszystkie klasy administracyjne trzeba rejestrować własnoręcznie w tym samym miejscu, gdzie znajduje się obiekt klasy AdminSite. Ale to chyba stosunkowo mała niedogodność w porównaniu z dostarczoną mocą.
Django jest coraz bliżej mitycznego "1.0" — właśnie wyszło 1.0-alpha-2, w którym dostaliśmy zrefaktorowane pola FileField i pochodne (czyli również ImageField), przebudowę przeszedł także podsystem sygnałów.
To tak gwoli kronikarskiej ścisłości, bo to są rzeczy, które albo wymagały ode mnie przepisania kawałka kodu, albo długo na nie czekałem.
W nowej pracy dostałem nowe zadanie do zrobienia — zdeployować gotową aplikację w Javie na kilka maszyn w kilku odmianach. Jako że do tej pory takimi rzeczami zajmował się dla mnie dział deploymentu (słodkie czasy), było to dla mnie coś zupełnie nowego. Do zrobienia była dość prosta rzecz:
-
skopiować kilka szablonów plików z jednego drzewa katalogów do drugiego,
-
podmienić odpowiednie ciągi znaków w plikach szablonów,
-
spakować wszystko do tar.gz,
-
wrzucić na zdalną maszynę przez scp,
-
rozpakować na zdalnej maszynie,
-
uruchomić aplikację pod screenem.
Zacząłem od poszukiwania narzędzia, które by mi to ułatwiło.
Skrypty w shellu odpadły na samym początku. Nie umiem ich pisać, poza paroma najbardziej podstawowymi zastosowaniami.
W pracy używamy Pavera, więc naturalną koleją rzeczy zasiadłem do klepania pliku pavement.py. Szybko okazało się, że to potwornie żmudna robota, gdy aplikacja nie jest napisana w Pythonie i wszystkie ułatwienia Pavera można sobie wsadzić głęboko.
Przypomniałem sobie wtedy, że parę lat temu robiłem podobne rzeczy przy użyciu anta, więc napisałem sobie odpowiedni plik build.xml. Było z tym trochę roboty, ale działa. Wreszcie. Ale niestety, to jest Java i XML, więc piętno zostało wybite.
Zacząłem się rozglądać za innym narzędziem i wtedy wpadł mi w oko Fabric. Z opisu wynika, że może być tym, co zastąpi mi anta... ale może też wcale tak nie być. Projekt jest w bardzo wczesnej fazie rozwoju, ale będę miał na niego oko, gdyby ant za bardzo mi dopiekł.
Zanim NFA zlądowało w trunku Django (określmy to umownie jako wersję 0.97-pre, w odróżnieniu od 1.0-alpha), aby mieć aplikację we własnej subdomenie trzeba było użyć specjalizowanego middleware, które podmieniało zawartość request.urlconf. W przypadku django.contrib.admin było to o tyle proste i oczywiste, że aplikacja administracyjna miała swój własny URLconf (w postaci django.contrib.admin.urls) i w najprostszym rozwiązaniu wystarczyło utrzymywać mapowanie subdomeny na URLconf. Wraz z nadejściem 1.0-alpha nie jest już tak łatwo, ponieważ tego URLconfa po prostu już nie ma. Aby to rozwiązanie działało nadal, musiałem trochę pokombinować.
Pierwsze podejście z wpisaniem na pałę django.contrib.admin.site.root jako wartości klucza admin w mapie domen oczywiście nie miało szans powodzenia (to jest funkcja, a nie moduł).
Podejście drugie: skoro jest to view, to trzeba zrobić taki URLconf, w którym będzie on wywoływany na / i wszystko, co dalej od tego URL-a. Powstał plik admin_urls.py z zawartością niemal identyczną, jak w dokumentacji Django, jedyna różnica polegała na tym, że obsługiwany URL nie zawierał admin:
urlpatterns = patterns('',
(r'^(.*)', admin.site.root),
)
I działa.
Django doszło z numerkiem do 1.0-alpha, co oznacza, że wszystko jest na dobrej drodze do dotrzymania obiecanego terminu wydania 1.0! Internet ma wreszcie szansę stać się cokolwiek przyjemniejszym miejscem (od strony programistycznej).
A ja siadam ponownie do sprzątania po rewolucji, jaką było dołączenie NFA do trunka...
W trunku Django zlądował właśnie branch newforms-admin, a niejako przy okazji django.newforms to już po prostu django.forms. Nic nie działa. Żadna aplikacja nie daje się uruchomić. Czeka mnie masa roboty.
I love this game!
Hehe... Widziałem już takie teksty, że niby żeby myśleć o skalowaniu, trzeba mieć po co się skalować, względnie żeby skalowanie zostawić na później, gdy już będzie potrzebne. Niejaki Ted Dziuba dał to po raz kolejny, tym razem dosadnie i z werwą.
Słyszysz? Przestań pieprzyć o skalowalności, i tak nikt nie będzie używał twojej aplikacji. No.
Przyszłoroczna konferencja EuroPython odbędzie się w Birmingham. Sprawdziłem sobie dzisiaj, jak tam się dostać i nie jest tak źle, latają tam 2 tanie linie lotnicze (bmibaby i norwegian), więc podróż nie powinna być szczególnie kosztowna. O reszcie konferencji trudno mi się wypowiedzieć, ale ceny noclegów nie były szczególnie niskie (£35 za pokój to sporo więcej, niż 150Lt...).
Wczoraj nie zrobiłem wielu zdjęć, bo byłem zajęty aspektem socjalnym konferencji. Tym niemniej, kilka jest. Z dziś (dzień 3 i ostatni) nie będzie, ponieważ rozładował mi się akumulator w aparacie i tyle tego było.
EuroPython 2008, dzień 2
Wczoraj nie natrzaskałem aż tyle, co w niedzielę, ale to głównie z powodu tego, że pół dnia spędziłem w hotelu na prelekcjach. Co nie przeszkodziło nam przewędrować prawie całą wileńską starówkę w poszukiwaniu Ostrej Bramy (nie znaleźliśmy jej, chociaż przez większość czasu kręciliśmy się w promieniu 200-300 metrów od niej).
EuroPython 2008, dzień 1
Kupiłem aparat Zorkę 5 i zrobiłem kilka zdjęć. Na początek zdjęcia z dnia 0 konferencji EuroPython 2008 w Wilnie, czyli z niedzieli. Przyjechaliśmy, zjedliśmy kolację, zameldowaliśmy się w hotelu i grzecznie poszliśmy spać.
EuroPython 2008, dzień 0
Jutro rano — kolejna porcja zdjęciówek, tym razem z dnia 1 (czyli z dziś).
Tytuł i zdjęcie mówią same za siebie...
Jak nigdy, przyszedłem do hotelu z konferencją punktualnie. Wziąłem moją karteczkę rejestracyjną i podreptałem na prelekcję naszego kolegi z pokoju, Marcina Kaszyńskiego. Po odrobieniu obowiązku towarzyskiego, zaczyna się teraz samo mięcho konferencji, czyli część społeczna. ;)
Jedyne do czego mogę mieć uwagi to sieć, zarówno hotelowa (za darmo, wolna) jak i publiczna (płatna, zazwyczaj nie działająca) są marnej jakości. Nie wiem, czego się spodziewałem...
Znaczy: do Wilna, tak jak rok temu na EuroPython. Tym razem wybieramy się w większym towarzystwie, a jak podejrzewam reprezentacja Polski też będzie liczniejsza, niż w ubiegłym roku.
Do zobaczenia w Wilnie!
Ponieważ PIDA zaczęła mnie wkurzać (co chwilę a to coś działa, a to nie działa), postanowiłem dać szansę KomodoEdit, mocno okrojonej, ale darmowej wersji niezłego Komodo IDE. Ponieważ mój nowy lapek ma 2GB RAM na pokładzie i mocny, dwurdzeniowy procesor, żerność Komodo nie była aż tak wielkim problemem jak dawniej. Obejrzałem dokładnie program i zacząłem zastanawiać się, jak do niego dołączyć to, do czego przyzwyczaiła mnie PIDA, to znaczy integrację z Pyflakes. Dla niezorientowanych: Pyflakes to program do statycznej analizy poprawności kodu Pythona, nieco mniej zaawansowany niż Pylint, jednak jednocześnie nie aż tak inwazyjny (oczywiście wiem, że Pylinta można tak skonfigurować, żeby nie sprawiał wrażenia faszystowskiego narzędzia).
Jak się szybko okazało, nie ma możliwości podpięcia Pyflakes tak, by uruchamiało się przy każdym zapisie bufora, jak dzieje się to w PIDA, jednak dla mnie ważniejsze było to, żeby działało, niż to, żeby działało identycznie. Postanowiłem przypiąć Pyflakes jako narzędzie zewnętrzne w Toolboksie Komodo. Oto skrócona recepta, jak to zrobić, żeby działało ładnie i produkowało fajne wyniki.
-
Toolbox -> Add -> New Command
-
Command (U):
/usr/bin/pyflakes %F
-
Run in: "Command Output Tab"
-
[*] Parse output with:
^(?P<file>.+?):(?P<line>\d+): (?P<content>.*)$
-
[*] Show parsed output as a list
A teraz opis słowno-muzyczny. Z menu Toolbox wybierz Add i następnie z podmenu New Command. Na zakładce Command w nowo otwartym okienku w pole Command wpisz polecenie uruchomienia Pyflakes ze ścieżką bieżącego bufora (u mnie: /usr/bin/pyflakes %F). Z listy Run in wybierz Command Output Tab. Zaznacz pole Parse output with i w pole polecenia parsującego wpisz wyrażenie regularne ^(?P<file>.+?):(?P<line>\d+): (?P<content>.*)$, które podzieli wyjście Pyflakes na 3 części. Zaznacz pole Show parsed output as a list, a Komodo wyświetli wyniki w postaci ładnej listy z 3 kolumnami — dwukrotne kliknięcie na każdej pozycji listy przeniesie kursor do odpowiedniej linii w edytorze.
Po tym wszystkim pozostaje jedynie przypisanie do tego jakiegoś wygodnego skrótu klawiszowego w tym samym okienku na zakładce Key Binding.
Poprawiono najbardziej denerwujący mnie błąd w Django z ostatnich czasów i dzięki temu wróciło archiwum. Oh, my...
Dosłownie w ostatniej chwili zdecydowałem się na zmianę dostawcy hostingu na djangohosting.ch, pomimo że byłem zdecydowany na coś innego. Po sugestii Thomasa (dzięki!) przyjrzałem się usłudze dokładniej i postanowiłem dać szansę. Wykupiłem najtańszą opcję, wypróbowałem one-click-django-installer i oczywiście zameldowałem się po SSH. Już pierwsze pół godziny sesji przekonało mnie, że jest tam o wiele przyjemniej niż na Alwaysdata — nie ma żadnych myków z przestawianiem $HOME, no i zwiększanie opcji jest bardziej granularne (oddzielnie procesy, porty, pamięć i przestrzeń dyskowa), a cała infrastruktura w dużej mierze przypomina to, co jest na MegiTeam.pl, z paroma udogodnieniami:
-
można uruchomić serwer developerski django i w razie problemów mieć dostęp do pełnego tracebacka;
-
pełna kontrola nad uruchamianiem FastCGI;
-
łatwiejszy dostęp do logów Lighttpd (chociaż jest w nich równie mało, jak w logach nginxa na megiteam).
Ogólnie wygląda to bardzo dobrze, a przy tym jest trochę taniej.
Dużo się ostatnio dzieje w Django — co chwilę ktoś commituje zmiany do repozytorium i można odnieść wrażenie, że opublikowanie daty wydania wersji 1.0 obudziło w developerach nowy zapał i chęć do posprintowania na zakończenie wyścigu.
To mylne wrażenie. Przytłaczająca większość pojawiających się w repozytorium zmian to są poprawki w dokumentacji, w docstringach oraz style fixes. A kod poprawiający rzeczywiste problemy czeka sobie na lepsze czasy.
Ten wpis był tytułem ochłodzenia emocji, gdyby ktoś widząc tempo commitów wnioskował z tego, jak dużo i szybko kod w Django jest poprawiany... ;)
To był tydzień pełen przemyśleń, ale w końcu doszedłem do jakichś wniosków. Mój najbliższy projekt będę hostował na Alwaysdata. Co prawda całość serwisu jest po francusku, ale jakoś daję sobie z tym radę.
Ujęło mnie to, że jest:
-
tanio (€ 6 za miesiąc w najtańszej opcji);
-
dobry support na forum (po angielsku!);
-
dość duża wolność, jak na shared plan;
-
krótki ping, serwery stoją w OVH w Roubaix (Francja).
W związku z małą ilością dostępnego miejsca na dysku, będę musiał media i uploady na S3, ale nie powinno być to jakoś szczególnie uciążliwe.
Konkurent (djangohosting.ch) przegrał głównie przez zerowy wybór w dziedzinie bazy danych (tylko MySQL, bez PostgreSQL), ale nie była to przegrana autorytatywna. Dam mu szansę następnym razem. ;)
No i będę miał w pracy MacBooka. Niby zwykłego, ale tak nie do końca, bo czarnego. Nigdy nie miałem Maca, nigdy też go nie używałem, więc zacząłem od czytania, co też mnie czeka po przesiadce z linuksa. Nie zapowiada się różowo, głównie z powodu problemów z bibliotekami. Ale może jakoś to przeżyję...
Już można się rejestrować na EuroPython 2008, do końca maja opłata wynosi €100 (od 1 czerwca €160, przy drzwiach €200), więc warto się pospieszyć. Ja się już zarejestrowałem. ;)
Większość mojego kodu w Pythonie (a właściwie kodu w ogóle, włączając w to HTML i skrypty SQL) piszę w PIDA. Jak dla mnie to środowisko pozostawia mi ultrawygodnego Vima, dodając to, czego potrzebuję podczas pracy: zarządzanie grupą plików jako projektem, kilka podstawowych poleceń systemu kontroli wersji, przeglądarkę klas i integrację z pyflakes. Niestety, PIDA ma wciąż sporo błędów i — to jest prawdziwe niestety — nie są one w ogóle poprawiane (prawdopodobnie dlatego, że nie ma kto tego zrobić, i nie, ja też tego nie dam rady zrobić). Bardzo to wkurzające, przede wszystkim zważywszy fakt, że żaden z wolnodostępnych edytorów nie oferuje tego, co PIDA...
Znalazłem kilka dni temu samouczek pisania własnych ramówek webowych w Pythonie w oparciu o WSGI i bibliotekę WebOb. Tak łatwo to chyba jeszcze nie było...
Na szczęście minęły czasy pączkujących ramówek (w tempie dwóch tygodniowo, strach było otworzyć lodówkę), ale może takie samouczki pokażą pretendentom, co ich czeka od strony kodu. Bo tego, co ich czeka od strony użytkowników to się nie da opisać żadnymi słowami. ;)
Co jakiś czas na pl.comp.lang.python pojawiają się ogłoszenia o pracy dla programistów w Pythonie. Abstrahując od dyskusyjnej właściwości tej grupy dla wysyłania ogłoszeń o pracy (niby jest pl.praca.oferowana, ale ogłoszeń o pracy dla Pythoniarzy nie jest aż tak wiele, więc ma to pewien walor...), to reakcje na takie ogłoszenia doprowadzają mnie do białej gorączki.
Przykład z ostatnich dni, pewna firma z Wrocławia zamieściła ogłoszenie o pracy. Napisali czym się zajmują, czego oczekują i tradycyjnie enigmatycznie co oferują. I od razu zaczęło się wydziwianie: a to że polszczyzna odbiega od wzorca metra prof. Miodka, a to że znowu nie podali proponowanych zarobków, a to że wymagają bardzo dobrej znajomości biblioteki standardowej. Takie pitu-pitu, żeby sobie tylko popyszczyć. Dużo osób narzeka, że gdzie indziej pracodawcy w ofercie podają kwoty, ale nikt jakoś nie zauważa, że dotyczy to ofert zamieszczanych w serwisach jobsowych, a nie w usenecie. I pewnie aby dopełnić obrazu nikt się nie pofatygował, żeby popatrzeć jak gdzie indziej wyglądają reakcje na takie ogłoszenia. Podpowiem — odpowiedzią jest milczenie.
Queryset-refactor zlądował w trunku Django. Wspominam z kronikarskiego obowiązku, bo nie zauważyłem żadnych problemów z moimi aplikacjami w związku z tym.
Osłabiający się wciąż dolar sprawił, że postanowiłem sprawdzić, czy MegiTeam nadal jest konkurencją dla WebFaction, przynajmniej jeżeli chodzi o cenę. Kalkulator w dłoń i liczymy.
Ponieważ MegiTeam nie ma opcji płatności miesięcznej, policzymy dla okresów 3, 6 i 12 miesięcy, stosując wszystkie podstawowe zniżki i porównując plany taryfowe, które sobie odpowiadają. W przypadku WebFaction będzie to "Shared 1" (80MB RAM, 600GB transferu miesięcznie, 10GB przestrzeni dyskowej), z uwzględnieniem zniżek za przedpłacenie. W przypadku MegiTeam, które nie oferuje aż takiego transferu, użyta będzie maksymalna wielkość 50GB transferu miesięcznie. Ceny w USD zostaną przeliczone według kursu średniego NBP z dzisiaj.
3 miesiące
WebFaction: 3 * $9.50 * 1.175 (17.5% VAT) = $33.49, czyli 73.83 zł
MegiTeam: 139.00 zł
6 miesięcy
WebFaction: 6 * $9.50 * 1.175 (17.5% VAT) = $66.98, czyli 147.65 zł
MegiTeam: 269.00 zł
12 miesięcy
WebFaction: 12 * $8.50 * 1.175 (17.5% VAT) = $119.85, czyli 264.20 zł
MegiTeam: 521.00 zł
Jak widać, przy rocznym okresie rozliczeniowym MegiTeam jest dwukrotnie droższe od WebFaction. Jeżeli do października nie nastąpi jakaś katastrofa na rynku walutowym, to się przeniosę z hostingiem za ocean. Może nie będzie aż tak strasznie wolniej...
Django Dash to konkurs programistyczny podobny do Ludum Dare czy PyWeek — chodzi o to, żeby w 48 godzin wyprodukować kompletną, działającą aplikację w Django. Mam pewien pomysł i nawet wizję, jak to zrobić, ale nie jestem pewien, czy wytrzymam te 48 godzin, czy po prostu nie jestem za stary na takie kodowanie. ;)
Dostałem to konto i po całym weekendzie zastanawiania się nie umiem sobie wyobrazić aplikacji, którą mógłbym napisać i odpalić na AppEngine. Ograniczenia są cokolwiek duże, ale sama perspektywa jest przez cały czas kusząca. Dam sobie jeszcze tydzień na przemyślenie, czy w ogóle w to brnąć.
Przez cały czas mam przeczucie, że to jest coś, co daje duże możliwości.
Thanks for signing up to try Google App Engine! Your account has been activated, so you can begin building applications!
No to zobaczymy, co się z tego da wyciągnąć... Mam pod ręką kilka eksperymentalnych projektów, któryś z nich zląduje na AppEngine.
W dzień-dwa po tym, jak Google ujawniło swoje AppEngine, firma Joyent, która sprzedaje (dość tanio) wirtualki na OpenSolarisie, postanowiła udostępnić swoją infrastrukturę bezpłatnie (co nie znaczy za darmo, ale po kolei) pod projekty w Pythonie, które mogą się pochwalić odpowiednio dużym ruchem. Z wielkimi fanfarami i używając biblijnego słownictwa ogłosili program, który nazwali (a jakże) Garden of Eden.
Haczyk jest taki, że chcą... nielimitowanego dostępu do danych klientów tak hostowanych serwisów. Imiona, nazwiska, adresy, telefony, numery faksów i cały ten szpej. Warto? Owszem warto... zastanowić się, co ważniejsze.
Wszystko wskazuje na to, że moje zgłoszenie do testowania Google AppEngine nie zostało wylosowane, 10000 kont zostało rozdanych, a mnie zostało czekanie na następny batch i ćwiczenie na lokalnym dev serwerze. Znajomy zaproponował, że da mi jedną ze swoich trzech przydziałowych aplikacji, żebym mógł sobie potestować, więc pewnie nie będzie to trzening tak całkiem na sucho...
W każdym razie już widać, że o ile z Django na AppEngine zostaje bardzo dużo, to modeli używać trzeba tych dostarczanych przez Google. Nie minęło wiele czasu, a już pojawiły się plany rozszerzenia zaplecza bazodanowego Django o obsługę API przechowywania danych na AppEngine. Może to zaowocować szybszym merge odgałęzienia queryset-refactor do głównej gałęzi rozwojowej Django.
W obecnej formie na AppEngine nie wszystko jest zaimplementowane (nie ma np. M2M), z niektórych rzeczy trzeba zrezygnować, jak z interfejsu administracyjnego, czy z djangowego mechanizmu sesji, ale i tak jest to na tyle ciekawe, żeby chcieć dostać sztukę dla dokładniejszego przyjrzenia się. :)
Google wystrzeliło z nowym pomysłem — AppEngine to nowy (i z opisu wynika, że rewolucyjny) hosting aplikacji webowych w ścisłej integracji z usługami Google, na razie przede wszystkim w Pythonie. W domyślnej instalacji jest Django 0.96.1, ale można też wziąć sobie wersję z SVN. Na razie trochę niejasne jest, w jaki sposób połączyć implementację modeli by Django z API, którego używania wymaga Google, ale sądzę, że wszystko się wyjaśni wkrótce.
I jak zwykle się spóźniłem z zapisaniem, trafiłem na waitlistę...
Coś wygląda na nieźle spieprzone w usłudze sprawdzania komentarzy przez Akismet — podanie w komentarzu linku do strony na Grupach Google (obojętne, dyskusje czy pliki), zamiast spodziewanego "true" lub "false" zwraca "" (pusty ciąg znaków). Na razie nikt nie zwrócił uwagi na takie zachowanie Akismet, ale ja się zacząłem rozglądać za czymś innym. Na razie plany rozszerzenia CommentModerator obejmują LinkSleeve i Defensio.
Jak na razie wygląda na to, że nie da się zamieścić komentarza, który zawiera link do Google Groups. Wolałbym nie rezygnować z automatycznego moderowania komentarzy, ale jeżeli nie da się tego uniknąć, to będę moderował komentarze ręcznie. Oh, my...
Zmieściłem się w czasie... prawie. ;)
Przeciągnąłem o 5 minut, ale obskoczyłem wszystko, pomimo problemów z warstwą prezentacyjną. Niestety, publiczność nie dopisała i mieliśmy pustki na sali.
Materiały są dostępne na stronie listy mailowej.
Kolejne, 4 spotkanie w ramach WARPY odbędzie się tam, gdzie zawsze, o 19:15. Poopowiadam trochę o tłumaczeniu i lokalizacji programów w Pythonie. Będzie o modułach gettext i locale, a dodatkowo pokażę parę tricków z biblioteką Babel.
Nauczony poprzednią wpadką postaram się ograniczyć moje pływanie w dygresjach. ;)
Umierają, umierają, żadna przecież nie trwa wiecznie. Idea WarPy okazała się jednak nieco bardziej żywotna, niż przypuszczałem — agonia potrwa co najmniej o 2 tygodnie dłużej.
27 marca Marcin Świderski (forgems) przedstawi wprowadzenie do Django, godzina i lokalizacja ta sama co zawsze (19:00 na "elce"). A tydzień później, czyli 3 kwietnia, w ramach nadrabiania zaległości, opowiem o tłumaczeniu i lokalizowaniu programów w Pythonie (czyli o i18n i l10n).
Idee w Polsce umierają po cichu.
Jesienią ubiegłego roku zaczęliśmy robić prelekcje o Pythonie w ramach WarPy. Odbyło się 3 spotkania, po czym współorganizator ze strony WO@PW i WarLug-a poprosił o przerwę z powodu sesji. Gdy sesja się skończyła, organizatorom i potencjalnym prelegentom z kolei zaczął się gorący okres w pracy.
WarPy ma swoją listę mailową, w żadnym przypadku nie jest organizacją zamkniętą, która w tajnych głosowaniach ustala kto i o czym ma opowiadać. Ja nie mam ostatnio czasu zadać pytania, czy są w ogóle jacyś chętni, a chętni sami się nie zgłaszają. W ten sposób po cichu i bez rozgłosu kończy się kolejna idea.
Bardzo, bardzo dokładnie przyglądam się wszystkiemu, co związane z cache na poziomie obiektów w Django (np. projektowi django-orm-cache). Znalazłem blogowy wpis (po rosyjsku, a jakże!), który opisuje rozwiązanie bardzo podobne do tego, którego i my używamy, ale o wiele bardziej kompletne. Wypadałoby to teraz gdzieś przetestować, ale w środowisku, w którym miałoby to jakikolwiek sens, nie za bardzo możemy — po prostu nie ma na to czasu...
Ku pamięci, jak to Aleksiej Koszeliew załatwił sprawę odświeżania obiektowego cache w Django. Polecam uwadze, jeżeli kogoś też interesuje to, jak inni dają sobie radę z wygaszaniem cache...
Mój pierwszy commit do repozytorium Django, jako oficjalnego maintainera polskiego tłumaczenia.
A my nie! (My, czyli kilka naszych aplikacji)
Iwan Sagalajew, pracujący dla yandex.ru, podzielił się kilkoma spostrzeżeniami po nieudanym odpaleniu nowego serwisu społecznego. Czytałem to z niekłamanym zadowoleniem — większość problemów, które tam opisał, nie ma nawet szans, żeby nas dotyczyć. Po kolei:
-
zbyt długi czas zapisu danych sesji, nie dotyczy nas, bo sesje trzymamy w memcache;
-
efekt "dog-pile", nie dotyczy nas, bo rzeczy kosztowne robimy poza aplikacją (poniekąd asynchronicznie);
-
pomimo nacisków naszego DB-speca, nie normalizujemy naszego modelu nadmiernie.
Nie mam złudzeń, że w pewnym momencie będziemy musieli troszkę przyciąć nasz radosny bałaganik, ale aplikacja została zaplanowana z tak dużym zapasem, że to na pewno nie nastąpi w ciągu najbliższych kilku miesięcy...
Mała aktualizacja, pod wpływem komentarza Bluszcza — to on to wymyślił. A żeby nie rozpłynął się w samozachwycie, to wymyślił też parę marnych rzeczy, ale tego co marne pozbędziemy się prędzej czy później...
Ktoś (imienia nie wymienię) wysłał klientowi teksty do przetłumaczenia... w Excelu. Klient zadowolony, że nie musi używać żadnych hackerskich narzędzi typu poEdit, oczywiście przetłumaczył teksty w tymże Excelu, a nam przyszło załamać ręce. Ale tylko na chwilę.
Dzięki bibliotece polib załatwiliśmy sprawę w pół godziny. A żeby zaoszczędzić stękań na przyszłość, następne partie tłumaczeń też będziemy wysyłać w Excelu. A co!
Jest kilka projektów związanych z Django, na które warto jest mieć oko, z różnych względów. Niektórych używam, więc z konieczności śledzę ich rozwój (m.in. django-registration i django-comment-utils), ale są też takie, które rokują rozwiązanie trapiących mnie problemów.
Jednym z takich projektów jest django-orm-cache, a problem, który ma on rozwiązać dotyczy każdego, kto używa cache na poziomie obiektów w Django. Został on opisany przez Davida Cramera (który także jest pomysłodawcą i jednym z developerów tego projektu). W skrócie chodzi o to, że używany na poziomie obiektów i ich grup cache staje się dodatkowym storage, w którym trzeba odzwierciedlić zmiany, wprowadzone w głównym storage (czyli w samych obiektach). Dobrego rozwiązania nie ma — można rygorystycznie pilnować wygaszania obiektów w cache przy każdej modyfikacji (przydają się tu sygnały), można wersjonować klucze przy użyciu jakiegoś znacznika, ale żadne z tych rozwiązań nie jest ani wygodne, ani łatwe. Django-orm-cache ma ułatwić życie, przejmując zadanie automatycznego wygaszania obiektów w cache przy każdej modyfikacji obiektu.
Projekt nie jest jeszcze w stanie nadającym się do użytku, ale widać w nim jakiś ruch i to dobrze wróży. Chciałbym już teraz mieć coś takiego w mojej narzędziówce w pracy...
Bluszcz zażyczył sobie RSS-ów dla konkretnych kategorii, żeby odfiltrować offtopiki na planecie. Tradycyjnie na wyrost kazałem mu się zgłosić w przyszłym tygodniu, ale to nie byłaby aplikacja w Django, gdyby rzeczywiście miało to tyle trwać. I trwało w sumie 20 minut. Proszę, masz RSS-y z kategorii (u mnie się one nazywają etykietami). Tę, która Cię interesuje, znajdziesz na stronie z wpisami pod etykietą python.
Django zdaje się być całkiem w poprzek przyjętej zasadzie w świecie OS/FS: release early, release often. Ostatnie oficjalne wydanie Django miało miejsce 11 miesięcy temu, (23 marca 2007 roku). Rozumiałbym to, gdyby w projekcie niewiele się działo i rzeczywiście nie byłoby co wydawać, ale działo się wiele. Od tamtego czasu doprowadzono do używalności newforms, wprowadzono pełną wewnętrzną unikodowość, znacząco zmieniono także wyjście wprowadzając automatyczne eskejpowanie zmiennych podczas renderowania szablonów. Każde z tych wydarzeń (a pewnie i kilka innych) zasługiwałoby na wydanie, choćby po to, by nie powiększać przepaści pomiędzy kodem wydanym oficjalnie, a kodem rozwojowym.
Nie podoba mi się to.
Na pewno nie średników i klamerek. ;)
Java jako język może nadawać się najwyżej do programowania pralek. Jako platforma może być przegniła do korzenia. Ale ma coś, czego co jakiś czas brak odczuwam bardzo mocno programując w Pythonie — zestaw specyfikacji do J2EE. Specyfikacja jak to specyfikacja, to suchy papier, może być bez sensu, ale złe są dopiero implementacje. Spośród tych wszystkich większość jest dość specyficzna dla Javy ("stwarzamy problemy by bohatersko je pokonywać", czy jakoś tak to było), ale są dwie, które ja uważam za perełki. I to są te dwie, których brakuje mi najbardziej:
-
JNDI, czyli uniwersalne usługi katalogowe;
-
JMS, czyli integracja komponentów aplikacji przez komunikaty.
Cała reszta może się schować, ale tych dwóch naprawdę mi brakuje. Nie to, żeby Python potrzebował takich specyfikacji, ja potrzebuję standardowych implementacji. O, jakby mi ułatwił życie taki serwer JMS z interfejsem w Pythonie (nie, ActiveMQ + Stomp to jeszcze nie jest to!)...
Tym razem zupełnie niechcący. Kończyła mi się umowa u komórkowców, więc ją sobie przedłużyłem, przy okazji wymieniając telefon na nowy. Dopiero po powrocie do domu, przeglądając instrukcję obsługi, znalazłem logo Symbiana i coś na temat S60... Czyli mam fona z S60! :D
Pierwszą rzeczą, którą sobie zainstalowałem był (a jakże inaczej) PyS60...
Na Django People jesteśmy teraz na 4 miejscu, razem z Niemcami. Uważam to za całkiem niezły wynik. Przed nami są Brazylia, Wielka Brytania i US of A (w tym przypadku chyba bardziej sprawiedliwe byłoby liczenie według stanów, ale się nie upieram), więc raczej kraje albo dużo bardziej liczne ludnościowo od nas, albo bardziej ambitne, jeżeli chodzi o pokazanie się. Mogę się mylić (i pewnie się mylę), ale każde miejsce wśród pierwszych 7-8 powinno nas zadowalać, choćby z powodu tego, ile osób przychodzi na WARPY (w tym tygodniu zostało odwołane). Python zajął już poczesne miejsce w galerii języków programowania, a Django jest już powszechnie rozpoznawane.
To dobrze. Roboty jest dużo, a ten wózek musi się toczyć.
W szczególności tutoriale do ramówek webowych. Każda ramówka w pewnym momencie dorabia się "20 minutes wiki tutorial" (czasem nawet w formie screencastu), ale w większości przypadków są one całkowicie bez sensu — nie pokazują tego, jak ramówka działa, nie uczą, jak jej używać, nie opisują jej komponentów. Zaczęło się od niesławnego screencastu RoR, a potem było już z górki. Pamiętam kilka lat temu, gdy w krótkim czasie wystartowały TurboGears i zaraz potem Django, że początkowo TG wygrywało popularnością właśnie z powodu posiadania takiego screencastu (i uzyskiwało opinię łatwiejszego do nauczenia). Lista mailowa TG pękała w szwach, zapowiadano nowe, ekscytujące możliwości kolejnych wydań ramówki, ale minęło kilka-kilkanaście miesięcy i to Django przejęło inicjatywę. Po kolejnych kilku-kilkunastu miesiącach TG uchroniło się przed zniknięciem, ale jak na pioniera radzi sobie coraz gorzej, już nie jest nawet numerem 2, bo w staraniach o przejęcie władzy nad sercami i umysłami developerów wyprzedziło je Pylons. Na szczęście, Pylons również posiada 20 minutes wiki tutorial, więc pozycja Django (które posiada jedynie nieoficjalny screeencast wiki na ShowMeDo) wydaje się być niezagrożona. ;)
Tak sobie właśnie przypomniałem... Jakiś czas temu prorokowałem, że z pierdyliarda ramówek webowych w Pythonie (był taki okres, kiedy powstawała jedna ramówka dziennie...), pozostanie nam 2-3 liderów i plankton — i tak się właśnie stało. Kto wymyśli tę, która zdetronizuje obecny numer 3 i stanie do wyścigu o pierwsze miejsce?
To mało czy dużo? Zależy... Moja aplikacja daje radę. Działa na jednym procesie FastCGI, swoje przydziałowe (?) 64MB RAM wykorzystuje w 100%. Szukałem jakichś wskazówek, jak ograniczyć apetyt aplikacji Django (uruchamianej na FastCGI) na RAM, ale znalazłem niewiele, a już na pewno nic nowego, nic, czego bym już nie wiedział. Być może nie jestem jeszcze aż tak bardzo zdesperowany, bo nie odczuwam, żeby aplikacji brakowało pamięci. Może to jest całkiem wystarczająca ilość, skoro aplikacja się jeszcze nie krztusi?
E, tam, nie mam chyba większych problemów... Jeszcze kilka lat temu nie przeszłoby mi przez myśl, że będzie mnie stać na hostowanie gdziekolwiek aplikacji w czymkolwiek innym, niż PHP. Zanim dorobiłem się hostingu na megiteam.pl nawet zastanawiałem się, czy nie iść na taniochę i nie przeprosić się z PHP. A tu — prawie jak spełnienie marzenia.
Aktualizacja z 18 stycznia: pani Magda Zarych, właścicielka megiteam.pl, uściśliła moje domysły. Aplikacja nie spożywa 64M, lecz w granicach 18M. Patrzyłem nie na to, co trzeba. Swoją drogą, przyjemnie, że firma wsłuchuje się w bicie serca klientów. ;)
Podkusiło mnie i postanowiłem zrobić tutorial Pylons, trochę z ciekawości, a trochę podążając za modą. Ogólne wrażenie było raczej... marne. Zarówno jeżeli chodzi o Pylons, jak i o sam tutorial.
Przede wszystkim, tutorial stanowił nie lada wyzwanie. W pewnym momencie zalecane do wykonania polecenie zakończyło się bardzo brzydkim tracebackiem, który niewiele mówił. Nie wiedząc, co zrobiłem źle, darowałem sobie dalsze przerabianie materiału, ale następnego dnia zaświtała mi pewna idea i po sprawdzeniu okazało się, że miałem rację — kilka sekcji dalej omówione zostało ustawienie konfiguracji, którego brakowało. Po pokonaniu tej przeszkody udało się dobrnąć do końca.
Teraz trochę o wrażeniu, jakie robi automatycznie wygenerowany szkielet aplikacji. Zawiera on dużo więcej, niż szkielet, jaki generuje Django. W prawie każdym pliku używana jest konstrukcja from package.module import * co powoduje, że właściwie nie wiadomo, skąd co pochodzi. Dodatkowo autor tutoriala ma zadziwiający zwyczaj używania jednoliterowych skrótów, jak np. "h" dla "helpers" czy "c" dla "context" (wygląda to na dość powszechny obyczaj). Ilość tekstu do napisania się zmniejsza, ale traci na tym czytelność. Do tego dokłada się jeszcze duża ilość modułów, które importowane są na wszelki wypadek, a przynajmniej bez jakiegokolwiek wytłumaczenia. Wszystko to składa się na ogólne wrażenie chaosu.
A teraz o samej ramówce, a raczej o tym, co jest zalecane jako jej elementy w wersji 0.9.6.1 (bieżącej w chwili pisania tego artykułu). SQLAlchemy jest potężne, skomplikowane i nieprzyjemne. Routes jest potężne, skomplikowane i nieprzyjemne. System szablonów Mako jest potężny, skomplikowany i nieprzyjemny. Paster jest potężny, skomplikowany i nieprzyjemny. W efekcie Pylons jako całość jest potężne, skomplikowane i nieprzyjemne. Zupełnie jak Spring. Na pewno można przy jego użyciu zrobić milion sprytnych rzeczy, ale nie chodzi o to, żeby robić sprytne rzeczy, tylko żeby zrobić co trzeba i mieć przy okazji trochę dobrej zabawy.
Konkluzja jest dość oczywista — dopóki nie będę musiał, nie porzucę Django dla Pylons. Z pracy przy projektach w Django mam przynajmniej sporą dozę radochy...
Tym razem będę się produkował ja. :)
Opowiem trochę o programowaniu aplikacji GUI przy użyciu PyGTK, raczej od podstaw, niż dla bardziej zaawansowanych.
Chciałem podłączyć moją usługę (webservice zrobione na podstawie twisted.web) do serwera Zeroconf i okazało się, że to całkowicie niemożliwe w obecnej sytuacji. Jedyna dobra biblioteka do tego, czyli avahi, jest tak mocno zintegrowana z DBus, że wymaga... głównej pętli programu zrobionej na GObject, żeby móc działać asynchronicznie. Porażka. Znalazłem wstęp do implementacji usług związanych z mDNS przy użyciu Twisted, ale oczywiście niepełną.
Chyba trzeba będzie sobie wreszcie ubrudzić ręce trochę poważniejszym kodem...
Pół dnia spędziłem na szukaniu błędu, który ukrył się w poniższym fragmencie kodu:
try:
contactsIndex = self.names.index('Contact')
contacts = items[contactsIndex]
mail = email_re.findall(contacts)[0]
if mail:
logger_main.debug('Email %s found within ad data' % mail)
report_counters['ads_with_emails'] = report_counters['ads_with_emails'] + 1
except:
mail = False
Wszystko wydawało się w porządku, ale nie było. Klauzula except bez wyspecyfikowania wyjątku zachowuje się tak, że wyłapuje wszystko. Dopiero zapisanie jej w postaci except Exception, e: i zalogowanie wyjątku pokazało, gdzie tak naprawdę ten błąd był. Zupełnie nie tam, gdzie się spodziewałem — klucz słownika report_counters wcale nie nazywał się 'ads_with_emails' (mniejsza o to, jak się nazywał).
Po prostu się nie spodziewałem. Perfidia.
Po Pro Django, Web Development Done Right szykuje się kolejna książka o tej ramówce — James Bennet zaanonsował, że pisze książkę, która omawia Django od strony praktycznej. Książka jest już listowana na Amazon.com, więc sprawa wygląda na poważną. Wypada się tylko cieszyć. Przyda się taka książka wszystkim początkującym.
Jednym z zagadnień przez tę książkę poruszanych ma być pierwsza aplikacja w Django, czyli właśnie silnik blogowy. Ja swój napisałem w ciągu kilku wieczornych sesji, właśnie jako projekt szkoleniowy z migracji na nową wersję Django (choć wcale nie był pierwszą aplikacją, moją pierwszą aplikację można już podziwiać od dłuższego czasu w Rumunii, na Węgrzech i w Wielkiej Brytanii). Trudno jednak wymagać, by ktokolwiek zaczynał zaznajamianie się z Django od dużego, komercyjnego projektu na zlecenie międzynarodowego klienta...
Coraz bardziej niecierpliwię się tym, że Django wciąż nie ma oficjalnego wydania wersji, która miałaby pełne wsparcie dla unikodu i działające newforms. Pojawiły się pogłoski, że następne wydanie to nie będzie oczekiwane przez wszystkich 0.97, ale od razu 1.0 — to by oznaczało, że ilość rzeczy, jakie trzeba będzie zrobić podczas migracji będzie podobna, jak przy 0.91. Tutaj nie ma to wielkiego znaczenia, ale w pracy będziemy mieli dylemat...
Dziś na PW kolejny wykład w ramach WARPY. Z powodów osobistych się nie wybiorę, niestety.
Następny wykład będzie 10 stycznia 2008, o ile nie zajdą jakieś nieprzewidziane okoliczności. I będę się starał wygłosić prelekcję o PyGTK.
Mądrzejsi ode mnie znaleźli błąd, poprawili i Django znowu działa przez FastCGI. Chwała nam i naszym kolegom, wiadomo komu precz!
Dwa straszliwe bugi objawiły się w projektach, które są mi z różnych powodów bardzo bliskie. Mało tego, żaden nie został jeszcze poprawiony.
Gajim ma problem z GnuPG — pewnym obejściem jest używanie gpg-agent (w sumie i tak powinienem go używać, ale chyba nie jestem jeszcze aż takim paranoikiem), ale jest możliwość doprowadzenia swojej konfiguracji do takiego stanu, że Gajim nie da się uruchomić. Wystarczy odhaczyć w ustawieniach konta "Używaj gpg-agent" i mieć wybrany jakiś klucz GPG do szyfrowania. Gajim daje segfaulta podczas uruchamiania. Sprawdzę jutro w pracy, czy ma to coś wspólnego z oprogramowaniem, jakie mam na domowej maszynce.
Django z powodu przeciekającego skądś obiektu SafeString zwraca nieprawidłową zawartość HttpResponse. Z tego powodu Flup odmawia współpracy i ogólnie w tej chwili trunk Django nie nadaje się do użycia przez FastCGI. Sprawdziłem to tymi rękami, na tym sajcie — nie działa. Nie jestem aż tak dobrze obeznany z internalsami Django, żebym zabierał się za zmiany w kodzie, zwłaszcza, że byłoby to grzebanie w HttpResponse... Nie da rady, trzeba czekać.
Albo WARPY, albo może WarPy... Nie wiem, jaka jest oficjalna pisownia tego dziwnego skrótowca, nawet pomimo tego, że w jego powstawaniu brałem udział kilka tygodni temu w pubie Wetlina. Dość, że robocza nazwa staje się powoli nazwą oficjalną.
We czwartek 29 listopada o 19.00 na Politechnice Warszawskiej w sali AL odbędzie się pierwsza prelekcja, którą wspólnymi siłami (Grono.net, Sensisoft, WARLUG i WO@PW) urządzamy celem popularyzacji Pythona. Zacznie Marek Pułczyński z Grona, od standardów pisania kodu w Pythonie. Z agendy wnoszę, że będzie głównie opowiadał o trzymaniu się konwencji, dokumentowaniu kodu przy użyciu docstrings i budowaniu pakietów, ale temat może odpłynąć, bo zagadnienie jest... dość nośne. Kolejne spotkania będą się odbywać co dwa tygodnie.
Na prelekcję za dwa tygodnie ostrzy sobie zęby Rafał Zawadzki, więc ja się wstrzelę pewnie dopiero za 6 tygodni. Na razie planuję opowiedzieć coś o PyGTK, ale przecież wszystko może się odmienić...
Każdy, kto programuje w Pythonie dłużej niż kilka godzin, na pewno kiedyś zetknął się z tym:
Python 2.5.1 (r251:54863, Oct 5 2007, 13:36:32)
[GCC 4.1.3 20070929 (prerelease) (Ubuntu 4.1.2-16ubuntu2)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import this
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!
Często zdarza mi się zapomnieć o jednej z tych zasad: że practicality beats purity. Mamy teraz taki drobiazg do zrobienia, przydałoby się wyjąć z jednego modelu wszystko, co jest związane z jego wyświetlaniem. Oczywiście, nie tylko dlatego, że to łamie podstawową zasadę MVC i miesza model z jego wyświetlaniem — z tego powodu jest to tylko brzydkie, a poza tym to jest też dość niezręczne w zarządzaniu. Wiele już myślałem nad tym, jak wyjąć konfigurację wyświetlania tego modelu żeby wyglądało to ładnie no i chyba właśnie wygrało to practicality — zrobimy oddzielny model na DisplayProperties.
Jak wiadomo, Django używa pakietu Flup żeby uruchamiać aplikacje w środowisku serwera oferującego FastCGI (Apache + mod_fastcgi, nginx, lighttpd). Trafiłem na coś, czego jeszcze do tej pory nie widziałem na oczy. Nigdzie i nigdy.
Nasza aplikacja nigdy nie wyrzuca błędu. Nie mówię o 500, w końcu istnieją aplikacje doskonałe, ale nawet 404, i to pomimo tego, że w kodzie w wielu miejscach leci wyjątek django.http.Http404. W takiej sytuacji aplikacja wyświetla jakąś statyczną stronę (nie ma to nic wspólnego z naszym szablonem 404.html czy 500.html) i zwraca 200. Dogrzebałem się, że jest to efekt działania ErrorMiddleware z Flupa, który w ten sposób próbuje zwrócić na siebie uwagę (przy okazji wysyła maila do administratora, ale u nas to nie występuje...). Podobno występuje to wtedy, gdy błąd taki nie zostanie obsłużony przez aplikację, co w naszym przypadku jest o tyle dziwne, że podobno przecież Django obsługuje wszystkie wyrzucone wyjątki przy użyciu odpowiedniej dla typu wyjątku funkcji.
Co ciekawsze, okazało się, że najnowsza wersja Flup-a (1.0) nie zawiera już tego middleware, więc gdyby się okazało, że Django bez problemu działa z tą wersją, to może oznaczać tylko głębokie problemy z naszą konfiguracją na styku mod_fastcgi i Django.
Zdarza się tak, że domyślny sposób buforowania, używany przez Django (buforowanie całych stron, buforowanie całego serwisu) nie wystarcza i trzeba buforować pojedyncze obiekty lub ich listy. Gdy się już zacznie, trudno jest przestać i wtedy okazuje się, że nie sposób dojść do tego, pod jakim kluczem co jest zbuforowane. Sytuacja komplikuje się dodatkowo, gdy używa się memcache, a już całkiem, gdy w buforze trzyma się dane sesji. Najłatwiej byłoby zrestartować memcached, ale przecież polecą sesje użytkowników... Zaczyna się gorączkowe szukanie klucza i strzelanie w ciemno, ale to naprawdę głupiego robota, bo klient memcache zawsze zwraca 1, gdy nie wystąpi żaden krytyczny błąd. Rozwiązaniem byłoby przejrzenie listy kluczy (np. przeiterowanie przez nie), ale czegoś takiego nie ma.
Wpadłem na pomysł prostego rejestru kluczy w postaci zbioru (set). Zdaje się to działać na tym serwisie (który jest trochę poligonem...), a jak działa w naprawdę dużym serwisie, to się dopiero okaże jutro, jak sprawdzę to w pracy. Na razie jestem dobrej myśli, bo idea zdaje się nie mieć słabych punktów... ;)
Subtelna różnica, która może okazać się zdradliwa... Gdy definiuje się wartość domyślną dla któregoś z atrybutów modelu w Django, jest ona ustawiana w momencie kompilowania modułu. Niepotrzebna para nawiasów w tym miejscu spowoduje, że zamiast wyniku wywołania jakiejś funkcji w momencie tworzenia egzeplarza klasy, wstawiona zostanie stała wartość, obliczona w momencie kompilacji modułu (czyli najczęściej przy uruchomieniu aplikacji). Warto jest się zastanowić, czy właśnie tego się chce, bo default=datetime.datetime.now daje inny wynik, niż default=datetime.datetime.now().
Spotkałem się w niedzielę z pewnym człowiekiem... Zaproponował mi wzięcie udziału w dużym, celującym wysoko międzynarodowym projekcie serwisu, oczywiście nie za darmo. Sama propozycja bardzo mile podłechtała moją próżność (podobnie jak kilka miesięcy wcześniejsza propozycja wzięcia udziału w rekrutacji do Google), więc bardzo się nie opierałem przed osobistym spotkaniem, pomimo tego, że miało ono nastąpić w sobotę lub w niedzielę. Katalizatorem dobrego nastawienia do propozycji było to, że człowiek ten przyjechał specjalnie w niedzielę z Krakowa, żeby spotkać się ze mną i z jeszcze jedną osobą z nieodległej firmy (technologicznie i geograficznie), którą też chciał w ten projekt wciągnąć. Umówiliśmy się na 17:00 w Złotych Tarasach, żeby omówić projekt i kilka wstępnych ustaleń.
Człowiek okazał się bardzo miłym młodzieńcem, naprawdę zapalonym do swojego pomysłu. Przedstawił swoją wizję dość klarownie, jednak próba pokazania mi działającego dema zakończyła się katastrofą i nie zobaczyłem nic, oprócz strony z debugiem błędu w Django. Pierwsze skojarzenie, jakie mi się nasunęło, to katastrofa, której doznaliśmy w lipcu, gdy próbowaliśmy pokazać klientowi aplikację na Węgrzech — wtedy przez 5 minut było OK, ale potem wszystko zaczęło się walić. Tutaj nie było nawet tych pięciu minut, co przy założeniu, że aplikacja ma być gotowa za miesiąc, oznaczać może tylko jedno — stan aplikacji jest bardziej niż niepokojący. Z innych rzeczy pozaproduktowych, kwestię finansową przemilczę, bo moja matka twierdzi, że przewróciło mi się w głowie i straciłem kontakt z rzeczywistością. Wystarczy że napiszę, że nie miałbym lepiej, niż mam teraz.
A teraz o samym produkcie. Nie poruszyła mnie jego idea. Być może jestem za stary na takie gwizdki, być może przez cały czas myślę jak człowiek z trzeciego świata, ale faktem jest, że nie poruszyła mnie wizja milionów ludzi płacących po $10 rocznie za coś, co tylko podbudowuje ich ego. Bardzo możliwe, że w ten sposób działają nastolatki, bardzo też możliwe, że postępujące otumanienie społeczeństw konsumpcyjnych idzie w parze z zatrzymaniem rozwoju mentalnego na poziomie nastolatków. Wiem, że można na tym robić pieniądze (vide przykład MySpace czy Facebook) i to nawet ogromne, jednak nie przekonuje mnie to. Wydaje mi się, że na długi czas zatrzymałem się na poziomie Web1.0 i z dużym trudem doszlusowałem do poziomu Web2.0, nie połykając go w całości jak leci, a jedynie przyswajając sobie kluczowe idee. Z każdą kolejną iteracją główna idea internetu wydaje mi się odchodzić w kierunku coraz głębszego autyzmu, a projekt przedstawiony przez tego człowieka wydaje mi się tak autystyczny, że bardziej już nie można. Więcej o filozoficznych podstawach mojego poglądu na rozwój internetu pewnie jeszcze kiedyś napiszę.
Konkudując w skrócie, mam jeszcze kilka godzin na zastanowienie się, ale nie przypuszczam, żeby cokolwiek było w stanie zmienić moją decyzję. Nie wchodzę w to.
Jeśli edytor, to wiadomo: Vim. A jeśli pisanie w nim kodu, to wiadomo, co jest jedną z podstaw: podświetlanie składni tak, żeby wszystko było dobrze widoczne, a jednocześnie w takim zestawie kolorów, który nie męczy oczu, nawet wtedy, gdy trzeba patrzeć w ekran po kilka godzin. W takich przypadkach wygrywają zestawy raczej mało kontrastowe i raczej te z ciemnym tłem. Oto trzej moi faworyci, w kolejności niekoniecznie przypadkowej:
-
oceandeep, utrzymany w zielono-niebieskiej tonacji, prawie bez mankamentów;
-
desert, brązowo-beżowy, cokolwiek nudny, ale bardzo spokojny;
-
zenburn, faworyt wielu, dla mnie trochę więcej niż akceptowalny, jakoś nie przekonuje mnie aż tak niski kontrast, ale czasem (szczególnie późno w nocy) jest ukojeniem dla oczu.
Ja muszę być jakimś fanatykiem, czy innym zboczeńcem... Kiedy kilka tygodni temu zastanawialiśmy się z moim PM-em, czy mam zająć się nowym projektem, czy może pozostać w obecnym i trzymać go za mordę, sam zaproponowałem, że zostanę, bo znam go dobrze i szkoda byłoby marnować taki kawał wiedzy. Teraz mam za swoje — na trzech instancjach produkcyjnych ciągły pożar, w planie rozwoju dwa duże kawały kodu do napisania, a ja spędzam większość czasu robiąc jako administrator i release manager jednocześnie, z otwartymi kilkunastoma sesjami SSH.
Ale spoko, jest dobrze. Większość kodu to Python + Django, więc i tak jest lepiej, niż w Generali. Pod względem ilości pracy sytuacja tam była dość luksusowa, ale co to była za praca... ;)