Webservices ujęcie ostatnie (chyba).
Usługom sieciowym pościęcono tu już kilka notek. Jednak zagadnienie to było potraktowane w bardzo uproszczony sposób. Co byś powiedział, gdybyśmy razem napisali taki półprofesjonalny webservice do udostępniania i zapisu danych? Przy okazji moglibyśmy trochę pogłębić wiedzę o HTTP. Jeśli już jednak taką wiedzę posiadasz, to nie zniechęcaj się, ponieważ użyjemy jej w praktyce. Załóżmy, że chcemy mieć bloga o tematyce technologicznej. Tematyka technologiczna zobowiązuje, więc chcemy mieć możliwość publikacji treści z urządzeń mobilnych. Oczywiście większość nowoczesnych tabletów i telefonów ma w pełni sprawne przeglądarki WWW, więc moglibyśmy blogować poprzez przeglądarkę. Jednak my zdecydujemy się na podejście obecnie częściej używanie - zastosujemy specjalną aplikację mobilną. Brzmi chyba dość atrakcyjnie, ale w dzisiejszej notce skupimy się tylko na stworzeniu werbservice do zapisu i czytania danych. Samą aplikacją mobilną zajmiemy się w następnym "odcinku".
Co będzie nam potrzebne?: * web2py (oczywiście) * moduł requests (bardzo ułatwi nam testowanie działania programu)
Moduł requests bywa bardzo pomocny w obsłudze protokołu HTTP. Oczywiście w twojej instalacji pythona masz już urllib2 jednak jest to dość stary moduł i robienie niektórych rzeczy za jego pomocą jest bardzo uciążliwe. Możliwość bardziej "ludzkiego"" podejścia do HTTP zapewnia właśnie requests. Moduł instalujemy na komuterze klienckim z którego będziemy tesować działanie bloga. Jeśli server i klient to ten sM komputer - po prostu zainstaluj sobie moduł. Instalujemy jak zwykle:
lub jeśli ktoś woli lub musi
Jak już mamy moduł zainstalowany, to możemy sprawdzić czy działa.
>>> import requests as rq >>> print rq.__doc__
W konsoli powinny się pojawić przykłady użycia modułu. Proste łatwe i przyjemne, prawda? Za to właśnie kochamy pythona.
Zgodnie z promowaną przeze mnie konwencją zacznijmy pracę od modelu danych.
# model # tablica do przechowywania notek bloga db.define_table('posts', Field('title', 'string', requires=IS_NOT_EMPTY()), Field('post_body','text', requires=IS_NOT_EMPTY()), Field('time_stamp','datetime',update=request.now) ) # tablica do przechowywania zdjęć i innych mediów db.define_table('media', Field('post_id','reference posts'), Field('media_file','upload'), Field('time_stamp','datetime',update=request.now) )
Struktura modelu danych nie powala na kolana, jednak w naszym wypadku powinna być wystarczająca. Teraz czas na kontroler. W poprzednich notkach posługiwaliśmy się dekoratorami @service.json i @service.run to uruchamiania prostych usług. Tym razem pójdziemy nieco dalej i zbudujemy RESTful webservice. Jeśli wpiszesz w wikipedii hasło REST, to dostaniesz dość enigmatyczną definicję. Mi osobiście bardzo podoba się tłumaczenie zawarte w tej notce bloga Patryka YARPO Jara. Pomijając wszystkie kwestie ideologiczne, chodzi o posługiwanie się standardowymi żądaniami protokołu HTTP, a są to GET, POST, PUT i DELETE, do działań na danych i zespołach danych (to mi definicja wyszła). Jeśli powyższe nie jest dla ciebie nowością, to możesz pominąć czytanie następnej sekcji.
O metodach już wspomniałem, są to: * GET - pobieranie danych * POST - wysyłanie danych * PUT - modyfikacja istniejących danych * DELETE - usuwanie danych
Pierwsze dwie metody są bardzo często używane. Niektórzy nawet zaczynają od nich dzień. Zobaczmy co robi GET. Zainstalowałeś już requests? Teraz nam się przyda.
import requests as rq url = 'http://web2py4pl.blogspot.com' r = rq.get(url) print r.status_code print r.content
Po wykonaniu powyższego powinien się wyświetlić wynik w postaci liczby 200 i HTMLowych robaczków składających się na tego bloga. 200, czyli wszystko poszło OK, gdybyśmy dostali np. 404 to znaczyłoby, że tego co chcemy nie ma na serwerze. Jeśli zafascynowały cię kody statusów HTTP to tu znajdziesz o nich więcej (bo jest ich więcej). Za każdym razem, gdy wysyłasz jakiś formularz na stronie WWW, najprawdopodobniej używasz metody POST by wysłać dane na server. Tak w wielkim skrócie twoja przeglądarka rozmawia z serverami. Ktoś bardziej złośliwy mógłby zabytać o przykład z użyciem metody POST - spokojnie, jeszcze do tego dojdziemy. Metody PUT i DELETE są trochę rzadziej używane (przynajmniej nie tak jawnie) ale nam się bardzo przydadzą.
Poniżej umieściłem przykład bardzo prostego kontrolera implementującego część modelu REST. Ma on na celu zobrazowanie tego, o czym rozważaliśmy bardzo teoretycznie.
# kontroler @request.restful() def myservice(): response.view = 'generic.json' def GET(*args,**vars): data = request.now return dict(data=data) def POST(*args,**vars): data = vars['data'] return dict(data=data) return dict(GET=GET, POST=POST)
Zanim omówimy powyższy kod, spróbujmy go uruchomić. Aby to zrobić, możesz przekleić treść do pliku kontrolera default. Zacznijmy od GET, wpiszmy w pasku adresu http://127.0.0.1/twoja_aplikacja/default/myservice powinieneś dostać wynik jak poniżej, czyli JSON zawierający klucz data z wartością bieżącej daty i godziny.
{ data: "2015-09-28 09:18:53" }
Posiadając samą przeglądarkę bez dodatkowych wtyczek i rozszerzeń, trudno nam będzie przetestować działanie metody POST naszego serwisu. Z tego właśnie powodu znów posłużymy się modułem requests.
import requests as rq import json url = ' http://127.0.0.1:8000/twoja_aplikacja/default/myservice' data = json.dumps({'data':'1939-09-01'}) headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} r = rq.post(url, data=data, headers=headers) print r.status_code print r.content
W tym przykładnie wysyłamy JSON do serwisu. Zmienna data zawiera JSON powstały ze słownika {'data':'1939-09-01'}. Zmienna headers jest słownikiem mówiącym modułowi requests, jakie dane będziemy wysyłać i w jakiej formie. Kod statusu i r.content to odpowiedź servera na nasze żądanie. Server zwrócił nam dokładnie to, co mu wysłaliśmy.
Myślę, że jak "zderzysz ze sobą" oba fragmenty kodu, to wszystko już będzie jasne. To co wysyłamy do servera, jest dostępne za pomoca vars. Args to nic innego, jak argumenty przekazane w URL. Uzbrojeni w tę wiedzę możemy spokojnie przystąpić do pisania naszej usługi do blogowania.
#kontroler aplikacji blogowej import StringIO import base64 @request.restful() def blogservice(): response.view = 'generic.json' def GET(*args,**vars): res = [] if len(args)>0: post_id = args[0] post = db(db.posts.id==post_id).select().first() res = dict(post_id=post.id, title=post.title, body=post.post_body, stamp=post.time_stamp) return res else: posts = db(db.posts.id>0).select() for post in posts: res.append(dict(post_id=post.id, title=post.title, body=post.post_body, stamp=post.time_stamp)) return dict(posts=res) def POST(*args,**vars): data = vars['post'] if 'title' and 'post_body' in data: pid=db.posts.insert(title=data['title'], post_body=data['post_body']) if 'media' in data: media = data['media'] for i in media: if 'media_file' in i: infile = i['media_file'].decode('base64') fh = StringIO.StringIO ( infile ) img = db.media.media_file.store(fh,i['media_name']) db.media.insert(post_id=pid, media_file=img) else: return dict(result='no media') return dict(result='OK') else: return dict(result='data error') def PUT (*args,**vars): data = vars['post'] if 'post' and 'title' and 'post_body' in data: n= db(db.posts.id==data['post']).update(title=data['title'], post_body=data['post_body']) return dict(result='%s rows changed' %n) else: return dict(result='data error') def DELETE (*args,**vars): pass # zostawiamy bez obsługi return dict(GET=GET, POST=POST, PUT=PUT, DELETE=DELETE)
Podsumowując: * jeśli zażądamy GET- dostaniemy wszystkie posty * jeśli zrobimy POST- wstawimy nowy rekord do tablicy posts * jak zrobimy PUT - zrobimy aktualizację rekordu o id równym post * DELETE, chytrze nie obsłużyliśmy by nie usuwać danych (jeśli chcesz możesz dość prosto obsłużyć)
Poniżej skrypt do przetestowania serwisu:
import requests as rq import json import base64 #plik zdjęcia picture = open('FullSizeRender.jpg', 'rb') fil = base64.b64encode(picture.read()) picture.close() url = 'http://127.0.0.1:8000/blogger/default/blogservice' media=[dict(media_file=fil, media_name='plik.jpg')] post = dict(post=dict(title='wpis testowy', post_body='to jest testowa zawartość', media=media)) data = json.dumps(post) headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} r = rq.post(url, data=data, headers=headers) print r.status_code print r.content
W następnej notce napiszemy widok do tego modelu i klienta dla systemu Android.