Skip to content

22. Serviços web com o framework Flask

Por serviço web, entendemos aqui qualquer aplicação web que forneça dados brutos consumidos por um cliente, frequentemente um script de consola nos exemplos que se seguem. Não nos preocupamos com uma tecnologia específica, como REST (REpresentational State Transfer) ou SOAP (Simple Object Access Protocol), que fornecem dados mais ou menos brutos num formato bem definido. O REST devolve JSON, enquanto o SOAP devolve XML. Cada uma destas tecnologias descreve com precisão como o cliente deve consultar o servidor e o formato que a resposta do servidor deve assumir. Neste curso, seremos muito mais flexíveis no que diz respeito à natureza do pedido do cliente e à resposta do servidor. No entanto, os scripts escritos e as ferramentas utilizadas são semelhantes aos da tecnologia REST.

22.1. Introdução

Os scripts Python podem ser executados por um servidor web. Um script deste tipo torna-se um programa de servidor capaz de servir vários clientes. Do ponto de vista do cliente, chamar um serviço web equivale a solicitar o URL desse serviço. O cliente pode ser escrito em qualquer linguagem, incluindo Python. Neste último caso, utilizamos as funções de Internet que acabámos de abordar. Também precisamos de saber como «comunicar» com um serviço web, ou seja, compreender o protocolo HTTP para a comunicação entre um servidor web e os seus clientes. Esse foi o objetivo da secção sobre o protocolo HTTP. Os clientes web descritos nesta parte do curso permitiram-nos explorar parte do protocolo HTTP.

Image

Na sua forma mais simples, as trocas cliente/servidor decorrem da seguinte forma:

  • o cliente abre uma ligação à porta 80 no servidor web;
  • ele faz um pedido de um documento;
  • o servidor web envia o documento solicitado e encerra a ligação;
  • o cliente fecha então a conexão;

O documento pode ser de vários tipos: texto em formato HTML, uma imagem, um vídeo, etc. Pode ser um documento existente (documento estático) ou um documento gerado dinamicamente por um script (documento dinâmico). Neste último caso, referimo-nos à programação web. O script para gerar documentos dinamicamente pode ser escrito em várias linguagens: PHP, Python, Perl, Java, Ruby, C#, VB.NET, etc.

A seguir, utilizaremos scripts Python para gerar dinamicamente documentos de texto.

Image

  • Em [1], o cliente estabelece uma ligação com o servidor, solicita um script Python e pode ou não enviar parâmetros para esse script;
  • Em [3], o servidor web executa o script Python utilizando o interpretador Python. O script gera um documento que é enviado ao cliente [2];
  • O servidor encerra a ligação. O cliente faz o mesmo;

O servidor web pode lidar com vários clientes ao mesmo tempo.

A seguir, utilizaremos dois servidores web:

O servidor Flask será utilizado em todos os exemplos. O servidor Apache será utilizado para hospedar a aplicação web que vamos desenvolver.

O framework Flask está escrito em Python. É um módulo que se instala num terminal do PyCharm:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>pip install flask
Collecting flask
  Downloading Flask-1.1.2-py2.py3-none-any.whl (94 kB)
     || 94 kB 1.1 MB/s
Collecting click>=5.1
  Downloading click-7.1.2-py2.py3-none-any.whl (82 kB)
     || 82 kB 5.8 MB/s
Collecting itsdangerous>=0.24
  Downloading itsdangerous-1.1.0-py2.py3-none-any.whl (16 kB)
Collecting Jinja2>=2.10.1
  Downloading Jinja2-2.11.2-py2.py3-none-any.whl (125 kB)
     || 125 kB 6.4 MB/s
Collecting Werkzeug>=0.15
  Downloading Werkzeug-1.0.1-py2.py3-none-any.whl (298 kB)
     || 298 kB 6.4 MB/s
Collecting MarkupSafe>=0.23
  Downloading MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl (16 kB)
Installing collected packages: click, itsdangerous, MarkupSafe, Jinja2, Werkzeug, flask
Successfully installed Jinja2-2.11.2 MarkupSafe-1.1.1 Werkzeug-1.0.1 click-7.1.2 flask-1.1.2 itsdangerous-1.1.0
  • Linha 1: o comando executado;
  • linha 19: os componentes que foram instalados:
    • [flask-1.1.2]: é uma estrutura de desenvolvimento web em Python;
    • [Werkzeug-1.0.1]: é o servidor web que responderá aos pedidos dos clientes;
    • [Jinja2-2.11.2]: é uma ferramenta que permite inserir elementos dinâmicos em páginas que, de outra forma, seriam estáticas;

22.2. scripts [flask/01]: primeiros elementos da programação web

Image

Os nossos exemplos serão executados na seguinte arquitetura:

Image

  • em [1], um script Python será executado tal como um script de consola padrão;
  • em [2], de forma transparente, um servidor web é instanciado e aguarda pedidos. Na verdade, aceitará apenas um único URL;
  • em [3], o navegador solicitará a única URL do servidor;
  • em [4], o servidor executará o script Python especificado pela consola [1];
  • em [5], o script devolverá os seus resultados ao servidor web, um documento de texto;
  • em [6], o servidor web envia este documento de texto para o navegador;

22.2.1. script [example_01]: noções básicas de HTML

Um navegador web pode exibir vários documentos, sendo os mais comuns os documentos HTML (HyperText Markup Language). Estes consistem em texto formatado com tags na forma <tag>texto</tag>. Assim, o texto <b>importante</b> exibirá o texto «importante» em negrito. Existem tags independentes, como a tag <hr/>, que exibe uma linha horizontal. Não iremos rever todas as tags que podem ser encontradas no texto HTML. Existem muitos programas de software WYSIWYG que permitem criar uma página web sem escrever uma única linha de código HTML. Estas ferramentas geram automaticamente o código HTML para um layout criado utilizando o rato e controlos predefinidos. Pode, assim, inserir (utilizando o rato) uma tabela na página e, em seguida, visualizar o código HTML gerado pelo software para descobrir as tags a utilizar na definição de uma tabela numa página web. É tão simples quanto isso. Além disso, o conhecimento de HTML é essencial, uma vez que as aplicações web dinâmicas têm de gerar elas próprias o código HTML para enviar aos clientes web. Este código é gerado programaticamente e, naturalmente, tem de saber o que gerar para que o cliente receba a página web que deseja.

Resumindo, não é necessário conhecer toda a linguagem HTML para começar a programar para a Web. No entanto, este conhecimento é necessário e pode ser adquirido através da utilização de construtores de páginas Web WYSIWYG, como o DreamWeaver e dezenas de outros. Outra forma de descobrir as complexidades do HTML é navegar na Web e visualizar o código-fonte de páginas que apresentam elementos interessantes com os quais ainda não se deparou.

Considere o exemplo seguinte, que destaca alguns elementos comumente encontrados num documento web, tais como:

  • uma tabela;
  • uma imagem;
  • um link;

Image

Um documento HTML é delimitado pelas tags <html>…</html>. É composto por duas partes:

  • <head>…</head>: esta é a parte do documento que não é exibida. Fornece informações ao navegador que irá exibir o documento. Contém frequentemente a tag <title>…</title>, que define o texto que aparecerá na barra de título do navegador. Pode também conter outras tags, nomeadamente as que definem as palavras-chave do documento, que são posteriormente utilizadas pelos motores de busca. Esta secção pode ainda conter scripts, geralmente escritos em JavaScript ou VBScript, que serão executados pelo navegador;
  • <body attributes>…</body>: esta é a secção que será exibida pelo navegador. As tags HTML contidas nesta secção indicam ao navegador o layout visual «desejado» para o documento. Cada navegador interpreta estas tags à sua maneira. Como resultado, dois navegadores podem exibir o mesmo documento web de forma diferente. Este é geralmente um dos desafios enfrentados pelos web designers;

O código HTML para o nosso documento de exemplo é o seguinte:


<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>Quelques balises HTML</title>
</head>
 
