Chapter 10: Usługi

Usługi

usługi internetowe
API

Standard W3C definiuje usługę internetową jako "system oprogramowania przeznaczony do obsługi interoperacyjnej interakcji maszyna-maszyna poprzez sieć". Jest to bardzo szeroka definicja i obejmuje dużą ilość protokołów zaprojektowanych do komunikacji maszyna-maszyna, takich jak XML, JSON, RSS itd.

W tym rozdziale omawiamy, jak udostępnić usługi internetowe używając web2py. Przykłady wykorzystania usług zewnętrznych (Twitter, Dropbox itd.) można znaleźć w rozdziale 9 i 14.

Platforma web2py dostarcza standardowo obsługę wielu protokołów, w tym XML, JSON, RSS, CSV, XMLRPC, JSONRPC, AMFRPC i SOAP. Platformę web2py można rozszerzyć o obsługę dodatkowych protokołów.

Każdy z tych protokołów jest obsługiwany na wiele sposobów. My dokonamy ich rozróżnienia ze względu na:

  • - renderowanie danych wyjściowych funkcji w określonym formacie (na przykład XML, JSON, RSS, CSV);
  • - zdalne wywołanie procedur (ang. Remote Procedure Calls, na przyklad XML-RPC, JSON-RPC, AMF-RPC).

Renderowanie słownika

HTML, XML i JSON

HTML
XML
JSON

Rozważmy następującą akcję:

1
2
3
def count():
    session.counter = (session.counter or 0) + 1
    return dict(counter=session.counter, now=request.now)

Akcja ta zwraca licznik, który zwiększa swoją wartość o jeden, gdy użytkownik ładuje ponownie stronę oraz zwraca znacznik czasu żądania strony.

Zwykle strona ta może być żądana z adresu:

1
http://127.0.0.1:8000/app/default/count

i renderowana w HTML. Bez pisania ani jednej linii kodu, można prosić web2py, aby zrenderował tą stronę przy użyciu innych innych protokołów, przez dodanie rozszerzenia do adesu URL:

1
2
3
http://127.0.0.1:8000/app/default/count.html
http://127.0.0.1:8000/app/default/count.xml
http://127.0.0.1:8000/app/default/count.json

Słownik zwracany przez akcje będzie renderowany odpowiednio w formacie HTML, XML i JSON.

Oto wyjście XML:

1
2
3
4
<document>
   <counter>3</counter>
   <now>2009-08-01 13:00:00</now>
</document>

a to wyjście JSON:

1
{ 'counter':3, 'now':'2009-08-01 13:00:00' }

Proszę zwrócić uwagę, że obiekty data, time i datetime są renderowane jako łańcuch w formacie ISO. Nie jest to standard JSON, ale konwencja w web2py.

Widoki generyczne

widok generyczny

Gdy wywoływane jest rozszerzenie, na przykład ".xml", web2py szuka pliku szablonu o nazwie "default/count.xml" i jeśli nie znajdzie, szuka szablonu o nazwie "generic.xml". W aplikacji szkieletowej znajduja się pliki generyczne "generic.html", "generic.xml" i "generic.json". Inne rozszerzenia mogą zostać łatwo zdefiniowane przez użytkownika.

Ze względów bezpieczeństwa widoki generyczne są dostępne tylko na localhost. W celu udostępnienia zdalnego dostępu trzeba ustawić response.generic_patterns.

Zakładając, ze używamy kopię aplikacji szkieletowej, edytujmy następujaca linię w models/db.py:

  • - ograniczenie dostępu tylko do localhost
1
response.generic_patterns = ['*'] if request.is_local else []
  • - zezwolenie na użycie wszystkich widoków generycznych
1
response.generic_patterns = ['*']
  • - zezwolenie tylko na rozszerzenie .json
1
response.generic_patterns = ['*.json']

Atrybut generic_patterns jest globalnym wzorcem, co oznacza, że można użyć wzorców, które dopasowują wartości z akcji aplikacji lub przekazują listę wzorców.

1
response.generic_patterns = ['*.json','*.xml']

Użycie tego w starszych aplikacjach web2py wymaga skopiowania plików "generic.*" z nowszej aplikacji szkieletowej (po wersji 1.60).

Oto kod dla "generic.html"

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{{extend 'layout.html'}}

{{=BEAUTIFY(response._vars)}}

<button onclick="document.location='{{=URL("admin","default","design",
args=request.application)}}'">admin</button>
<button onclick="jQuery('#request').slideToggle()">request</button>
<div class="hidden" id="request"><h2>request</h2>{{=BEAUTIFY(request)}}</div>
<button onclick="jQuery('#session').slideToggle()">session</button>
<div class="hidden" id="session"><h2>session</h2>{{=BEAUTIFY(session)}}</div>
<button onclick="jQuery('#response').slideToggle()">response</button>
<div class="hidden" id="response"><h2>response</h2>{{=BEAUTIFY(response)}}</div>
<script>jQuery('.hidden').hide();</script>

To jest kod dla "generic.xml"

1
2
3
4
5
6
7
8
{{
try:
   from gluon.serializers import xml
   response.write(xml(response._vars),escape=False)
   response.headers['Content-Type']='text/xml'
except:
   raise HTTP(405,'no xml')
}}

a to kod dla "generic.json"

1
2
3
4
5
6
7
8
{{
try:
   from gluon.serializers import json
   response.write(json(response._vars),escape=False)
   response.headers['Content-Type']='text/json'
except:
   raise HTTP(405,'no json')
}}

Dowolny słownik można zrenderować w formacie HTML, XML i JSON o ile zawiera podstawowe typy Pythona (int, float, string, list, tuple, dictionary). response._vars zawiera słownik zwracany przez akcję.

Jeśli słownik zawiera inne obiekty zdefiniowane przez użytkownika lub obiekty specyficzne dla web2py, musi zostać zrenderowany przez własny widok.

Renderowanie obiektu Rows

as_list

Jeśli potrzeba zrenderować zestaw Rows w wybranym formacie XML, JSON lub innym, najpierw trzeba przekształcić obiekt Rows w listę słowników używając metodę as_list().

Przyjmijmy następujacy model:

1
db.define_table('person', Field('name'))

Poniższa akcja może zostać zrenderowana w HTML, ale nie w XML lub JSON:

1
2
3
def everybody():
    people = db().select(db.person.ALL)
    return dict(people=people)

natomiast ta oto akcja może być zrenderowana w XML i JSON:

1
2
3
def everybody():
    people = db().select(db.person.ALL).as_list()
    return dict(people=people)

Własne formaty

Jeśli przykładowo, chce się zrenderować wyjście akcji jako speklowany plik Pythona:

1
http://127.0.0.1:8000/app/default/count.pickle

trzeba stworzyć nowy plik widoku "default/count.pickle", który zawiera:

