Pular para conteúdo

GeoProcess Consumer-Basic

Introdução

O módulo Consumer-Basic é uma estrutura básica pronta para auxiliar no processo de construção de consumidores. Ele oferece um modelo simples e adaptável, projetado para ser copiado e ajustado conforme as necessidades específicas do usuário na plataforma GeoProcess.

Para auxiliar na construção inicial do layout e na escolha dos objetos gráficos, o desenvolvimento de consumidores também pode ser iniciado no Visual Builder, permitindo uma visualização provisória do dashboard de resposta.

Visão Geral

A seguir está a estrutura principal de pastas e arquivos do projeto Consumer-Basic.

Estrutura de Pastas Consumidor Básico

Na raiz do projeto, estão os principais componentes do Consumer-Basic, incluindo:

  • main.py: Módulo principal em Python e ponto de entrada da aplicação consumidora.
  • consumer/: Pasta onde o módulo do consumidor está localizado. Neste exemplo, o módulo é basicworker.py.
  • logs/: Armazena os logs gerados pelo consumidor, facilitando o acompanhamento de sua execução.
  • answer/: Contém as saídas geradas pela execução do consumidor, permitindo o acesso direto aos resultados.
  • questions.json: Arquivo de configuração essencial, que define diversas opções de comportamento do consumidor.
  • env_sample.conf e env.conf: Arquivos de configuração adicionais, com parâmetros de ambiente necessários para a execução do módulo.
  • tests/: Pasta dedicada aos testes de software, onde são suportados três tipos: testes unitários, testes de contrato e testes de integração.

A seguir, detalharemos alguns dos principais arquivos, módulos e diretórios mencionados na estrutura do consumidor básico.

Módulo main.py

Na raiz do projeto Consumer-Basic, o arquivo main.py atua como ponto de entrada para a aplicação. Ele instancia um controlador, passando como argumento o módulo do consumidor básico que será utilizado. O código de main.py é mostrado abaixo:

from baseconsumer.controller import Controller
from consumer.basicworker import BasicWorker

if __name__ == '__main__':
    c = Controller(BasicWorker())

Esse trecho inicializa o consumidor da aplicação de forma genérica, sem especificar detalhes da lógica interna, que são implementados no módulo basicworker.py. A seguir, será abordada a estrutura e implementação desse módulo.

Módulo consumer/basicworker.py

Na raiz do projeto, existe um pacote chamado consumer que contém o código do consumidor básico implementado. Dentro desse pacote, encontramos o módulo basicworker.py, onde é declarada a classe BasicWorker, que herda de Worker (definida na biblioteca PGST-LIB). Essa classe fornece um esqueleto genérico de consumidor que deve ser adaptado para propósitos específicos. Abaixo, apresentamos um diagrama de classes que ilustra a relação entre as classes Worker e BasicWorker, destacando seus atributos e métodos principais.

classDiagram
  ABC <|-- Worker : Herança
  Worker <|-- BasicWorker : Herança
  class Worker {
    +Logger : logger
    +boolean : testMode
    +PostGis : postGis
    +dict : questionsInfo
    +Answer : answer
    +str : dbName
    +str : dbUser
    +str : dbPassword
    +str : dbHost
    +str : dbPort
    +str : gsUser
    +str : gsPassword
    +str : gsHost
    +Worker()
    +getVersion()*
    +getName()*
    +startDB()
    +confGeoServer()
    +getPostGis()
    +createAnswer()
    +setQuestions(data)
    +parametersToView(questionId, data)
    +process(contract)
    +test()
  }
  class BasicWorker {
    +Logger : logger
    +dict : messages
    +getVersion()
    +getName()
    +process1(answer_request)
  }

A seguir, apresentamos uma visão geral dos métodos da classe BasicWorker.

class BasicWorker(Worker):
    ...
    def getVersion(self):
        return "1.0"

    def getName(self):
        return "Consumer Basic"

    @process(name="Answer 1", questionId="Q1")
    def process1(self, answer_request):
        ...

Os métodos principais da classe BasicWorker incluem:

  • getVersion: Retorna a versão do código do consumidor.
  • getName: Retorna o nome do consumidor.
  • process1: Processa a primeira pergunta do consumidor e retorna a resposta correspondente.

Em seguida, apresentamos a implementação do método process1. Esse método é responsável pelo processamento da primeira pergunta do consumidor e retorna a resposta a essa pergunta:

