Chapter 10: Serviços

 Serviços

Web Services
API

O W3C define um serviço da Web como "um sistema de software projetado para suportar interação máquina a máquina interoperável em uma rede". Essa é uma definição ampla e engloba um grande número de protocolos criados não para comunicação entre máquinas, mas para comunicação máquina a máquina, como XML, JSON, RSS etc.

Neste capítulo, discutiremos como expor os serviços da Web usando o web2py. Se você estiver interessado em exemplos de como consumir serviços de terceiros (Twitter, Dropbox, etc.) você deve olhar para o Capítulo 9 e Capítulo 14.

O web2py fornece, fora da caixa, suporte para muitos protocolos, incluindo XML, JSON, RSS, CSV, XMLRPC, JSONRPC, AMFRPC e SOAP. O web2py também pode ser estendido para suportar protocolos adicionais.

Cada um desses protocolos é suportado de várias maneiras e fazemos uma distinção entre:

  • Renderização da saída de uma função em um determinado formato (por exemplo, XML, JSON, RSS, CSV)
  • Chamadas de Procedimento Remoto (por exemplo, XMLRPC, JSONRPC, AMFRPC)

 Renderizando um dicionário

 HTML, XML e JSON

HTML
XML
JSON

Considere a seguinte ação:

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

Essa ação retorna um contador que é aumentado em um quando um visitante recarrega a página e o registro de data e hora da solicitação de página atual.

Normalmente, essa página seria solicitada por meio de:

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

e processado em HTML. Sem escrever uma linha de código, podemos solicitar que o web2py renderize essa página usando diferentes protocolos, adicionando uma extensão ao URL:

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

O dicionário retornado pela ação será renderizado em HTML, XML e JSON, respectivamente.

Aqui está a saída XML:

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

Aqui está a saída JSON:

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

Observe que os objetos date, time e datetime são renderizados como strings no formato ISO. Isso não faz parte do padrão JSON, mas sim de uma convenção web2py.

 Visualizações genéricas

Quando, por exemplo, a extensão ".xml" é chamada, o web2py procura um arquivo de modelo chamado "default/count.xml" e, se não encontrá-lo, procura um modelo chamado "generic.xml". Os arquivos "generic.html", "generic.xml", "generic.json" são fornecidos com o aplicativo de scaffolding atual. Outras extensões podem ser facilmente definidas pelo usuário.

Por motivos de segurança, as visualizações genéricas só podem ser acessadas no host local. Para ativar o acesso de clientes remotos, talvez seja necessário definir o response.generic_patterns.

Supondo que você esteja usando uma cópia do aplicativo scaffold, edite a seguinte linha em models/db.py

  • restringir o acesso apenas ao host local
    response.generic_patterns = ['*'] if request.is_local else []
    
  • para permitir todas as visualizações genéricas
    response.generic_patterns = ['*']
    
  • para permitir apenas .json
response.generic_patterns = ['*.json']

O generic_patterns é um padrão glob, isso significa que você pode usar qualquer padrão que corresponda às ações do seu aplicativo ou passar uma lista de padrões.

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

Para usá-lo em um aplicativo web2py mais antigo, talvez seja necessário copiar os arquivos "genéricos. *" De um aplicativo de andaime posterior (após a versão 1.60).

Aqui está o código para "generic.html"