1
2
3
4
5
{{
import cPickle
response.headers['Content-Type'] = 'application/python.pickle'
response.write(cPickle.dumps(response._vars),escape=False)
}}

Jeśli chce się, by był to szablon dla renderowania dowolnych akcji w postaci peklowanego pliku, trzeba tylko zapisać powyższy plik pod nazwą "generic.pickle".

Nie wszystkie obiekty są możliwe do speklowania i nie wszystkie speklowane obiekty można odpeklować. Bezpiecznie jest, trzymanie się podstawowych obiektów Pythona i ich kombinacji. Obiekty, które nie zawierają odniesień do strumieni plikowych lub połączeń z bazą danych są zazwyczaj możliwe do peklowania, ale mogą być odpeklowane tylko w środowisku, w którym klasy wszystkich speklowanych obiektów są już zdefiniowane.

RSS

RSS

Platforma web2py zawiera widok "generic.rss", który może renderowć słownik zwracany przez akcję, jako kanał RSS.

Ponieważ kanały RSS maja ustaloną strukturę (title, link, description, items itd), to słownik zwracany przez akcję musi mieć taką właśnie strukturę:

1
2
3
4
5
{'title'      : '',
 'link'       : '',
 'description': '',
 'created_on' : '',
 'entries'    : []}

i podobną strukturę musi mieć każdy wpis:

1
2
3
4
{'title'      : '',
 'link'       : '',
 'description': '',
 'created_on' : ''}

Na przykład, następująca akcja może zostać zrenderowana jak kanał RSS:

1
2
3
4
5
6
7
8
9
def feed():
    return dict(title="my feed",
                link="http://feed.example.com",
                description="my first feed",
                entries=[
                  dict(title="my feed",
                  link="http://feed.example.com",
                  description="my first feed")
                ])

w wyniku odwiedzenia adresu URL:

1
http://127.0.0.1:8000/app/default/feed.rss

Rozpatrzmy dla odmiany taki oto model:

1
2
3
4
5
db.define_table('rss_entry',
    Field('title'),
    Field('link'),
    Field('created_on','datetime'),
    Field('description'))

poniższa akcja może również być renderowana jako kanał RSS:

1
2
3
4
5
def feed():
    return dict(title="my feed",
                link="http://feed.example.com",
                description="my first feed",
                entries=db().select(db.rss_entry.ALL).as_list())

Metoda as_list() obiektu Rows konwertuje wiersze do listy słowników.

Jeśli zostaną znalezione dodatkowe elementy listy z nazwami klucza nie wykazanymi tutaj, zostaną zignorowane.

Oto widok "generic.rss" dostarczony w web2py:

1
2
3
4
5
6
7
8
{{
try:
   from gluon.serializers import rss
   response.write(rss(response._vars),escape=False)
   response.headers['Content-Type']='application/rss+xml'
except:
   raise HTTP(405,'no rss')
}}

Jako jeszcze jeden przykład aplikacji RSS, rozważmy czytnik RSS, który zbiera dane z kanału "slashdot" fi zwraca nowy kanał RSS web2py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def aggregator():
    import gluon.contrib.feedparser as feedparser
    d = feedparser.parse(
        "http://rss.slashdot.org/Slashdot/slashdot/to")
    return dict(title=d.channel.title,
                link = d.channel.link,
                description = d.channel.description,
                created_on = request.now,
                entries = [
                  dict(title = entry.title,
                  link = entry.link,
                  description = entry.description,
                  created_on = request.now) for entry in d.entries])

Jest to dostępne na:

1
http://127.0.0.1:8000/app/default/aggregator.rss

CSV

CSV

Format CSV (Comma Separated Values) to protokół do reprezentowania danych tabelarycznych.

Rozważmy następujący model:

1
2
3
4
db.define_table('animal',
    Field('species'),
    Field('genus'),
    Field('family'))

i na stępującą akcję:

1
2
3
def animals():
    animals = db().select(db.animal.ALL)
    return dict(animals=animals)

Platforma web2py nie zawiera "generic.csv", musimy więc wykonać sami widok "default/animals.csv", który będzie serializował "animals" do CSV. Oto możliwa implementacja:

1
2
3
4
5
6
7
{{
import cStringIO
stream=cStringIO.StringIO()
animals.export_to_csv_file(stream)
response.headers['Content-Type']='application/vnd.ms-excel'
response.write(stream.getvalue(), escape=False)
}}

Proszę mieć na uwadze, że można również zdefiniować plik "generic.csv", ale trzeba by określić nazwę obiektu, który ma zostać zserializowany (w naszym przykładzie "animals"). Jest to powód nie zdefiniowania pliku "generic.csv" w web2py.

Zdalne wywołanie procedur - RPC

RPC

Platforma web2py posiada mechanizm do włączania dowolnej funkcji jako usługi internetowej. Mechanizm tu opisany różni się od mechanizmu opisanego poprzednio ponieważ:

  • funkcja może pobierać argumenty;
  • funkcja może być zdefiniowana w modelu lub w module, zamiast w kontrolerze;
  • można dokładnie określić, czy metoda RPC powinna być obsługiwana;
  • wymusza bardziej rygorystyczną konwencję nazewniczą URL;
  • jest inteligentniejszy od poprzednich metod, ponieważ działa przez określony zestaw protokołów; z tego powodu nie jest łatwy do rozszerzenia.

W celu użycia tej funkcjonalności, po pierwsze, trzeba zaimportować i inicjować obiekt usługi:

1
2
from gluon.tools import Service
service = Service()

Jest to już zrobione w pliku modelu "db.py" model w aplikacji szkieletowej.

Po drugie, trzeba udostępnić w kontrolerze obsługę usługi:

1
2
3
def call():
    session.forget()
    return service()

Jest to już zrobione w kontrolerze "default.py" aplikacji szkieletowej. Usuń session.forget(), jeśli planujesz stosowanie ciasteczek sesji w usługach.

Po trzecie, trzeba udekorować funkcje, które chce się udostępniać jako usługi. Oto lista obecnie obsługiwanych dekoratorów:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@service.run
@service.xml
@service.json
@service.rss
@service.csv
@service.xmlrpc
@service.jsonrpc
@service.jsonrpc2
@service.amfrpc3('domain')
@service.soap('FunctionName',returns={'result':type},args={'param1':type,})

Dla przykładu, rozważmy poniższą funkcję z dekoratorem:

1
2
3
@service.run
def concat(a,b):
    return a+b

Funkcja ta może zostać zdefiniowana w modelu lub kontrolerze, tam gdzie jest zdefiniowana akcja call. Po jej zdefiniowaniu jest możliwe jej zdalne wywołanie na dwa sposoby:

1
2
http://127.0.0.1:8000/app/default/call/run/concat?a=hello&b=world
http://127.0.0.1:8000/app/default/call/run/concat/hello/world

W obu przypadkach żądanie HTTP zwraca:

