Skip to content

Commit cfe24bf

Browse files
committed
Merge pull request #69 from samuelgrigolato/master
Serviço de entrega de dados IBGE
2 parents 5441be3 + 7944317 commit cfe24bf

8 files changed

+284
-1
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
*.swp
44
run.py
55
build
6+
celerybeat-schedule

IbgeTracker.py

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
import requests
4+
from lxml.html import fromstring
5+
from database import MongoDb as Database
6+
7+
8+
class IbgeTracker():
9+
10+
def __init__(self):
11+
self.url_ufs = 'http://www.ibge.gov.br/home/geociencias' + \
12+
'/areaterritorial/principal.shtm'
13+
self.url_cidades = 'http://www.ibge.gov.br/home/geociencias' + \
14+
'/areaterritorial/area.php?nome=%'
15+
16+
def _request(self, url):
17+
response = requests.post(url)
18+
response.raise_for_status()
19+
return response.text
20+
21+
def _get_info_ufs(self, siglas):
22+
texto = self._request(self.url_ufs)
23+
html = fromstring(texto)
24+
seletorcss_linhas = "div#miolo_interno > table > tr"
25+
linhas = html.cssselect(seletorcss_linhas)
26+
linhas.pop() # a primeira é o cabeçalho
27+
infos = []
28+
for linha in linhas:
29+
seletorcss_celulas = "td"
30+
celulas = linha.cssselect(seletorcss_celulas)
31+
codigo_ibge = celulas[0].text_content()
32+
if codigo_ibge in siglas:
33+
sigla = siglas[codigo_ibge]
34+
infos.append({
35+
'sigla': sigla,
36+
'codigo_ibge': codigo_ibge,
37+
'nome': celulas[1].text_content(),
38+
'area_km2': celulas[2].text_content()
39+
})
40+
41+
# neste ponto, após a carga
42+
# das cidades, a lista
43+
# 'infos' deve estar populada
44+
45+
return infos
46+
47+
def _get_info_cidades(self):
48+
texto = self._request(self.url_cidades)
49+
html = fromstring(texto)
50+
seletorcss_linhas = "div#miolo_interno > table > tr"
51+
linhas = html.cssselect(seletorcss_linhas)
52+
linhas.pop() # a primeira é o cabeçalho
53+
infos = []
54+
for linha in linhas:
55+
seletorcss_celulas = "td"
56+
celulas = linha.cssselect(seletorcss_celulas)
57+
infos.append({
58+
'codigo_ibge_uf': celulas[0].text_content(),
59+
'sigla_uf': celulas[1].text_content(),
60+
'codigo_ibge': celulas[2].text_content(),
61+
'nome': celulas[3].text_content(),
62+
'area_km2': celulas[4].text_content()
63+
})
64+
return infos
65+
66+
def _track_ufs(self, db, siglas):
67+
infos = self._get_info_ufs(siglas)
68+
for info in infos:
69+
db.insert_or_update_uf(info)
70+
71+
def _track_cidades(self, db):
72+
infos = self._get_info_cidades()
73+
siglas = {}
74+
for info in infos:
75+
codigo_ibge_uf = info['codigo_ibge_uf']
76+
sigla_uf = info['sigla_uf']
77+
nome = info['nome']
78+
if codigo_ibge_uf not in siglas:
79+
siglas[codigo_ibge_uf] = sigla_uf
80+
81+
# a chave única de uma cidade não
82+
# pode ser só o nome, pois
83+
# existem cidades com mesmo nome
84+
# em estados diferentes
85+
info['sigla_uf_nome_cidade'] = '%s_%s' % (sigla_uf, nome)
86+
87+
db.insert_or_update_cidade(info)
88+
89+
return siglas
90+
91+
def track(self, db):
92+
"""
93+
Atualiza as bases internas do mongo
94+
com os dados mais recentes do IBGE
95+
referente a ufs e cidades
96+
"""
97+
siglas = self._track_cidades(db) # siglas é um dict cod_ibge -> sigla:
98+
# { '35': 'SP', '35': 'RJ', ... }
99+
self._track_ufs(db, siglas)
100+
101+
102+
def _standalone():
103+
db = Database()
104+
ibge = IbgeTracker()
105+
ibge.track(db)
106+
107+
108+
if __name__ == "__main__":
109+
_standalone()

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@ test: pep8
55

66
.PHONY: pep8
77
pep8:
8-
@flake8 * --ignore=F403,F401 --exclude=requirements.txt,*.pyc,*.md,COPYING,Makefile,*.wsgi
8+
@flake8 * --ignore=F403,F401 --exclude=requirements.txt,*.pyc,*.md,COPYING,Makefile,*.wsgi,*celerybeat-schedule*

PostmonServer.py

+56
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,24 @@ def format_result(result):
5353
return result
5454

5555