{{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>

Aqui está o código para "generic.xml"

{{
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')
}}

E aqui está o código para "generic.json"

{{
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')
}}

Qualquer dicionário pode ser renderizado em HTML, XML e JSON desde que contenha apenas tipos primitivos de python (int, float, string, list, tuple, dictionary). response._vars  contém o dicionário retornado pela ação.

Se o dicionário contiver outros objetos definidos pelo usuário ou específicos da web2py, eles deverão ser renderizados por uma exibição personalizada.

 Renderização Rows

as_list

Se você precisar renderizar um conjunto de linhas conforme retornado por um select em XML ou JSON ou outro formato, primeiro transformar o objeto Rows em uma lista de dicionários usando o as_list()  método.

Considere, por exemplo, o seguinte modelo:

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

A seguinte ação pode ser renderizada em HTML, mas não em XML ou JSON:

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

enquanto a seguinte ação pode ser renderizada em XML e JSON:

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

 Formatos personalizados

Se, por exemplo, você deseja renderizar uma ação como um pickle do Python:

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

você só precisa criar um novo arquivo de visualização "default/count.pickle" que contém:

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

Se você quiser ser capaz de renderizar qualquer ação como um arquivo decantado, basta salvar o arquivo acima com o nome "generic.pickle".

Nem todos os objetos são selecionáveis e nem todos os objetos decapados podem ser decapados. É seguro manter os objetos primitivos do Python e combinações deles. Objetos que não contêm referências a fluxos de arquivos ou conexões de banco de dados são geralmente selecionáveis, mas eles só podem ser removidos em um ambiente no qual as classes de todos os objetos decapados já estão definidas.

 RSS

RSS

O web2py inclui uma visualização "generic.rss" que pode renderizar o dicionário retornado pela ação como um feed RSS.

Como os feeds RSS possuem uma estrutura fixa (título, link, descrição, itens etc.), para que isso funcione, o dicionário retornado pela ação deve ter a estrutura adequada:

{'title': '',
 'link': '',
 'description': '',
 'created_on': '',
 'entries': []}

e cada entrada nas entradas deve ter a mesma estrutura similar:

{'title': '',
 'link': '',
 'description': '',
 'created_on': ''}

Por exemplo, a seguinte ação pode ser renderizada como um feed RSS:

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")
                         ])

simplesmente visitando o URL:

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

Como alternativa, assumindo o seguinte modelo:

db.define_table('rss_entry',
                Field('title'),
                Field('link'),
                Field('created_on', 'datetime'),
                Field('description'))

a seguinte ação também pode ser renderizada como um feed RSS:

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())

o as_list()  O método de um objeto Rows converte as linhas em uma lista de dicionários.

Se itens de dicionário adicionais forem encontrados com nomes de chaves não listados explicitamente aqui, eles serão ignorados.

Aqui está a visão "generic.rss" fornecida pelo web2py:

{{
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')
}}

Como mais um exemplo de um aplicativo RSS, consideramos um agregador RSS que coleta dados do feed "slashdot" e retorna um novo feed web2py rss.

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])

Pode ser acessado em:

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

 CSV

CSV

O formato Comma Separated Values (CSV) é um protocolo para representar dados tabulares.

Considere o seguinte modelo:

db.define_table('animal',
                Field('species'),
                Field('genus'),
                Field('family'))

e a seguinte ação:

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

web2py não fornece um "generic.csv"; você deve definir uma exibição personalizada "default/animals.csv" que serializa os animais em CSV. Aqui está uma implementação possível:

{{
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)
}}

Observe que também é possível definir um arquivo "generic.csv", mas é necessário especificar o nome do objeto a ser serializado ("animals" no exemplo). É por isso que não fornecemos um arquivo "generic.csv".

 Chamadas de procedimento remoto

RPC

O web2py fornece um mecanismo para transformar qualquer função em um serviço da web. O mecanismo descrito aqui difere do mecanismo descrito anteriormente porque:

  • A função pode receber argumentos
  • A função pode ser definida em um modelo ou em um módulo em vez de um controlador
  • Você pode querer especificar em detalhes qual método RPC deve ser suportado
  • Aplica uma convenção de nomenclatura de URL mais estrita
  • É mais inteligente que os métodos anteriores porque funciona para um conjunto fixo de protocolos. Pela mesma razão, não é tão facilmente extensível.

Para usar este recurso:

Primeiro, você deve importar e iniciar um objeto de serviço.

from gluon.tools import Service
service = Service()

Isso já é feito no arquivo de modelo "db.py" no aplicativo scaffolding.

Em segundo lugar, você deve expor o manipulador de serviços no controlador:

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

Isso já é feito no controlador "default.py" do aplicativo scaffolding. Remover session.forget()  se você planeja usar cookies de sessão com os serviços.