@process(name="Answer 1", questionId="Q1")
def process1(self, answer_request):
    self.logger.info("Processando a primeira pergunta")
    answer = Answer()

    area = answer_request.getPropertie("map")
    self.logger.debug(area)
    answer.addMapLayer("Área da Busca", "area_busca", area)

    answer.addChart("Gráfico 01 - Tipo: Colunas",
                    "Dados",
                    ["Coluna 1", "Coluna 2", "Coluna 3", "Coluna 4"],
                    [30, 15, 20, 35])

    answer.addChart("Gráfico 02 - Tipo: Linha",
                    "Valores",
                    ["01","02","03","04"],
                    [0.30, 0.15, 0.20, 0.35],
                    ChartType.LINE,
                    color=Color.RED)

    answer.addTable("Tabela 01",
                    "Descrição da Tabela 1",
                    ["Estado", "Cidade", "Status", "Código"],
                    [["Minas Gerais", "Belo Horizonte", "Cadastrado", "053"],
                    ["São Paulo", "São Carlos", "Não Cadastrado", "015"],
                    ["Bahia", "Salvador", "Cadastrado", "088"],
                    ["Minas Gerais", "Lavras", "Cadastrado", "017"],
                    ["São Paulo", "Santos", "Não Cadastrado", "023"]])

    answer.addMessage(self.messages["001"])
    self.logger.info("Término da primeira pergunta")
    return answer

Informação

Repare que, por ser uma aplicação de exemplo, os dados estão injetados diretamente no código. Em uma aplicação real, esses dados são carregados de algum banco de dados.

Essa implementação modela uma pergunta e estrutura a resposta com diversos elementos visuais e textuais. O método process1 possui um decorador @process, que recebe como parâmetros name e questionId, identificando a pergunta e seu nome de forma clara (linha 1). Este método aceita apenas um argumento, answer_request, que é um dicionário (JSON) contendo as informações de entrada necessárias (linha 2).

O processamento inicia com um log informando que a pergunta está sendo processada (linha 3). Em seguida, um objeto answer do tipo Answer é instanciado para armazenar a resposta final (linha 4). O método então lê a propriedade map do answer_request, representando uma área específica selecionada pelo usuário (linha 6), e adiciona essa área ao mapa com o título "Área da Busca" (linha 8).

A resposta inclui ainda uma série de gráficos e tabelas:

  • Um gráfico de colunas, intitulado "Gráfico 01 - Tipo: Colunas", que ilustra os valores [30, 15, 20, 35] associados a quatro categorias (linhas 10 a 13).
  • Um gráfico de linha com o título "Gráfico 02 - Tipo: Linha" e coloração vermelha, que representa os valores [0.30, 0.15, 0.20, 0.35] ao longo de quatro pontos de dados (linhas 15 a 20).
  • Uma tabela chamada "Tabela 01", contendo quatro colunas ("Estado", "Cidade", "Status" e "Código") e cinco registros (linhas 22 a 29).

Por fim, o método adiciona ao log uma mensagem com o status da resposta, utilizando a chave de mensagem self.messages["001"] (linha 31). O objeto answer, contendo todos os elementos construídos durante o processamento, é então retornado como resposta final (linha 33).

Todos os objetos mencionados são instanciados por meio de funções específicas da classe BasicWorker. A seguir, são detalhadas as construções de cada elemento, explicando sua assinatura e exemplos de aplicação.

O método addMapLayer adiciona uma camada de mapa à resposta. Abaixo está a assinatura do método (definida na biblioteca PGST-LIB na classe Answer) e um exemplo de aplicação.

def addMapLayer(self, title, layerLabel, geoJson=None, geoWkb=None, group="Principal", color=None, colorPin=None, tip=None, pinType="pin_1"):

answer.addMapLayer("Área da Busca", "area_busca", area)
Saída do exemplo: Exemplo Tabela

O método addChart adiciona diferentes tipos de gráficos à resposta. Abaixo, são destacados a assinatura do método e exemplos de utilização, de acordo com o código acima. Os quatro primeiros parâmetros são obrigatórios, enquanto os demais são opcionais.

def addChart(self, title, label, labels, data, type=ChartType.BAR, layout=ChartLayout.DEFAULT, color=Color.BLUE):

answer.addChart("Gráfico 01 - Tipo: Colunas",
                  "Dados",
                  ["Coluna 1", "Coluna 2", "Coluna 3", "Coluna 4"],
                  [30, 15, 20, 35])
Saída desse exemplo: Exemplo Tabela

answer.addChart("Gráfico 02 - Tipo: Linha",
                "Valores",
                ["01","02","03","04"],
                [0.30, 0.15, 0.20, 0.35],
                ChartType.LINE,
                color=Color.RED)
Saída desse exemplo: Exemplo Tabela

Dica

Os tipos disponíveis para ChartType são: BAR, LINE, PIE, e POLAR_AREA. Os tipos disponíveis para ChartLayout são: DEFAULT e FULL, em que DEFAULT suporta dois gráficos lado a lado na página, enquanto FULL permite um gráfico em largura total da página. * Os valores de Color disponíveis são: RED, BLUE, GREEN, ORANGE, PURPLE, YELLOW, e BROWN.

