susflow 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
susflow/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .ftp import FTPError, ArquivoNaoEncontradoError
2
+ from .reader import LeituraError, ler
3
+ from .systems import sinasc, sim, sinan
susflow/cache.py ADDED
@@ -0,0 +1,26 @@
1
+ """
2
+ susflow/cache.py
3
+ ================
4
+ Resolução de cache local. Compartilhado por todos os sistemas.
5
+ Pasta padrão: ~/.susflow/cache/, espelhando a estrutura do FTP.
6
+ """
7
+
8
+ from pathlib import Path
9
+
10
+ _CACHE_PADRAO = Path.home() / ".susflow" / "cache"
11
+
12
+
13
+ def caminho_local(caminho_ftp: str, raiz: Path | None = None) -> Path:
14
+ """
15
+ Retorna o path local correspondente a um caminho FTP.
16
+ Ex: /dissemin/publicos/SINASC/NOV/DNRES/DNSP2022.dbc
17
+ → ~/.susflow/cache/dissemin/publicos/SINASC/NOV/DNRES/DNSP2022.dbc
18
+ """
19
+ raiz = Path(raiz) if raiz else _CACHE_PADRAO
20
+ # remove a barra inicial para não criar path absoluto ao fazer /
21
+ relativo = caminho_ftp.lstrip("/")
22
+ return raiz / relativo
23
+
24
+
25
+ def existe(caminho_ftp: str, raiz: Path | None = None) -> bool:
26
+ return caminho_local(caminho_ftp, raiz).exists()
susflow/config.py ADDED
@@ -0,0 +1,358 @@
1
+ """
2
+ susflow/config.py
3
+ =================
4
+ Metadados de todos os sistemas do DATASUS.
5
+
6
+ Campos de cada sistema:
7
+ ftp_dir — caminho absoluto no FTP
8
+ pattern — padrão do nome ({UF}, {YYYY}, {YY}, {PREFIX}, {DISEASE}, {TYPE})
9
+ granularity — "year" | "month"
10
+ year_digits — 2 ou 4
11
+ format — "dbc" | "dbf" | "zip"
12
+ scope — "uf" | "national"
13
+ year_range — (ano_inicio, ano_fim) inclusive
14
+ """
15
+
16
+ FTP_HOST = "ftp.datasus.gov.br"
17
+
18
+ UFS = [
19
+ "AC", "AL", "AM", "AP", "BA", "CE", "DF", "ES", "GO",
20
+ "MA", "MG", "MS", "MT", "PA", "PB", "PE", "PI", "PR",
21
+ "RJ", "RN", "RO", "RR", "RS", "SC", "SE", "SP", "TO",
22
+ ]
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # SIM — /dissemin/publicos/SIM/CID10/
26
+ # DORES/ → DO{UF}{YYYY}.dbc por UF, anual, 4 dígitos
27
+ # DOFET/ → DO{TYPE}{YY}.dbc nacional, anual, 2 dígitos
28
+ # tipos: EXT, FET, INF, MAT
29
+ # ---------------------------------------------------------------------------
30
+ SIM = {
31
+ "description": "Sistema de Informações sobre Mortalidade",
32
+ "ftp_base": "/dissemin/publicos/SIM/CID10",
33
+
34
+ # Documentação técnica e layouts de campos
35
+ "docs": {
36
+ "ftp_dir": "/dissemin/publicos/SIM/CID10/DOCS",
37
+ "arquivos": {
38
+ "Docs_Tabs_CID10.zip": "Layouts, tabelas e dicionário de variáveis",
39
+ "Estrutura_do_SIM_2025.pdf": "Estrutura atual dos arquivos (referência principal)",
40
+ "Estrutura_SIM_Anterior.pdf": "Estrutura anterior — necessária para bases legadas",
41
+ },
42
+ },
43
+
44
+ # Dados agregados tabulados
45
+ "tab": {
46
+ "ftp_dir": "/dissemin/publicos/SIM/CID10/TAB",
47
+ "arquivos": {
48
+ "OBITOS_CID10_TAB.zip": "Óbitos agregados por CID-10 (série histórica tabulada)",
49
+ },
50
+ },
51
+
52
+ # Tabelas de apoio (CID-10, municípios, ocupações, países, UFs)
53
+ "tabelas": {
54
+ "ftp_dir": "/dissemin/publicos/SIM/CID10/TABELAS",
55
+ "arquivos": {
56
+ "CID10.DBF": "Classificação Internacional de Doenças — CID-10",
57
+ "CIDCAP10.DBF": "Capítulos do CID-10",
58
+ "CADMUN.DBF": "Cadastro de municípios",
59
+ "CADMUN.xls": "Cadastro de municípios (formato Excel)",
60
+ "TABOCUP.DBF": "Tabela de ocupações (CBO)",
61
+ "TABPAIS.DBF": "Tabela de países",
62
+ "TABUF.DBF": "Tabela de unidades federativas",
63
+ },
64
+ },
65
+
66
+ "uf": {
67
+ "ftp_dir": "/dissemin/publicos/SIM/CID10/DORES",
68
+ "pattern": "DO{UF}{YYYY}.dbc",
69
+ "granularity": "year",
70
+ "year_digits": 4,
71
+ "format": "dbc",
72
+ "scope": "uf",
73
+ "year_range": (1996, 2024),
74
+ },
75
+
76
+ "special": {
77
+ "ftp_dir": "/dissemin/publicos/SIM/CID10/DOFET",
78
+ "pattern": "DO{TYPE}{YY}.dbc",
79
+ "granularity": "year",
80
+ "year_digits": 2,
81
+ "format": "dbc",
82
+ "scope": "national",
83
+ "year_range": (1996, 2024),
84
+ "types": {
85
+ "EXT": "Óbitos por causas externas",
86
+ "FET": "Óbitos fetais",
87
+ "INF": "Óbitos infantis",
88
+ "MAT": "Óbitos maternos",
89
+ },
90
+ },
91
+ }
92
+
93
+ # ---------------------------------------------------------------------------
94
+ # SINASC — /dissemin/publicos/SINASC/NOV/
95
+ # DNRES/ → DN{UF}{YYYY}.dbc por UF, anual, 4 dígitos
96
+ # DNRES/ → DNBR{YYYY}.dbc agregado nacional (2014–2017 apenas)
97
+ # DNRES/ → DNEX{YYYY}.dbc exceções/suplementar (pontual, ex: 2021)
98
+ # DOCS/ → documentação técnica (caminho a confirmar — não mapeado ainda)
99
+ # ---------------------------------------------------------------------------
100
+ SINASC = {
101
+ "description": "Sistema de Informações sobre Nascidos Vivos",
102
+
103
+ "uf": {
104
+ "ftp_dir": "/dissemin/publicos/SINASC/NOV/DNRES",
105
+ "pattern": "DN{UF}{YYYY}.dbc",
106
+ "granularity": "year",
107
+ "year_digits": 4,
108
+ "format": "dbc",
109
+ "scope": "uf",
110
+ "year_range": (1996, 2022),
111
+ },
112
+
113
+ # Agregado nacional — série incompleta (apenas 2014–2017 confirmados no FTP)
114
+ "nacional": {
115
+ "ftp_dir": "/dissemin/publicos/SINASC/NOV/DNRES",
116
+ "pattern": "DNBR{YYYY}.dbc",
117
+ "granularity": "year",
118
+ "year_digits": 4,
119
+ "format": "dbc",
120
+ "scope": "national",
121
+ "year_range": (2014, 2017),
122
+ },
123
+
124
+ # Arquivo de exceção/suplementar — pontual, não é uma série regular
125
+ "excecoes": {
126
+ "ftp_dir": "/dissemin/publicos/SINASC/NOV/DNRES",
127
+ "pattern": "DNEX{YYYY}.dbc",
128
+ "format": "dbc",
129
+ "nota": "Arquivos pontuais com registros suplementares. Confirmado: DNEX2021.dbc.",
130
+ },
131
+
132
+ # Documentação técnica — caminho exato a confirmar (não mapeado ainda)
133
+ "docs": {
134
+ "ftp_dir": "/dissemin/publicos/SINASC/NOV/DOCS",
135
+ "arquivos": {
136
+ "Estrutura_SINASC_para_CD.pdf": "Estrutura dos arquivos (formato legado de CD-ROM)",
137
+ "Legislacao_PDF.pdf": "Legislação relacionada ao SINASC",
138
+ "NASC98.HLP": "Arquivo de ajuda do sistema legado (1998)",
139
+ "Portaria.pdf": "Portaria regulamentadora",
140
+ },
141
+ "nota": "Caminho FTP não confirmado — diretório DOCS não foi mapeado ainda.",
142
+ },
143
+ }
144
+
145
+ # ---------------------------------------------------------------------------
146
+ # SINAN — /dissemin/publicos/SINAN/DADOS/
147
+ # FINAIS/{DISEASE}BR{YY}.dbc dados consolidados
148
+ # PRELIM/{DISEASE}BR{YY}.dbc dados preliminares
149
+ # DOCS/ documentação técnica (caminho a confirmar)
150
+ # ---------------------------------------------------------------------------
151
+ SINAN = {
152
+ "description": "Sistema de Informações de Agravos de Notificação",
153
+ "ftp_dir": "/dissemin/publicos/SINAN/DADOS/FINAIS",
154
+ "ftp_dir_prelim": "/dissemin/publicos/SINAN/DADOS/PRELIM",
155
+ "pattern": "{DISEASE}BR{YY}.dbc",
156
+ "granularity": "year",
157
+ "year_digits": 2,
158
+ "format": "dbc",
159
+ "scope": "national",
160
+ "diseases": {
161
+ "ACBI": "Acidente por animal peçonhento",
162
+ "ACGR": "Acidente de trabalho grave",
163
+ "ANIM": "Acidente por animal (outros)",
164
+ "ANTR": "Antraz (Carbúnculo)",
165
+ "BOTU": "Botulismo",
166
+ "CANC": "Câncer relacionado ao trabalho",
167
+ "CHAG": "Doença de Chagas",
168
+ "CHIK": "Chikungunya",
169
+ "COLE": "Cólera",
170
+ "COQU": "Coqueluche",
171
+ "DCRJ": "Doença de Creutzfeldt-Jakob",
172
+ "DENG": "Dengue",
173
+ "DERM": "Dermatose ocupacional",
174
+ "DIFT": "Difteria",
175
+ "ESPO": "Esporotricose",
176
+ "ESQU": "Esquistossomose",
177
+ "FMAC": "Febre maculosa",
178
+ "FTIF": "Febre tifoide",
179
+ "HANS": "Hanseníase",
180
+ "HANT": "Hantavirose",
181
+ "IEXO": "Intoxicação exógena",
182
+ "LEIV": "Leishmaniose visceral",
183
+ "LEPT": "Leptospirose",
184
+ "LERD": "LER/DORT",
185
+ "LTA": "Leishmaniose tegumentar americana",
186
+ "MALA": "Malária",
187
+ "MENI": "Meningite",
188
+ "MENT": "Transtorno mental relacionado ao trabalho",
189
+ "NTRA": "Noma (Cancrum oris)",
190
+ "PAIR": "Perda auditiva induzida por ruído",
191
+ "PEST": "Peste",
192
+ "PFAN": "Paralisia flácida aguda / Poliomielite",
193
+ "PNEU": "Pneumoconiose",
194
+ "RAIV": "Raiva humana",
195
+ "ROTA": "Rotavírus",
196
+ "SDTA": "Surto de doença transmitida por alimento",
197
+ "TETA": "Tétano acidental",
198
+ "TETN": "Tétano neonatal",
199
+ "TOXC": "Toxoplasmose congênita",
200
+ "TOXG": "Toxoplasmose gestacional",
201
+ "TRAC": "Tracoma",
202
+ "TUBE": "Tuberculose",
203
+ "VIOL": "Violência doméstica / sexual / autoprovocada",
204
+ "ZIKA": "Zika vírus",
205
+ },
206
+
207
+ # Documentação técnica — caminho FTP a confirmar (não mapeado ainda)
208
+ "docs": {
209
+ "ftp_dir": "/dissemin/publicos/SINAN/DOCS",
210
+ "arquivos": {
211
+ "Docs_TAB_SINAN.zip": "Layouts, tabelas e dicionário de variáveis de todos os agravos",
212
+ "POP_I_Acesso_a_Microdados_5.pdf": "Guia de acesso aos microdados do SINAN",
213
+ "POP_II_Descompactacao_expansao_conversao_3.pdf": "Guia de descompactação e conversão dos arquivos .dbc",
214
+ "POP_III_Instalacao_do_tabulador_TabWin_3.pdf": "Guia de instalação do TabWin (tabulador oficial)",
215
+ "Nota_Tecnica_Doenca_de_Creutzfeldt-Jakob(DCJ).pdf": "Nota técnica — Doença de Creutzfeldt-Jakob",
216
+ "Nota_Tecnica_Intoxicacao_Exogena.pdf": "Nota técnica — Intoxicação Exógena",
217
+ "Nota_Tecnica_Rotavirus.pdf": "Nota técnica — Rotavírus",
218
+ "Nota_Tecnica_Surtos_de_DTA.pdf": "Nota técnica — Surtos de Doença Transmitida por Alimento",
219
+ "Nota_Tecnica_Toxoplasmose.pdf": "Nota técnica — Toxoplasmose",
220
+ },
221
+ "nota": "Caminho FTP não confirmado — diretório DOCS não foi mapeado ainda.",
222
+ },
223
+ }
224
+
225
+ # ---------------------------------------------------------------------------
226
+ # SIHSUS — /dissemin/publicos/SIHSUS/200801_/Dados/
227
+ # {PREFIX}{UF}{YY}{MM}.dbc por UF, mensal, 2 dígitos
228
+ # Prefixo principal: RD (AIH Reduzida — registro de internação)
229
+ # Exceção: CH e CM usam BR fixo (escopo nacional), não {UF}
230
+ # ---------------------------------------------------------------------------
231
+ SIHSUS = {
232
+ "description": "Sistema de Informações Hospitalares do SUS",
233
+ "ftp_dir": "/dissemin/publicos/SIHSUS/200801_/Dados",
234
+ "ftp_dir_old": "/dissemin/publicos/SIHSUS/199201_200712",
235
+ "pattern": "{PREFIX}{UF}{YY}{MM}.dbc",
236
+ "granularity": "month",
237
+ "year_digits": 2,
238
+ "format": "dbc",
239
+ "scope": "uf",
240
+ "year_range": (2008, 2026),
241
+ "prefixes": {
242
+ "RD": "AIH reduzida (internações — dado principal)",
243
+ "SP": "Serviços profissionais",
244
+ "RJ": "AIH rejeitada",
245
+ "ER": "AIH com erro",
246
+ },
247
+ # CH e CM usam BR fixo — padrão: {PREFIX}BR{YY}{MM}.dbc
248
+ "prefixes_nacionais": {
249
+ "CH": "Cabeçalho nacional (dados agregados)",
250
+ "CM": "Comunicação de movimento",
251
+ },
252
+ }
253
+
254
+ # ---------------------------------------------------------------------------
255
+ # SIASUS — /dissemin/publicos/SIASUS/200801_/Dados/
256
+ # {PREFIX}{UF}{YY}{MM}.dbc por UF, mensal, 2 dígitos
257
+ # Prefixo principal: PA (Produção Ambulatorial)
258
+ # ---------------------------------------------------------------------------
259
+ SIASUS = {
260
+ "description": "Sistema de Informações Ambulatoriais do SUS",
261
+ "ftp_dir": "/dissemin/publicos/SIASUS/200801_/Dados",
262
+ "ftp_dir_old": "/dissemin/publicos/SIASUS/199407_200712",
263
+ "pattern": "{PREFIX}{UF}{YY}{MM}.dbc",
264
+ "granularity": "month",
265
+ "year_digits": 2,
266
+ "format": "dbc",
267
+ "scope": "uf",
268
+ "year_range": (2008, 2026),
269
+ "prefixes": {
270
+ "PA": ("Produção Ambulatorial (BPA)", 2008, 2026),
271
+ "BI": ("BPA Individualizado", 2008, 2026),
272
+ "AD": ("APAC de Laudos Diversos", 2008, 2026),
273
+ "AM": ("APAC de Medicamentos", 2008, 2026),
274
+ "AMP": ("APAC de Medicamentos Padronizados", 2020, 2026),
275
+ "AQ": ("APAC de Quimioterapia", 2008, 2026),
276
+ "AR": ("APAC de Radioterapia", 2008, 2026),
277
+ "ACF": ("APAC Confecção de Fístula Arteriovenosa", 2014, 2026),
278
+ "ATD": ("APAC Tratamento Dialítico", 2014, 2026),
279
+ "PS": ("RAAS Psicossocial", 2013, 2026),
280
+ "AB": ("APAC Acompanhamento Pós Cirurgia Bariátrica (novo)", 2025, 2026),
281
+ "ABO": ("APAC Acompanhamento Pós Cirurgia Bariátrica (legado)", 2015, 2018),
282
+ "AN": ("APAC de Nefrologia (encerrado, substituído por ATD)", 2008, 2014),
283
+ "SAD": ("RAAS Atenção Domiciliar (encerrado)", 2013, 2015),
284
+ },
285
+ }
286
+
287
+ # ---------------------------------------------------------------------------
288
+ # CNES — /dissemin/publicos/CNES/200508_/Dados/
289
+ # {TYPE}/{TYPE}{UF}{YY}{MM}.dbc por UF, mensal, 2 dígitos
290
+ # Arquivo fica 2 níveis dentro de Dados/: ex. ST/STSP2501.dbc
291
+ # Subtype principal: ST (Estabelecimentos)
292
+ # ---------------------------------------------------------------------------
293
+ CNES = {
294
+ "description": "Cadastro Nacional de Estabelecimentos de Saúde",
295
+ "ftp_base": "/dissemin/publicos/CNES/200508_/Dados",
296
+ "pattern": "{TYPE}/{TYPE}{UF}{YY}{MM}.dbc",
297
+ "granularity": "month",
298
+ "year_digits": 2,
299
+ "format": "dbc",
300
+ "scope": "uf",
301
+ "subtypes": {
302
+ "ST": ("Estabelecimentos (dado principal)", 2005, 2026),
303
+ "PF": ("Profissionais de saúde", 2005, 2026),
304
+ "DC": ("Dados complementares", 2005, 2026),
305
+ "EQ": ("Equipamentos", 2005, 2026),
306
+ "SR": ("Serviços especializados", 2005, 2026),
307
+ "LT": ("Leitos", 2005, 2026),
308
+ "HB": ("Habilitações", 2007, 2026),
309
+ "EF": ("Centros cirúrgicos e obstétricos", 2007, 2026),
310
+ "EP": ("Equipes de saúde", 2007, 2026),
311
+ "RC": ("Regras contratuais", 2007, 2026),
312
+ "IN": ("Incentivos", 2007, 2026),
313
+ "GM": ("Gestão e metas", 2014, 2026),
314
+ "EE": ("Equipamentos e produções (encerrado)", 2007, 2019),
315
+ },
316
+ }
317
+
318
+ # ---------------------------------------------------------------------------
319
+ # PNI — /dissemin/publicos/PNI/DADOS/
320
+ # DPNI{UF}{YY}.DBF por UF, anual, 2 dígitos
321
+ # Formato DBF puro (sem compressão blast) — usar dbfread, não pyreaddbc
322
+ # ---------------------------------------------------------------------------
323
+ PNI = {
324
+ "description": "Programa Nacional de Imunizações",
325
+ "ftp_dir": "/dissemin/publicos/PNI/DADOS",
326
+ "pattern": "DPNI{UF}{YY}.DBF",
327
+ "granularity": "year",
328
+ "year_digits": 2,
329
+ "format": "dbf",
330
+ "scope": "uf",
331
+ "year_range": (1994, 2019),
332
+ }
333
+
334
+ # ---------------------------------------------------------------------------
335
+ # IBGE/POP — /dissemin/publicos/IBGE/POP/
336
+ # POPBR{YY}.zip nacional, anual, 2 dígitos
337
+ # ---------------------------------------------------------------------------
338
+ IBGE_POP = {
339
+ "description": "Estimativas populacionais IBGE",
340
+ "ftp_dir": "/dissemin/publicos/IBGE/POP",
341
+ "pattern": "POPBR{YY}.zip",
342
+ "granularity": "year",
343
+ "year_digits": 2,
344
+ "format": "zip",
345
+ "scope": "national",
346
+ "year_range": (1980, 2012),
347
+ }
348
+
349
+ ALL_SYSTEMS = {
350
+ "SIM": SIM,
351
+ "SINASC": SINASC,
352
+ "SINAN": SINAN,
353
+ "SIHSUS": SIHSUS,
354
+ "SIASUS": SIASUS,
355
+ "CNES": CNES,
356
+ "PNI": PNI,
357
+ "IBGE_POP": IBGE_POP,
358
+ }
susflow/ftp.py ADDED
@@ -0,0 +1,111 @@
1
+ """
2
+ susflow/ftp.py
3
+ ==============
4
+ Camada de transporte: toda comunicação com o FTP do DATASUS fica aqui.
5
+ """
6
+
7
+ import time
8
+
9
+ from ftplib import FTP, all_errors
10
+ from pathlib import Path
11
+
12
+ from .config import FTP_HOST
13
+
14
+ _TIMEOUT = 30 # segundos
15
+ _TENTATIVAS = 3
16
+ _BACKOFF = 2 # segundos entre tentativas
17
+
18
+
19
+ class FTPError(Exception):
20
+ """Erro de comunicação com o FTP do DATASUS."""
21
+
22
+ class ArquivoNaoEncontradoError(FTPError):
23
+ """Arquivo não existe no FTP."""
24
+
25
+ def _conectar() -> FTP:
26
+ """Abre uma conexão FTP limpa. Reconectar por operação evita o bug '200 Type set to A'."""
27
+ ftp = FTP()
28
+ ftp.connect(FTP_HOST, 21, timeout=_TIMEOUT)
29
+ ftp.login()
30
+ ftp.set_pasv(True)
31
+ return ftp
32
+
33
+
34
+ def _tentar(fn, *args, **kwargs):
35
+ """Executa fn com retentativas e backoff."""
36
+ ultimo_erro = None
37
+ for tentativa in range(_TENTATIVAS):
38
+ try:
39
+ return fn(*args, **kwargs)
40
+
41
+ except all_errors as e:
42
+ ultimo_erro = e
43
+ if tentativa < _TENTATIVAS - 1:
44
+ time.sleep(_BACKOFF)
45
+
46
+ raise FTPError(f"Falha após {_TENTATIVAS} tentativas: {ultimo_erro}") from ultimo_erro
47
+
48
+
49
+ def listar(caminho: str) -> list[str]:
50
+ """
51
+ Lista os nomes de arquivo em um diretório FTP.
52
+ Retorna apenas arquivos (não subdiretórios).
53
+ """
54
+ def _listar():
55
+ ftp = _conectar()
56
+
57
+ try:
58
+ itens: list[str] = []
59
+ ftp.retrlines("LIST", itens.append) # LIST no cwd após cwd()
60
+ ftp.cwd(caminho)
61
+ itens.clear()
62
+ ftp.retrlines("LIST", itens.append)
63
+
64
+ arquivos = []
65
+ for linha in itens:
66
+ if "<DIR>" in linha:
67
+ continue
68
+
69
+ nome = linha.split()[-1]
70
+ arquivos.append(nome)
71
+
72
+ return arquivos
73
+
74
+ finally:
75
+ ftp.quit()
76
+
77
+ return _tentar(_listar)
78
+
79
+
80
+ def baixar(caminho_ftp: str, destino: Path) -> Path:
81
+ """
82
+ Baixa um arquivo do FTP para o caminho local `destino`.
83
+ Cria os diretórios necessários. Retorna o path do arquivo salvo.
84
+ Levanta ArquivoNaoEncontradoError se o arquivo não existir no FTP.
85
+ """
86
+ destino = Path(destino)
87
+ destino.parent.mkdir(parents=True, exist_ok=True)
88
+
89
+ def _baixar():
90
+ ftp = _conectar()
91
+
92
+ try:
93
+ with open(destino, "wb") as f:
94
+ ftp.retrbinary(f"RETR {caminho_ftp}", f.write)
95
+
96
+ except all_errors as e:
97
+ if destino.exists():
98
+ destino.unlink()
99
+
100
+ if "550" in str(e):
101
+ raise ArquivoNaoEncontradoError(
102
+ f"Arquivo não encontrado no FTP: {caminho_ftp}"
103
+ ) from e
104
+ raise
105
+
106
+ finally:
107
+ ftp.quit()
108
+
109
+ return destino
110
+
111
+ return _tentar(_baixar)
susflow/reader.py ADDED
@@ -0,0 +1,97 @@
1
+ """
2
+ susflow/reader.py
3
+ =================
4
+ Camada de leitura: converte arquivos locais (.dbc, .dbf, .zip) em DataFrame.
5
+ """
6
+
7
+ import tempfile
8
+ import zipfile
9
+
10
+ from pyreaddbc import dbc2dbf
11
+ from pathlib import Path
12
+ from dbfread import DBF
13
+
14
+ import pandas as pd
15
+
16
+
17
+ class LeituraError(Exception):
18
+ """Falha ao converter arquivo para DataFrame."""
19
+
20
+ def ler(arquivo: Path) -> pd.DataFrame:
21
+ """
22
+ Lê um arquivo local e retorna um DataFrame.
23
+ Suporta .dbc, .dbf e .zip (que contenha .dbc ou .dbf).
24
+ Colunas sempre em maiúsculo, strings decodificadas em latin-1.
25
+ """
26
+ arquivo = Path(arquivo)
27
+ sufixo = arquivo.suffix.lower()
28
+
29
+ if sufixo == ".dbc":
30
+ return _ler_dbc(arquivo)
31
+
32
+ if sufixo == ".dbf":
33
+ return _ler_dbf(arquivo)
34
+
35
+ if sufixo == ".zip":
36
+ return _ler_zip(arquivo)
37
+
38
+ raise LeituraError(f"Formato não suportado: {sufixo}")
39
+
40
+
41
+ def _ler_dbc(arquivo: Path) -> pd.DataFrame:
42
+ try:
43
+ with tempfile.NamedTemporaryFile(suffix=".dbf", delete=False) as tmp:
44
+ tmp_path = Path(tmp.name)
45
+
46
+ try:
47
+ dbc2dbf(str(arquivo), str(tmp_path))
48
+ return _ler_dbf(tmp_path)
49
+
50
+ finally:
51
+ if tmp_path.exists():
52
+ tmp_path.unlink()
53
+
54
+ except LeituraError:
55
+ raise
56
+
57
+ except Exception as e:
58
+ raise LeituraError(f"Falha ao ler .dbc: {arquivo}") from e
59
+
60
+
61
+ def _ler_dbf(arquivo: Path) -> pd.DataFrame:
62
+ try:
63
+ tabela = DBF(str(arquivo), encoding="latin-1", load=True)
64
+ df = pd.DataFrame(iter(tabela))
65
+ df.columns = df.columns.str.upper()
66
+ return df
67
+
68
+ except Exception as e:
69
+ raise LeituraError(f"Falha ao ler .dbf: {arquivo}") from e
70
+
71
+
72
+ def _ler_zip(arquivo: Path) -> pd.DataFrame:
73
+ try:
74
+ with tempfile.TemporaryDirectory() as tmp:
75
+
76
+ with zipfile.ZipFile(arquivo) as zf:
77
+ nomes = [n for n in zf.namelist() if not n.endswith("/")]
78
+
79
+ if not nomes:
80
+ raise LeituraError(f"ZIP vazio: {arquivo}")
81
+
82
+ zf.extractall(tmp)
83
+
84
+ # lê o primeiro arquivo reconhecível dentro do zip
85
+ for nome in nomes:
86
+ extraido = Path(tmp) / nome
87
+ sufixo = extraido.suffix.lower()
88
+ if sufixo in (".dbc", ".dbf"):
89
+ return ler(extraido)
90
+
91
+ raise LeituraError(f"Nenhum .dbc ou .dbf encontrado dentro de {arquivo}")
92
+
93
+ except LeituraError:
94
+ raise
95
+
96
+ except Exception as e:
97
+ raise LeituraError(f"Falha ao ler .zip: {arquivo}") from e
@@ -0,0 +1,3 @@
1
+ dbfread==2.0.7
2
+ pandas==3.0.3
3
+ pyreaddbc==1.2.0
@@ -0,0 +1,3 @@
1
+ from . import sim, sinasc, sinan, sihsus, siasus, cnes, pni
2
+
3
+ __all__ = ["sim", "sinasc", "sinan", "sihsus", "siasus", "cnes", "pni"]