Terceiro, você deve decorar as funções que deseja expor como um serviço. Aqui está uma lista de decoradores atualmente suportados:

@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,})

Por exemplo, considere a seguinte função decorada:

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

Esta função pode ser definida em um modelo ou no controlador onde o call  ação é definida. Esta função pode agora ser chamada remotamente de duas maneiras:

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

Nos dois casos, a solicitação http retorna:

helloworld

Se o @service.xml  decorador é usado, a função pode ser chamada via:

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

e a saída é retornada como XML:

<document>
   <result>helloworld</result>
</document>

Pode serializar a saída da função, mesmo que seja um objeto DAL Rows. Neste caso, de fato, ele irá chamar as_list()  automaticamente.

Se o @service.json  decorador é usado, a função pode ser chamada via:

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

e a saída retornou como JSON.

Se o @service.csv  decorator é usado, o manipulador de serviços requer, como valor de retorno, um objeto iterável de objetos iteráveis, como uma lista de listas. Aqui está um exemplo:

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

Este serviço pode ser chamado visitando um dos seguintes URLs:

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

e retorna:

hello,world
1,2

o @service.rss  O decorador espera um valor de retorno no mesmo formato da visualização "generic.rss" discutida na seção anterior.

Múltiplos decoradores são permitidos para cada função.

Até agora, tudo discutido nesta seção é simplesmente uma alternativa ao método descrito na seção anterior. O poder real do objeto de serviço vem com XMLRPC, JSONRPC e AMFRPC, como discutido abaixo.

 XMLRPC

XMLRPC

Considere o seguinte código, por exemplo, no controlador "default.py":

@service.xmlrpc
def add(a, b):
    return a + b

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

Agora em um shell python você pode fazer

>>> 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

O módulo xmlrpclib do Python fornece um cliente para o protocolo XMLRPC. O web2py atua como o servidor.

O cliente se conecta ao servidor via ServerProxy e pode chamar remotamente funções decoradas no servidor. Os dados (a, b) são passados para a (s) função (ões), não através de variáveis GET/POST, mas apropriadamente codificados no corpo da solicitação usando o protocolo XMLPRC, e assim ele carrega consigo informações de tipo (int ou string ou outro) . O mesmo é verdadeiro para o valor de retorno (s). Além disso, qualquer exceção levantada no servidor se propaga de volta ao cliente.

 Assinatura ServerProxy

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

Os argumentos importantes são:

  • location  é o URL remoto do servidor. Existem exemplos abaixo.
  • verbose=True  ativa diagnósticos úteis
  • version  define a versão do jsonrpc. É ignorado pelo jsonrpc. Defina isto para version='2.0'  para suportar jsonrpc2. Por ser ignorado pelo jsonrpc, a configuração ganha suporte para ambas as versões. Não é suportado pelo XMLRPC.

 Bibliotecas XMLRPC

Existem bibliotecas XMLRPC para muitas linguagens de programação (incluindo C, C++, Java, C#, Ruby,e Perl), e eles podem interoperar entre si. Esse é um dos melhores métodos para criar aplicativos que se comunicam independentemente da linguagem de programação.

O cliente XMLRPC também pode ser implementado dentro de uma ação web2py, para que uma ação possa se comunicar com outro aplicativo web2py (mesmo dentro da mesma instalação) usando XMLRPC. Cuidado com os deadlocks de sessão neste caso. Se uma ação chama via XMLRPC uma função no mesmo aplicativo, o chamador deve liberar o bloqueio de sessão antes da chamada:

session.forget(response)

 

 JSONRPC

JSONRPC
JSONRPC2

Nesta seção, vamos usar o mesmo exemplo de código do XMLRPC, mas vamos expor o serviço usando JSONRPC:

@service.jsonrpc
@service.jsonrpc2
def add(a, b):
    return a + b

def call():
    return service()

JSONRPC é muito semelhante ao XMLRPC, mas usa JSON em vez de XML como protocolo de serialização de dados.

 Acessando serviços JSONRPC de web2py

Claro que podemos chamar o serviço de qualquer programa em qualquer idioma, mas aqui faremos em Python. O web2py é fornecido com o módulo "gluon/contrib/simplejsonrpc.py" criado por Mariano Reingart. Aqui está um exemplo de como usar para chamar o serviço acima:

>>> 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)