1
helloworld

Gdy użyje się dekoratora @service.xml, funkcję tą będzie można wywołać poprzez:

1
2
http://127.0.0.1:8000/app/default/call/xml/concat?a=hello&b=world
http://127.0.0.1:8000/app/default/call/xml/concat/hello/world

co zwróci wyjście w formacie XML:

1
2
3
<document>
   <result>helloworld</result>
</document>

Można serializować dane wyjściowe tej funkcji, nawet jeśli jest to obiekt Rows DAL. W tym przypadku, w rzeczywistości, będzie automatycznie wywoływana metoda as_list().

Jeśli zastosuje się dekorator @service.json, funkcję będzie można wywołać poprzez:

1
2
http://127.0.0.1:8000/app/default/call/json/concat?a=hello&b=world
http://127.0.0.1:8000/app/default/call/json/concat/hello/world

co zwróci wyjście w formacie JSON.

Jeśli zastosuje się dekorator @service.csv, wymagana jest obsługa usługi, jako że zwracaną wartością jest iterowalny obiekt iterowalnych obiektów, taki jak lista list. Oto przykład:

1
2
3
@service.csv
def table1(a,b):
    return [[a,b],[1,2]]

Usługa ta może zostać wywołana przez odwiedzenie następujących adresów URL:

1
2
http://127.0.0.1:8000/app/default/call/csv/table1?a=hello&b=world
http://127.0.0.1:8000/app/default/call/csv/table1/hello/world

co zwóci:

1
2
hello,world
1,2

Dekorator @service.rss oczekuje, że zwracana wartość będzie miała ten sam format, co widok "generic.rss" omówiony w poprzednim rozdziale.

Możliwe jest też stosowanie wielu dekoratorów w każdej funkcji.

Wszystko co do tej pory omówiliśmy w tym rozdziale jest po prostu alternatywą metody omówionej w poprzednim rozdziale. Prawdziwa siła obiektu usługi ujawnia się w protokołach XMLRPC, JSONRPC i AMFRPC, omówionych dalej.

XML-RPC

XML-RPC

Rozważmy następujący kod umieszczony, na przykład, w kontrolerze "default.py":

1
2
3
4
5
6
7
@service.xmlrpc
def add(a,b):
    return a+b

@service.xmlrpc
def div(a,b):
    return a/b

Teraz w powłoce Pythona można zrobić tak:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
>>> from xmlrpclib import ServerProxy
>>> server = ServerProxy(
       'http://127.0.0.1:8000/app/default/call/xmlrpc')
>>> print server.add(3,4)
7
>>> print server.add('hello','world')
'helloworld'
>>> print server.div(12,4)
3
>>> print server.div(1,0)
ZeroDivisionError: integer division or modulo by zero

Moduł xmlrpclib Pythona dostarcza klienta protokołu XML-RPC, web2py działa jako serwer.

Klient łączy się z serwerem poprzez obiekt ServerProxy i może zdalnie wywoływać udekorowaną funkcję w serwerze. Dane (a,b) są przekazywane do funkcji, nie przez zmienne GET/POST, ale są odpowiednio zakodowane w ciele żądania przy użyciu protokołu XML-PRC i przenosza z sobą informację o typie danych (int lub string lub coś innego). Jest to prawdą dla zwracanych wartości. Co więcej, każdy wyjatek zgłoszony na serwerze jest przenoszony do klienta.

Sygnatura ServerProxy

1
a_server = ServerProxy(location,transport=None,encoding=None,verbose=False,version=None)

Ważnymi argumentami są:

  • location jest to zdalny adres URL serwera. Przykłady znajdują się poniżej;
  • verbose=True aktywuje przydatną diagnostykę;
  • version ustawia wersję biblioteki jsonrpc. Jest to ignorowane przez JSON-RPC. Ustaw ten argument na version='2.0', aby obsługiwać jsonrpc2. Ponieważ argument ten jest ignorowany przez jsonrpc, ustawienie go umożliwia obsługę obydwu wersji. Argument nie jest obsługiwany przez XML-RPC.

Biblioleteki XML-RPC

