vra-anac 0.1.0__tar.gz

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.
vra_anac-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 peterwkdev-creator
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,153 @@
1
+ Metadata-Version: 2.4
2
+ Name: vra-anac
3
+ Version: 0.1.0
4
+ Summary: Pontualidade dos voos domésticos do Brasil pelo VRA da ANAC: lê o CSV do mês e calcula o indicador
5
+ Author: Peter
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/peterwkdev-creator/vra-anac
8
+ Project-URL: Source, https://github.com/peterwkdev-creator/vra-anac
9
+ Project-URL: Issues, https://github.com/peterwkdev-creator/vra-anac/issues
10
+ Project-URL: Changelog, https://github.com/peterwkdev-creator/vra-anac/blob/main/CHANGELOG.md
11
+ Keywords: anac,vra,aviacao,voos,pontualidade,atrasos,dados-abertos,brasil
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Natural Language :: Portuguese (Brazilian)
14
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
15
+ Requires-Python: >=3.11
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Dynamic: license-file
19
+
20
+ # vra-anac
21
+
22
+ [![testes](https://github.com/peterwkdev-creator/vra-anac/actions/workflows/testes.yml/badge.svg)](https://github.com/peterwkdev-creator/vra-anac/actions/workflows/testes.yml)
23
+ [![PyPI](https://img.shields.io/pypi/v/vra-anac?label=PyPI)](https://pypi.org/project/vra-anac/)
24
+ [![Python](https://img.shields.io/pypi/pyversions/vra-anac?label=Python)](https://pypi.org/project/vra-anac/)
25
+ [![licença: MIT](https://img.shields.io/badge/licen%C3%A7a-MIT-blue)](https://github.com/peterwkdev-creator/vra-anac/blob/main/LICENSE)
26
+
27
+ Quantos voos domésticos chegaram no horário, por voo, rota, companhia ou
28
+ aeroporto, a partir do arquivo mensal do **VRA (Voo Regular Ativo) da
29
+ ANAC**. Lê o CSV como a ANAC publica e aplica a mesma régua do
30
+ [chegounahora.com.br](https://chegounahora.com.br/): **no horário = chegada
31
+ até 15 min depois da prevista**.
32
+
33
+ Só a biblioteca padrão do Python, sem rede: você baixa o CSV, ela faz a conta.
34
+
35
+ ![O comando vra-anac lê o CSV de agosto/2026 da ANAC e mostra a pontualidade por companhia e por rota](https://raw.githubusercontent.com/peterwkdev-creator/vra-anac/main/docs/vitrine.gif)
36
+
37
+ ## Instalar
38
+
39
+ ```bash
40
+ pip install vra-anac
41
+ ```
42
+
43
+ Ou uma versão exata, pela tag do GitHub:
44
+
45
+ ```bash
46
+ pip install "git+https://github.com/peterwkdev-creator/vra-anac@v0.1.0"
47
+ ```
48
+
49
+ Python 3.11 ou mais novo.
50
+
51
+ ## Usar
52
+
53
+ ```python
54
+ import vra_anac
55
+
56
+ # o CSV do mês que você baixou da ANAC (aqui, a amostra dos testes)
57
+ atualizado, voos = vra_anac.ler_arquivo("tests/fixtures/vra-amostra.csv")
58
+ t = vra_anac.tabelas()
59
+
60
+ for chave, c in vra_anac.agrupar(voos, por="voo").items():
61
+ print(f"{t.rotulo(chave)}: {c.pct('chegada')}% no horário (n = {c.n})")
62
+ # G3 1015 SDU→CGH: 100% no horário (n = 1)
63
+ # G3 1016 CGH→SDU: 0% no horário (n = 2)
64
+ # ...
65
+ ```
66
+
67
+ Pela linha de comando, a tabela do mês (ou `--json`):
68
+
69
+ ```bash
70
+ vra-anac VRA_20268.csv --por companhia
71
+ ```
72
+
73
+ ```
74
+ VRA da ANAC, 08/2026 (arquivo atualizado em 2026-10-07): 68.355 voos domésticos regulares, 1 fora do mês.
75
+ No horário = chegada até 15 min depois da prevista. Só os grupos com 20 voos ou mais.
76
+
77
+ no horário cancelado voos companhia
78
+ 87% 1% 23.080 Azul (AZU)
79
+ 84% 2% 20.295 GOL (GLO)
80
+ 78% 1% 23.850 LATAM (TAM)
81
+ 76% 16% 1.098 Azul Conecta (ACN)
82
+ 0% 100% 24 Apuí Táxi Aéreo (CQB)
83
+
84
+ Dados: ANAC, Voo Regular Ativo (VRA), licença CC-BY. https://www.gov.br/anac/...
85
+ ```
86
+
87
+ `--por` aceita `voo` (padrão), `rota`, `companhia`, `origem` e `destino`;
88
+ `--minimo` (padrão 20) deixa de fora o grupo com poucos voos, que oscila por
89
+ acaso de um mês para o outro.
90
+
91
+ ## Onde baixar o CSV
92
+
93
+ Na página do [Voo Regular Ativo (VRA)](https://www.gov.br/anac/pt-br/acesso-a-informacao/dados-abertos/areas-de-atuacao/voos-e-operacoes-aereas/voo-regular-ativo-vra)
94
+ da ANAC, um arquivo por mês (`VRA_AAAAM.csv`, o mês sem zero). A ANAC
95
+ publica com cerca de 4 semanas de atraso.
96
+
97
+ ## O que a conta faz
98
+
99
+ - **Voo doméstico regular**: tipo de linha `N` e código de autorização `0`.
100
+ Extras, fretamentos e voos internacionais ficam de fora.
101
+ - **No horário**: chegada real até 15 min depois da prevista; adiantado
102
+ conta como no horário. A partida tem o mesmo critério, ao lado
103
+ (`c.pct("partida")`).
104
+ - **Faixas**: até 30 min, até 60 min, mais de 60 min e cancelado
105
+ (`c.distribuicao("chegada")`). O atraso é contado em segundos inteiros:
106
+ 30 min exatos ficam na faixa de até 30.
107
+ - **Cancelado** conta no total e nunca como no horário.
108
+ - **Porcentagem** arredondada com o meio para cima (12,5 → 13).
109
+ - **O voo** é empresa + número + origem + destino. O número sai sem o `Z`
110
+ que o VRA às vezes põe na frente (`Z1001` e `1001` são o mesmo voo
111
+ vendido). Isso é inferência, conferida em alguns voos contra o horário
112
+ publicado pela companhia, não regra escrita da ANAC.
113
+ - **O mês** sai das datas das linhas, não do nome do arquivo; o que cai em
114
+ outro mês aparece contado à parte (`vra_anac.periodo`).
115
+ - Os horários são os do arquivo, em Brasília.
116
+
117
+ ## O que vem junto
118
+
119
+ As tabelas que levam o código ICAO do VRA ao que aparece no bilhete
120
+ (`GLO` → `G3`, `SBSP` → `CGH Congonhas`): companhias do cadastro da ANAC e
121
+ do diretório da IATA, e aeroportos da lista de aeródromos públicos da ANAC
122
+ e do OurAirports (domínio público). Cada linha cita a fonte e a data da
123
+ consulta (`t.companhias`, `t.aeroportos`). Código fora da tabela aparece
124
+ como veio no arquivo.
125
+
126
+ ## Como é testado
127
+
128
+ `python -m unittest`, do checkout, sem instalar nada. A amostra dos testes
129
+ tem linhas reais do VRA de julho/2026, com os bytes do arquivo da ANAC (BOM
130
+ e CRLF), e cada número esperado foi contado à mão. O CI roda do Python 3.11
131
+ ao 3.14 e no Windows, e instala o pacote num ambiente limpo antes de cada
132
+ versão.
133
+
134
+ ## Fontes e limites
135
+
136
+ - **Dados: ANAC, Voo Regular Ativo (VRA), licença
137
+ [CC-BY](https://opendefinition.org/licenses/cc-by/).** Quem publicar
138
+ resultado feito com esta biblioteca precisa dar o crédito à ANAC; a linha
139
+ de comando já escreve a atribuição na saída.
140
+ - Esta biblioteca **não é da ANAC** nem de companhia aérea. O resultado
141
+ mostra o que aconteceu no mês, não prevê o próximo voo.
142
+ - O VRA não traz o motivo do atraso (o campo de justificativa vem vazio).
143
+
144
+ ## Contribuir
145
+
146
+ Ver o [CONTRIBUTING.md](https://github.com/peterwkdev-creator/vra-anac/blob/main/CONTRIBUTING.md).
147
+ Resultado diferente da sua conta: abra uma issue pelo modelo "Resultado
148
+ diferente do esperado".
149
+
150
+ ## Licença
151
+
152
+ Código sob a [licença MIT](https://github.com/peterwkdev-creator/vra-anac/blob/main/LICENSE).
153
+ Os dados do VRA são da ANAC, sob CC-BY.
@@ -0,0 +1,134 @@
1
+ # vra-anac
2
+
3
+ [![testes](https://github.com/peterwkdev-creator/vra-anac/actions/workflows/testes.yml/badge.svg)](https://github.com/peterwkdev-creator/vra-anac/actions/workflows/testes.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/vra-anac?label=PyPI)](https://pypi.org/project/vra-anac/)
5
+ [![Python](https://img.shields.io/pypi/pyversions/vra-anac?label=Python)](https://pypi.org/project/vra-anac/)
6
+ [![licença: MIT](https://img.shields.io/badge/licen%C3%A7a-MIT-blue)](https://github.com/peterwkdev-creator/vra-anac/blob/main/LICENSE)
7
+
8
+ Quantos voos domésticos chegaram no horário, por voo, rota, companhia ou
9
+ aeroporto, a partir do arquivo mensal do **VRA (Voo Regular Ativo) da
10
+ ANAC**. Lê o CSV como a ANAC publica e aplica a mesma régua do
11
+ [chegounahora.com.br](https://chegounahora.com.br/): **no horário = chegada
12
+ até 15 min depois da prevista**.
13
+
14
+ Só a biblioteca padrão do Python, sem rede: você baixa o CSV, ela faz a conta.
15
+
16
+ ![O comando vra-anac lê o CSV de agosto/2026 da ANAC e mostra a pontualidade por companhia e por rota](https://raw.githubusercontent.com/peterwkdev-creator/vra-anac/main/docs/vitrine.gif)
17
+
18
+ ## Instalar
19
+
20
+ ```bash
21
+ pip install vra-anac
22
+ ```
23
+
24
+ Ou uma versão exata, pela tag do GitHub:
25
+
26
+ ```bash
27
+ pip install "git+https://github.com/peterwkdev-creator/vra-anac@v0.1.0"
28
+ ```
29
+
30
+ Python 3.11 ou mais novo.
31
+
32
+ ## Usar
33
+
34
+ ```python
35
+ import vra_anac
36
+
37
+ # o CSV do mês que você baixou da ANAC (aqui, a amostra dos testes)
38
+ atualizado, voos = vra_anac.ler_arquivo("tests/fixtures/vra-amostra.csv")
39
+ t = vra_anac.tabelas()
40
+
41
+ for chave, c in vra_anac.agrupar(voos, por="voo").items():
42
+ print(f"{t.rotulo(chave)}: {c.pct('chegada')}% no horário (n = {c.n})")
43
+ # G3 1015 SDU→CGH: 100% no horário (n = 1)
44
+ # G3 1016 CGH→SDU: 0% no horário (n = 2)
45
+ # ...
46
+ ```
47
+
48
+ Pela linha de comando, a tabela do mês (ou `--json`):
49
+
50
+ ```bash
51
+ vra-anac VRA_20268.csv --por companhia
52
+ ```
53
+
54
+ ```
55
+ VRA da ANAC, 08/2026 (arquivo atualizado em 2026-10-07): 68.355 voos domésticos regulares, 1 fora do mês.
56
+ No horário = chegada até 15 min depois da prevista. Só os grupos com 20 voos ou mais.
57
+
58
+ no horário cancelado voos companhia
59
+ 87% 1% 23.080 Azul (AZU)
60
+ 84% 2% 20.295 GOL (GLO)
61
+ 78% 1% 23.850 LATAM (TAM)
62
+ 76% 16% 1.098 Azul Conecta (ACN)
63
+ 0% 100% 24 Apuí Táxi Aéreo (CQB)
64
+
65
+ Dados: ANAC, Voo Regular Ativo (VRA), licença CC-BY. https://www.gov.br/anac/...
66
+ ```
67
+
68
+ `--por` aceita `voo` (padrão), `rota`, `companhia`, `origem` e `destino`;
69
+ `--minimo` (padrão 20) deixa de fora o grupo com poucos voos, que oscila por
70
+ acaso de um mês para o outro.
71
+
72
+ ## Onde baixar o CSV
73
+
74
+ Na página do [Voo Regular Ativo (VRA)](https://www.gov.br/anac/pt-br/acesso-a-informacao/dados-abertos/areas-de-atuacao/voos-e-operacoes-aereas/voo-regular-ativo-vra)
75
+ da ANAC, um arquivo por mês (`VRA_AAAAM.csv`, o mês sem zero). A ANAC
76
+ publica com cerca de 4 semanas de atraso.
77
+
78
+ ## O que a conta faz
79
+
80
+ - **Voo doméstico regular**: tipo de linha `N` e código de autorização `0`.
81
+ Extras, fretamentos e voos internacionais ficam de fora.
82
+ - **No horário**: chegada real até 15 min depois da prevista; adiantado
83
+ conta como no horário. A partida tem o mesmo critério, ao lado
84
+ (`c.pct("partida")`).
85
+ - **Faixas**: até 30 min, até 60 min, mais de 60 min e cancelado
86
+ (`c.distribuicao("chegada")`). O atraso é contado em segundos inteiros:
87
+ 30 min exatos ficam na faixa de até 30.
88
+ - **Cancelado** conta no total e nunca como no horário.
89
+ - **Porcentagem** arredondada com o meio para cima (12,5 → 13).
90
+ - **O voo** é empresa + número + origem + destino. O número sai sem o `Z`
91
+ que o VRA às vezes põe na frente (`Z1001` e `1001` são o mesmo voo
92
+ vendido). Isso é inferência, conferida em alguns voos contra o horário
93
+ publicado pela companhia, não regra escrita da ANAC.
94
+ - **O mês** sai das datas das linhas, não do nome do arquivo; o que cai em
95
+ outro mês aparece contado à parte (`vra_anac.periodo`).
96
+ - Os horários são os do arquivo, em Brasília.
97
+
98
+ ## O que vem junto
99
+
100
+ As tabelas que levam o código ICAO do VRA ao que aparece no bilhete
101
+ (`GLO` → `G3`, `SBSP` → `CGH Congonhas`): companhias do cadastro da ANAC e
102
+ do diretório da IATA, e aeroportos da lista de aeródromos públicos da ANAC
103
+ e do OurAirports (domínio público). Cada linha cita a fonte e a data da
104
+ consulta (`t.companhias`, `t.aeroportos`). Código fora da tabela aparece
105
+ como veio no arquivo.
106
+
107
+ ## Como é testado
108
+
109
+ `python -m unittest`, do checkout, sem instalar nada. A amostra dos testes
110
+ tem linhas reais do VRA de julho/2026, com os bytes do arquivo da ANAC (BOM
111
+ e CRLF), e cada número esperado foi contado à mão. O CI roda do Python 3.11
112
+ ao 3.14 e no Windows, e instala o pacote num ambiente limpo antes de cada
113
+ versão.
114
+
115
+ ## Fontes e limites
116
+
117
+ - **Dados: ANAC, Voo Regular Ativo (VRA), licença
118
+ [CC-BY](https://opendefinition.org/licenses/cc-by/).** Quem publicar
119
+ resultado feito com esta biblioteca precisa dar o crédito à ANAC; a linha
120
+ de comando já escreve a atribuição na saída.
121
+ - Esta biblioteca **não é da ANAC** nem de companhia aérea. O resultado
122
+ mostra o que aconteceu no mês, não prevê o próximo voo.
123
+ - O VRA não traz o motivo do atraso (o campo de justificativa vem vazio).
124
+
125
+ ## Contribuir
126
+
127
+ Ver o [CONTRIBUTING.md](https://github.com/peterwkdev-creator/vra-anac/blob/main/CONTRIBUTING.md).
128
+ Resultado diferente da sua conta: abra uma issue pelo modelo "Resultado
129
+ diferente do esperado".
130
+
131
+ ## Licença
132
+
133
+ Código sob a [licença MIT](https://github.com/peterwkdev-creator/vra-anac/blob/main/LICENSE).
134
+ Os dados do VRA são da ANAC, sob CC-BY.
@@ -0,0 +1,48 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "vra-anac"
7
+ # A versão tem uma fonte só: o __version__ do pacote (lido sem importar).
8
+ dynamic = ["version"]
9
+ # Uma frase só, até 100 caracteres (teto do catálogo MCP): a mesma no About
10
+ # do GitHub (gh repo edit --description) e no server.json, se houver.
11
+ description = "Pontualidade dos voos domésticos do Brasil pelo VRA da ANAC: lê o CSV do mês e calcula o indicador"
12
+ readme = "README.md"
13
+ license = "MIT"
14
+ license-files = ["LICENSE"]
15
+ authors = [{ name = "Peter" }]
16
+ requires-python = ">=3.11"
17
+ dependencies = []
18
+ keywords = ["anac", "vra", "aviacao", "voos", "pontualidade", "atrasos", "dados-abertos", "brasil"]
19
+ classifiers = [
20
+ "Programming Language :: Python :: 3",
21
+ "Natural Language :: Portuguese (Brazilian)",
22
+ "Topic :: Scientific/Engineering :: Information Analysis",
23
+ ]
24
+
25
+ # sem vitrine (Pages) na 0.1: a página inicial é o próprio repositório
26
+ # (nomes que o PyPI reconhece)
27
+ [project.urls]
28
+ Homepage = "https://github.com/peterwkdev-creator/vra-anac"
29
+ Source = "https://github.com/peterwkdev-creator/vra-anac"
30
+ Issues = "https://github.com/peterwkdev-creator/vra-anac/issues"
31
+ Changelog = "https://github.com/peterwkdev-creator/vra-anac/blob/main/CHANGELOG.md"
32
+
33
+ # o comando que o pip instala no PATH, além do `python -m vra_anac`
34
+ [project.scripts]
35
+ vra-anac = "vra_anac.__main__:main"
36
+
37
+ [tool.setuptools]
38
+ # a pasta das tabelas entra aqui também: sem isto, o setuptools avisa que a
39
+ # vê como pacote e não sabe se é para levar
40
+ packages = ["vra_anac", "vra_anac.dados"]
41
+
42
+ # as tabelas de companhias e aeroportos só vão na instalação se estiverem
43
+ # aqui; sem isto, FileNotFoundError fora do checkout.
44
+ [tool.setuptools.package-data]
45
+ vra_anac = ["dados/*.csv"]
46
+
47
+ [tool.setuptools.dynamic]
48
+ version = { attr = "vra_anac.__version__" }
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,109 @@
1
+ """A linha de comando: saída em texto e JSON, erros e ajuda em português."""
2
+ from __future__ import annotations
3
+
4
+ import contextlib
5
+ import io
6
+ import json
7
+ import re
8
+ import sys
9
+ import unittest
10
+ from pathlib import Path
11
+ from unittest import mock
12
+
13
+ from vra_anac.__main__ import ATRIBUICAO, _milhar, main
14
+
15
+ FIXTURE = str(Path(__file__).parent / "fixtures" / "vra-amostra.csv")
16
+ INGLES = re.compile(r"\b(usage|options|positional|arguments?|required|invalid|"
17
+ r"error|show this help|expected)\b", re.I)
18
+
19
+
20
+ def _rodar(*args):
21
+ saida, erro = io.StringIO(), io.StringIO()
22
+ with contextlib.redirect_stdout(saida), contextlib.redirect_stderr(erro):
23
+ try:
24
+ codigo = main(list(args))
25
+ except SystemExit as e:
26
+ codigo = e.code
27
+ return codigo, saida.getvalue(), erro.getvalue()
28
+
29
+
30
+ class Saida(unittest.TestCase):
31
+ def test_texto_por_voo(self):
32
+ codigo, saida, _ = _rodar(FIXTURE, "--minimo", "1")
33
+ self.assertEqual(codigo, 0)
34
+ self.assertIn("07/2026 (arquivo atualizado em 2026-08-28): 5 voos domésticos "
35
+ "regulares, 1 fora do mês.", saida)
36
+ self.assertIn("Só os grupos com 1 voo ou mais.", saida)
37
+ linhas = [l.split() for l in saida.splitlines() if "%" in l]
38
+ # o de 100% primeiro; depois, no empate em 0%, o de mais voos
39
+ self.assertEqual(linhas[0], ["100%", "0%", "1", "G3", "1015", "SDU→CGH"])
40
+ self.assertEqual(linhas[1], ["0%", "50%", "2", "G3", "1016", "CGH→SDU"])
41
+ self.assertTrue(saida.rstrip().endswith(ATRIBUICAO))
42
+
43
+ def test_milhar_com_ponto(self):
44
+ self.assertEqual([_milhar(n) for n in (5, 999, 1098, 68355, 1234567)],
45
+ ["5", "999", "1.098", "68.355", "1.234.567"])
46
+
47
+ def test_minimo_padrao_20_deixa_a_fixture_vazia(self):
48
+ codigo, saida, _ = _rodar(FIXTURE)
49
+ self.assertEqual(codigo, 0)
50
+ self.assertIn("Nenhum grupo com 20 voos ou mais.", saida)
51
+
52
+ def test_json(self):
53
+ codigo, saida, _ = _rodar(FIXTURE, "--por", "companhia", "--minimo", "1", "--json")
54
+ self.assertEqual(codigo, 0)
55
+ d = json.loads(saida)
56
+ self.assertEqual((d["mes"], d["voos"], d["fora_do_mes"]), ("2026-07", 5, 1))
57
+ self.assertIn("ANAC", d["atribuicao"])
58
+ glo = d["grupos"][0]
59
+ self.assertEqual((glo["rotulo"], glo["voos"], glo["chegada_no_horario_pct"],
60
+ glo["cancelado_pct"]), ("GOL (GLO)", 4, 25, 25))
61
+ self.assertEqual(glo["chegada"], {"no_horario": 25, "ate30": 0, "ate60": 50,
62
+ "acima60": 0, "cancelado": 25})
63
+
64
+
65
+ class Erros(unittest.TestCase):
66
+ def test_arquivo_ausente(self):
67
+ codigo, _, erro = _rodar("nao-existe.csv")
68
+ self.assertEqual(codigo, 1)
69
+ self.assertIn("erro: arquivo não encontrado: nao-existe.csv", erro)
70
+
71
+ def test_pasta(self):
72
+ codigo, _, erro = _rodar(str(Path(__file__).parent))
73
+ self.assertEqual(codigo, 1)
74
+ self.assertIn("é uma pasta", erro)
75
+
76
+ def test_arquivo_que_nao_e_o_vra(self):
77
+ codigo, _, erro = _rodar(__file__)
78
+ self.assertEqual(codigo, 1)
79
+ self.assertIn("1ª linha não é o metadado esperado", erro)
80
+
81
+ def test_erros_do_argparse_em_portugues(self):
82
+ for args in ([], [FIXTURE, "--minimo", "0"], [FIXTURE, "--por", "estado"],
83
+ [FIXTURE, "--xyz"]):
84
+ with self.subTest(args=args):
85
+ codigo, _, erro = _rodar(*args)
86
+ self.assertEqual(codigo, 2)
87
+ self.assertIn("uso:", erro)
88
+ self.assertIsNone(INGLES.search(erro), erro)
89
+
90
+
91
+ class Ajuda(unittest.TestCase):
92
+ def test_ajuda_em_portugues_com_atribuicao(self):
93
+ codigo, saida, _ = _rodar("--help")
94
+ self.assertEqual(codigo, 0)
95
+ self.assertTrue(saida.startswith("uso: "))
96
+ self.assertIsNone(INGLES.search(saida), saida)
97
+ self.assertIn("ANAC", saida)
98
+
99
+ def test_nome_do_comando_instalado(self):
100
+ with mock.patch.object(sys, "argv", ["/venv/bin/vra-anac"]):
101
+ _, saida, _ = _rodar("--help")
102
+ self.assertTrue(saida.startswith("uso: vra-anac "))
103
+ with mock.patch.object(sys, "argv", ["/x/vra_anac/__main__.py"]):
104
+ _, saida, _ = _rodar("--help")
105
+ self.assertTrue(saida.startswith("uso: python -m vra_anac "))
106
+
107
+
108
+ if __name__ == "__main__":
109
+ unittest.main()
@@ -0,0 +1,127 @@
1
+ """O indicador: no horário até 15 min pela chegada, faixas 30 e 60,
2
+ cancelado sobre o total. Cada número esperado foi contado à mão."""
3
+ from __future__ import annotations
4
+
5
+ import unittest
6
+ from datetime import datetime, timedelta
7
+ from pathlib import Path
8
+
9
+ import vra_anac
10
+ from vra_anac import indicador
11
+ from vra_anac.leitura import Operacao
12
+
13
+ FIXTURE = Path(__file__).parent / "fixtures" / "vra-amostra.csv"
14
+
15
+
16
+ def _op(chegada_min=0, partida_min=0, cancelado=False, numero="1000",
17
+ empresa="GLO", origem="SBSP", destino="SBRJ"):
18
+ """Voo inventado: atrasos em minutos sobre o previsto."""
19
+ pp = datetime(2026, 7, 1, 10, 0)
20
+ cp = pp + timedelta(hours=1)
21
+ pr = None if cancelado else pp + timedelta(minutes=partida_min)
22
+ cr = None if cancelado else cp + timedelta(minutes=chegada_min)
23
+ return Operacao(empresa, numero, origem, destino, pp, pr, cp, cr, cancelado)
24
+
25
+
26
+ class Porcento(unittest.TestCase):
27
+ def test_meio_para_cima(self):
28
+ self.assertEqual(indicador.porcento(1, 8), 13) # 12,5
29
+ self.assertEqual(indicador.porcento(1, 200), 1) # 0,5; round() daria 0
30
+ self.assertEqual(indicador.porcento(0, 3), 0)
31
+
32
+ def test_sobre_zero(self):
33
+ with self.assertRaises(ValueError):
34
+ indicador.porcento(0, 0)
35
+
36
+
37
+ class Limites(unittest.TestCase):
38
+ def test_15_min_exatos_e_no_horario_15_e_1_s_nao(self):
39
+ c = vra_anac.Contagem()
40
+ c.somar(_op(chegada_min=15))
41
+ op = _op()
42
+ c.somar(Operacao(op.empresa, op.numero, op.origem, op.destino, op.partida_prevista,
43
+ op.partida_real, op.chegada_prevista,
44
+ op.chegada_prevista + timedelta(minutes=15, seconds=1), False))
45
+ self.assertEqual(c.chegada_no_horario, 1)
46
+
47
+ def test_30_e_60_exatos_nao_passam_da_faixa(self):
48
+ c = vra_anac.Contagem()
49
+ for m in (30, 31, 60, 61):
50
+ c.somar(_op(chegada_min=m))
51
+ self.assertEqual(c.chegada_acima, {30: 3, 60: 1})
52
+
53
+ def test_adiantado_conta_no_horario(self):
54
+ c = vra_anac.Contagem()
55
+ c.somar(_op(chegada_min=-20, partida_min=-5))
56
+ self.assertEqual((c.chegada_no_horario, c.partida_no_horario), (1, 1))
57
+
58
+ def test_realizado_sem_horario_real_para(self):
59
+ op = _op()
60
+ sem = Operacao(op.empresa, op.numero, op.origem, op.destino, op.partida_prevista,
61
+ op.partida_real, op.chegada_prevista, None, False)
62
+ with self.assertRaisesRegex(ValueError, "sem horário real"):
63
+ vra_anac.Contagem().somar(sem)
64
+
65
+
66
+ class Contagem(unittest.TestCase):
67
+ def setUp(self):
68
+ self.c = vra_anac.Contagem()
69
+ # 8 voos: 4 no horário, 1 com 20 min, 1 com 45, 1 com 90, 1 cancelado
70
+ for m in (0, 5, -3, 15, 20, 45, 90):
71
+ self.c.somar(_op(chegada_min=m, partida_min=m + 10))
72
+ self.c.somar(_op(cancelado=True))
73
+
74
+ def test_percentuais(self):
75
+ self.assertEqual(self.c.n, 8)
76
+ self.assertEqual(self.c.pct("chegada"), 50)
77
+ self.assertEqual(self.c.pct("cancelado"), 13) # 12,5
78
+ self.assertEqual(self.c.pct("chegada>30"), 25) # 45 e 90
79
+ self.assertEqual(self.c.pct("chegada>60"), 13) # 90
80
+ # partida = chegada + 10: no horário 0, 5 e -3 (o 15 vira 25)
81
+ self.assertEqual(self.c.pct("partida"), 38) # 3/8 = 37,5
82
+
83
+ def test_distribuicao_soma_o_n(self):
84
+ fatias = self.c.distribuicao("chegada")
85
+ self.assertEqual([(f.chave, f.voos) for f in fatias],
86
+ [("no_horario", 4), ("ate30", 1), ("ate60", 1),
87
+ ("acima60", 1), ("cancelado", 1)])
88
+ self.assertEqual(sum(f.voos for f in fatias), self.c.n)
89
+ self.assertEqual(fatias[1].largura, "12.500")
90
+
91
+
92
+ class Agrupar(unittest.TestCase):
93
+ def setUp(self):
94
+ _, self.ops = vra_anac.ler_arquivo(FIXTURE)
95
+
96
+ def test_por_voo_na_fixture(self):
97
+ g = vra_anac.agrupar(self.ops, "voo")
98
+ self.assertEqual(len(g), 4)
99
+ c = g[("GLO", "1016", "SBSP", "SBRJ")]
100
+ # 23/07 chegou 56 min depois; 15/07 cancelado
101
+ self.assertEqual((c.n, c.pct("chegada"), c.pct("cancelado"), c.pct("chegada>30")),
102
+ (2, 0, 50, 50))
103
+ self.assertEqual(g[("GLO", "1015", "SBRJ", "SBSP")].pct("chegada"), 100)
104
+
105
+ def test_por_companhia_rota_origem_destino(self):
106
+ self.assertEqual({k: c.n for k, c in vra_anac.agrupar(self.ops, "companhia").items()},
107
+ {("GLO",): 4, ("AZU",): 1})
108
+ self.assertEqual(vra_anac.agrupar(self.ops, "companhia")[("GLO",)].pct("chegada"), 25)
109
+ self.assertEqual(vra_anac.agrupar(self.ops, "rota")[("SBSP", "SBRJ")].n, 2)
110
+ self.assertEqual(vra_anac.agrupar(self.ops, "origem")[("SBSP",)].n, 2)
111
+ self.assertEqual(vra_anac.agrupar(self.ops, "destino")[("SBRJ",)].n, 2)
112
+
113
+ def test_minimo(self):
114
+ self.assertEqual(list(vra_anac.agrupar(self.ops, "voo", minimo=2)),
115
+ [("GLO", "1016", "SBSP", "SBRJ")])
116
+
117
+ def test_numero_com_z_soma_no_mesmo_voo(self):
118
+ g = vra_anac.agrupar([_op(numero="1000"), _op(numero="Z1000")], "voo")
119
+ self.assertEqual({k: c.n for k, c in g.items()}, {("GLO", "1000", "SBSP", "SBRJ"): 2})
120
+
121
+ def test_agrupamento_desconhecido(self):
122
+ with self.assertRaisesRegex(ValueError, "agrupamento desconhecido"):
123
+ vra_anac.agrupar(self.ops, "estado")
124
+
125
+
126
+ if __name__ == "__main__":
127
+ unittest.main()