Use "http://127.0.0.1:8000/app/default/call/jsonrpc2" para jsonrpc2, e crie o objeto de serviço como este:

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

 JSONRPC e Pijama

JSONRPC
Pyjamas

Como exemplo de aplicação aqui, discutimos o uso de Chamadas de Procedimento Remoto JSON com Pijamas. O pijama é uma port Python do Google Web Toolkit (originalmente escrito em Java). O pijama permite escrever um aplicativo cliente no Python. Pijamas traduz esse código em JavaScript. O web2py atende o JavaScript e se comunica com ele por meio de solicitações AJAX originadas do cliente e acionadas por ações do usuário.

Aqui descrevemos como fazer o pijama funcionar com o web2py. Ele não requer nenhuma biblioteca adicional além de web2py e pijamas.

Vamos construir um aplicativo "todo" simples com um cliente Pyjama (todo JavaScript) que converse com o servidor exclusivamente via JSONRPC.

Primeiro, crie um novo aplicativo chamado "todo".

Em segundo lugar, em "models/db.py", digite o seguinte código:

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

(Nota: classe de serviço é de gluon.tools).

Terceiro, em "controllers/default.py", digite o seguinte código:

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()

O objetivo de cada função deve ser óbvio.

Em quarto lugar, em "views/default/todoApp.html", digite o seguinte código:

<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>

Esta visão apenas executa o código do pijama em "static/output/todoapp" - código que ainda não criamos.

Em quinto lugar, em "static/TodoApp.py" (observe que é TodoApp, não todoApp!), insira o seguinte código do cliente:

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()

Sexta, corra o pijama antes de servir o aplicativo:

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

Isso irá traduzir o código Python em JavaScript para que ele possa ser executado no navegador.

Para acessar este aplicativo, visite o URL:

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

Esta subseção foi criada por Chris Prinos com a ajuda de Luke Kenneth Casson Leighton (criadores de pijamas), atualizado por Alexei Vinidiktov. Foi testado com Pijamas 0.5p1. O exemplo foi inspirado por esta página do Django na ref. [blogspot1] .

 AMFRPC
PyAMF
 
Adobe Flash

AMFRPC é o protocolo de chamada de procedimento remoto usado pelos clientes do Flash para se comunicar com um servidor. O web2py suporta o AMFRPC, mas requer que você execute web2py a partir do código fonte e que você pré-instale a biblioteca PyAMF. Isso pode ser instalado a partir do shell do Linux ou Windows, digitando:

easy_install pyamf

(por favor consulte a documentação do PyAMF para mais detalhes).

Nesta subseção, presumimos que você já esteja familiarizado com Programação ActionScript.

Vamos criar um serviço simples que aceita dois valores numéricos, adiciona-os e retorna a soma. Nós chamaremos nosso aplicativo web2py de "pyamf_test" e chamaremos o serviço addNumbers .

Primeiro, usando o Adobe Flash (qualquer versão a partir do MX 2004), crie o aplicativo cliente Flash começando com um novo arquivo Flash FLA. No primeiro quadro do arquivo, adicione estas linhas:

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();

Esse código permite que o cliente Flash se conecte a um serviço que corresponde a uma função chamada "addNumbers" no arquivo "/ pyamf_test/default/gateway". Você também deve importar classes de comunicação remota do ActionScript versão 2 MX para habilitar o recurso Remoting in Flash. Adicione o caminho para essas classes para as configurações de caminho de classe no IDE do Adobe Flash ou apenas coloque a pasta "mx" ao lado do arquivo recém-criado.

Observe os argumentos do construtor Service. O primeiro argumento é a URL correspondente ao serviço que queremos criar. O terceiro argumento é o domínio do serviço. Escolhemos chamar esse domínio de "mydomain".

Em segundo lugar, crie um campo de texto dinâmico chamado "txt_result" e coloque-o no palco.