56+
def _get_estado_info(db, sigla):
57+
sigla = sigla.upper()
58+
return db.get_one_uf(sigla, fields={'_id': False, 'sigla': False})
59+
60+
61+
def _get_cidade_info(db, sigla_uf, nome_cidade):
62+
sigla_uf = sigla_uf.upper()
63+
sigla_uf_nome_cidade = '%s_%s' % (sigla_uf, nome_cidade)
64+
fields = {
65+
'_id': False,
66+
'sigla_uf': False,
67+
'codigo_ibge_uf': False,
68+
'sigla_uf_nome_cidade': False,
69+
'nome': False
70+
}
71+
return db.get_one_cidade(sigla_uf_nome_cidade, fields=fields)
72+
73+
5674
@route('/cep/<cep:re:\d{5}-?\d{3}>')
5775
@app_v1.route('/cep/<cep:re:\d{5}-?\d{3}>')
5876
def verifica_cep(cep):
@@ -79,13 +97,51 @@ def verifica_cep(cep):
7997

8098
if result:
8199
response.headers['Cache-Control'] = 'public, max-age=2592000'
100+
sigla_uf = result['estado']
101+
estado_info = _get_estado_info(db, sigla_uf)
102+
if estado_info:
103+
result['estado_info'] = estado_info
104+
nome_cidade = result['cidade']
105+
cidade_info = _get_cidade_info(db, sigla_uf, nome_cidade)
106+
if cidade_info:
107+
result['cidade_info'] = cidade_info
82108
return format_result(result)
83109
else:
84110
response.status = '404 O CEP %s informado nao pode ser '
85111
'localizado' % cep
86112
return
87113

88114

115+
@app_v1.route('/uf/<sigla>')
116+
def uf(sigla):
117+
db = Database()
118+
result = _get_estado_info(db, sigla)
119+
if result:
120+
response.headers['Cache-Control'] = 'public, max-age=2592000'
121+
return format_result(result)
122+
else:
123+
message = '404 A sigla %s informada ' \
124+
'nao pode ser localizada'
125+
response.status = message % sigla
126+
print response.status
127+
return
128+
129+
130+
@app_v1.route('/cidade/<sigla_uf>/<nome>')
131+
def cidade(sigla_uf, nome):
132+
db = Database()
133+
result = _get_cidade_info(db, sigla_uf, nome)
134+
if result:
135+
response.headers['Cache-Control'] = 'public, max-age=2592000'
136+
return format_result(result)
137+
else:
138+
message = '404 A cidade %s (%s) informada ' \
139+
'nao pode ser localizada'
140+
response.status = message % (sigla_uf, nome)
141+
print response.status
142+
return
143+
144+
89145
@app_v1.route('/rastreio/<provider>/<track>')
90146
def track_pack(provider, track):
91147
if provider == 'ect':

PostmonTaskScheduler.py

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
from datetime import timedelta
4+
from celery import Celery
5+
from celery.utils.log import get_task_logger
6+
from IbgeTracker import IbgeTracker
7+
from database import MongoDb as Database
8+
import os
9+
10+
USERNAME = os.environ.get('POSTMON_DB_USER')
11+
PASSWORD = os.environ.get('POSTMON_DB_PASSWORD')
12+
if all((USERNAME, PASSWORD)):
13+
broker_conn_string = 'mongodb://%s:%s@localhost:27017' \
14+
% (USERNAME, PASSWORD)
15+
else:
16+
broker_conn_string = 'mongodb://localhost:27017'
17+
18+
print(broker_conn_string)
19+
20+
app = Celery('postmon', broker=broker_conn_string)
21+
22+
app.conf.update(
23+
CELERY_TASK_SERIALIZER='json',
24+
CELERY_ACCEPT_CONTENT=['json'], # Ignore other content
25+
CELERY_RESULT_SERIALIZER='json',
26+
CELERY_TIMEZONE='America/Sao_Paulo',
27+
CELERY_ENABLE_UTC=True,
28+
CELERYBEAT_SCHEDULE={
29+
'track_ibge_daily': {
30+
'task': 'PostmonTaskScheduler.track_ibge',
31+
'schedule': timedelta(days=1) # útil para
32+
# testes: timedelta(minutes=1)
33+
}
34+
}
35+
)
36+
37+
logger = get_task_logger(__name__)
38+
39+
40+
@app.task
41+
def track_ibge():
42+
logger.info('Iniciando tracking do IBGE...')
43+
db = Database()
44+
ibge = IbgeTracker()
45+
ibge.track(db)
46+
logger.info('Finalizou o tracking do IBGE')

README.md

+51
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ As dependências estão listadas no arquivo requirements.txt.
2020
* nosetests
2121
* webtest
2222
* packtrack
23+
* celery
2324

2425
Rodando testes
2526
----------------
@@ -41,6 +42,21 @@ ou
4142