O método addTable adiciona uma tabela à resposta. Abaixo estão a assinatura do método (PGST-LIB) e um exemplo de aplicação. Os quatro primeiros parâmetros são obrigatórios.

def addTable(self, title, description, head, data, identifier="not_defined"):

answer.addTable("Tabela 01",
                "Descrição da Tabela 1",
                ["Estado", "Cidade", "Status", "Código"],
                [["Minas Gerais", "Belo Horizonte", "Cadastrado", "053"],
                ["São Paulo", "São Carlos", "Não Cadastrado", "015"],
                ["Bahia", "Salvador", "Cadastrado", "088"],
                ["Minas Gerais", "Lavras", "Cadastrado", "017"],
                ["São Paulo", "Santos", "Não Cadastrado", "023"]])
Saída desse exemplo: Exemplo Tabela

O método addMessage adiciona uma mensagem à resposta. Abaixo estão a assinatura do método e um exemplo de aplicação.

def addMessage(self, message):

answer.addMessage(self.messages["001"])
Saída desse exemplo: Exemplo Tabela

Nota

Embora o exemplo tenha apenas uma pergunta (implementada no método process1), a aplicação consumidora pode suportar múltiplas perguntas conforme necessário. Uma segunda pergunta, por exemplo, seria implementada no método process2.

Após a execução da aplicação consumidora, a resposta visual é exibida no dashboard do sistema, mostrando o processamento da pergunta process1. Abaixo estão exemplos de como a resposta é exibida no frontend do sistema.

Resposta Consumer Basic 1

Resposta Consumer Basic 2

Arquivo de resposta answer.json

A resposta é exibida visualmente conforme ilustrado acima, no PORTAL. No entanto, para que isso aconteça, o módulo Consumer-Basic envia um arquivo JSON com o resultado do processamento da pergunta para o GATEWAY, que o encaminha ao Portal. Abaixo, apresentamos um exemplo simplificado do conteúdo desse arquivo JSON de resposta (algumas linhas foram omitidas).

Arquivo: answer.json
answer.json
{
    "items": [
        {
            "title": "Map",
            "type": "map",
            "background": "bing",
            "data": [
                {
                    "title": "\u00c1rea da Busca",
                    "label": "area_busca",
                    "data": {
                        "type": "FeatureCollection",
                        "features": [
                            {
                                "type": "Feature",
                                "geometry": {
                                    "type": "Polygon",
                                    "coordinates": [
                                        [
                                            [
                                                -69.48913,
                                                -10.579878
                                            ],
                                            ...
                                        ]
                                    ]
                                },
                                "properties": {}
                            }
                        ]
                    }
                }
            ]
        },
        {
            "title": "Gr\u00e1fico 01 - Tipo: Colunas",
            "type": "chartjs",
            "model": "bar",
            "layout": "default",
            "labels": [ "Coluna 1", "Coluna 2", "Coluna 3", "Coluna 4" ],
            "scale": "",
            "dataset": [
                {
                    "data": [30, 15, 20, 35], "label": "Dados", "color": "blue"
                }
            ]
        }
        ...
        {
            "title": "Tabela 01",
            "type": "table",
            "description": "Descri\u00e7\u00e3o da Tabela 1",
            "data": {
                "head": [
                    "Estado",
                    "Cidade",
                    "Status",
                    "C\u00f3digo"
                ],
                "data": [
                    [
                        "Minas Gerais",
                        "Belo Horizonte",
                        "Cadastrado",
                        "053"
                    ],
                    ...
                    [
                        "S\u00e3o Paulo",
                        "Santos",
                        "N\u00e3o Cadastrado",
                        "023"
                    ]
                ]
            },
            "id": "table1"
        }
    ],
    ...
    "messages": [
        {
            "type": "info",
            "code": "W001",
            "title": "GeoProcess Consumer",
            "message": "Aplica\u00e7\u00e3o B\u00e1sica Consumidor"
        }
    ],
    "files": [],
    "chain": {},
    "parameters": {
        "tipo de dado (econ\u00f4micos ou demogr\u00e1ficos)": "Fict\u00edcio"
    },
    "answer_request_id": 1,
    "question_id": "1",
    "user_info": {
        "user": "dev",
        "name": "Developer",
        "email": "dev@ufla.br",
        "ip": "127.0.0.1"
    },
    "date": "29-09-2023 10:17:49"
}

Configurando o arquivo questions.json

O arquivo questions.json, localizado na raiz do projeto, define as configurações principais do consumidor. Ao iniciar a execução, o consumidor envia esse arquivo ao Gateway, que cadastra o consumidor na plataforma GeoProcess. Esse arquivo especifica:

  • Projetos e temas vinculados: Indica a quais projetos e temas o consumidor está associado.
  • Perguntas que o consumidor pode responder: Define as perguntas disponíveis e os parâmetros necessários para cada uma.