Terceiro, você precisa configurar um gateway web2py que possa se comunicar com o cliente Flash definido acima.

Prossiga criando um novo aplicativo web2py chamado pyamf_test  que hospedará o novo serviço e o gateway AMF para o cliente flash. Edite o controlador "default.py" e verifique se ele contém

@service.amfrpc3('mydomain')
def addNumbers(val1, val2):
    return val1 + val2

def call(): return service()

Em quarto lugar, compilar e exportar/publicar o cliente flash SWF como pyamf_test.swf , coloque os arquivos "pyamf_test.amf", "pyamf_test.html", "AC_RunActiveContent.js" e "crossdomain.xml" na pasta "static" do dispositivo recém-criado que hospeda o gateway, "pyamf_test".

Agora você pode testar o cliente visitando:

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

O gateway é chamado em segundo plano quando o cliente se conecta a addNumbers.

Se você estiver usando AMF0 em vez de AMF3, você também pode usar o decorador:

@service.amfrpc

ao invés de:

@service.amfrpc3('mydomain')

Neste caso, você também precisa alterar o URL do serviço para:

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

 SOAP

SOAP

O web2py inclui um cliente e servidor SOAP criado por Mariano Reingart. Pode ser usado muito parecido com o XML-RPC:

Considere o seguinte código, por exemplo, no controlador "default.py":

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

Agora, em um shell python, você pode fazer:

>>> 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}

Para obter uma codificação adequada ao retornar valores de texto, especifique string como u'proper utf8 text '.

Você pode obter o WSDL para o serviço em

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

E você pode obter documentação para qualquer um dos métodos expostos:

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

 API de baixo nível e outras receitas

 simplejson
JSON
 
simplejson

O web2py inclui o gluon.contrib.simplejson, desenvolvido por Bob Ippolito. Este módulo fornece o decodificador de codificador Python-JSON mais padrão.

O SimpleJSON consiste em duas funções:

  • gluon.contrib.simplesjson.dumps(a)  codifica um objeto Python a  em JSON.
  • gluon.contrib.simplejson.loads(b)  decodifica os dados JSON em b  em um objeto Python.

Tipos de objeto que podem ser serializados incluem tipos primitivos, listas e dicionários. Objetos compostos podem ser serializados com exceção de classes definidas pelo usuário.

Aqui está uma ação de exemplo (por exemplo, no controller "default.py") que serializa a lista do Python que contém os dias da semana usando esta API de baixo nível:

def weekdays():
    names=['Sunday', 'Monday', 'Tuesday', 'Wednesday',
           'Thursday', 'Friday', 'Saturday']
    import gluon.contrib.simplejson

    return gluon.contrib.simplejson.dumps(names)

Abaixo está uma página HTML de amostra que envia uma solicitação Ajax para a ação acima, recebe a mensagem JSON e armazena a lista em uma variável JavaScript correspondente:

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

O código usa a função jQuery $.getJSON , que executa a chamada Ajax e, em resposta, armazena os nomes dos dias da semana em uma variável JavaScript local data  e passa a variável para a função de retorno de chamada. No exemplo, a função de retorno de chamada simplesmente alerta o visitante de que os dados foram recebidos.

 PyRTF
PyRTF
 
RTF

Outra necessidade comum de sites da Web é gerar documentos de texto legíveis para o Word. A maneira mais simples de fazer isso é usando o formato de documento Rich Text Format (RTF). Este formato foi inventado pela Microsoft e desde então se tornou um padrão.

O web2py inclui o gluon.contrib.pyrtf, desenvolvido por Simon Cusack e revisado por Grant Edwards. Este módulo permite gerar documentos RTF programaticamente, incluindo texto formatado colorido e imagens.

No exemplo a seguir, iniciamos duas classes RTF básicas, Documento e Seção, anexamos o último ao primeiro e inserimos um texto simulado no último:

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)

No final, o documento é serializado por q.dumps(doc) . Observe que, antes de retornar um documento RTF, é necessário especificar o tipo de conteúdo no cabeçalho, senão o navegador não sabe como lidar com o arquivo.