4243
Caso queira rodar em outra porta, basta passá-la como parametro no chamado do _standalone
4344

45+
Para rodar o [Scheduler](#scheduler):
46+
47+
$ celery worker -B -A PostmonTaskScheduler -l info
48+
49+
Recomenda-se a utilização do [Supervisord](http://supervisord.org/) para manter o Celery rodando. Exemplo de configuração para o _supervisord.conf_:
50+
51+
[program:celeryd]
52+
command=celery worker -B -A PostmonTaskScheduler -l info
53+
directory=POSTMON_HOME/repositorio
54+
stdout_logfile=POSTMON_LOG_DIR/celeryd.log
55+
stderr_logfile=POSTMON_LOG_DIR/celeryd_err.log
56+
autostart=true
57+
autorestart=true
58+
startsecs=10
59+
stopwaitsecs=600
4460

4561
MongoDB com autenticação
4662
------------------------
@@ -60,3 +76,38 @@ Agora que seu Mongo está com password exporte as variaveis de ambiente.
6076
export POSTMON_DB_USER=admin
6177
export POSTMON_DB_PASSWORD=123456
6278
```
79+
80+
Scheduler
81+
---------
82+
83+
O Postmon conta com um scheduler baseado na ferramenta [Celery](http://www.celeryproject.org/). Até o momento, a única funcionalidade implementada nessa estrutura é a rotina de coleta de dados do [IBGE](#ibge).
84+
85+
O Celery usa, como Broker, a mesma instância do MongoDB utilizada no módulo de CEP.
86+
87+
O comando apresentado na seção [rodando a aplicação](#rodando-a-aplicação-localmente-na-porta-9876) pode ser quebrado em dois caso seja necessário rodar o Celery Worker separado do Celery Beat. Para mais informações sobre essa questão leia a [documentação do Celery](http://docs.celeryproject.org/en/latest/).
88+
89+
Além do Broker, o Celery Beat depende internamente de uma base de dados, criada automaticamente na primeira execução, onde são armazenadas informações sobre os schedules. Por padrão essa base fica em um arquivo chamado _celerybeat_schedule_, criado no diretório onde o Celery Beat foi executado. Esse local pode ser alterado através do switch -s, conforme exemplo abaixo:
90+
91+
$ celery worker -B -A PostmonTaskScheduler -l info -s /novo/caminho/para/arquivo/celerybeat_schedule
92+
93+
IBGE
94+
-------------
95+
96+
O Postmon fornece as seguintes informações extraídas do site do IBGE:
97+
98+
* Código do município/UF
99+
* Área territorial (em km²)
100+
101+
Essas informações estão presentes nos atributos *estado_info* e *cidade_info* da rota de busca de _cep_, bem como nas seguintes rotas:
102+
103+
* /uf/{sigla-uf}
104+
* /cidade/{sigla-uf}/{nome-cidade}
105+
106+
Exemplos:
107+
108+
* /uf/SP
109+
* /cidade/SP/São Paulo
110+
* /cidade/SP/Araraquara
111+
* /cidade/RJ/Macaé
112+
113+
A rotina de atualização desses dados está configurada para rodar diariamente.

database.py

+19
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ def __init__(self, address='localhost'):
2525
def get_one(self, cep, **kwargs):
2626
return self._db.ceps.find_one({'cep': cep}, **kwargs)
2727

28+
def get_one_uf(self, sigla, **kwargs):
29+
return self._db.ufs.find_one({'sigla': sigla}, **kwargs)
30+
31+
def get_one_cidade(self, sigla_uf_nome_cidade, **kwargs):
32+
spec = {'sigla_uf_nome_cidade': sigla_uf_nome_cidade}
33+
return self._db.cidades.find_one(spec, **kwargs)
34+
35+
def get_one_uf_by_nome(self, nome, **kwargs):
36+
return self._db.ufs.find_one({'nome': nome}, **kwargs)
37+
2838
def insert_or_update(self, obj, **kwargs):
2939

3040
update = {'$set': obj}
@@ -33,5 +43,14 @@ def insert_or_update(self, obj, **kwargs):
3343

3444
self._db.ceps.update({'cep': obj['cep']}, update, upsert=True)
3545

46+
def insert_or_update_uf(self, obj, **kwargs):
47+
update = {'$set': obj}
48+
self._db.ufs.update({'sigla': obj['sigla']}, update, upsert=True)
49+
50+
def insert_or_update_cidade(self, obj, **kwargs):
51+
update = {'$set': obj}
52+
chave = 'sigla_uf_nome_cidade'
53+
self._db.cidades.update({chave: obj[chave]}, update, upsert=True)
54+
3655
def remove(self, cep):
3756
self._db.ceps.remove({'cep': cep})

requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ webtest==1.4.3
77
packtrack==0.1.0
88
xmltodict
99
flake8
10+
celery[mongodb]

0 commit comments

Comments
 (0)