Abaixo, são mostradas as configurações desse arquivo.

Arquivo: questions.json
questions.json
{
    "projects": [
        {
            "consumer_project_id": "PGSTCB1",
            "version": "1.0.2",
            "title": "Projeto PGST Consumer Basic",
            "role_tag": "PGSTCB"
        }
    ],
    "themes": [
        {
            "consumer_theme_id": "TPGSTCB1",
            "version": "1.0.2",
            "title": "Tema Único",
            "role_tag": "TPGSTCB1",
            "consumer_project_id": "PGSTCB1"
        }
    ],
    "questions": [
        {
            "consumer_question_id": "1",
            "version": "1.0.1",
            "consumer_theme_id": "TPGSTCB1",
            "title": "Questão exemplo básica com busca por área",
            "description": "Exemplo básico do consumidor com busca por área",
            "role_tag": "QPGSTCB1",
            "consumer_sources": [
                "Fonte de exemplo"
            ],
            "config_items": [
                { 
                    "id": "map",
                    "label": "Região",
                    "type": "polygon",
                    "minArea": 1,
                    "text": "Você deve selecionar uma área."
                }
            ]
        }
    ]
}

No arquivo questions.json, é essencial definir uma lista de projetos aos quais o consumidor está vinculado. No exemplo abaixo, nosso consumidor está associado a um único projeto:

"projects": [
    {
        "consumer_project_id": "PGSTCB1",
        "version": "1.0.2",
        "title": "Projeto PGST Consumer Basic",
        "role_tag": "PGSTCB"
    }
]
  • consumer_project_id: Identificador único do consumidor, utilizado pelo banco de dados do GeoProcess para o cadastro e controle do projeto.
  • version: Define a versão do projeto. Esse atributo deve ser atualizado sempre que houver uma nova versão do consumidor.
  • title: Título exibido no dashboard do Portal do sistema, representando o nome do projeto.
  • role_tag: Tag de permissão do projeto, utilizada pelo sistema Keycloak para gerenciar as permissões de acesso dos usuários.

Informação

A maioria dos consumidores que executam poucas tarefas ou possuem um número limitado de perguntas tendem a estar associados a um único projeto. No sistema, o conceito de projeto ajuda a organizar as tarefas e perguntas respondidas pelo consumidor, melhorando a visualização e o gerenciamento. Além disso, a separação em diferentes projetos permite configurar permissões distintas para diferentes grupos de usuários. À medida que o consumidor se expande em complexidade e funcionalidades, pode ser vantajoso associá-lo a múltiplos projetos para otimizar essa organização.

Além do projeto, é necessário definir também o tema associado no arquivo questions.json. Um tema serve para organizar as perguntas dos projetos em grupos. Veja a configuração de um tema no exemplo abaixo:

"themes": [
    {
        "consumer_theme_id": "TPGSTCB1",
        "version": "1.0.2",
        "title": "Tema Único",
        "role_tag": "TPGSTCB1",
        "consumer_project_id": "PGSTCB1"
    }
]
  • consumer_theme_id: Identificador único do tema, usado no banco de dados do GeoProcess.
  • version: Define a versão do tema. Atualize esse campo sempre que houver uma nova versão do tema.
  • title: Nome do tema exibido no dashboard.
  • role_tag: Tag usada para definir permissões de usuários com base no tema, através do sistema Keycloak.
  • consumer_project_id: Identifica o projeto ao qual o tema está vinculado. Deve coincidir com o valor de consumer_project_id de um dos projetos cadastrados na seção "projects".

Assim, ao organizar as perguntas em temas, o sistema permite uma gestão mais estruturada das permissões e da visualização das perguntas no dashboard.

Finalmente, o arquivo questions.json deve incluir as perguntas que o consumidor pode responder, que constituem a parte mais importante desse arquivo de configuração. Vamos analisar um exemplo de configuração de pergunta:

"questions": [
    {
        "consumer_question_id": "Q1",
        "version": "1.0.2",
        "consumer_theme_id": "TPGSTCB1",
        "title": "Pergunta do exemplo básico",
        "description": "Descrição da pergunta do exemplo básico do consumidor com uma busca por área",
        "role_tag": "QPGSTCB1",
        "consumer_sources": [
            "QSPGSTCB1"
        ],
        "config_items": [
            {
                "id": "map",
                "label": "Região Selecionada",
                "type": "polygon",
                "minArea": 1,
                "text": "Você deve selecionar uma área."
            }
        ]
    }
]