Dependendo da configuração, o navegador pode perguntar se deseja salvar esse arquivo ou abri-lo usando um editor de texto.

 ReportLab e PDF
ReportLab
 
PDF

O web2py também pode gerar documentos PDF, com uma biblioteca adicional chamada "ReportLab" [ReportLab]  .

Se você estiver executando o web2py a partir do código-fonte, é suficiente ter o ReportLab instalado. Se você estiver executando a distribuição binária do Windows, será necessário descompactar o ReportLab na pasta "web2py /". Se você estiver executando a distribuição binária do Mac, você precisa descompactar o ReportLab na pasta:

web2py.app/Contents/Resources/

A partir de agora, assumimos que o ReportLab está instalado e que o web2py pode encontrá-lo. Vamos criar uma ação simples chamada "get_me_a_pdf" que gera um documento PDF.

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

Observe como geramos o PDF em um arquivo temporário exclusivo tmpfilename , lemos o PDF gerado a partir do arquivo, depois apagamos o arquivo.

Para obter mais informações sobre a API do ReportLab, consulte a documentação do ReportLab. É altamente recomendável usar a API do Platypus do ReportLab, como Paragraph , Spacer etc.

 Serviços Web Restful

REST

REST significa "REpresentational State Transfer" e é um tipo de arquitetura de serviço web e não, como SOAP, um protocolo. Na verdade, não há padrão para o REST.

Falando livremente REST diz que um serviço pode ser pensado como uma coleção de recursos. Cada recurso deve ser identificado por um URL. Existem quatro ações de métodos em um recurso e elas são POST (criar), GET (leitura), PUT (atualização) e DELETE, da qual significa a sigla CRUD (create-read-update-delete). Um cliente se comunica com o recurso fazendo uma solicitação HTTP para a URL que identifica o recurso e usando o método HTTP POST/PUT/GET/DELETE para passar as instruções para o recurso. O URL pode ter uma extensão, por exemplo json  que especificam como o protocolo para codificar os dados.

Então, por exemplo, uma solicitação POST para

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

significa que você quer criar um novo person . Neste caso, person  pode corresponder a um registro na tabela person  mas também pode ser algum outro tipo de recurso (por exemplo, um arquivo).

Da mesma forma, um pedido GET para

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

indica uma solicitação para uma lista de pessoas (registros dos dados person ) no formato json.

Um pedido GET para

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

indica uma solicitação para as informações associadas a person/1  (o registro com id==1 ) e no formato json.

No caso de web2py, cada solicitação pode ser dividida em três partes:

  • Uma primeira parte que identifica a localização do serviço, ou seja, a ação que expõe o serviço:
    http://127.0.0.1:8000/myapp/default/api/
  • O nome do recurso ( person , persons , person/1 etc.)
  • O protocolo de comunicação especificado pela extensão.

Observe que sempre podemos usar o roteador para eliminar qualquer prefixo indesejado na URL e, por exemplo, simplificar isso:

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

nisso:

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

mas isso é uma questão de gosto e já discutimos em detalhes no capítulo 4.

Em nosso exemplo, usamos uma ação chamada api  mas isso não é um requisito. Podemos, de fato, nomear a ação que expõe o serviço RESTful da maneira que quisermos e, na verdade, podemos até criar mais de um. Por uma questão de argumento, vamos continuar a assumir que a nossa ação RESTful é chamada api .

Vamos supor também que definimos as duas tabelas a seguir:

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

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

e eles são os recursos que queremos expor.

A primeira coisa que fazemos é criar a ação RESTful:

def api():
    return locals()

Agora nós o modificamos para que a extensão seja filtrada dos argumentos de requisição (de modo que request.args  pode ser usado para identificar o recurso) e para que ele possa lidar com os diferentes métodos separadamente:

@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()

Agora, quando fazemos uma solicitação HTTP GET para

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

ele chama e retorna GET('person','1')  onde GET é a função definida dentro da ação. Notar que:

  • não precisamos definir todos os quatro métodos, apenas aqueles que desejamos expor.
  • a função do método pode ter argumentos nomeados
  • a extensão é armazenada em request.extension  e o tipo de conteúdo é definido automaticamente.