Są to biblioteki dla wielu języków programowania (w tym C, C++, Java, C#, Ruby i Perl) i moga współdziałać z sobą. Jest to najlepsza metoda na tworzenie aplikacji, które porozumiewają się z sobą niezależnie od języka programowania.

Klient XMLRPC może zostać zaimplementowany wewnątrz akcji web2py, tak że jedna akcja może porozumieć się z inną aplikacją web2py (nawet w obrębie tej samej instalacji) używając XML-RPC. Trzeba w tym przypadku uważać na możliwość zakleszczenia się sesji. Jeśli akcja wywołuje w tej samej aplikacji funkcję przez XML-RPC, to wywołujacy musi zwolnić blokadę sesji przed wywołaniem:

1
session.forget(response)

JSONRPC

JSON-RPC
JSON-RPC2

W tym rozdziale wykorzystamy ten sam przykładowy kod co dla XML-RPC, ale udostępnimy usługę stosując protokół JSON-RPC a nie XML:

1
2
3
4
5
6
7
@service.jsonrpc
@service.jsonrpc2
def add(a,b):
    return a+b

def call():
    return service()

Protokół JSON-RPC jest bardzo podobny do XML-RPC, ale do serializacji danych stosuje format JSON zamiast XML.

Udostępnianie usług JSON-RPC w web2py

Usługę można wywoływać z dowolnego programu w dowolnym języku programowania, ale tutaj robić to będziemy w Pythonie. Platforma web2py dostarczana jest z modułem "gluon/contrib/simplejsonrpc.py" stworzonym przez Mariano Reingarta. Oto przykład, jak zastosować wywołanie powyższej usługi:

1
2
3
4
>>> from gluon.contrib.simplejsonrpc import ServerProxy
>>> URL = "http://127.0.0.1:8000/app/default/call/jsonrpc"
>>> service = ServerProxy(URL, verbose=True)
>>> print service.add(1, 2)

Użyj "http://127.0.0.1:8000/app/default/call/jsonrpc2" dla jsonrpc2 i utwórz obiekt usługi podobny do tego:

1
service = ServerProxy(URL,verbose=True,version='2.0')

JSON-RPC a Pyjamas

JSONRPC
Pyjamas

Jako przykład aplikacji, omówimy tutaj stosowanie protokołu JSON-RPC w Pyjamas. Pyjamas jest portem Pythona dla Google Web Toolkit (oryginalnie napisanego w Java). Pyjamas umożliwia pisanie aplikacji klienckich w Pythonie. Pyjamas tłumaczy ten kod na JavaScript. Platforma web2py obsługuje ten kod JavaScript i komunikuje się z Pyjams za pomocą żądań AJAX pochodzących od klienta i wyzwalanych przez akcje użytkownika.

Tutaj opisujemy jak zrealizowac zaadaptować Pyjamas w web2py. Nie wymaga to żadnych dodatkowych bibliotek, innych niż web2py i Pyjamas.

Zbudujemy prostą aplikację "todo" z klientem Pyjamas (wszystko w JavaScript), która komunikuje się z serwerem za pośrednictwem JSON-RPC.

Po pierwsze, utwórz nową aplikacje o nazwie "todo".

Po drugie, w "models/db.py" wprowadź następujacy kod:

1
2
3
db=DAL('sqlite://storage.sqlite')
db.define_table('todo', Field('task'))
service = Service()

(Uwaga: klasa Service znajduje się w gluon.tools).

Po trzecie, w "controllers/default.py", wprowadź następujący kod:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    def index():
    redirect(URL('todoApp'))

    @service.jsonrpc
    def getTasks():
        todos = db(db.todo).select()
        return [(todo.task,todo.id) for todo in todos]

    @service.jsonrpc
    def addTask(taskFromJson):
        db.todo.insert(task= taskFromJson)
        return getTasks()

    @service.jsonrpc
    def deleteTask (idFromJson):
        del db.todo[idFromJson]
        return getTasks()

    def call():
        session.forget()
        return service()

    def todoApp():
        return dict()

Cel każdej funkcji powinien być oczywisty.

Po czwarte w "views/default/todoApp.html", wprowadź taki oto kod:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<html>
  <head>
    <meta name="pygwt:module"
     content="{{=URL('static','output/TodoApp')}}" />
    <title>
      simple todo application
    </title>
  </head>
  <body bgcolor="white">
    <h1>
      simple todo application
    </h1>
    <i>
      type a new task to insert in db,
      click on existing task to delete it
    </i>
    <script language="javascript"
     src="{{=URL('static','output/pygwt.js')}}">
    </script>
  </body>
</html>

Ten widok po prostu wykonuje kod Pyjamas w "static/output/todoapp" - kod ten jescze nie został stworzony.

Po piąte, w "static/TodoApp.py" (zwróć uwagę, że jest to TodoApp, a nie todoApp!), wprowadź poniższy kod klienta:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
from pyjamas.ui.RootPanel import RootPanel
from pyjamas.ui.Label import Label
from pyjamas.ui.VerticalPanel import VerticalPanel
from pyjamas.ui.TextBox import TextBox
import pyjamas.ui.KeyboardListener
from pyjamas.ui.ListBox import ListBox
from pyjamas.ui.HTML import HTML
from pyjamas.JSONService import JSONProxy

class TodoApp:
    def onModuleLoad(self):
        self.remote = DataService()
        panel = VerticalPanel()

        self.todoTextBox = TextBox()
        self.todoTextBox.addKeyboardListener(self)

        self.todoList = ListBox()
        self.todoList.setVisibleItemCount(7)
        self.todoList.setWidth("200px")
        self.todoList.addClickListener(self)
        self.Status = Label("")

        panel.add(Label("Add New Todo:"))
        panel.add(self.todoTextBox)
        panel.add(Label("Click to Remove:"))
        panel.add(self.todoList)
        panel.add(self.Status)
        self.remote.getTasks(self)

        RootPanel().add(panel)

    def onKeyUp(self, sender, keyCode, modifiers):
        pass

    def onKeyDown(self, sender, keyCode, modifiers):
        pass

    def onKeyPress(self, sender, keyCode, modifiers):
        """
        This function handles the onKeyPress event, and will add the
        item in the text box to the list when the user presses the
        enter key. In the future, this method will also handle the
        auto complete feature.
        """
        if keyCode == KeyboardListener.KEY_ENTER and            sender == self.todoTextBox:
            id = self.remote.addTask(sender.getText(),self)
            sender.setText("")
            if id<0:
                RootPanel().add(HTML("Server Error or Invalid Response"))

    def onClick(self, sender):
        id = self.remote.deleteTask(
                sender.getValue(sender.getSelectedIndex()),self)
        if id<0:
            RootPanel().add(
                HTML("Server Error or Invalid Response"))

    def onRemoteResponse(self, response, request_info):
        self.todoList.clear()
        for task in response:
            self.todoList.addItem(task[0])
            self.todoList.setValue(self.todoList.getItemCount()-1,
                                   task[1])

    def onRemoteError(self, code, message, request_info):
        self.Status.setText("Server Error or Invalid Response: "                             + "ERROR " + code + " - " + message)

class DataService(JSONProxy):
    def __init__(self):
        JSONProxy.__init__(self, "../../default/call/jsonrpc",
                           ["getTasks", "addTask","deleteTask"])

if __name__ == '__main__':
    app = TodoApp()
    app.onModuleLoad()

Po szóste, przed serwowaniem tej aplikacji uruchom Pyjamas :

1
2
cd /path/to/todo/static/
python /python/pyjamas-0.5p1/bin/pyjsbuild TodoApp.py

Przetłumaczy to kod Pythona na JavaScript, tak że bedzie go można wykonać w przeglądarce.

W celu dostępu do aplikacji odwiedź ten adres URL:

1
http://127.0.0.1:8000/todo/default/todoApp

Niniejszy podrozdział został stworzony przez Chrisa Prinosa z pomocą Luke Kenneth Casson Leightona (twórcy Pyjamas) i zaktualizowany przez Alexeia Vinidiktova. Zostało to przetestowane z Pyjamas 0.5p1. Przykład został zainspirowany stroną Django na ref.[blogspot1].

AMF-RPC

PyAMF
Adobe Flash

AMF-RPC to protokół zdalnego wywołania procedur (ang. Remote Procedure Call) używany przez klientów Flash do komunikacji z serwerem. Platforma web2py obsługuje AMF-RPC, ale wymaga uruchomienia web2py w wersji źródłowej i preinstalacji biblioteki PyAMF. Biblioteke tą można zainstalować z poziomu powłoki Linux lub Windows poleceniem:

1
easy_install pyamf

(szczegóły można znaleźć w dokumentacji PyAMF).

W tym rozdziale zakładamy, że czytelnik jest zaznajomiony z programowaniem w ActionScript.

Utworzymy prostą usługę, która pobiera dwie numeryczne wartości, dodaje je do siebie i zwraca sumę. Nazwiemy naszą aplikacje "pyamf_test" a usługę addNumbers.

Po pierwsze, użyj Adobe Flash (dowolna wersję, począwszy od MX 2004), utwórz aplikację kliencką Flash, zaczynając nowy plik FLA Flash. W pierwszej ramce pliku dodaj te linie kodu:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import mx.remoting.Service;
import mx.rpc.RelayResponder;
import mx.rpc.FaultEvent;
import mx.rpc.ResultEvent;
import mx.remoting.PendingCall;

var val1 = 23;
var val2 = 86;

service = new Service(
    "http://127.0.0.1:8000/pyamf_test/default/call/amfrpc3",
    null, "mydomain", null, null);

var pc:PendingCall = service.addNumbers(val1, val2);
pc.responder = new RelayResponder(this, "onResult", "onFault");

function onResult(re:ResultEvent):Void {
    trace("Result : " + re.result);
    txt_result.text = re.result;
}

function onFault(fault:FaultEvent):Void {
    trace("Fault: " + fault.fault.faultstring);
}

stop();

Kod ten pozwala klientowi Flash połączyć się z usługą, która odpowiada funkcji o nazwie "addNumbers" w pliku "/pyamf_test/default/gateway". Trzeba również zaimportować klasy zdalnej obsługi w wersji 2 MX ActionScript, aby udostępnić zadalną obsługę we Flash. Dodaj ścieżkę do tych klas w ustawieniach "classpath" w Adobe Flash IDE lub po prostu umieść folder "mx" przy nowo utworzonym pliku.

Proszę zwrócić uwagę na argumenty konstruktora klasy Service. Pierwszy argument jest adresem URL odpowiadającym usłudze, którą się chce utworzyć. Trzeci argument jest domeną usługi. Wybraliśmy dla domeny nazwę "mydomain".

Po drugie, utwórz pole tekstu dynamicznego o nazwie "txt_result" i umieść to na scenie.

Po trzecie, trzeba ustawić bramkę web2py, która może komunikować się z klientem Flash zdefiniowanym powyżej.

Teraz trzeba utworzyć nową aplikację web2py w nazwie pyamf_test, która będzie hostować nową usługę bramkę AMF dla klienta Flash. Edytuj kontroler "default.py" i upewnij się, że zawiera on:

1
2
3
4
5
@service.amfrpc3('mydomain')
def addNumbers(val1, val2):
    return val1 + val2

def call(): return service()

Po czwarte, skompiluj i eksportuj (opublikuj) klienta Flash SWF jako pyamf_test.swf, umieść pliki "pyamf_test.amf", "pyamf_test.html", "AC_RunActiveContent.js" i "crossdomain.xml" w folderze "static" nowo utworzonego mechanizmu, który hostuje bramkę, "pyamf_test".

Teraz można przetestować klienta, odwiedzając:

1
http://127.0.0.1:8000/pyamf_test/static/pyamf_test.html

Bramka jest wywoływana w tle, gdy klient łączy się z "addNumbers".

Gdy używa się AMF0 zamiast AMF3, można wykorzystać dekorator:

1
@service.amfrpc

zamiast:

1
@service.amfrpc3('mydomain')

W tym przypadku trzeba zmienić adres URL usługi na:

1
http://127.0.0.1:8000/pyamf_test/default/call/amfrpc

SOAP

SOAP

Platforma web2py zawiera serwer i klienta protokołu SOAP, stworzone przez Mariano Reingarta. Może to być stosowane bardzo podobnie do XML-RPC:

Dla przykładu rozważmy następujący kod w kontrolerze "default.py":

1
2
3
@service.soap('MyAdd',returns={'result':int},args={'a':int,'b':int,})
def add(a,b):
    return a+b

Teraz w powłoce Pythona można zrobić tak:

1
2
3
4
>>> from gluon.contrib.pysimplesoap.client import SoapClient
>>> client = SoapClient(wsdl="http://localhost:8000/app/default/call/soap?WSDL")
>>> print client.MyAdd(a=1,b=2)
{'result': 3}

Dla uzyskania prawidłowego kodowania podczas zwracania wartości tekstowej, trzeba określić łańcuch tekstowy w postaci u'właściwy tekst utf8'.

Można uzyskać WSDL dla usługi z:

1
http://127.0.0.1:8000/app/default/call/soap?WSDL

a dokumentację dla dowolnej dostępnej metody z:

1
http://127.0.0.1:8000/app/default/call/soap

API niskiego poziomu i inne recepty

SimpleJSON

JSON
SimpleJSON

Platforma web2py zawiera moduł gluon.contrib.simplejson, zaprojektowany przez Boba Ippolito, dostarczający standardowy koder-dekoder Python-JSON.

SimpleJSON składa się z dwóch funkcji:

  • gluon.contrib.simplesjson.dumps(a) koduje obiekt a Pythona do JSON.
  • gluon.contrib.simplejson.loads(b) dekoduje dane JSON do obiektu b Pythona.

Obiekty, które mogą być serializowane, zawierają podstawowe typy, listy i słowniki. Przetwarzane obiekty można serializować z wyjątkiem klas zdefiniowanych przez użytkownika.

Oto przykładowa akcja (dla przykładu w kontrolerze "default.py"), która serializuje listę Pythona zawierajaca dni tygodnia prz użyciu API niskiego poziomu:

1
2
3
4
5
def weekdays():
    names=['Sunday','Monday','Tuesday','Wednesday',
           'Thursday','Friday','Saturday']
    import gluon.contrib.simplejson
    return gluon.contrib.simplejson.dumps(names)

Poniżej przedstawiona jest przykładowa strona HTML wysyłająca żądanie Ajax do powyższej akcji, otrzymuje komunikat JSON i przechowuje listę w odpowiedniej zmiennej JavaScript:

1
2
3
4
5
{{extend 'layout.html'}}
<script>
$.getJSON('/application/default/weekdays',
          function(data){ alert(data); });
</script>

Kod ten używa funkcję jQuery $.getJSON, która wykonuje wywołanie Ajax i w odpowiedzi zapisuje nazwy dni tygodnia w lokalnej zmiennej JavaScript data i przekazuje tą zmienną do funkcji wywołania zwrotnego. W tym przykładzie funkcja wywołania zwrotnego informuje odwiedzającego, że dane zostały odebrane.

PyRTF

PyRTF
RTF

Innym częstym przypadkiem jest potrzeba generowania czytelnych dokumentów tekstowych Word. Najprostszym sposobem jest zastosowanie formatu Rich Text Format (RTF). Format ten, wprowadzony przez Microsoft, stał się już standardem.

Platforma web2py zawiera moduł gluon.contrib.pyrtf, stworzony przez Simona Cusacka i poprawiony przez Granta Edwardsa. Moduł ten pozwala na programowe generowanie dokumentów RTF, w tym tekstu formatowanego w kolorze i obrazów.

W poniższym przykładzie inicjujemy dwie bazowe klasy RTF, Document i Section, dodajemy drugą do pierwszej i wstawimy trochę próbnego tekstu:

1
2
3
4
5
6
7
8
9
def makertf():
    import gluon.contrib.pyrtf as q
    doc=q.Document()
    section=q.Section()
    doc.Sections.append(section)
    section.append('Section Title')
    section.append('web2py is great. '*100)
    response.headers['Content-Type']='text/rtf'
    return q.dumps(doc)

Na koniec obiekt Document jest serializowany przez q.dumps(doc). Proszę zwrócić uwagę, że przed zwróceniem dokumentu RTF, niezbędne jest określenie w nagłówka 'Content-Type', gdyż bez tego przeglądarka nie będzie wiedzieć, jak obsłużyć plik.

W zależności od konfiguracji, przeglądarka może zapytać, czy zapisać ten plik, czy uzyć edytora tekstu.

ReportLab i PDF

ReportLab
PDF

W web2py można również generować dokumenty PDF, wykorzystując dodatkową bibliotekę o nazwie "ReportLab"[ReportLab] .

Jeśli uruchomiło się web2py ze źródła, to wystarczy mieć zainstalowany ReportLab (jako pakiet systemowy). Jeśli korzysta się z wersji binarnej, należy rozpakować ReportLab w folderze "web2py/". W web2py z dystrybucji binarnej na Mac, trzeba rozpakować ReportLab w folderze:

1
web2py.app/Contents/Resources/

Teraz zakładamy, że jest już zainstalowana biblioteka ReportLab i że web2py może ją znaleźć. Utworzymy prostą akcję o nazwie "get_me_a_pdf" generującą dokument PDF.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from reportlab.platypus import *
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.rl_config import defaultPageSize
from reportlab.lib.units import inch, mm
from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY
from reportlab.lib import colors
from uuid import uuid4
from cgi import escape
import os

def get_me_a_pdf():
    title = "This The Doc Title"
    heading = "First Paragraph"
    text = 'bla '* 10000

    styles = getSampleStyleSheet()
    tmpfilename=os.path.join(request.folder,'private',str(uuid4()))
    doc = SimpleDocTemplate(tmpfilename)
    story = []
    story.append(Paragraph(escape(title),styles["Title"]))
    story.append(Paragraph(escape(heading),styles["Heading2"]))
    story.append(Paragraph(escape(text),styles["Normal"]))
    story.append(Spacer(1,2*inch))
    doc.build(story)
    data = open(tmpfilename,"rb").read()
    os.unlink(tmpfilename)
    response.headers['Content-Type']='application/pdf'
    return data

Proszę zauważyć, że generujemy PDF do unikalnego pliku tymczasowego, tmpfilename, po czym odczytujemy wygenerowany PDF z tego pliku i następnie usuwamy ten plik.

Więcej informacji o API ReportLab API można znaleźć w dokumentacji ReportLab. Zalecamy korzystanie z "Platypus API of ReportLab", takich jak Paragraph, Spacer itd.

Usługi internetowe RESTful

REST

REST, to skrót od "REpresentational State Transfer" i jest to rodzaj architektury usług internetowych a nie protokół jak SOAP. W rzeczywistości nie ma żadnego standardu REST.

Mówiąc ogólnie, REST stanowi, że usługa może być traktowana jako kolekcja zasobów. Każdy zasób powinien być identyfikowany przez adres URL. Są cztery metody działania na zasobie i są to: POST (tworzenie), GET (odczytywanie), PUT (aktualizowanie) i DELETE, z których to nazw utworzony jest akronim CRUD (create-read-update-delete). Klient komunikuje się z zasobem tworząc żądanie HTTP o adresie URL, który identyfikuje zasób i wykorzystuje metodę POST/PUT/GET/DELETE HTTP do przekazania instrukcji dla zasobu. Adres URL może mieć rozszerzenie, na przykład json, co określa protokół kodowania danych.

Tak więc na przykład żądanie POST do zasobu:

http://127.0.0.1/myapp/default/api/person

oznacza, że chce się utworzyć nowy zasób person. W tym przypadku person może odpowiadać rekordowi w tablicy person, ale może też oznaczać jakiś inny rodzaj zasobu (na przykład plik).

Podobnie, żądanie GET do zasobu:

http://127.0.0.1/myapp/default/api/persons.json

wskazuje żądanie listy osób (rekordów w tabeki person) w formacie JSON.

Żądnie GET do zasobu:

http://127.0.0.1/myapp/default/api/person/1.json

oznacza żądanie informacji związanej z person/1 (rekordem o id==1) w formacie JSON.

W przypadku web2py każde żądanie można podzielić na trzy części:

  • pierwsza część identyfikuje lokalizację usługi, czyli akcję udostępniającą usługę:
http://127.0.0.1/myapp/default/api/
  • druga zawiera nazwę zasobu (person, persons, person/1 itd.);
  • trzecia, to rozszerzenie określające protokół komunikacyjny.

Prosze mieć na uwadze, że zawsze można użyć router do wyeliminowania niechcianego przedrostka w adresie URL, na przykład można uprościć:

http://127.0.0.1/myapp/default/api/person/1.json

do:

http://127.0.0.1/api/person/1.json

ale jest to sprawa indywidualnego wyboru, co omawiamy w rozdziale 4.

W naszym przykładzie użyliśmy akcji o nazwie api, ale nie jest to wymóg. Możemy nazwać akcję wskazującą usługę RESTful w dowolny sposób a nawet można utworzyc więcej akcji niż jedna. W dalszej częsci rozdziału, będziemy nadal zakładać, ze nasza akcja usługi RESTful ma nazwę api.

Będziemy również zakładać, że mamy zdefiniowane dwie tabele:

db.define_table('person',Field('name'),Field('info'))
db.define_table('pet',Field('ownedby',db.person),Field('name'),Field('info'))

oraz zasoby, które chcemy udostępnić.

Pierwszą rzeczą, jaka zrobimy, to utworzymy akcję usługi RESTful:

def api():
    return locals()

Teraz zmodyfikujemy to tak, aby rozszerzenie zostało odfiltrowane z argumentami żadania (request.args będzie mógł być wtedy użyty do identyfikacji zasobu) i tak, że będzie można obsługiwać oddzielnie różne metody:

@request.restful()
def api():
    def GET(*args,**vars):
        return dict()
    def POST(*args,**vars):
        return dict()
    def PUT(*args,**vars):
        return dict()
    def DELETE(*args,**vars):
        return dict()
    return locals()

Gdy teraz wykonamy żądanie GET HTTP do:

http://127.0.0.1:8000/myapp/default/api/person/1.json

to wywołane i zwrócone będzie GET('person','1'), gdzie GET jest funkcją zdefiniowaną wewnątrz akcji. Proszę zwrócić uwagę, że:

  • nie musimy definiować wszystkich czterech metod, tylko te które chcemy udostępnić;
  • funkcja metody może pobierać nazwane argumenty;
  • rozszerzenie jest przechowywane w request.extension a typ zawartości jest ustawiany automatycznie.

Dekorator @request.restful() powoduje, że rozszerzenie z info ścieżki jest przechowywane w request.extension, odwzorowuje metodę żdania do odpowiedniej funkcji w akcji (POST, GET, PUT, DELETE) oraz przekazuje request.args i request.vars do wybranej funkcji.

Teraz zbudujemy usługę dla POST i GET poszczególnych rekordów:

@request.restful()
def api():
    response.view = 'generic.json'
    def GET(tablename,id):
        if not tablename=='person': raise HTTP(400)
        return dict(person = db.person(id))
    def POST(tablename,**fields):
        if not tablename=='person': raise HTTP(400)
        return db.person.validate_and_insert(**fields)
    return locals()

Proszę zauważyć, że:

  • GET i POST są realizowane przez różne funkcje;
  • funkcje oczekują prawidłowych argumentów (nie nazwane argumenty są parsowane przez request.args a nazwane argumenty przez request.vars);
  • sprawdzają one, czy dane wejściowe są prawidłowe i ewentualnie zgłaszają wyjątek;
  • wykonanie GET wybiera i zwraca rekord db.person(id). Dane wyjściowe są konwertowane automatycznie do JSON, ponieważ wywołany został widok generyczny.
  • POST wykonuje validate_and_insert(..) i zwraca id nowego rekordu lub ewentualne błędy walidacji. Zmienne **fields są zmiennymi metody POST.

parse_as_rest (eksperymentalne)

Dokonane do tej pory wyjaśnienie logiki jest wystarczajace dla tworzenie różnego typu usług internetowych RESTful, ale web2py pomaga nam stosować jeszcze więcej.

W rzeczywistości, web2py dostarcza składnię do opisania, które tabele bazy danych chcemy udostępnić i jak odwzorować zasoby na adresy URL i vice versa.

parse_as_rest

Jest to realizowane przy pomocy wzorców URL. Wzorzec jest łańcuchem odwzorowującym argumenty żądania z adresu URL na zapytanie bazy danych. Są 4 typy atomowych wzorców:

  • łańcuch zawierający na przykład "friend";
  • stała łańcuchowa odpowiadająca jakiejś tabeli, na przykład "friend[person]" będzie dopasowywał "friends" z adresu URL do tabeli "person";
  • zmienne, które będą używane do filtrowania. Na przykład "{person.id}" będzie stosował filtr db.person.name=={person.id};
  • nazwy pól, representowane przez ":field"

Atomowe wzorce mogą być łączone w złożone wzorce URL przy użyciu "/", takie jak:

/friend[person]/{person.id}/:field

który pobiera adres URL formularza:

http://..../friend/1/name

W zapytaniu dla person.id, które wzraca nazwę osoby. Tutaj "friend[person]" dopasowuje "friend" i filtruje tabelę "person". "{person.id}" dopasowuje "1" i filtruje "person.id==1". ":field" dopasowując "name" i zwraca:

db(db.person.id==1).select().first().name

Wiele wzorców URL można połączyć w litę, tak że jedna pojedyncza akcja RESTful może obsługiwać różne typy żądań.

DAL posiada metodę parse_as_rest(pattern,args,vars), w której dana lista wzorców, request.args i request.vars dopasowuje wzorzec i zwraca odpowiedź (GET only).

Oto bardziej skomplikowany przykład:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@request.restful()
def api():
    response.view = 'generic.'+request.extension
    def GET(*args,**vars):
        patterns = [
            "/friends[person]",
            "/friend/{person.name.startswith}",
            "/friend/{person.name}/:field",
            "/friend/{person.name}/pets[pet.ownedby]",
            "/friend/{person.name}/pet[pet.ownedby]/{pet.name}",
            "/friend/{person.name}/pet[pet.ownedby]/{pet.name}/:field"
            ]
        parser = db.parse_as_rest(patterns,args,vars)
        if parser.status == 200:
            return dict(content=parser.response)
        else:
            raise HTTP(parser.status,parser.error)
    def POST(table_name,**vars):
        if table_name == 'person':
            return db.person.validate_and_insert(**vars)
        elif table_name == 'pet':
            return db.pet.validate_and_insert(**vars)
        else:
            raise HTTP(400)
    return locals()

który rozumie następujące adresy URL odpowiadające wymienionym wzorcom:

  • GET wszystkie osoby:
http://.../api/friends
  • GET jedną osobę z nazwą rozpoczynającą się od "t":
http://.../api/friend/t
  • GET wartość pola "info" pierwszej osoby o imieniu "Tim"
http://.../api/friend/Tim/info
  • GET listę zwierząt powyższej osoby (friend):
http://.../api/friend/Tim/pets
  • GET zwierzęcia o nazwie "Snoopy" osoby o imieniu "Tim":
http://.../api/friend/Tim/pet/Snoopy
  • GET wartości pola "info" dla zwierzęcia:
http://.../api/friend/Tim/pet/Snoopy/info

Akcja ta udostępnia dwa adresy URL metody POST:

  • POST nowego przyjaciela (friend),
  • POST nowego zwierzęcia.

Jeśli ma się zainstalowane narzędzie "curl", można spróbować:

$ curl -d "name=Tim" http://127.0.0.1:8000/myapp/default/api/friend.json
{"errors": {}, "id": 1}
$ curl http://127.0.0.1:8000/myapp/default/api/friends.json
{"content": [{"info": null, "name": "Tim", "id": 1}]}
$ curl -d "name=Snoopy&ownedby=1" http://127.0.0.1:8000/myapp/default/api/pet.json
{"errors": {}, "id": 1}
$ curl http://127.0.0.1:8000/myapp/default/api/friend/Tim/pet/Snoopy.json
{"content": [{"info": null, "ownedby": 1, "name": "Snoopy", "id": 1}]}

Możliwe jest zadeklarowanie bardziej złożonych zapytań takich, w których wartość w adresie URL zostaje użyta do utworzenia zapytania bez udziału równości. Na przykład:

patterns = ['friends/{person.name.contains}'

odwzorowuje

http://..../friends/i

na

db.person.name.contains('i')

Podobnie:

patterns = ['friends/{person.name.ge}/{person.name.gt.not}'

odwzorowuje

http://..../friends/aa/uu

na

(db.person.name>='aa')&(~(db.person.name>'uu'))

Ważnymi atrybutami pola we wzorcu są: contains, startswith, le, ge, lt, gt, eq (równe, domyślne), ne (nie równe). Innymi atrybutami specyficznymi dla pól date i datetimeday, month, year, hour, minute, second.

Proszę mieć na uwadze, że składnia wzorca nie została zaprojektowana do ogólnego stosowania. Nie każde możliwe zapytanie może być opisane poprzez wzorzec, ale dla wielu zapytań można zastosować wzorzec. Skladnia będzie rozszerzona w przyszłości.

Często zachodzi potrzeba udostępnienia jakichś adresów URL usługi RESTful, ale z ograniczeniem możliwych zapytań. Można to wykonać przez przekazanie do metody parse_as_rest dodatkowego argumentu queries. Argument queries jest słownikiem (tablename,query), gdzie query jest zapytaniem DAL ograniczajacym dostęp do tabeli tablename.

Można również uporządkować wyniki używając w tym celu zmienne GET:

http://..../api/friends?order=name|~info

co porządkuje listę przyjaciół alfabetycznie (name) a następnie przez odwrócenie wartości info w order.

Można również ograniczyć liczbę rekordów przez określenie zmiennych GET limit i offset:

http://..../api/friends?offset=10&limit=1000

ci zwróci 1000 przyjaciów (osób) z pominieciem pierwszych 10. Domyślna wartość limit to 1000 a offset to 0.

Rozważmy teraz skrajny przypadek. Chcemy budować wszystkie możliwe wzorce dla wszystkich tabel (z wyjatkiem tabel auth_). Chcemy mieć możliwość wyszukiwania według dowolnych pól tekstowych, całkowitoliczbowych i zmiennoprzecinkowych (przez zakres) oraz datowych (również przez zakres). Chcemy też mieć możliwość wykonywania żądań POST do dowolnej tabeli:

W ogólnym przypadku wymaga to zastosowania wielu wzorców. W web2py jest to proste:

@request.restful()
def api():
    response.view = 'generic.'+request.extension
    def GET(*args,**vars):
        patterns = 'auto'
        parser = db.parse_as_rest(patterns,args,vars)
        if parser.status == 200:
            return dict(content=parser.response)
        else:
            raise HTTP(parser.status,parser.error)
    def POST(table_name,**vars):
        return db[table_name].validate_and_insert(**vars)
    return locals()

Ustawienie patterns='auto' spowoduje w web2py wygenerowanie wszystkich możliwych wzorców dla tabel nie związanych z uwierzytelnianiem. Jest nawet wzorzec na zapytanie o wzorce:

http://..../api/patterns.json

co dla tabel person i pet da wynik:

{"content": [
   "/person[person]",
   "/person/id/{person.id}",
   "/person/id/{person.id}/:field",
   "/person/id/{person.id}/pet[pet.ownedby]",
   "/person/id/{person.id}/pet[pet.ownedby]/id/{pet.id}",
   "/person/id/{person.id}/pet[pet.ownedby]/id/{pet.id}/:field",
   "/person/id/{person.id}/pet[pet.ownedby]/ownedby/{pet.ownedby}",
   "/person/id/{person.id}/pet[pet.ownedby]/ownedby/{pet.ownedby}/:field",
   "/person/name/pet[pet.ownedby]",
   "/person/name/pet[pet.ownedby]/id/{pet.id}",
   "/person/name/pet[pet.ownedby]/id/{pet.id}/:field",
   "/person/name/pet[pet.ownedby]/ownedby/{pet.ownedby}",
   "/person/name/pet[pet.ownedby]/ownedby/{pet.ownedby}/:field",
   "/person/info/pet[pet.ownedby]",
   "/person/info/pet[pet.ownedby]/id/{pet.id}",
   "/person/info/pet[pet.ownedby]/id/{pet.id}/:field",
   "/person/info/pet[pet.ownedby]/ownedby/{pet.ownedby}",
   "/person/info/pet[pet.ownedby]/ownedby/{pet.ownedby}/:field",
   "/pet[pet]",
   "/pet/id/{pet.id}",
   "/pet/id/{pet.id}/:field",
   "/pet/ownedby/{pet.ownedby}",
   "/pet/ownedby/{pet.ownedby}/:field"
]}

Można określić wzorce auto tylko dla niektórych tabel:

patterns = [':auto[person]',':auto[pet]']

smart_query (eksperymentalnie)

smart_query

Czasem zachodzi potrzeba większej elastyczności i chce się mieć możliwość przekazania do usługi RESTful dowolnego zapytania, takiego jak:

http://.../api.json?search=person.name starts with 'T' and person.name contains 'm'

Można to zrobić stosując:

@request.restful()
def api():
    response.view = 'generic.'+request.extension
    def GET(search):
        try:
            rows = db.smart_query([db.person,db.pet],search).select()
            return dict(result=rows)
        except RuntimeError:
            raise HTTP(400,"Invalid search string")
    def POST(table_name,**vars):
        return db[table_name].validate_and_insert(**vars)
    return locals()

Metoda db.smart_query pobiera dwa argumenty:

  • dozwoloną w zapytaniu liste pól lub tabele,
  • łańcuch zawierajacy zapytanie wyrażone w naturalnym języki

i zwraca obiekt db.set z rekordami, które zostały znalezione.

Proszę zauważyć, że wyszukiwany łańcuch jest parsowany, ale nie sprawdzany lub wykonywany, tak więc nie stanowi to zagrożenia bezpieczeństwa.

Kontrola dostępu

Dostęp do API można ograniczyć, jak zwykle, stosując dekoratory. Tak więc, na przykład:

auth.settings.allow_basic_login = True

@auth.requires_login()
@request.restful()
def api():
   def GET(s):
       return 'access granted, you said %s' % s
   return locals()

może teraz być dostępny z:

$ curl --user name:password http://127.0.0.1:8000/myapp/default/api/hello
access granted, you said hello

Usługi a uwierzytelnianie

uwierzytelnianie

W poprzednim rozdziale omówiliśmy zastosowanie następujacych dekoratorów:

1
2
3
@auth.requires_login()
@auth.requires_membership(...)
@auth.requires_permission(...)

W zwykłych akcjach (nie dekorowanych jako usługi), dekoratory te mogą zostać użyte, nawet jeśli wyjście jest renderowane w formacie innym niz HTML.

W funkcjach definiowanych jako usługi i dekorowanych przy użyciu dekoratorów @service..., nie można wykorzystać dekoratorów @auth.... Te dwa rodzaje dekoratorów nie moga byc mieszane. Jeśli ma być wykonane uwierzytelnianie, to to trzeba użyć udekorowana akcję call:

1
2
@auth.requires_login()
def call(): return service()

Istnieje możliwośc utworzenia instancji wielu obiektów, rejestrujących te same funkcje i udostępniających kilka z nich z uwierzytelnieniem a kilka bez:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public_service=Service()
private_service=Service()

@public_service.jsonrpc
@private_service.jsonrpc
def f(): return 'public'

@private_service.jsonrpc
def g(): return 'private'

def public_call(): return public_service()

@auth.requires_login()
def private_call(): return private_service()

Założono tu, że osoba logująca przekazuje poświadczenie w nagłówku HTTP (prawidłowe ciasteczko sesji lub używając podstawowe uwierzyteniania, tak jak opisano to w poprzednim rozdziale). Klient musi to obsługiwać, ale nie wszyscy klienci to robią.

Jeślí używa się ServerProxy() opisany dalej, można przekazać poświadczenie uwierzytelnienia w adresie URL, podobnie do tego:

1
2
URL='http://user:password@127.0.0.1:8000/app/default/private_call/jsonrpc2'
service = ServerProxy(URL, version='2.0')

gdzie funkcja private_call w kontrolerze jest udekorowana dla uwierzytelniania użytkownika.

 top