<body style="background-image: url(/static/images/standard.jpg)">
  <h1 style="text-align: left">Quelques balises HTML</h1>
  <hr />
 
  <table border="1">
    <thead>
      <tr>
        <th>Colonne 1</th>
        <th>Colonne 2</th>
        <th>Colonne 3</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>cellule(1,1)</td>
        <td style="text-align: center;">cellule(1,2)</td>
        <td>cellule(1,3)</td>
      </tr>
      <tr>
        <td>cellule(2,1)</td>
        <td>cellule(2,2)</td>
        <td>cellule(2,3</td>
      </tr>
    </tbody>
  </table>
  <br /><br />
  <table border="0">
    <tr>
      <td>Une image</td>
      <td>
        <img border="0" src="/static/images/cerisier.jpg" />
      </td>
    </tr>
    <tr>
      <td>Le site de Polytech'Angers</td>
      <td><a href="http://www.polytech-angers.fr/fr/index.html">ici</a></td>
    </tr>
  </table>
</body>
</html>
HTML
Etiquetas HTML e exemplos
título do documento
<title>Algumas tags HTML</title> (linha 5)
O texto [Algumas tags HTML] aparecerá na barra de título do navegador quando o documento for exibido
barra horizontal
<hr />: exibe uma linha horizontal (linha 10)
tabela
<table attributes>….</table>: para definir a tabela (linhas 12, 32)
<thead>…</thead>: para definir os cabeçalhos das colunas (linhas 13, 19)
<tbody>…</tbody>: para definir o conteúdo da tabela (linhas 20, 31)
<atributos tr>…</tr>: para definir uma linha (linhas 21, 25)
<td attributes>…</td>: para definir uma célula (linha 22)
exemplos:
<table border="1">…</table>: o atributo border define a espessura da borda da tabela
<td style="text-align: center;">cell(1,2)</td> (linha 23): define uma célula cujo conteúdo será cell(1,2). Este conteúdo será centralizado horizontalmente (text-align: center).
imagem
<img border="0" src="/static/images/cherrytree.jpg"/> (linha 38): define uma imagem sem borda (border="0") cujo ficheiro de origem é [/static/images/cherrytree.jpg] no servidor web (src="/static/images/cherrytree.jpg"). Se esta ligação estiver num documento web acessível através do URL [http://server/chemin/balises.html], então o navegador irá solicitar o URL [http://server/static/images/cherry-tree.jpg] para recuperar a imagem aqui referenciada.
link
<a href="http://www.polytech-angers.fr/fr/index.html">aqui</a> (linha 43): faz com que o texto «aqui» funcione como um link para a URL http://www.polytech-angers.fr/fr/index.html.
fundo da página
<body style="background-image: url(/static/images/standard.jpg)"> (linha 8): indica que a imagem a ser utilizada como fundo da página está localizada no URL [/static/images/standard.jpg] no servidor web. No contexto do nosso exemplo, o navegador irá solicitar o URL [http://server/static/images/standard.jpg] para recuperar esta imagem de fundo.

Podemos ver neste exemplo simples que, para construir todo o documento, o navegador tem de enviar três pedidos ao servidor:

  • [http://server/chemin/balises.html] para recuperar o código-fonte HTML do documento;
  • [http://server/static/images/cerisier.jpg] para recuperar a imagem cerisier.jpg;
  • [http://server/static/images/standard.jpg] para recuperar a imagem de fundo standard.jpg;

O script [example_01] permitirá-nos apresentar a página estática anterior [tags.html]:

Image

  • em [1], o script [example_01] que será executado;
  • em [3], o documento HTML que será exibido pelo script;
  • em [2], as imagens do documento HTML;

O script [example_01] é o seguinte:

import os

from flask import Flask, make_response, render_template

#  flask application
script_dir = os.path.dirname(os.path.abspath(__file__))
app = Flask(__name__, template_folder=f"{script_dir}/../templates", static_folder=f"{script_dir}/../static")


#  Home URL
@app.route('/')
def index():
    #  page display
    return make_response(render_template("balises.html"))


#  hand
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • Linha 7: Instanciamos uma aplicação Flask. Uma aplicação Flask é uma aplicação web;
    • o primeiro parâmetro é o nome atribuído à aplicação. Pode escolher qualquer nome que desejar. Aqui, utilizámos o atributo predefinido [__name__], que está definido como [__main__] (linha 18);
    • o segundo parâmetro é um parâmetro nomeado, o que significa que a sua posição na lista de parâmetros não importa. O parâmetro nomeado [template_folder] especifica a pasta onde se encontram as páginas estáticas da aplicação web. As páginas estáticas são entregues tal como estão ao navegador. Aqui, as páginas estáticas serão encontradas na pasta [templates] dentro da árvore de diretórios do projeto. Na linha 7, especificámos um caminho relativo para a pasta [script_dir] que contém o script [example_01] que está a ser executado;
    • o terceiro parâmetro também é um parâmetro nomeado. [static_folder] especifica a pasta onde se encontram os recursos do documento HTML (imagens, vídeos, etc.). Aqui também, especificámos um caminho relativo para a pasta [script_dir] que contém o script [example_01] executado;
  • linhas 10–14: definimos os URLs aceites pela aplicação web. Cada URL está associado a uma função que é executada quando o URL é solicitado por um navegador web;
  • linha 11: a única URL da aplicação é [/]. Note-se que em [@app.route('/')], [app] é a variável inicializada na linha 7. A definição das rotas (as várias URLs tratadas pela aplicação) deve, portanto, vir após a definição da aplicação [app]. O nome da aplicação é arbitrário;
  • linhas 12–14: a função que é executada quando a URL [/] é solicitada à aplicação web [example_01];
  • linha 12: a função associada a uma URL pode ter qualquer nome. Por vezes, pode ter parâmetros para recuperar elementos da URL que lhe está associada. Aqui, não tem;
  • linha 14:
    • A função [render_template] devolve uma cadeia de caracteres que corresponde ao documento de texto gerado pelo seu parâmetro. Neste caso, esse parâmetro é [balises.html]. Devido à variável [template_folder] na linha 7, este documento será procurado na pasta [f"{script_dir}/../templates"]. É precisamente aí que se encontra;
    • a função [make_response] gera uma resposta HTTP para o navegador que solicitou a URL [/]. Vimos na secção |o protocolo HTTP| que uma resposta HTTP tem duas partes:
      • cabeçalhos HTTP;
      • o documento solicitado pelo navegador, neste caso um documento HTML;

Na linha 14, não passámos quaisquer parâmetros à função [make_response] para gerar cabeçalhos HTTP. Por conseguinte, esta irá gerar os cabeçalhos predefinidos. Veremos mais tarde como definir estes cabeçalhos HTTP.

  • Por fim, quando o navegador solicita a URL / da aplicação Flask, recebe a página [tags.html];
  • Linhas 17–20: Estas linhas são utilizadas para iniciar o servidor web que irá executar a aplicação web [example_01];
    • Linha 18: Esta condição é verdadeira apenas quando o script [example_01] é executado numa consola;
    • Linha 19: A aplicação [app] da linha 7 é configurada:
    • o parâmetro denominado [ENV="development"] define o servidor web para o modo de desenvolvimento: assim que o programador modifica um elemento da aplicação, este é regenerado e entregue ao servidor web. O programador não precisa de solicitar uma nova execução;
    • o parâmetro denominado [DEBUG=True] permite ao programador definir pontos de interrupção no código da aplicação;
    • Linha 20: A aplicação web é iniciada: um servidor web é instanciado e a aplicação web é implementada nele para responder a pedidos de clientes web;

Eis um exemplo de uma execução:

Image

Os seguintes registos aparecem então na consola de execução:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/flask/01/main/exemple_01.py
 * Serving Flask app "exemple_01" (lazy loading)
 * Environment: development
 * Debug mode: on
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 334-263-283
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
  • Linha 2: O servidor exibe o script executado;
  • linha 3: estamos no modo de desenvolvimento;
  • linhas 4-5: o servidor deteta que foi iniciado no modo [debug]. Em seguida, reinicia (linha 5). O modo [debug] torna, portanto, o arranque ligeiramente mais lento;
  • Linha 8: O URL onde a aplicação web implementada [example_01] está disponível;

Usando um navegador web, vamos solicitar a URL [http://127.0.0.1:5000/]:

Image

Recebemos, de facto, o documento [tags.html] esperado.

22.2.2. script [example_02]: gerar dinamicamente um documento HTML

Image

O script [example_02] [1] irá gerar o seguinte documento [example_02.html] [2]:


<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <title>{{page.title}}</title>
</head>
<body>
    <b>{{page.contents}}</b>
</body>
</html>

Este documento é dinâmico porque o seu conteúdo não é totalmente conhecido até que o servidor web o forneça. Especificamente, as linhas 5 e 8 contêm dois elementos que são desconhecidos no momento em que a página é escrita. Só são conhecidos quando a página é enviada a um cliente. São então substituídos pelos seus valores, que são cadeias de caracteres.

  • Linhas 5, 8: A sintaxe {{expression}} faz parte da linguagem de modelos Jinja2 [https://jinja.palletsprojects.com/en/2.11.x/]. Antes de a página ser enviada a um cliente, os elementos dinâmicos da página (linhas 5 e 8) são avaliados e substituídos pelos seus valores;
  • linha 5: utilizámos a sintaxe [page.title]. Partimos, portanto, do princípio de que, quando a página é gerada antes de ser enviada, uma variável [page] é conhecida; veremos como. Na sintaxe {{expressão}}, podemos utilizar quaisquer nomes de variáveis que desejarmos. Nas linhas 5 e 8, poderíamos, assim, ter {{title}} e {{contents}}. Poderíamos então dizer que [title] e [contents] são parâmetros da página. No que se segue, utilizaremos sempre a mesma técnica:
    • o único parâmetro da página será um dicionário [page];
    • os atributos deste dicionário serão utilizados na página. Aqui, [page.title] na linha 5 e [page.contents] na linha 8;

A aplicação web [example_02.py] é a seguinte:

from flask import Flask, make_response, render_template

#  flask application
script_dir = os.path.dirname(os.path.abspath(__file__))
app = Flask(__name__, template_folder=f"{script_dir}/../templates", static_folder=f"{script_dir}/../static")


#  Home URL
@app.route('/')
def index():
    #  page content in the form of a dictionary
    page = {"title": "un titre", "contents": "un contenu"}
    #  page display
    return make_response(render_template("exemple_02.html", page=page))


#  hand
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • Já explicámos as linhas 4–5 e 18–20 no exemplo anterior. Continuaremos a utilizar esta estrutura nos nossos exemplos;
  • linha 9: a única URL servida pela aplicação web é a URL /;
  • linha 14: o documento servido na URL / é o documento [example_02.html] que acabámos de discutir. Sabemos que ele tem um parâmetro, um dicionário chamado [page];
  • linha 12: definimos o dicionário que será passado como parâmetro para a página [example_02.html]. Pode ter qualquer nome. No entanto, deve conter os atributos [title, contents] utilizados no documento HTML;
  • linha 14: a função [render_template] é responsável pela renderização da cadeia de caracteres do documento [example_02.html]. Uma vez que se trata de um documento parametrizado, passamos o(s) parâmetro(s) esperado(s) à função [render_template]. Fazemos isso aqui atribuindo um valor ao parâmetro denominado [page]. Na operação [page=page]:
    • À esquerda do sinal = está o parâmetro [page] utilizado no documento [example_02.html];
    • à direita do sinal =, temos o valor [page] definido na linha 12;
    • Em geral, se um documento HTML tiver parâmetros [param1, param2, …, paramn], passamos os seus valores para a função [render_template] na forma [render_template(document, param1=value1, param2=value2, …] ;

Antes de executar [example_02], temos de interromper a execução de [example_01]:

Image

Se, enquanto o script 1 está a ser executado, parecer que o script 2 está a ser executado, é provável que o script 2 ainda esteja a ser executado. Para regressar a um estado conhecido, pode parar todos os processos atualmente em execução no PyCharm (canto superior direito da janela do PyCharm):

Image

Vamos executar o script [example_02]:

Image

Os registos da consola são então os seguintes:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/flask/01/main/exemple_02.py
 * Serving Flask app "exemple_02" (lazy loading)
 * Environment: development
 * Debug mode: on
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 334-263-283
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

A linha 8 indica a porta de implementação (5000) da aplicação [example_02] (linha 1) na máquina [localhost]. Uma vez que as linhas anteriores são sempre as mesmas, não as voltaremos a apresentar.

Utilizando um navegador, solicitamos o URL [http://localhost:5000/]:

Image

  • a expressão {{page.title}} produziu [1];
  • a expressão {{page.contents}} produziu [2];

22.2.3. script [example_03]: utilizando fragmentos de página

Image

  • em [1], o script [example_03.py] irá gerar o documento dinâmico [example_03.html] [2]. Este será construído a partir dos fragmentos de página [fragment_01.html, fragment_02.html] [3];

O documento [example_03.html] terá o seguinte aspeto:


<!DOCTYPE html>
<html lang="fr">
{% include "fragments/fragment_01.html" %}
<body>
{% include "fragments/fragment_02.html" %}
</body>
</html>
  • As linhas 3 e 5 utilizam a diretiva [include] do Jinja2 para incluir elementos externos no documento;
  • a sintaxe é {% include … %}. O parâmetro da diretiva [include] é o caminho para o documento a ser incluído. Este caminho é relativo ao parâmetro [template_folder] da aplicação Flask:

app = Flask(__name__, template_folder="../templates", static_folder="../static")

Portanto, neste caso, os caminhos dos documentos são relativos à pasta [templates].

O fragmento [fragment_01.html] (os nomes são, obviamente, arbitrários) é o seguinte:


<meta charset="UTF-8">
<title>{{page.title}}</title>

O fragmento [fragment_02.html] é o seguinte:


<b>{{page.contents}}</b>

Se reconstruirmos o documento [example_03.html] utilizando estes fragmentos, obtemos o seguinte código:


<!DOCTYPE html>
<html lang="fr">
<meta charset="UTF-8">
<title>{{page.title}}</title>
<body>
<b>{{page.contents}}</b>
</body>
</html>

Temos, portanto, um documento idêntico ao [example_02.html], mas construído a partir de fragmentos.

O script web [example_03.py] é o seguinte:

import os

from flask import Flask, make_response, render_template

#  flask application
script_dir = os.path.dirname(os.path.abspath(__file__))
app = Flask(__name__, template_folder=f"{script_dir}/../templates", static_folder=f"{script_dir}/../static")


#  Home URL
@app.route('/')
def index():
    #  page content
    page = {"title": "un autre titre", "contents": "un autre contenu"}
    #  page display
    return make_response(render_template("views/exemple_03.html", page=page))


#  hand
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()

O código é semelhante ao do [example_02.py]. A linha 16 mostra como referenciar documentos localizados em subpastas da [template_folder] a partir da linha 7.

A execução do script [example_03.py] produz os seguintes resultados no navegador:

Image

22.3. [flask/02] scripts: serviço web de data e hora

Image

O documento [date_time_server.html] é o seguinte:


<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <title>Date et heure du moment</title>
</head>
<body>
    <b>Date et heure du moment : {{page.date_heure}}</b>
</body>
</html>
  • linha 8: a página aceita o parâmetro [page.date_time];

O serviço web [date_time_server.py] é o seguinte:

#  imports
import os
import time

from flask import Flask, make_response, render_template

#  flask application
script_dir = os.path.dirname(os.path.abspath(__file__))
app = Flask(__name__, template_folder=f"{script_dir}")


#  Home URL
@app.route('/')
def index():
    #  dispatch time to customer
    #  time.localtime: number of milliseconds since 01/01/1970
    #  time.strftime formats time and date
    #  date-time display format
    #  d: 2-digit day
    #  m: 2-digit month
    #  y: 2-digit year
    #  H: hour 0.23
    #  M: minutes
    #  S: seconds

    #  current date / time
    time_of_day = time.strftime('%d/%m/%y %H:%M:%S', time.localtime())
    #  generate the document to be sent to the customer
    page = {"date_heure": time_of_day}
    document = render_template("date_time_server.html", page=page)
    print("document", type(document), document)
    #  HTTP response to customer
    response = make_response(document)
    print("response", type(response), response)
    return response


#  hand only
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • linha 13: a aplicação web serve apenas a URL /;
  • linhas 15–24: explicam como recuperar a data e a hora e como as apresentar;
  • linha 27: cadeia de caracteres que representa a data e a hora atuais;
  • linhas 28–30: geram o documento dinâmico [date_time_server.html] passando-lhe o dicionário [page] da linha 29;
  • linha 31: exibe o tipo de [document] e o próprio documento. Queremos mostrar que se trata de uma string;
  • linha 33: gera a resposta HTTP a ser enviada ao cliente (ainda não foi enviada);
  • linha 34: exibimos o seu tipo e valor;
  • linha 35: a resposta HTTP é enviada ao cliente;

A execução do script produz o seguinte resultado num navegador:

Image

Os registos na consola são os seguintes:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\flask\02\date_time_server.py
 * Serving Flask app "date_time_server" (lazy loading)
 * Environment: development
 * Debug mode: on
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 334-263-283
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
127.0.0.1 - - [10/Jul/2020 09:32:09] "GET / HTTP/1.1" 200 -
document <class 'str'> <!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <title>Date et heure du moment</title>
</head>
<body>
    <b>Date et heure du moment : 10/07/20 09:42:33</b>
</body>
</html>
response <class 'flask.wrappers.Response'> <Response 195 bytes [200 OK]>
  • Linha 10: Podemos ver que o tipo do valor devolvido por [render_template] é [str]. Esta string não é outra senão o documento [date_time_server.html] depois de ter sido analisado (linhas 10–19);
  • Linha 20: Podemos ver que o tipo do valor devolvido por [make_response] é [flask.wrappers.Response]. A função [Response.__str__] foi implicitamente chamada para exibir o objeto [Response]. A string devolvida por esta função fornece duas informações sobre a resposta HTTP que será enviada:
    • o documento enviado tem 195 bytes;
    • o estado da resposta HTTP é [200 OK]. Veremos mais tarde que temos acesso a este código de estado;

22.4. scripts [flask/03]: serviços web que geram texto simples

Vimos num exemplo anterior que o serviço web devolveu o seguinte documento:


<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <title>Date et heure du moment</title>
</head>
<body>
    <b>Date et heure du moment : {{page.date_heure}}</b>
</body>
</html>

Um cliente web pode estar interessado apenas na informação [page.date_time] na linha 8 e não na marcação HTML circundante. O serviço web poderia devolver esta informação como uma simples cadeia de caracteres. Apresentaremos aqui exemplos deste tipo de serviço web.

22.4.1. script [main_01]

Image

  • [main_01] é o serviço web;
  • [config] é o script de configuração da aplicação web;
  • o serviço web utiliza algumas das entidades definidas em [2];

O script [config] é o seguinte:

def configure():
    #  absolute path configuration relative path reference
    rootDir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    #  application dependencies
    absolute_dependencies = [
        #  Person, Utilities, MyException
        f"{rootDir}/classes/02/entities",

    ]
    #  set the syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #  return the config
    return {}

O principal objetivo desta configuração é definir o caminho Python para o serviço web. Precisamos de conseguir localizar as entidades [2] (linha 8).

O script web [main_01] é o seguinte:

#  configure the application
import config
config=config.configure()

#  imports
from flask import Flask, make_response
from flask_api import status

#  dependencies
from Personne import Personne

#  flask application (no static documents here)
app = Flask(__name__)


#  Home URL
@app.route('/')
def index():
    #  a person
    personne = Personne().fromdict({"prénom": "Aglaë", "nom": "de la Hûche", "âge": 87})
    #  answer HTTP
    response = make_response(str(personne))
    #  headers HTTP
    response.headers.set("Content-type", "application/json; charser=utf8")
    #  we return the answer HTTP
    return response, status.HTTP_200_OK


#  hand only
if __name__ == '__main__':
    #  start the server
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • linhas 1-3: define-se o Python Path da aplicação;
  • linhas 5-10: importam os elementos necessários ao script;
  • linha 17: o serviço web apenas serve a URL /;
  • linha 20: é criado um objeto [Person];
  • linha 22: é criada uma resposta HTTP com a string que representa a pessoa. A função [Person.__str__] é chamada. Isto devolve a string JSON do dicionário [asdict] da pessoa (ver |classe BaseEntity|). O parâmetro da função [make_response] é o documento de texto enviado ao cliente, ou seja, neste caso, a string JSON de uma pessoa;
  • linha 24: adicionamos um cabeçalho [Content-type] aos cabeçalhos HTTP da resposta, que informa ao cliente que tipo de documento irá receber — neste caso, um documento JSON codificado em UTF-8;
  • linha 26: devolvemos uma tupla de dois elementos:
    • a resposta ao cliente, incluindo cabeçalhos HTTP e o documento;
    • o código de estado da resposta. Aqui, queremos devolver o código de estado [200 OK]. Os vários códigos de estado são definidos por constantes no módulo [flask_api] importado na linha 7;

O módulo [flask_api] não está disponível por predefinição. É necessário instalá-lo. Pode fazê-lo num terminal do PyCharm:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>pip install flask_api
Collecting flask_api
  Downloading Flask_API-2.0-py3-none-any.whl (119 kB)
     || 119 kB 544 kB/s
Requirement already satisfied: Flask>=1.1 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from flask_api) (1.1.2)
Requirement already satisfied: Jinja2>=2.10.1 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask>=1.1->flask_api) (2.11.2)
Requirement already satisfied: Werkzeug>=0.15 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask>=1.1->flask_api) (1.0.1)
Requirement already satisfied: click>=5.1 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask>=1.1->flask_api) (7.1.2)
Requirement already satisfied: itsdangerous>=0.24 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask>=1.1->flask_api) (1.1.0)
Requirement already satisfied: MarkupSafe>=0.23 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Jinja2>=2.10.1->Flask>=1.1->flask_api) (1.1.1
)
Installing collected packages: flask-api
Successfully installed flask-api-2.0

Ao executar o script web [main_01], os seguintes resultados são apresentados num navegador:

Image

  • em [2], a cadeia JSON recebida;
  • em [3-4], apresentamos o conteúdo do documento recebido. Vemos que não há marcação HTML, apenas a cadeia JSON;

Agora, vamos analisar o papel do cabeçalho [Content-Type] enviado ao cliente pelo serviço web. Colocamos o navegador no modo de programador (normalmente F12) e solicitamos novamente o mesmo URL. Abaixo encontra-se uma captura de ecrã do navegador Chrome:

Image

  • Em [1], selecione o separador [Rede];
  • Em [2, 4]: a URL solicitada pelo navegador;
  • Em [3], selecione o separador [Headers] (cabeçalhos HTTP);
  • em [5], o código de estado da resposta HTTP recebida;
  • em [6], o cabeçalho que indica ao cliente que receberá um texto JSON. Isto permite que o cliente se adapte à resposta. Assim, o tipo de letra utilizado pelo Chrome para apresentar uma resposta JSON ou uma resposta de texto simples não é o mesmo;

Image

  • em [8], selecione o separador [Resposta] para aceder ao documento enviado pelo serviço web, neste caso uma simples cadeia JSON;

22.4.2. Postman

O [Postman] é a ferramenta que nos permitirá consultar os vários URLs de uma aplicação web. Permite-nos:

  • utilizar qualquer URL: estas são construídas manualmente;
  • consultar o servidor web utilizando GET, POST, PUT, OPTIONS, etc.;
  • especificar os parâmetros GET ou POST;
  • definir os cabeçalhos HTTP para o pedido;
  • receber uma resposta em formato JSON, XML ou HTML;
  • aceder aos cabeçalhos HTTP da resposta. Isto dá-lhe acesso à resposta HTTP completa do servidor;

O [Postman] é uma excelente ferramenta educativa para compreender a comunicação cliente/servidor através do protocolo HTTP.

O [Postman] está disponível no URL [https://www.getpostman.com/downloads/]. Prossiga com a instalação da sua versão do [Postman]. Durante a instalação, ser-lhe-á pedido que crie uma conta: isso não será necessário neste caso. A conta do [Postman] é utilizada para sincronizar diferentes dispositivos, de modo a que a configuração de um seja replicada noutro. Nada disso é necessário neste caso.

Uma vez instalado, o [Postman] apresenta a seguinte interface:

Image

  • em [2-3], pode aceder às definições do produto;

Image

  • em [6], a versão utilizada neste documento;

Aqui, vamos utilizar o [Postman] para testar o serviço web JSON anterior:

  • executamos o script [flask/03/main_01];
  • Em seguida, fazemos uma solicitação para a URL [http://localhost:5000/] usando o Postman; Image
  • Em [1], criamos uma solicitação;
  • em [2], será uma solicitação HTTP GET;
  • em [3], a URL do serviço web que está a ser consultado;
  • em [4], enviamos a solicitação para o serviço web; Image
  • em [5], selecione o separador [Body], que exibe o documento recebido;
  • em [6], selecione o separador [Pretty], que apresenta o documento recebido com a formatação adequada, neste caso formatado para uma cadeia JSON;
  • em [7], o documento JSON recebido;
  • em [8-9], o documento recebido sem formatação; Image
  • em [10], são apresentados os cabeçalhos HTTP recebidos pelo Postman;
  • em [11], o estado HTTP da resposta recebida;
  • em [12], os cabeçalhos HTTP recebidos;
  • em [13], o cabeçalho [Content-type] que permitiu ao Postman saber que iria receber uma cadeia JSON. O Postman utilizou esta informação para formatar o documento recebido de uma determinada forma;

Existe outra forma de utilizar o Postman. Envolve a utilização da consola do Postman (Ctrl-Alt-C). Isto permite-lhe visualizar o diálogo cliente/servidor. Além do atalho Ctrl-Alt-C, a consola do Postman é acessível através de um ícone no canto inferior esquerdo da janela principal do Postman:

Image

O console do Postman regista os diálogos cliente/servidor que ocorrem quando uma solicitação do Postman é executada:

Image

  • em [3], a lista de pedidos efetuados pelo Postman desde que foi iniciado. Os mais recentes encontram-se no final da lista;
  • em [4], o pedido HTTP efetuado pelo Postman;
  • em [5-6], a resposta HTTP enviada pelo servidor web;
  • em [7], pode visualizar os registos no modo [raw], ou seja, sem qualquer formatação;

No modo [raw], a janela da consola tem este aspeto:

Image

  • em [8], o pedido HTTP feito pelo Postman ao servidor web;
  • em [9], a resposta HTTP enviada pelo servidor web;
  • em [10], pode voltar ao modo [pretty logs];

Para facilitar a compreensão das explicações, iremos numerar as linhas obtidas a partir da consola do Postman.

Para o cliente:

1
2
3
4
5
6
7
8
GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: 70e2acaa-b3e5-46f6-8375-989e6b94e694
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

Para o servidor:

1
2
3
4
5
6
HTTP/1.0 200 OK
Content-type: application/json; charser=utf8
Content-Length: 56
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Mon, 13 Jul 2020 17:19:56 GMT
{"prénom": "Aglaë", "nom": "de la Hûche", "âge": 87}

A partir de agora, utilizaremos principalmente:

  • [Postman] como cliente web;
  • a consola do [Postman] no [modo raw] para explicar o diálogo cliente/servidor;

22.4.3. script [main_02]

Image

O script web [main_02] é o seguinte:

#  configure the application
import config
config=config.configure()

#  imports
from flask import Flask, make_response
from flask_api import status

#  dependencies
from Personne import Personne

#  flask application
app = Flask(__name__)


#  Home URL
@app.route('/')
def index():
    #  a person
    personne = Personne().fromdict({"prénom": "Aglaë", "nom": "de la Hûche", "âge": 87})
    #  content
    response = make_response(f"personne[{personne.prénom}, {personne.nom}, {personne.âge}]")
    #  headers HTTP
    response.headers.set("Content-Type", "text/plain; charset=utf8")
    #  answer HTTP
    return response, status.HTTP_200_OK


#  hand only
if __name__ == '__main__':
    #  start the server
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • O script [main_02] é semelhante ao script [main_01]. A diferença reside em dois aspetos:
    • linha 22: o documento enviado ao cliente é uma cadeia de caracteres bruta, e não uma cadeia JSON;
    • linha 24: isto reflete-se no cabeçalho HTTP [Content-Type], que especifica o tipo [text/plain] para o documento;

Executamos o script web [main_02] e, em seguida, usamos o [Postman] para o consultar:

Image

  • em [1-3], fazemos o pedido ao serviço web;
  • em [5], o estado OK da resposta;
  • em [4, 6], os cabeçalhos HTTP da resposta;
  • em [7], o cabeçalho [Content-Type];
  • em [8-10], o documento enviado pelo serviço web, uma sequência de caracteres;

A consola do Postman apresenta os seguintes registos:

Pedido do cliente:

1
2
3
4
5
6
7
8
GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: 7c7fc9f3-8df8-49ae-9dc8-53c2d87d111a
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

Resposta do servidor:


HTTP/1.0 200 OK
Content-Type: text/plain; charset=utf8
Content-Length: 34
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Mon, 13 Jul 2020 17:34:22 GMT
 
personne[Aglaë, de la Hûche, 87]

22.4.4. script [main_03]

Image

O script web [main_03] é o seguinte:

#  configure the application
import config
config = config.configure()

#  imports
from flask import Flask, make_response
from flask_api import status

#  dependencies
from MyException import MyException
from Personne import Personne

#  flask application
app = Flask(__name__)


#  Home URL
@app.route('/')
def index():
    #  an incorrect person
    msg_erreur = None
    try:
        personne = Personne().fromdict({"prénom": "", "nom": "", "âge": 87})
    except MyException as erreur:
        msg_erreur = f"{erreur}"
    #  mistake?
    if msg_erreur:
        response = make_response(msg_erreur)
        status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
    else:
        response = make_response(f"personne[{personne.prénom}, {personne.nom}, {personne.âge}]")
        status_code = status.HTTP_200_OK
    #  headers HTTP
    response.headers.set("Content-Type", "text/plain; charset=utf8")
    #  answer HTTP
    return response, status_code


#  hand only
if __name__ == '__main__':
    #  start the server
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • linha 23: ocorre um erro ao instanciar uma pessoa incorreta;
  • linhas 27–29: devido ao erro:
    • linha 28: prepare uma resposta HTTP com a mensagem de erro como conteúdo;
    • linha 29: definimos o código de estado HTTP para um valor de erro [500 Erro interno do servidor];
  • linha 34: informamos ao cliente que estamos a enviar texto simples;
  • linha 36: enviamos a resposta HTTP ao cliente;

Iniciamos o serviço web [main_03] e usamos o Postman para o consultar:

Image

  • Em [1-3], enviamos o pedido;
  • em [4], recebemos uma resposta com um código de estado [500 INTERNAL SERVER ERROR];
  • em [5-7]: a resposta é um texto que descreve o erro que ocorreu;

Image

  • em [8-10], os cabeçalhos HTTP da resposta do serviço web;

Na consola do Postman, os resultados no modo [raw] são os seguintes:

Pedido do cliente:

1
2
3
4
5
6
7
8
GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: 925ff036-a360-47af-adf6-78173c01a247
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

Resposta do servidor:


HTTP/1.0 500 INTERNAL SERVER ERROR
Content-Type: text/plain; charset=utf8
Content-Length: 74
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Mon, 13 Jul 2020 17:39:24 GMT
 
MyException[11, Le prénom doit être une chaîne de caractères non vide]

22.5. scripts [flask/04]: informações encapsuladas na solicitação

Image

O script [request_parameters.py] demonstra que o serviço web tem acesso a várias informações encapsuladas na solicitação de um cliente web. O código é o seguinte:

#  import
from flask import Flask, make_response, request
from flask_api import status
#  flask application
app = Flask(__name__)


#  Home URL
@app.route('/', methods=['GET', 'POST'])
def index():
    #  query parameters
    request_data = {}
    request_data["environ"] = f"{request.environ}"
    request_data["path"] = request.path
    request_data["full_path"] = request.full_path
    request_data["script_root"] = request.script_root
    request_data["url"] = request.url
    request_data["base_url"] = request.base_url
    request_data["url_root"] = request.url_root
    request_data["accept_charsets"] = request.accept_charsets
    request_data["accept_encodings"] = request.accept_encodings
    request_data["accept_languages"] = request.accept_languages
    request_data["accept_mimetypes"] = request.accept_mimetypes
    request_data["args"] = request.args
    request_data["content_encoding"] = request.content_encoding
    request_data["content_length"] = request.content_length
    request_data["content_type"] = request.content_type
    request_data["endpoint"] = request.endpoint
    request_data["files"] = request.files
    request_data["form"] = request.form
    request_data["host"] = request.host
    request_data["method"] = request.method
    request_data["query_string"] = request.query_string.decode()
    request_data["referrer"] = request.referrer
    request_data["remote_addr"] = request.remote_addr
    request_data["remote_user"] = request.remote_user
    request_data["scheme"] = request.scheme
    request_data["script_root"] = request.script_root
    request_data["user_agent"] = f"{request.user_agent}"
    request_data["values"] = request.values
    #  answer HTTP
    response = make_response(request_data)
    #  headers HTTP
    response.headers["Content-Type"] = "application/json; charset=utf-8"
    #  send reply HTTP
    return response, status.HTTP_200_OK


#  hand
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • Linha 9: Estamos a fazer uma alteração. Especificamos quais os verbos permitidos no pedido do cliente. O Postman fornece a lista:

Image

Os dois primeiros [GET, POST] são os mais utilizados e serão também os únicos utilizados neste documento. Voltando à linha 9 do código, o parâmetro [methods] contém a lista de métodos da lista acima que são permitidos pela URL. Na ausência deste parâmetro, apenas o método [GET] é permitido. É o que tem acontecido até agora;

  • linha 12: iremos construir o dicionário [request_data];
  • linha 13: o pedido do cliente está disponível num objeto predefinido [request], importado na linha 2, do tipo [werkzeug.local.LocalProxy]. As linhas seguintes recuperam vários atributos deste objeto;
  • em vez de detalhar cada atributo do objeto [request], vamos executar este código e observar os resultados. Assim, compreenderemos melhor o significado dos vários atributos apresentados;
  • Linha 42: O dicionário [request_data] será o conteúdo da resposta HTTP. Lembre-se de que este deve ser texto. O Flask converte automaticamente dicionários em cadeias JSON;
  • linha 44: informamos ao cliente que ele receberá JSON;
  • linha 46: enviamos a resposta ao cliente;

Usando o cliente Postman, enviamos a seguinte solicitação para o serviço web anterior:

Image

  • em [1-2], a solicitação enviada;
  • em [2], a solicitação está configurada. Os parâmetros são anexados à URL no formato [?param1=valor1&param2=valor2]. Existem duas maneiras de inserir esses parâmetros no Postman:
    • introduzi-los diretamente na URL;
    • introduzi-los em [3-4];

Ambos os métodos são equivalentes;

Adicionamos parâmetros adicionais à solicitação:

Image

  • em [5-7], adicionamos parâmetros ao corpo da solicitação. Enquanto os parâmetros da URL são visíveis para o utilizador num navegador web, os que se encontram no corpo da solicitação não são visíveis. O navegador (ou o Postman, neste caso) envia-os para o servidor após os cabeçalhos HTTP. A solicitação do cliente web passa então a ter a mesma estrutura que a resposta do servidor web: cabeçalhos HTTP seguidos de um documento. Isto irá introduzir dois novos cabeçalhos HTTP na solicitação do cliente:
    • [Content-Type]: o cliente informa ao servidor que tipo de documento está a enviar;
    • [Content-Length]: o tamanho do documento em bytes;
  • em [6], a codificação a utilizar para os parâmetros declarados em [7]. Estes podem ser codificados de várias formas. [x-www-form-urlencoded] é um método frequentemente utilizado pelos navegadores;

Eis a solicitação que será gerada:

Image

A resposta a esta solicitação é a seguinte:

Image

  • em [1-5], recebemos uma cadeia JSON [3];
  • O que normalmente interessa ao serviço web são os parâmetros da URL [?param1=valor1&param2=valor2] e aqueles transmitidos no corpo da solicitação (documento). É assim que, geralmente, o cliente envia informações ao serviço web. Conforme mostrado em [5], os parâmetros da URL estão disponíveis em [request.args];

O restante da resposta é o seguinte:

Image

  • em [9], os atributos dos parâmetros incluídos no corpo da solicitação:
    • [content_type] é o tipo do documento que acompanha a solicitação. Vimos que este documento continha informações do tipo [param=value] codificadas no formato [x-www-form-urlencoded]. O Postman, portanto, gerou um cabeçalho HTTP [Content-Type] indicando a natureza do documento;
    • [content_length] é o tamanho deste documento em bytes;
  • Em [10], o atributo [request.environ] contém uma grande quantidade de informações sobre o ambiente no qual a solicitação do cliente é processada. A maior parte dessas informações pode ser encontrada nos outros atributos do objeto [request];
  • em [11], os parâmetros presentes no corpo da solicitação estão disponíveis no atributo [request.form];
  • em [12], o método utilizado para enviar a solicitação, neste caso o método [GET];
  • em [13], o atributo [request.values] é o dicionário de todos os parâmetros, incluindo os da URL e os do corpo do documento. Para recuperar os parâmetros da solicitação, use o atributo:
    • [request.args] para recuperar os presentes na URL;
    • [request.form] para recuperar os que estão no corpo do documento;

Na consola do Postman, os registos são os seguintes:

Pedido do cliente:

GET /?param1=valeur1&param2=valeur2 HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: cbfac6aa-71a0-4076-a0c3-91d36d74a4c0
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 60

nom=s%C3%A9l%C3%A9n%C3%A9&pr%C3%A9nom=agla%C3%AB&%C3%A2ge=77
  • linha 9: o tipo de documento enviado ao servidor na linha 12;
  • linha 11: os cabeçalhos HTTP da solicitação são separados do documento enviado por uma linha em branco. É assim que o servidor identifica o fim dos cabeçalhos HTTP do cliente;
  • linha 12: o documento «codificado por URL». Todos os caracteres acentuados foram codificados;

A resposta do cliente é a seguinte:


HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 2433
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 15 Jul 2020 06:09:09 GMT
 
{
  "accept_charsets": [], 
  "accept_encodings": [
    [
      "gzip", 
      1
    ], 
    [
      "deflate", 
      1
    ], 
    [
      "br", 
      1
    ]
  ], 
  "accept_languages": [], 
  "accept_mimetypes": [
    [
      "*/*", 
      1
    ]
  ], 
  "args": {
    "param1": "valeur1", 
    "param2": "valeur2"
  }, 
  "base_url": "http://localhost:5000/", 
  "content_encoding": null, 
  "content_length": 60, 
  "content_type": "application/x-www-form-urlencoded", 
  "endpoint": "index", 
  "environ": "{'wsgi.version': (1, 0), 'wsgi.url_scheme': 'http', 'wsgi.input': <_io.BufferedReader name=908>, 'wsgi.errors': <_io.TextIOWrapper name='<stderr>' mode='w' encoding='utf-8'>, 'wsgi.multithread': True, 'wsgi.multiprocess': False, 'wsgi.run_once': False, 'werkzeug.server.shutdown': <function WSGIRequestHandler.make_environ.<locals>.shutdown_server at 0x00000173CA6E5160>, 'SERVER_SOFTWARE': 'Werkzeug/1.0.1', 'REQUEST_METHOD': 'GET', 'SCRIPT_NAME': '', 'PATH_INFO': '/', 'QUERY_STRING': 'param1=valeur1&param2=valeur2', 'REQUEST_URI': '/?param1=valeur1&param2=valeur2', 'RAW_URI': '/?param1=valeur1&param2=valeur2', 'REMOTE_ADDR': '127.0.0.1', 'REMOTE_PORT': 50592, 'SERVER_NAME': '127.0.0.1', 'SERVER_PORT': '5000', 'SERVER_PROTOCOL': 'HTTP/1.1', 'HTTP_USER_AGENT': 'PostmanRuntime/7.26.1', 'HTTP_ACCEPT': '*/*', 'HTTP_CACHE_CONTROL': 'no-cache', 'HTTP_POSTMAN_TOKEN': 'cbfac6aa-71a0-4076-a0c3-91d36d74a4c0', 'HTTP_HOST': 'localhost:5000', 'HTTP_ACCEPT_ENCODING': 'gzip, deflate, br', 'HTTP_CONNECTION': 'keep-alive', 'CONTENT_TYPE': 'application/x-www-form-urlencoded', 'CONTENT_LENGTH': '60', 'werkzeug.request': <Request 'http://localhost:5000/?param1=valeur1&param2=valeur2' [GET]>}", 
  "files": {}, 
  "form": {
    "nom": "s\u00e9l\u00e9n\u00e9", 
    "pr\u00e9nom": "agla\u00eb", 
    "\u00e2ge": "77"
  }, 
  "full_path": "/?param1=valeur1&param2=valeur2", 
  "host": "localhost:5000", 
  "method": "GET", 
  "path": "/", 
  "query_string": "param1=valeur1&param2=valeur2", 
  "referrer": null, 
  "remote_addr": "127.0.0.1", 
  "remote_user": null, 
  "scheme": "http", 
  "script_root": "", 
  "url": "http://localhost:5000/?param1=valeur1&param2=valeur2", 
  "url_root": "http://localhost:5000/", 
  "user_agent": "PostmanRuntime/7.26.1", 
  "values": {
    "nom": "s\u00e9l\u00e9n\u00e9", 
    "param1": "valeur1", 
    "param2": "valeur2", 
    "pr\u00e9nom": "agla\u00eb", 
    "\u00e2ge": "77"
  }
}
  • linhas 1-5: os cabeçalhos HTTP da resposta terminam com uma linha em branco;
  • linhas 41-45: os caracteres acentuados foram codificados em UTF-8;

Se agora utilizarmos o método [POST] para enviar o mesmo pedido com os mesmos parâmetros, obteremos a mesma resposta, exceto que em [12], teremos [‘method’: ‘POST’].

Então, qual é a diferença entre os métodos GET e POST? A diferença é subtil e decorre da forma como os navegadores os têm utilizado historicamente:

  • Os parâmetros na URL são convenientes porque uma URL configurada desta forma pode servir como um link dentro de um documento HTML. O utilizador também pode alterar os próprios parâmetros para obter respostas diferentes do servidor. Neste caso, os navegadores utilizam normalmente o método [GET], e não há corpo (content_length=0) na solicitação enviada ao servidor web (sem parâmetros ocultos);
  • Por vezes, não queremos que os parâmetros sejam exibidos na URL. É o caso das palavras-passe enviadas para o servidor. Além disso, o espaço ocupado pelos parâmetros da URL é limitado (uma URL não pode exceder um determinado comprimento). Os parâmetros do corpo da solicitação não têm essa limitação. Adicionalmente, demasiados parâmetros na URL tornam-na ilegível. Consideremos o caso comum de um formulário de registo num site. Historicamente, quando as páginas HTML ainda não incluíam JavaScript, os navegadores enviavam as informações introduzidas através de uma solicitação POST. Isto era referido como «valores postados»;

Assim, nos primórdios da programação web:

  • os métodos GET eram geralmente associados à solicitação de informações a um servidor web;
  • Os métodos POST eram geralmente associados ao envio de informações do navegador para o servidor. O servidor era então «enriquecido» por esses dados;

Desde então, surgiu o JavaScript. Enquanto nos exemplos anteriores o programador não tinha controlo (clicar num link desencadeava inevitavelmente um GET, enviar um formulário envolvia inevitavelmente um POST), o JavaScript devolveu-lhe esse controlo. Neste modelo, a página HTML está ligada a código JavaScript que pode contornar o navegador. Assim, um clique num link pode ser interceptado pelo código JavaScript, que pode então executar código que envia um pedido ao servidor. Esta solicitação será transparente para o utilizador. Ele não a verá. Este código atua como um cliente web e, tal como fizemos com o Postman, o programador pode criar qualquer solicitação que desejar. Voltando ao exemplo de clicar num link, ele pode realizar uma solicitação POST quando, por padrão, o navegador teria realizado uma solicitação GET. Esses desenvolvimentos tornaram as diferenças entre GET e POST menos relevantes.

No entanto, os programadores seguem frequentemente estas regras:

  • Uma solicitação GET não deve modificar o estado do servidor. Solicitações GET sucessivas feitas com os mesmos parâmetros na URL devem retornar o mesmo documento. Além disso, uma solicitação GET normalmente não tem corpo (nenhum documento associado), apenas parâmetros na URL;
  • Uma solicitação POST pode modificar o estado do servidor. Os parâmetros são, na maioria das vezes, enviados no corpo da solicitação. Estes são referidos como valores postados. O exemplo do formulário é o mais revelador: os valores inseridos pelo utilizador são colocados no corpo da solicitação POST, e o servidor armazena-os em algum lugar, frequentemente numa base de dados;

No restante deste documento, não seguimos nenhuma regra específica.

22.6. scripts [flask-05]: gestão da memória do utilizador

22.6.1. Introdução

Nos exemplos anteriores de cliente/servidor, o processo funcionava da seguinte forma:

  • o cliente abre uma ligação à porta 80 na máquina do servidor web;
  • envia a sequência de texto: cabeçalhos HTTP, linha em branco, [documento];
  • em resposta, o servidor envia uma sequência do mesmo tipo;
  • o servidor encerra a ligação com o cliente;
  • o cliente encerra a ligação ao servidor;

Se o mesmo cliente fizer uma nova solicitação ao servidor web pouco tempo depois, é estabelecida uma nova ligação entre o cliente e o servidor. O servidor não consegue determinar se o cliente que se está a ligar já visitou o site anteriormente ou se esta é a primeira solicitação. Entre ligações, o servidor «esquece» o seu cliente. Por esta razão, diz-se que o protocolo HTTP é um protocolo sem estado. No entanto, é útil que o servidor se lembre dos seus clientes. Por exemplo, se uma aplicação for segura, o cliente enviará ao servidor um nome de utilizador e uma palavra-passe para se autenticar. Se o servidor «esquecer» o seu cliente entre conexões, o cliente teria de se autenticar a cada nova conexão, o que não é viável.

Para rastrear um cliente, o servidor pode proceder de várias maneiras:

  1. quando um cliente faz um pedido inicial, o servidor inclui um identificador na sua resposta que o cliente deve então reenviar com cada novo pedido. Utilizando este identificador, que é único para cada cliente, o servidor consegue reconhecer o cliente. Pode então gerir uma cache para esse cliente sob a forma de uma cache associada de forma única ao identificador do cliente. É assim que funcionam os serviços PHP, por exemplo;
  2. Quando um cliente faz uma solicitação inicial, o servidor inclui na sua resposta não um identificador, mas a própria memória do utilizador. Não armazena nada no lado do servidor. Para manter a sua memória, o cliente web deve reenviar essa memória com cada nova solicitação. Esta memória é modificada (ou não) com cada nova solicitação e reenviada (ou não) para o cliente. Este é o método utilizado pela estrutura Flask;

As diferenças entre os dois métodos são as seguintes:

  • O Método 1 utiliza menos largura de banda. Apenas um identificador é trocado entre o cliente e o servidor. À medida que a memória do utilizador cresce, isto não tem efeito sobre o identificador, que permanece o mesmo. Este não é o caso do Método 2, onde a memória do utilizador é trocada a cada pedido e pode crescer ao longo de vários pedidos;
  • O Método 1 consome mais espaço de memória. Isto porque o servidor armazena a memória do utilizador nos seus sistemas de ficheiros. Se houver um milhão de utilizadores, isto poderá potencialmente representar um problema. O Método 2 não armazena nada no servidor;

Tecnicamente, é assim que funciona em ambos os métodos:

  • Na resposta a um novo cliente, o servidor inclui o cabeçalho HTTP [Set-Cookie: Key=ID] ou [Set-Cookie: memory]. Com o Método 1, faz isto apenas na primeira solicitação. Com o Método 2, faz isto sempre que a memória do utilizador muda;
  • Nas suas solicitações, o cliente devolve sistematicamente o que recebeu — um identificador ou uma memória. Faz isso através do cabeçalho HTTP [Cookie: Key=Value];

Pode-se questionar como é que o servidor sabe que está a lidar com um novo cliente em vez de um cliente recorrente. É a presença do cabeçalho HTTP Cookie nos cabeçalhos HTTP do cliente que o indica. Para um novo cliente, este cabeçalho está ausente.

O conjunto de ligações de um determinado cliente é chamado de sessão.

O servidor pode manter outros tipos de memória:

Image

  • em [1], a memória ao nível do pedido é específica. É utilizada quando o pedido do cliente web não é processado por um único serviço (ou aplicação), mas por vários. Para passar informação ao serviço i+1, o serviço i pode enriquecer o pedido processado com essa informação. Isto é designado por memória ao nível do pedido. Não utilizaremos este tipo de memória neste documento;
  • em [2, 4], a memória do utilizador que acabámos de descrever. Pode ser implementada localmente [2] ou mantida utilizando o cliente [4];
  • em [3], a memória de «nível de aplicação» é geralmente de leitura única. É partilhada por todos os utilizadores. Contém frequentemente elementos da configuração da aplicação web, que é partilhada por todos os utilizadores da aplicação. Devemos ter cuidado com este tipo de memória: a gravação na mesma deve ocorrer num momento em que os utilizadores ainda não tenham enviado pedidos, na maioria das vezes no arranque da aplicação. Assim que os pedidos começam a chegar, torna-se difícil gravar nesta memória. Quando o servidor web está a servir vários utilizadores simultaneamente e dois deles tentam escrever na memória de nível «aplicação», existe o risco de esta memória ficar corrompida. Isto porque, embora o Utilizador 1 tenha começado a escrever na memória de nível «aplicação», pode ser interrompido antes de terminar. Isto resulta numa memória de aplicação incompleta. Uma vez que é partilhada, o Utilizador 2 pode lê-la e obter um estado incorreto;

22.6.2. script [session_scope_01]

Image

Os scripts [session_scope_xx] ilustram a gestão da memória do utilizador.

O script [session_scope_01] é o seguinte:

#  configure the application
import config
config = config.configure()

#  dependencies
import json
from flask import Flask, make_response, session
from flask_api import status

#  flask application
app = Flask(__name__)

#  session secret key
app.secret_key = config["SECRET_KEY"]


@app.route('/set-session', methods=['GET'])
def set_session():
    #  put something in the session
    session['nom'] = 'séléné'
    #  send an empty response
    response = make_response()
    response.headers['Content-Length'] = 0
    return response, status.HTTP_200_OK


@app.route('/get-session', methods=['GET'])
def get_session():
    #  we retrieve the session and send the response
    response = make_response(json.dumps({"nom": session['nom']}, ensure_ascii=False))
    response.headers['Content-Type'] = 'application/json; charset=utf-8'
    return response, status.HTTP_200_OK


#  hand only
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • linha 11: é instanciada uma aplicação Flask;
  • linha 14: ao atributo [secret_key] desta aplicação é atribuído um valor retirado do ficheiro de configuração utilizado nas linhas 1–3. Uma sessão Flask só é possível se este atributo for inicializado. Pode colocar qualquer coisa nele. É utilizado para encriptar uma parte dos «dados do utilizador» que serão enviados para o cliente. Geralmente colocamos algo que seja difícil de adivinhar. No ficheiro [config], a chave secreta é definida da seguinte forma:

    # on rend la config
    config = {
        # configuration Flask
        "SECRET_KEY": "vibnFfrdWYUp?*LQ"
    }
  • Pela primeira vez, estamos a definir uma aplicação web que serve algo diferente da URL /
    • linha 17: a URL [/set-session] é utilizada para inicializar a sessão do utilizador;
    • linha 27: a URL [/get-session] é utilizada para recuperar a sessão do utilizador;
  • linha 20: colocamos algo na sessão do utilizador, neste caso um nome. A sessão é gerida de forma semelhante a um dicionário. Não é possível colocar qualquer coisa na sessão. Os valores inseridos devem poder ser convertidos para JSON ( ). Para os tipos predefinidos do Python, isto acontece automaticamente sem intervenção d . Para objetos personalizados que o Python não reconhece, deve realizar a conversão para JSON manualmente;
  • linha 22: criamos uma resposta HTTP sem conteúdo (sem parâmetros passados para `make_response`);
  • linha 23: informamos ao cliente que ele receberá um documento vazio (com 0 bytes);
  • linha 24: enviamos a resposta HTTP ao cliente. A URL [/set-session] não faz, portanto, nada além de inicializar uma sessão do utilizador;
  • linha 27: a URL [/get-session] permite ao utilizador ver o que está na sua sessão;
  • linha 30: criamos uma resposta HTTP contendo a string JSON da sessão do utilizador. Aqui, criámos a string JSON nós próprios, em vez de deixarmos o Flask gerá-la. Isto porque não queremos que os caracteres acentuados sejam escapados (ensure_ascii=False);
  • Linha 31: informamos ao cliente que estamos a enviar JSON;
  • linha 32: enviamos a resposta HTTP ao cliente;

O objetivo deste script é demonstrar que a sessão do utilizador nos permite ligar os pedidos sucessivos do utilizador:

  • A solicitação 1 enviará uma solicitação para a URL [/set-session];
  • A solicitação 2 irá solicitar a URL [/get-session] e recuperar o nome que a solicitação 1 inicializou;

O script [config] que configura os scripts na pasta [flask/05] é o seguinte:

def configure():
    #  absolute path configuration relative path reference
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    #  application dependencies
    absolute_dependencies = [
        #  Person, Utilities, MyException
        f"{root_dir}/classes/02/entities",
    ]
    #  set the syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #  return the config
    config = {
        #  flask configuration
        "SECRET_KEY": "vibnFfrdWYUp?*LQ"
    }

    return config

Executamos o script [session_scope_01] e, em seguida, usamos o Postman para solicitar a URL [/set-session]. Antes de o fazer, vamos verificar alguns elementos do pedido que será efetuado:

Image

  • em [1], aceder aos cookies do Postman; Image
  • em [2-4], verificamos os cookies conhecidos do Postman e eliminamos todos [4-5];

Agora, vamos verificar a solicitação HTTP que será gerada:

Image

  • em [9]: alguns dos cabeçalhos HTTP que o Postman incluirá na solicitação com base na configuração que definimos para ele. Esta verificação permite-lhe confirmar que não omitiu nenhum parâmetro ou, inversamente, deixou parâmetros desnecessários;

Assim que isto estiver feito, podemos executar a solicitação:

Image

Existem diferentes formas de verificar o resultado. Pode começar por observar a janela principal:

Image

  • em [1-2], a solicitação enviada ao serviço web;
  • em [3-6], os cabeçalhos HTTP da resposta;
  • em [4], como não especificámos o tipo de resposta no código, o Flask utilizou o tipo padrão [text/html];
  • em [5], o cliente sabe que não há nenhum documento na resposta;
  • linha 6: o cabeçalho [Set-Cookie] foi enviado pelo servidor Flask. O seu valor é chamado de cookie de sessão. Consiste em três elementos:
    • [session=value]: value representa os dados da sessão do utilizador numa forma codificada. Estes dados são descodificáveis (ver |https://blog.miguelgrinberg.com/post/how-secure-is-the-flask-user-session|). No entanto, devido à chave secreta utilizada pelo servidor, o utilizador não pode modificar os dados recebidos e, em seguida, reenviá-los para o servidor. Quando o servidor recebe uma sessão, tem assim a garantia de receber uma sessão intacta;
    • [HttpOnly]: a presença deste atributo indica ao navegador que o recebe que o cookie não deve ser acessível a qualquer JavaScript que a página que está a apresentar possa conter;
    • [Path=/] é o caminho para o qual o cookie de sessão deve ser reenviado, ou seja, qualquer caminho dentro da aplicação web. Sempre que o utilizador solicitar explicitamente (digitando um URL) ou implicitamente (clicando num link) um URL deste domínio, o navegador reenviará automaticamente o cookie de sessão que recebeu;

A desvantagem da janela principal é que não tem acesso ao pedido completo que levou a esta resposta. O que é apresentado nesta janela é confuso:

Image

  • Nos cabeçalhos HTTP [3-4], um cookie de sessão é mostrado em [5]. Pode-se pensar que o Postman incluiu um cookie de sessão na solicitação, mas não é esse o caso. Os cabeçalhos [3] representam, na verdade, os cabeçalhos HTTP que serão enviados na próxima solicitação, conforme está configurado atualmente. O Postman acabou de receber um cookie de sessão, que irá enviar de volta na próxima solicitação. É por isso que temos [5];

Pode aceder à caixa de diálogo cliente/servidor na consola do Postman premindo Ctrl-Alt-C:


GET /set-session HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: 3673b73f-7600-4df4-8c4b-c37973e50df8
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
 
HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 0
Vary: Cookie
Set-Cookie: session=eyJub20iOiJzXHUwMGU5bFx1MDBlOW5cdTAwZTkifQ.Xw6jGQ.y5Icu70wTIN-B0o_hwx0xDH247I; HttpOnly; Path=/
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 15 Jul 2020 06:32:57 GMT
  • Linha 14: o cookie de sessão enviado pelo servidor;

Agora vamos solicitar a URL [/get-session]:

GET /get-session HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: ce991398-2d9a-46d0-9ccd-c7ff3c7f4d6d
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: session=eyJub20iOiJzXHUwMGU5bFx1MDBlOW5cdTAwZTkifQ.Xw6jGQ.y5Icu70wTIN-B0o_hwx0xDH247I

HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 20
Vary: Cookie
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 15 Jul 2020 06:36:52 GMT

{"nom": "séléné"}
  • linha 9: o cliente Postman reenviou o cookie de sessão que tinha recebido para o servidor;
  • linha 18: a cadeia JSON enviada pelo servidor;

Este exemplo ilustra vários pontos:

  • o cliente Postman reenvia o cookie de sessão que recebe do servidor Flask. Os navegadores web fazem sempre isto;
  • vemos que a solicitação 2 [/get-session] recuperou informações criadas durante a solicitação 1 [/set-session]. Isso funciona efetivamente como o estado do utilizador;
  • Linhas 11–16: O servidor Flask não devolveu um cookie de sessão. Isto nem sempre acontece. O servidor Flask só devolve o cookie de sessão se a última solicitação tiver modificado a sessão do utilizador;

22.6.3. script [session_scope_02]

Image

O script [session_02] é o seguinte:

#  dependencies
import os

from flask import Flask, make_response, session
from flask_api import status

#  flask application
app = Flask(__name__)

#  session secret key
app.secret_key = os.urandom(12).hex()


#  Home URL
@app.route('/', methods=['GET'])
def index():
    #  we manage three meters
    if session.get('n1') is None:
        session['n1'] = 0
    else:
        session['n1'] = session['n1'] + 1
    if session.get('n2') is None:
        session['n2'] = 10
    else:
        session['n2'] = session['n2'] + 1
    if session.get('n3') is None:
        session['n3'] = 100
    else:
        session['n3'] = session['n3'] + 1
    #  meter dictionary
    compteurs = {"n1": session['n1'], "n2": session['n2'], "n3": session['n3']}
    #  we send the answer
    response = make_response(compteurs)
    response.headers['Content-Type'] = 'application/json; charset=utf-8'
    return response, status.HTTP_200_OK


#  hand
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • linha 11: aqui, a chave secreta é gerada usando uma função. A vantagem desta função é que ela gera aleatoriamente uma sequência complexa. Note que a variável [app] é a instância da classe Flask criada na linha 8;
  • linha 15: desta vez, haverá apenas uma rota, a rota /;
  • Linhas 17–29: Gerimos uma sessão que contém três contadores [n1, n2, n3]. Na primeira chamada do utilizador, [n1, n2, n3] = [0, 10, 100], e em cada chamada subsequente, estes contadores são incrementados em 1;
  • linha 18: na primeira solicitação, a sessão da aplicação está vazia. A expressão [session.get('key')] retorna o valor [None]. Para solicitações subsequentes, esta expressão retornará o valor associado à chave;
  • linha 31: estes contadores são colocados num dicionário;
  • linha 33: este dicionário é o corpo da resposta HTTP. Lembre-se de que o Flask converte automaticamente dicionários em cadeias JSON;
  • linha 34: o cliente web é informado de que receberá JSON;
  • linha 35: enviamos a resposta HTTP ao cliente;

Vamos executar este script e consultar a aplicação web criada desta forma utilizando o Postman, após eliminar todos os cookies do cliente Postman [1-3]:

Image

Na consola do Postman, as trocas entre cliente e servidor são as seguintes:


GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: c7db536d-9352-4aa6-9877-04560e03d935
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
 
HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 41
Vary: Cookie
Set-Cookie: session=eyJuMSI6MCwibjIiOjEwLCJuMyI6MTAwfQ.Xw6nLg.v49CeDWwqP-6Dp9Qt330GAe-dNA; HttpOnly; Path=/
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 15 Jul 2020 06:50:22 GMT
 
{
"n1": 0, 
"n2": 10, 
"n3": 100
}
  • em [14], o cookie de sessão enviado pelo servidor;
  • em [18-22], a resposta do servidor na forma de uma cadeia JSON;

Vamos fazer o mesmo pedido uma segunda vez. Os registos alteram-se da seguinte forma:


GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: 8205ad85-37b3-41f2-a171-70dd3b3a1679
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: session=eyJuMSI6MCwibjIiOjEwLCJuMyI6MTAwfQ.Xw6nLg.v49CeDWwqP-6Dp9Qt330GAe-dNA
 
HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 41
Vary: Cookie
Set-Cookie: session=eyJuMSI6MSwibjIiOjExLCJuMyI6MTAxfQ.Xw6nsw.OuxIQnGhmhSsan5Qu_FL3Iyu-9k; HttpOnly; Path=/
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 15 Jul 2020 06:52:35 GMT
 
{
"n1": 1, 
"n2": 11, 
"n3": 101
}
  • Linha 9: O cliente Postman reenvia o cookie de sessão que recebeu;
  • linha 15: na sua resposta, o servidor envia um novo cookie de sessão, porque o pedido do cliente modificou o estado do utilizador (= a sessão);
  • linhas 19–23: os novos valores do contador;

22.6.4. script [session_scope_03]

Este novo script tem como objetivo demonstrar que diferentes tipos de Python podem ser colocados numa sessão: listas, dicionários e objetos. O único requisito é que os objetos colocados na sessão devem ser serializáveis para JSON. Se não forem serializáveis por predefinição (listas, dicionários), deve realizar a conversão para JSON manualmente.

#  configure the application
import config
config = config.configure()

#  dependencies
import json
import os

from flask import Flask, make_response, session
from flask_api import status
from Personne import Personne

#  flask application
app = Flask(__name__)

#  session secret key
app.secret_key = os.urandom(12).hex()


#  Home URL
@app.route('/', methods=['GET'])
def index():
    #  list management
    liste = session.get('liste')
    if liste is None:
        #  1st request
        liste = [0, 10, 100]
    else:
        #  following requests
        for i in range(len(liste)):
            liste[i] += 1
    #  put the list back in the session
    session['liste'] = liste

    #  dictionary management
    dico = session.get('dico')
    if not dico:
        #  1st request
        dico = {"un": 0, "deux": 10, "trois": 100}
    else:
        #  following requests
        dico = session['dico']
        for key in dico.keys():
            dico[key] += 1
    #  put the dictionary back in the session
    session['dico'] = dico

    #  managing a person
    personne_json = session.get('personne')
    if personne_json is None:
        #  1st request
        personne = Personne().fromdict({"prénom": "aglaë", "nom": "séléné", "âge": 70})
    else:
        #  following requests
        personne = Personne().fromjson(personne_json)
        personne.âge += 1
    #  we put the person back in the session
    session['personne'] = personne.asjson()

    #  results dictionary
    résultats = {"liste": liste, "dict": dico, "personne": personne.asdict()}

    #  we send a jSON response
    response = make_response(json.dumps(résultats, ensure_ascii=False))
    response.headers['Content-Type'] = 'application/json; charset=utf-8'
    return response, status.HTTP_200_OK


#  hand
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • linhas 1-3: a aplicação web é configurada;
  • linhas 5-11: as dependências são importadas;
  • linha 14: a aplicação Flask é instanciada;
  • linha 17: o atributo [secret_key] é inicializado. Isto permite a utilização de sessões;
  • linha 21: a única rota da aplicação;
  • linhas 23–33: gestão de uma lista na sessão. Colocámos nela elementos que são serializáveis por predefinição em JSON;
  • linhas 35–46: gestão de um dicionário na sessão. Colocámos nele elementos que são serializáveis por predefinição em JSON;
  • linhas 48–58: gestão de uma pessoa. Um objeto [Person] não é serializável por padrão em JSON. Portanto, devem ser tomadas precauções;
  • linha 58: usamos o método [BaseEntity.asjson] para armazenar a cadeia JSON da pessoa na sessão. Note-se que poderíamos ter usado [person.asdict], pois [person.asdict] é um dicionário que contém valores serializáveis por predefinição para JSON;
  • linha 55: uma vez que armazenámos uma string JSON na sessão, recuperamos a pessoa a partir dela utilizando o método [BaseEntity.fromjson];
  • linha 61: criamos o dicionário [results], que será enviado como resposta ao cliente. Sabemos que, neste caso, o Flask envia a string JSON do dicionário. Portanto, este deve conter apenas valores que sejam serializáveis para JSON por predefinição;
  • linha 64: Definimos explicitamente a cadeia JSON do dicionário [results] na resposta HTTP. O Flask teria feito isso por predefinição. No entanto, por predefinição, utiliza o parâmetro [ensure_ascii=True], o que não se adequava às nossas necessidades;
  • linha 65: informamos ao cliente que ele receberá JSON;
  • linha 66: enviamos a resposta ao cliente;

Iniciamos a aplicação web. Eliminamos todos os cookies do cliente Postman. Em seguida, o cliente solicita o URL [http://localhost:5000]. O diálogo cliente/servidor na consola do Postman é o seguinte:


GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: 5f8b7c63-aa8a-4429-a2fa-62141423d933
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
 
HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 135
Vary: Cookie
Set-Cookie: session=.eJw9isEKwyAQRH-lzHkPm15K91dqD2mzBMFq0AgF8d-jsRQG9u3MK1jsO0AKFs1fyMSEPQabOjbOHsKV4GzaFfJgmnr4Sdg0puB9a1EMtmgys959-BjIxWBe3XxWLwNq_39IQ3Q_f5zhnHxdtYs3rqgH4gQvMg.Xw6yGw.Bwpt3q-sH03gFLmg2FIPXV_ZNt8; HttpOnly; Path=/
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 15 Jul 2020 07:36:59 GMT
 
{"liste": [0, 10, 100], "dict": {"un": 0, "deux": 10, "trois": 100}, "personne": {"prénom": "aglaë", "nom": "séléné", "âge": 70}}

Fazemos o pedido uma segunda vez:


GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: 40fd00ea-d45c-46b7-a51e-d4d433a37b5c
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: session=.eJw9isEKwyAQRH-lzHkPm15K91dqD2mzBMFq0AgF8d-jsRQG9u3MK1jsO0AKFs1fyMSEPQabOjbOHsKV4GzaFfJgmnr4Sdg0puB9a1EMtmgys959-BjIxWBe3XxWLwNq_39IQ3Q_f5zhnHxdtYs3rqgH4gQvMg.Xw6yGw.Bwpt3q-sH03gFLmg2FIPXV_ZNt8
 
HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 135
Vary: Cookie
Set-Cookie: session=.eJw9isEKwyAQRH-lzHkP2kupv9LtIW2WIBgNGqEg_nu3seQ0b2Zew-zfCa5hlvqBs5aw5-SLolGuUaETgi-7wD0sqaHPk7BJLilGXdEYW-ZqjNxjWhnuwpiWMB3Ti0Haz6MMMfz9EcM5-LrIT7zZjv4F5NYvOQ.Xw6ydQ.PMWRCqKx9HNnb_DyK-ha-9pCF7M; HttpOnly; Path=/
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 15 Jul 2020 07:38:29 GMT
 
{"liste": [1, 11, 101], "dict": {"deux": 11, "trois": 101, "un": 1}, "personne": {"prénom": "aglaë", "nom": "séléné", "âge": 71}}
  • linha 9: o cliente reenvia o cookie de sessão que recebeu;
  • linha 15: o servidor envia outro de volta porque o conteúdo da sessão mudou (linha 19). Note que este conteúdo é armazenado no cookie de sessão de forma encriptada;

22.7. scripts [flask/06]: informações partilhadas por todos os utilizadores

22.7.1. Introdução

Esta secção tem como objetivo mostrar como gerir informações a nível da aplicação, ou seja, informações partilhadas por todos os utilizadores. Estas informações consistem normalmente em dados de configuração da aplicação. Vimos que uma aplicação web pode manter diferentes tipos de memória:

Image

Aqui, estamos interessados na memória da aplicação [3].

22.7.2. script [application_scope_01]

O script [application_scope_01] demonstra uma forma de gerir dados no âmbito da «aplicação»:

#  configure the application
import config
config = config.configure()

#  dependencies
from flask import Flask, make_response
from flask_api import status

#  flask application
app = Flask(__name__)


#  Home URL
@app.route('/', methods=['GET'])
def index():
    #  we aim to show that the application remains in memory between requests from different clients
    #  every customer deals with the same application

    #  app_infos represents application-level information, not session-level information
    #  i.e. it concerns all users, not just one in particular
    #  this information is stored here in [config] (not mandatory)

    #  results dictionary
    résultats = {"config": config}

    #  we send the answer
    response = make_response(résultats)
    response.headers['Content-Type'] = 'application/json; charset=utf-8'
    return response, status.HTTP_200_OK


#  hand
if __name__ == '__main__':
    #  check whether this code is executed several times
    print("application app lancée")
    #  launch the web application
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • Linhas 1–3: Recuperamos o dicionário de configuração. Vamos mostrar que o código localizado fora das funções de roteamento é executado apenas uma vez. A aplicação Flask permanece na memória. Todas as informações inicializadas fora das rotas são globais para elas e, portanto, conhecidas por elas. Assim, o dicionário [config] da linha 3 será devolvido pela rota / (linha 24). Vamos mostrar que todos os clientes web receberão o mesmo dicionário e que, portanto, este é partilhado por todos os clientes. Trata-se, portanto, de informação com âmbito «aplicação»;
  • linha 35: adicionamos um registo para verificar se o código nas linhas fora da função de encaminhamento (linhas 1–10, 32–38) é executado várias vezes;

A configuração [config] é a seguinte:

1
2
3
4
5
6
7
8
def configure():
    #  return the config
    config = {
        #  flask configuration
        "SECRET_KEY": "vibnFfrdWYUp?*LQ"
    }

    return config

Lançamos esta aplicação. Os registos na consola do PyCharm são os seguintes:

Image

  • em [1], lançamento inicial da aplicação;
  • em [2], como solicitámos o modo [Debug], a aplicação é reiniciada no modo [Debug];

Agora, utilizando um navegador (Chrome, abaixo), introduzimos o URL [http://127.0.0.1:5000/]:

Image

Agora, utilizando o navegador Firefox:

Image

Agora, utilizando o cliente Postman:

GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: 51e75099-8ecb-4f27-ae3b-9386e982ede4
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 39
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 15 Jul 2020 10:34:26 GMT

{
"SECRET_KEY": "vibnFfrdWYUp?*LQ"
}

Agora, voltemos à consola [Run] do Pycharm:

Image

  • os dois registos [1, 2] continuam lá, mas não há mais nenhum, apesar de podermos ver os três pedidos recebidos pelo servidor web;

Para ter a certeza absoluta de que a aplicação não é recarregada a cada nova solicitação, podemos adicionar um contador à configuração e incrementá-lo a cada nova solicitação. Veremos então que cada cliente vê o contador no estado deixado pelo cliente anterior. No entanto, é importante notar que os clientes não devem modificar dados no âmbito da aplicação, uma vez que estes são partilhados entre todos os clientes, e num cenário em que o servidor atende vários clientes simultaneamente, sem garantia de que a solicitação de um cliente será executada na íntegra sem interrupção, um cliente 1 que enviou uma solicitação 1 que foi interrompida antes da conclusão pode deixar os dados partilhados num estado corrompido para os clientes subsequentes.

22.7.3. script [application_scope_02]

Image

O script [application_scope_02] fará exatamente o que não deveria: permitir que os clientes modifiquem informações partilhadas com outros utilizadores. Partilharemos um contador entre os utilizadores, que o irão incrementar. Veremos que cada utilizador pode visualizar as alterações feitas no contador por outros utilizadores.

O script é o seguinte:

#  dependencies

from flask import Flask, make_response
from flask_api import status

#  flask application
app = Flask(__name__)

#  application scope data
config = {
    "counter": 0
}


#  Home URL
@app.route('/', methods=['GET'])
def index():
    #  we aim to show that the [config] dictionary is shared by all clients
    #  web application

    #  increment the counter
    config["counter"] += 1
    #  we send the answer
    response = make_response(config)
    response.headers['Content-Type'] = 'application/json; charset=utf-8'
    return response, status.HTTP_200_OK


#  hand
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • linhas 10–12: o dicionário [config] partilhado pelos utilizadores. Contém um contador;
  • linha 22: sempre que um utilizador solicita a URL /, o contador de configuração é incrementado;
  • linhas 23–26: a cadeia JSON do dicionário é enviada a cada cliente;

Executamos este script. Em seguida, solicitamos a URL [http://127.0.0.1:5000/] utilizando um primeiro navegador:

Image

Em seguida, fazemos o mesmo com um segundo navegador:

Image

Depois, uma terceira vez com o Postman:

Image

Vemos que cada cliente recupera o contador no estado em que o cliente anterior o deixou. Assim, têm acesso à mesma informação.

22.7.4. script [application_scope_03]

O script [application_scope_03] demonstra por que razão as informações partilhadas entre utilizadores devem ser de leitura apenas.

Image

O script é o seguinte:

#  dependencies
import threading
from time import sleep

from flask import Flask, make_response
from flask_api import status

#  flask application
app = Flask(__name__)

#  application scope data
config = {
    "counter": 0
}


#  Home URL
@app.route('/', methods=['GET'])
def index():
    #  we aim to show that the [config] dictionary is shared by all clients
    #  of the web application and must be read-only

    #  thread name
    thread_name = threading.current_thread().name
    #  read the counter
    counter = config["counter"]
    print(f"compteur lu : {counter}, par le thread {thread_name}")
    #  we stop for 5 seconds - so other customers will be served
    sleep(5)
    #  increment the configuration counter
    config["counter"] = counter + 1
    #  log
    print(f"compteur écrit : {config['counter']}, par le thread {thread_name}")
    #  we send the answer
    response = make_response(config)
    response.headers['Content-Type'] = 'application/json; charset=utf-8'
    return response, status.HTTP_200_OK


#  hand
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run(threaded=True)
  • linha 43: alterámos o modo de execução da aplicação web. Escrevemos [threaded=True] para indicar que a aplicação deve atender utilizadores simultaneamente. Isto é feito utilizando threads de execução:
    • pode haver múltiplas threads de execução simultâneas, cada uma a servir um utilizador;
    • o processador da máquina é partilhado por estas threads;
    • uma thread pode ser interrompida antes de concluir o seu trabalho. Será retomada mais tarde;
  • linha 19: a função [index] pode ser executada simultaneamente por várias threads;
  • linha 24: recuperamos o nome da thread que está a executar a função [index];
  • linha 26: o valor do contador é lido. Para efeitos da nossa demonstração, dividimos o incremento do contador da seguinte forma:
    • Passo 1: A thread 1 lê o contador (1, por exemplo);
    • Passo 2: A thread 1 faz uma pausa de 5 segundos (linha 29). Como a thread 1 solicitou uma pausa, o processador é transferido para outra thread, a thread 2. O objetivo é que esta nova thread leia o mesmo valor do contador (=1). Em seguida, ela também faz uma pausa de 5 segundos e perde o processador;
    • Passo 3: Incrementar o contador, linha 31, com base no valor lido no Passo 1 (=1). O Thread 1 é o primeiro a fazer isto: define o contador para 2 e, em seguida, termina a execução da função [index]. Depois, é a vez do Thread 2 acordar e também definir o contador para 2 com base no valor lido no Passo 1 (=1). Por fim, depois de ambas as threads terem sido executadas, o contador está em 2 quando deveria estar em 3;
  • Linha 33: Exibimos o valor do contador para verificação;

Executamos o script e, em seguida, solicitamos a URL [http://loaclhost:5000/] utilizando dois navegadores e, depois, o Postman. Os registos na consola do PyCharm são os seguintes:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/flask/06/application_scope_03.py
 * Serving Flask app "application_scope_03" (lazy loading)
 * Environment: development
 * Debug mode: on
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 334-263-283
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
compteur lu : 0, par le thread Thread-2
compteur lu : 0, par le thread Thread-4
compteur écrit : 1, par le thread Thread-2
127.0.0.1 - - [16/Jul/2020 08:55:37] "GET / HTTP/1.1" 200 -
compteur écrit : 1, par le thread Thread-4
127.0.0.1 - - [16/Jul/2020 08:55:40] "GET / HTTP/1.1" 200 -
compteur lu : 1, par le thread Thread-5
compteur écrit : 2, par le thread Thread-5
127.0.0.1 - - [16/Jul/2020 08:55:46] "GET / HTTP/1.1" 200 -
  • linhas 9-10: os dois primeiros threads, 2 e 4, lêem o mesmo valor 0 do contador;
  • linha 11: o thread 2 define o contador para 1;
  • linha 13: o segmento 4 incrementa o contador para 1. A partir deste ponto, o valor do contador está incorreto;
  • linhas 15–16: o thread 5 não é interrompido e lida corretamente com o valor do contador;

A principal lição a reter deste exemplo é que o código de uma aplicação web não deve alterar o valor das informações partilhadas pelos utilizadores.

22.8. scripts [flask/07]: gestão de rotas

Image

Aqui, focamo-nos na gestão das rotas de uma aplicação, ou seja, os URLs servidos pela aplicação web.

22.8.1. script [main_01]: rotas configuradas

O script [main_01] apresenta a capacidade de configurar rotas:

from flask import Flask, make_response
from flask_api import status

#  flask application
app = Flask(__name__)


#  reply sent
def send_plain_response(réponse: str):
    #  we send the answer
    response = make_response(réponse)
    response.headers['Content-Type'] = 'text/plain; charset=utf-8'
    return response, status.HTTP_200_OK


#  /name/firstname
@app.route('/<string:nom>/<string:prenom>', methods=['GET'])
def index(nom, prenom):
    #  answer
    return send_plain_response(f"{prenom} {nom}")


#  init-session
@app.route('/init-session/<string:type>', methods=['GET'])
def init_session(type: str):
    #  answer
    return send_plain_response(f"/init-session/{type}")


#  authenticate-user
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur():
    #  answer
    return send_plain_response("/authentifier-utilisateur")


#  calculate-tax
@app.route('/calculer-impot', methods=['POST'])
def calculer_impot():
    #  answer
    return send_plain_response("/calculer-impot")


#  lister-simulations
@app.route('/lister-simulations', methods=['GET'])
def lister_simulations():
    #  answer
    return send_plain_response("/lister-simulations")


#  delete-simulation
@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
def supprimer_simulation(numero: int):
    #  answer
    return send_plain_response(f"/supprimer-simulation/{numero}")


#  end of session
@app.route('/fin-session', methods=['GET'])
def fin_session():
    #  answer
    return send_plain_response(f"/fin-session")


#  hand
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • linha 17: especificamos o tipo dos parâmetros da URL. Isto permite que o Flask realize validações. Se o parâmetro não for do tipo esperado, o pedido do cliente será rejeitado (erro 400 Bad Request). Assim, o Flask faz parte do trabalho que teríamos de fazer;
  • linha 18: para os parâmetros, devemos usar os nomes exatos dos parâmetros da linha 17, mas não necessariamente a sua ordem;
  • linha 20: usamos a função [send_plain_response] para enviar a resposta ao cliente web;
  • linha 9: a função [send_plain_response] recebe a string a ser enviada ao cliente;
  • linha 11: o corpo da resposta HTTP é construído;
  • linha 12: informamos ao cliente que estamos a enviar texto simples;
  • linha 13: a resposta HTTP é enviada;
  • linhas 23–62: rotas adicionais configuradas que serão utilizadas mais tarde num exercício de aplicação;

Executamos o script e consultamo-lo utilizando o cliente Postman:

Image

22.8.2. script [main_02]: externalização de rotas

No script anterior [main_01], o código pode tornar-se bastante extenso se houver muitas rotas. O script [main_02] mostra como externalizar as rotas.

Image

O script [routes_02] agrupa as funções associadas às rotas do script anterior:

from flask import make_response
from flask_api import status


def send_response(réponse: str):
    #  we send the answer
    response = make_response(réponse)
    response.headers['Content-Type'] = 'text/plain; charset=utf-8'
    return response, status.HTTP_200_OK


#  Home URL
def index(nom, prenom):
    #  answer
    return send_response(f"{prenom} {nom}")


#  init-session
def init_session(type: str):
    #  answer
    return send_response(f"/init-session/{type}")


#  authenticate-user
def authentifier_utilisateur():
    #  answer
    return send_response("/authentifier-utilisateur")


#  calculate-tax
def calculer_impot():
    #  answer
    return send_response("/calculer-impot")


#  lister-simulations
def lister_simulations():
    #  answer
    return send_response("/lister-simulations")


#  delete-simulation
def supprimer_simulation(numero: int):
    #  answer
    return send_response(f"/supprimer-simulation/{numero}")


#  end of session
def fin_session():
    #  answer
    return send_response(f"/fin-session")

Note que o script [routes_02] não é um script de rotas. É uma lista de funções. O script principal [main_02] é o que liga as rotas às funções:

from flask import Flask

#  route functions are deported to their own script
import routes_02

#  flask application
app = Flask(__name__)

#  route/function associations
app.add_url_rule('/<string:nom>/<string:prenom>', methods=['GET'], view_func=routes_02.index)
app.add_url_rule('/init-session/<string:type>', methods=['GET'], view_func=routes_02.init_session)
app.add_url_rule('/authentifier-utilisateur', methods=['POST'], view_func=routes_02.authentifier_utilisateur)
app.add_url_rule('/calculer-impot', methods=['POST'], view_func=routes_02.calculer_impot)
app.add_url_rule('/lister-simulations', methods=['GET'], view_func=routes_02.lister_simulations)
app.add_url_rule('/supprimer-simulation/<int:numero>', methods=['GET'], view_func=routes_02.supprimer_simulation)
app.add_url_rule('/fin-session', methods=['GET'], view_func=routes_02.fin_session)

#  hand
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • linha 4: importar o script que contém as funções associadas às rotas;
  • linhas 9–16: mapeamento de rota/função;

Com este método, cada função associada a uma rota pode ser objeto de um script separado, se necessário.

Os resultados são os mesmos que os obtidos com o script anterior [main_01].