o @request.restful()  decorador certifica-se de que a extensão na informação do caminho é armazenada em request.extension , mapeia o método de solicitação para a função correspondente dentro da ação (POST, GET, PUT, DELETE) e passa request.args  e request.vars  para a função selecionada.

Agora, criamos um serviço para o POST e obtemos registros individuais:

@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()

Notar que:

  • o GET e POST são tratados por diferentes funções
  • a função espera os argumentos corretos (argumentos não nomeados analisados por request.args  e os argumentos nomeados são de request.vars )
  • eles verificam se a entrada está correta e, eventualmente, levantam uma exceção
  • GET executa um select e retorna o registro, db.person(id) . A saída é convertida automaticamente em JSON porque a visualização genérica é chamada.
  • POST realiza um validate_and_insert(..)  e retorna o id  do novo registro ou, alternativamente, erros de validação. As variáveis do POST, **fields são as variáveis de postagem.

  parse_as_rest  (experimental)

A lógica explicada até agora é suficiente para criar qualquer tipo de serviço web RESTful, mas o web2py nos ajuda ainda mais.

Na verdade, o web2py fornece uma sintaxe para descrever quais tabelas de banco de dados queremos expor e como mapear recursos em URLs e vice-versa.

parse_as_rest

Isso é feito usando padrões de URL. Um padrão é uma cadeia que mapeia os argumentos de solicitação de uma URL em uma consulta de banco de dados. Existem 4 tipos de padrões atômicos:

  • constantes String por exemplo "amigo"
  • Constante de string correspondente a uma tabela. Por exemplo, "amigo [pessoa]" corresponderá a "amigos" no URL da tabela "pessoa".
  • Variáveis a serem usadas para filtrar. Por exemplo, "{person.id}" aplicará um db.person.name=={person.id}  filtro.
  • Nomes de campos, representados por ": campo"

Padrões atômicos podem ser combinados em padrões de URL complexos usando "/" como em

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

que dá uma url da forma

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

Em uma consulta por uma pessoa.id que retorna o nome da pessoa. Aqui "amigo [pessoa]" corresponde a "amigo" e filtra a tabela "pessoa". "{person.id}" corresponde a "1" e filtra "person.id == 1". ": campo" corresponde a "nome" e retorna:

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

Vários padrões de URL podem ser combinados em uma lista para que uma única ação RESTful possa atender a diferentes tipos de solicitações.

O DAL tem um método parse_as_rest(pattern, args, vars)  que, dada uma lista de padrões, o request.args  e a request.vars  corresponde ao padrão e retorna uma resposta (somente GET).

Então, aqui está um exemplo mais complexo:

@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 dict(db.person.validate_and_insert(**vars))
        elif table_name == 'pet':
            return dict(db.pet.validate_and_insert(**vars))
        else:
            raise HTTP(400)
    return locals()

O que compreende os seguintes URLs que correspondem aos padrões listados:

  • GET todas as pessoas
    http://.../api/friends
  • GET uma pessoa com nome começando com "t"
    http://.../api/friend/t
  • OBTER o valor do campo "info" da primeira pessoa com nome igual a "Tim"
    http://.../api/friend/Tim/info
  • Obter uma lista de animais de estimação da pessoa (amigo) acima
    http://.../api/friend/Tim/pets
  • GET o animal de estimação com o nome "Snoopy de pessoa com nome" Tim "
    http://.../api/friend/Tim/pet/Snoopy
  • OBTER o valor do campo "info" para o animal de estimação
    http://.../api/friend/Tim/pet/Snoopy/info

A ação também expõe duas URLs POST:

  • POSTAR um novo amigo
  • POSTAR um novo animal de estimação

Se você tem o utilitário "curl" instalado, você pode tentar:

$ 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}]}

É possível declarar consultas mais complexas, em que um valor na URL é usado para criar uma consulta que não envolve igualdade. Por exemplo

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

mapas

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