Aqui está a função de cada atributo:

  • consumer_question_id: ID da pergunta, usado no banco de dados do GeoProcess. Esse valor deve ser igual ao especificado no decorator da pergunta no código do consumidor, como em @process(name="Answer 1", questionId="Q1").
  • version: Define a versão da pergunta. Atualize sempre que a pergunta for alterada.
  • consumer_theme_id: Indica o tema associado à pergunta.
  • title: Título que será exibido no dashboard do portal.
  • description: Descrição detalhada da pergunta, exibida ao usuário no dashboard.
  • role_tag: Usado para definir permissões de acesso a essa pergunta para os usuários do sistema.
  • consumer_source: Informação sobre a origem dos dados usados na pergunta.

Configuração de Parâmetros da Pergunta (config_items):

O atributo config_items contém uma lista dos parâmetros necessários para responder a pergunta. Cada parâmetro possui atributos específicos, conforme o exemplo abaixo:

  • id: Nome utilizado para acessar esse parâmetro no código. Neste exemplo, o valor map pode ser usado para acessar a área selecionada pelo usuário via area = answer_request.getPropertie("map").
  • label: Nome que será exibido no dashboard para esse parâmetro.
  • type: Tipo de dado esperado. Valores possíveis incluem text, check, city, polygon, range, options.
  • minArea: Especifica a área mínima para seleção, aplicável ao tipo polygon.
  • text: Mensagem de ajuda exibida ao usuário sobre como preencher o parâmetro.

A tabela a seguir destaca alguns atributos adicionais que podem ser configurados para cada tipo de parâmetro:

Atributo Tipo Default Descrição
id str Obrigatório Define um nome identificador para acessar o parâmetro.
label str Obrigatório Define um nome (rótulo) para aparecer no dashboard.
type str Obrigatório Define um tipo para o parâmetro informado: [text, check, city, polygon, range, options]
maxlength int ou None None Utilizado quando definido em type um text. Define o tamanho máximo do texto.
min int ou None None Utilizado quando definido em type um range. Define o valor mínimo para um dado inteiro.
max int ou None None Utilizado quando definido em type um range. Define o valor máximo para um dado inteiro.
step int ou None None Utilizado quando definido em type um range. Define um slider com intervalo entre min e max.
true str ou None None Utilizado quando definido em type um check. Define o valor do texto se marcado o checkbox.
false str ou None None Utilizado quando definido em type um check. Define o valor do texto se não marcado o checkbox.
required bool ou None None Utilizado quando definido em type um text ou city. Define se o campo é obrigatório ou não.
minArea int ou None None Utilizado quando definido em type um polygon. Define o valor da menor área possível de ser selecionada.
text str ou None None Utilizado quando definido em type um text, city ou polygon. Define o texto mostrado.
options list[opcao] ou None [] Utilizado quando definido em type um options.

Na seção config_items, são definidos os parâmetros de entrada que o consumidor precisa para processar esta pergunta. Neste exemplo, o parâmetro de entrada esperado é um mapa que contém uma região específica, definida por um polígono selecionado pelo usuário. Esse parâmetro é essencial para que o consumidor identifique a área de interesse e possa executar a tarefa ou análise solicitada.

Neste caso, o exemplo está configurado com apenas uma pergunta, para simplificar a configuração e o fluxo de execução. Caso o consumidor possua múltiplas perguntas, cada uma terá suas próprias configurações de entrada definidas nesta mesma estrutura.

A figura abaixo ilustra os relacionamentos estabelecidos entre os componentes definidos no arquivo questions.json, incluindo consumidor, projetos (projects), temas (themes), perguntas (questions) e parâmetros de configuração (config_items). A estrutura permite organizar e vincular os elementos de forma a atender às necessidades do consumidor na plataforma GeoProcess.

Relacionamentos Arquivo Questions.json

Observe as relações específicas:

  • Um consumidor pode estar associado a múltiplos projetos (n projetos).
  • Cada projeto pode conter múltiplos temas (n temas).
  • Cada tema pode incluir várias perguntas (n perguntas).
  • Cada pergunta pode ter diversos parâmetros de configuração (n config_items), que definem os dados de entrada necessários para o seu processamento.

Essa organização flexível facilita a escalabilidade do consumidor e permite que novas perguntas, projetos e temas sejam adicionados com facilidade, mantendo uma estrutura clara e bem organizada.

Arquivo de Configuração .conf

O projeto inclui um arquivo de configuração de exemplo chamado env_sample.conf, que deve ser usado como base para criar o arquivo de configuração principal, env.conf, na etapa Configurando a Aplicação deste tutorial. Importante: o arquivo env_sample.conf é apenas um modelo e não deve ser modificado diretamente. Em vez disso, copie-o para um novo arquivo env.conf, que poderá ser personalizado de acordo com as necessidades do desenvolvedor. Abaixo, apresentamos um exemplo de configuração com uma breve descrição dos parâmetros:

LOG_LEVEL=INFO
MODE=RUN
CONSUMER_CODE=basic
PORTAL_DIR=pgst-portal
#DOCKER
TIME_WAIT_RESTART=10
RB_HOST_DOCKER=host.docker.internal
RB_PORT_DOCKER=5672
#LOCAL
RB_HOST=127.0.0.1
RB_PORT=5672
RB_USER=admin
RB_PASSWORD=password
#OPTIONAL
#RB_HOST=127.0.0.1
#RB_PORT=5672
#WMS_SERVER="http://localhost:8080/geoserver/wms"
#DB_NAME=pgst
#DB_USER=postgres
#DB_PASSWORD=root
#DB_HOST=127.0.0.1
#DB_PORT=5432
#SATELLITE_IMAGE_URL="http://s2maps-tiles.eu/wmts/1.0.0/s2cloudless-2021_3857/default/g/{z}/{y}/{x}.jpg"
#METRICS_PORT=9091

Parâmetros Obrigatórios:

  • LOG_LEVEL: Define o nível de detalhes exibidos no log do sistema. Opções disponíveis: DEBUG, INFO, WARNING, ERROR, CRITICAL.
  • MODE: Define o modo de execução do consumidor. Opções possíveis: RUN, RUN_LOCAL, UNIT_TEST, CONTRACT_TEST.
  • CONSUMER_CODE: Define o identificador do consumidor.
  • PORTAL_DIR: Define o caminho relativo do diretório do Portal.

Parâmetros Opcionais:

  • WMS_SERVER: Define a URL do servidor WMS (Web Map Service) do GeoServer, caso seja necessário para acessar mapas geoespaciais.
  • DB_NAME: Nome do banco de dados PostgreSQL.
  • DB_USER: Usuário do banco de dados.
  • DB_PASSWORD: Senha do banco de dados.
  • DB_HOST: Endereço do servidor do banco de dados (IP).
  • DB_PORT: Porta para conexão com o banco de dados (padrão PostgreSQL é 5432).

Essas configurações fornecem uma base inicial para o sistema, que pode ser adaptada conforme o ambiente de desenvolvimento e as necessidades específicas do projeto.

Arquivo de Log do Sistema

Na pasta raiz do projeto, existe uma pasta chamada logs que armazena os logs do sistema no arquivo app.log. Esse arquivo registra todas as mensagens importantes sobre a execução e status do sistema. Abaixo, um exemplo típico do conteúdo do app.log:

Arquivo: app.log
app.log
2023-11-30 09:47:54,397 - baseconsumer.controller (37) - INFO     - MainThread   - PGST Lib version is 0.1 alfa
2023-11-30 09:47:54,397 - baseconsumer.controller (39) - INFO     - MainThread   - Get Mode CONTRACT_TEST from command line parameter
2023-11-30 09:47:54,397 - baseconsumer.controller (44) - INFO     - MainThread   - Mode CONTRACT_TEST
2023-11-30 09:47:54,397 - baseconsumer.controller (75) - WARNING  - MainThread   - Running just test code 001
2023-11-30 09:47:54,439 - test_001 (14) - INFO     - MainThread   - Test001 - test_process
2023-11-30 09:47:54,439 - consumer.basicworker (51) - INFO     - MainThread   - Processando a primeira pergunta
2023-11-30 09:47:54,439 - consumer.basicworker (99) - INFO     - MainThread   - Término da primeira pergunta
2023-11-30 09:47:54,442 - baseconsumer.controller (86) - INFO     - MainThread   - Answer data was written at consumer-basic/answer/answer.json
2023-11-30 09:47:54,442 - baseconsumer.controller (87) - INFO     - MainThread   - Answer Viewer can be accessed at http://127.0.0.1:8000
2023-11-30 09:47:54,442 - baseconsumer.controller (88) - INFO     - MainThread   - To finish press x, or other key to execute the test again

Cada linha de log registra, na ordem:

  • Data e hora da mensagem.
  • Módulo Python que gerou a mensagem.
  • Linha do código onde foi feita a chamada do log.
  • Nível de criticidade da mensagem.
  • Thread que executou a chamada.
  • Conteúdo da mensagem.

Níveis de Mensagem e Métodos de Invocação

O sistema de log possui diferentes níveis de mensagem para especificar a importância e criticidade das ocorrências, com cinco níveis e seis métodos de invocação. O nível DEBUG é o mais detalhado (geralmente usado para desenvolvimento), enquanto CRITICAL indica uma falha grave.

Nível Forma de invocação do método Descrição
DEBUG logger.debug(msg) Mensagens detalhadas para depuração
INFO logger.info(msg) Informações gerais sobre o funcionamento do sistema.
WARNING logger.warning(msg) Indicações de possíveis problemas ou condições de alerta (ex.: "pouco espaço no HD").
ERROR logger.error(msg) Mensagens de erro significativo.
ERROR logger.exception(msg) Informações de erro sério, originado de uma exceção.
CRITICAL logger.critical(msg) Indica um erro crítico que pode comprometer a continuidade da execução do sistema.