para dentro

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

E da mesma forma:

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

mapas

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

para dentro

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

atributos válidos para um campo em um padrão são: contains , startswith , le , ge , lt , gt , eq  (igual, padrão), ne  (não igual). Outros atributos especificamente para campos de data e data e hora são day , month , year , hour , minute , second .

Observe que essa sintaxe de padrão não foi projetada para ser geral. Nem todas as consultas possíveis podem ser descritas por meio de um padrão, mas muitas delas são. A sintaxe pode ser estendida no futuro.

Muitas vezes, você deseja expor algumas URLs RESTful, mas deseja restringir as possíveis consultas. Isso pode ser feito passando um argumento extra queries  ao parse_as_rest  método. queries  é um dicionário de (tablename,query)  onde query é uma consulta DAL para restringir o acesso à tabela tablename .

Também podemos solicitar resultados usando as variáveis GET do pedido

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

qual ordem em ordem alfabética ( name ) e depois por informação invertida order .

Também podemos limitar o número de registros, especificando um limit  e offset  GET variáveis

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

que irá retornar até 1000 amigos (pessoas) e pular os 10 primeiros. limit  o padrão é 1000 e offset  padrão para 0.

Let's now consider an extreme case. We want to build all possible patterns for all tables (except auth_  tabelas). Queremos poder pesquisar por qualquer campo de texto, qualquer campo inteiro, qualquer campo duplo (por intervalo) e qualquer data (também por intervalo). Também queremos poder publicar em qualquer tabela:

No caso geral, isso requer muitos padrões. O Web2py simplifica:

@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 dict(db[table_name].validate_and_insert(**vars))
    return locals()

Configurações patterns='auto'  resulta em web2py gerando todos os padrões possíveis para todas as tabelas não-auth. Existe até um padrão para consultar sobre padrões:

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

que para fora person  e pet  tabelas resulta em:

{"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"
]}

Você pode especificar padrões automáticos para apenas algumas tabelas:

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

  smart_query  (experimental)

smart_query

Há momentos em que você precisa de mais flexibilidade e deseja passar a um serviço RESTful uma consulta arbitrária como

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

Você pode fazer isso usando

@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 dict(db[table_name].validate_and_insert(**vars))
    return locals()

O método db.smart_query  leva dois argumentos:

  • uma lista de campo ou tabela que deve ser permitida na consulta
  • uma string contendo a consulta expressa em linguagem natural

e retorna um db.set  objeto com os registros que foram encontrados.

Observe que a cadeia de pesquisa é analisada, não avaliada ou executada e, portanto, não oferece nenhum risco de segurança.

 Controle de acesso

O acesso à API pode ser restrito como de costume usando decoradores. Então, por exemplo

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()

agora pode ser acessado com

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

 Serviços e Autenticação

Authentication

No capítulo anterior, discutimos o uso dos seguintes decoradores:

@auth.requires_login()
@auth.requires_membership(...)
@auth.requires_permission(...)

Para ações normais (não decoradas como serviços), esses decoradores podem ser usados mesmo se a saída for renderizada em um formato diferente de HTML.

Para funções definidas como serviços e decoradas usando o @service...  decoradores, o @auth...  decoradores não devem ser usados. Os dois tipos de decoradores não podem ser misturados. Se a autenticação é para ser realizada, é o call  ações que precisam ser decoradas:

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

Observe que também é possível instanciar vários objetos de serviço, registrar as mesmas funções diferentes com eles e expor alguns deles com autenticação e outros não:

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()

Isso pressupõe que o chamador esteja passando credenciais no cabeçalho HTTP (um cookie de sessão válido ou usando basic authentication , conforme discutido no capítulo anterior). O cliente deve suportá-lo; nem todos os clientes fazem.

Se estiver usando ServerProxy () descrito acima, você pode passar credenciais de autenticação básica na URL, da seguinte forma:

URL='http://user:[email protected]:8000/app/default/private_call/jsonrpc2'
service = ServerProxy(URL, version='2.0')

onde a função private_call  no controlador é decorado para autenticação do usuário

 top