Configuração do Nível de Log

No arquivo env.conf, o atributo LOG_LEVEL define o nível mínimo de criticidade das mensagens a serem registradas no log:

  • LOG_LEVEL=DEBUG: Exibe todas as mensagens, de DEBUG a CRITICAL, recomendável para desenvolvimento.
  • LOG_LEVEL=INFO: Exibe apenas mensagens de INFO a CRITICAL, omitindo mensagens de DEBUG (configuração padrão para produção).

Essa configuração permite ajustar o nível de detalhamento do log conforme o ambiente e as necessidades de monitoramento do sistema.

Módulos para Testes

Na raiz do projeto, existe uma pasta chamada tests que organiza os testes de software. Existem três tipos de testes disponíveis:

  1. Testes Unitários (unit): Testam métodos e partes isoladas do sistema.
  2. Testes de Contrato (contract): Avaliam a interação do sistema com o envio de mensagens e suas respostas.
  3. Testes de Integração (integration): Verificam a integração completa do sistema.

A seguir, detalharemos cada tipo de teste.

Testes Unitários:

Os testes unitários estão na pasta tests/unit, com um arquivo dedicado a cada conjunto de testes. Neste exemplo, temos o arquivo test_001.py, que verifica, entre outros aspectos, a versão do sistema.

Exemplo de teste unitário (tests/unit/test_001.py):

class Test001(unittest.TestCase):
    logger = logging.getLogger(__name__)
    worker = BasicWorker()
    def test_version(self):
        self.logger.info("Testando o método test_version do módulo unit/test_001.py")
        valor_real = self.worker.getVersion()
        valor_esperado = "1.0"
        self.assertEqual(valor_esperado, valor_real, "A versão do software não é a esperada")

Esse teste verifica se a versão do consumidor é 1.0. Se o teste passar, confirma-se que a versão está correta; caso contrário, a mensagem: "A versão do software não é a esperada" é exibida.

Para instruções de execução dos testes, consulte a seção Executando a Aplicação.

Testes de Contrato:

Os testes de contrato estão localizados na pasta tests/contract. Dentro dessa pasta, há uma subpasta para cada teste a ser executado, com o nome representando a pergunta avaliada. Neste exemplo, a subpasta é 001 (temos apenas uma pergunta), e será criada para o primeiro teste de contrato. Dentro dela há um arquivo chamado test_001.py, que define os testes de contrato deste consumidor. Abaixo, apresentamos uma parte do código do módulo test_001.py:

...
class Test001():
    logger = logging.getLogger(__name__)
    worker = BasicWorker()
    def test_process(self):
        self.logger.info("Testando o método test_process do módulo contract/001/test_001.py")
        answer = self.worker.test()
        assert not answer.isError()

Observe que o código do teste é genérico: ele cria uma instância de BasicWorker. Quando o teste de contrato é executado, o método test_process é automaticamente chamado, o qual, por sua vez, invoca o método test da classe Worker. Em seguida, verifica se não há erro na resposta gerada.

Durante a execução desse teste, o arquivo answerRequest.json, localizado na mesma pasta, é lido. Veja abaixo o conteúdo desse arquivo:

Arquivo: answerRequest.json
answerRequest.json
{
    "header": {
        "type": "answer_request",
        "status": "OK",
        "next_consumer": null
    },
    "content": {
        "user_info": {
            "user": "dev",
            "name": "Developer",
            "email": "dev@ufla.br",
            "ip": "127.0.0.1"
        },
        "consumer_question_id": "Q1",
        "answer_request_id": 11,
        "data": {
            "map": {
                "type": "FeatureCollection",
                "features": [
                    {
                    "type": "Feature",
                    "properties": {},
                    "geometry": {
                        "coordinates": [
                        [
                            [
                            -52.79853429987142,
                            -26.668723440791226
                            ],
                            [
                            -52.79853429987142,
                            -27.722008785135223
                            ],
                            [
                            -50.72010819942622,
                            -27.722008785135223
                            ],
                            [
                            -50.72010819942622,
                            -26.668723440791226
                            ],
                            [
                            -52.79853429987142,
                            -26.668723440791226
                            ]
                        ]
                        ],
                        "type": "Polygon"
                    }
                    }
                ]
            }
        }
    }
}

Durante a leitura desse arquivo, o método identifica o atributo consumer_question_id com o valor "Q1". Assim, o método process1 da classe BasicWorker é acionado, pois possui o decorador @process(name="Answer 1", questionId="Q1").

O arquivo answerRequest.json fornece os dados de entrada para o consumidor e é necessário apenas para a execução do teste de contrato. Em produção, este arquivo não será utilizado, pois os dados de entrada serão fornecidos diretamente pelo Portal. Para instruções sobre como executar esse teste, consulte a seção Executando a Aplicação.

Instalação

Pré-Requisitos

Para que o pgst-consumer-basic funcione corretamente, é necessário atender aos seguintes pré-requisitos:

  • Python
  • Pip
  • Git

Além disso, alguns subprojetos do GeoProcess também são necessários. Para isso, clone os seguintes repositórios:

  • pgst-portal
git clone https://github.com/ZETTA-ORG/pgst-portal.git
  • pgst-lib
git clone https://github.com/ZETTA-ORG/pgst-lib.git

Após a instalação dos pré-requisitos, você poderá preparar o pgst-consumer-basic.

Preparando a Aplicação

Primeiramente, clone o projeto pgst-consumer-basic com o seguinte comando:

git clone https://github.com/ZETTA-ORG/pgst-consumer-basic.git

Observação

Esse projeto deve ser clonado dentro de uma pasta geoprocess, conforme a organização do projeto.

Configurando a Aplicação

Em seguida, copie o arquivo de configuração do ambiente com o comando apropriado para o seu sistema operacional:

cp env_sample.conf env.conf
copy env_sample.conf env.conf

Depois, abra o arquivo env.conf e atualize-o conforme as configurações do seu ambiente de trabalho. Atenção especial aos caminhos de diretórios especificados nesse arquivo.

Para isolar as dependências do Python, crie um ambiente virtual venv com o comando a seguir:

python3 -m venv venv
py -m venv venv

Ative o ambiente virtual no seu computador utilizando o comando abaixo:

source venv/bin/activate
venv\bin\Activate.bat

Agora, instale as dependências do projeto utilizando o comando abaixo:

pip3 install -r requirements.txt

Em seguida, instale as dependências do projeto pgst-lib dentro do projeto principal:

pip3 install -e ../pgst-lib/

Executando a Aplicação

Para executar a aplicação em modo de teste unitário, utilize a linha de comando abaixo:

python3 main.py UNIT_TEST

Para executar a aplicação em modo teste de contrato, utilize a linha de comando abaixo:

python3 main.py CONTRACT_TEST 001

Usando esse modo a resposta pode ser acessada depois do teste no arquivo answer.json na pasta answer. A seguir temos um exemplo desse arquivo.

Arquivo: answer.json
answer.json
{
    "items": [
        {
            "title": "Map",
            "type": "map",
            "background": "bing",
            "data": [
                {
                    "title": "\u00c1rea da Busca",
                    "label": "area_busca",
                    "data": {
                        "type": "FeatureCollection",
                        "features": [
                            {
                                "type": "Feature",
                                "geometry": {
                                    "type": "Polygon",
                                    "coordinates": [
                                        [
                                            [
                                                -69.48913,
                                                -10.579878
                                            ],
                                            ...
                                        ]
                                    ]
                                },
                                "properties": {}
                            }
                        ]
                    }
                }
            ]
        },
        {
            "title": "Gr\u00e1fico 01 - Tipo: Colunas",
            "type": "chartjs",
            "model": "bar",
            "layout": "default",
            "labels": [ "Coluna 1", "Coluna 2", "Coluna 3", "Coluna 4" ],
            "scale": "",
            "dataset": [
                {
                    "data": [30, 15, 20, 35], "label": "Dados", "color": "blue"
                }
            ]
        }
        ...
        {
            "title": "Tabela 01",
            "type": "table",
            "description": "Descri\u00e7\u00e3o da Tabela 1",
            "data": {
                "head": [ "Nome", "Idade" ],
                "data": [
                    [ "Paulo", "21" ],
                    [ "Jo\u00e3o", "35" ],
                    [ "Maria", "28" ]
                ]
            }
        }
        ...
    ],
    "messages": [
        {
            "type": "info",
            "code": "W001",
            "title": "PGST Consumer",
            "message": "Aplica\u00e7\u00e3o B\u00e1sica Consumidor"
        }
    ],
    "files": [],
    "chain": {},
    "parameters": {
        "tipo de dado (econ\u00f4micos ou demogr\u00e1ficos)": "Fict\u00edcio"
    },
    "answer_request_id": 1,
    "question_id": "1",
    "user_info": {
        "user": "dev",
        "name": "Developer",
        "email": "dev@ufla.br",
        "ip": "127.0.0.1"
    },
    "date": "29-09-2023 10:17:49"
}

Abra a aplicação através da URL http://127.0.0.1:8000/.

Informação

Repare que quando executamos a aplicação básica apenas no modo de teste de contrato, uma resposta na web é exibida. Isso é possível, pois no nosso consumidor temos um servidor web embutido, na verdade esse servidor web está no projeto PGST-Lib.

Informação

Para encerrar a execução do consumidor, basta apertar o botão x ou pressionar Ctrl + C.

Github do Projeto

A seguir temos o link para o github do projeto.

PGST-CONSUMER-BASIC