csv-mean-filter 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.
@@ -0,0 +1,20 @@
1
+ # Ruflo local secrets and runtime data
2
+ .env
3
+ .env.local
4
+ .env.*.local
5
+ .claude-flow/data/
6
+ .claude-flow/logs/
7
+ .claude-flow/sessions/
8
+
9
+ # Python
10
+ __pycache__/
11
+ *.pyc
12
+ .venv/
13
+
14
+ # Ruflo / Claude Code (ferramentas locais)
15
+ .claude/
16
+ .claude-flow/
17
+ .swarm/
18
+ .mcp.json
19
+ CLAUDE.md
20
+ iniciar_ruflo.bat
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 jjuffagtotal-source
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,137 @@
1
+ Metadata-Version: 2.5
2
+ Name: csv-mean-filter
3
+ Version: 0.1.0
4
+ Summary: CLI em Python para calcular a média de colunas CSV com filtros dinâmicos, zero dependências e 36 testes unitários.
5
+ Author: jjuffagtotal-source
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Programming Language :: Python :: 3
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+
13
+ # analise
14
+
15
+ Script de linha de comando em Python que lê um arquivo CSV e calcula a média de uma coluna numérica, com filtro opcional por valor de outra coluna.
16
+
17
+ ## Funcionalidades
18
+
19
+ - Calcula a média de qualquer coluna numérica de um CSV com cabeçalho.
20
+ - Filtro opcional `--filtrar-coluna COLUNA=VALOR` para considerar só as linhas que batem com o valor.
21
+ - Ignora células vazias.
22
+ - Lê CSVs gravados pelo Excel (UTF-8 com BOM).
23
+ - Mensagens de erro claras e códigos de saída previsíveis.
24
+ - Usa apenas a biblioteca padrão: nenhuma dependência externa.
25
+
26
+ ## Requisitos
27
+
28
+ - Python 3.10 ou superior.
29
+
30
+ ## Instalação
31
+
32
+ Não há pacotes para instalar. Basta clonar o repositório:
33
+
34
+ ```bash
35
+ git clone <url-do-repositorio>
36
+ cd <pasta-do-repositorio>
37
+ ```
38
+
39
+ ## Instalação via pip
40
+
41
+ O projeto também pode ser instalado como pacote, o que disponibiliza o comando `csv-mean` no terminal. Na raiz do repositório:
42
+
43
+ ```bash
44
+ pip install .
45
+ ```
46
+
47
+ Futuramente, quando o pacote estiver publicado no PyPI:
48
+
49
+ ```bash
50
+ pip install csv-mean-filter
51
+ ```
52
+
53
+ Depois de instalado, use `csv-mean` no lugar de `python src/analise.py`:
54
+
55
+ ```bash
56
+ $ csv-mean dados.csv salario --filtrar-coluna idade=30
57
+ 1500.0
58
+ ```
59
+
60
+ Recomenda-se instalar dentro de um ambiente virtual (`python -m venv .venv`).
61
+
62
+ ## Uso
63
+
64
+ ```bash
65
+ python src/analise.py ARQUIVO COLUNA [--filtrar-coluna COLUNA=VALOR]
66
+ ```
67
+
68
+ ### Exemplos
69
+
70
+ Dado o arquivo `dados.csv`:
71
+
72
+ ```csv
73
+ nome,idade,salario
74
+ Ana,30,1000
75
+ Bia,25,3000
76
+ Caio,30,2000
77
+ ```
78
+
79
+ Média de uma coluna:
80
+
81
+ ```bash
82
+ $ python src/analise.py dados.csv salario
83
+ 2000.0
84
+ ```
85
+
86
+ Média só das linhas em que `idade` é `30`:
87
+
88
+ ```bash
89
+ $ python src/analise.py dados.csv salario --filtrar-coluna idade=30
90
+ 1500.0
91
+ ```
92
+
93
+ ### Regras do filtro
94
+
95
+ - O formato é `coluna=valor`. A divisão acontece no primeiro `=`, então `obs=a=b` filtra a coluna `obs` pelo valor `a=b`.
96
+ - A comparação é exata e diferencia maiúsculas de minúsculas, após remover espaços nas pontas. `30` não bate com `30.0`.
97
+ - `coluna=` (valor vazio) seleciona as linhas em que a coluna está vazia.
98
+ - Valores não numéricos em linhas que não batem com o filtro são ignorados.
99
+
100
+ ### Códigos de saída
101
+
102
+ | Código | Situação |
103
+ |--------|----------|
104
+ | `0` | Sucesso: a média é impressa na saída padrão. |
105
+ | `1` | Arquivo inexistente, coluna inexistente, valor não numérico ou nenhum valor numérico (mensagem `Erro: ...` na saída de erro). |
106
+ | `2` | Argumentos inválidos, como um filtro sem `=` (`use o formato coluna=valor`). |
107
+
108
+ ### Uso como módulo
109
+
110
+ Com a pasta `src/` no `PYTHONPATH` (ou executando a partir dela):
111
+
112
+ ```python
113
+ from analise import calcular_media, interpretar_filtro
114
+
115
+ calcular_media("dados.csv", "salario") # 2000.0
116
+ calcular_media("dados.csv", "salario", ("idade", "30")) # 1500.0
117
+ calcular_media("dados.csv", "salario", interpretar_filtro("idade=30"))
118
+ ```
119
+
120
+ ## Testes
121
+
122
+ Os testes usam apenas o `unittest` da biblioteca padrão. Na raiz do projeto:
123
+
124
+ ```bash
125
+ python -m unittest discover -s tests -v
126
+ ```
127
+
128
+ ## Estrutura
129
+
130
+ ```
131
+ src/analise.py # script e funções
132
+ tests/test_analise.py # testes unitários
133
+ ```
134
+
135
+ ## Licença
136
+
137
+ Distribuído sob a licença MIT. Veja o arquivo [LICENSE](LICENSE).
@@ -0,0 +1,125 @@
1
+ # analise
2
+
3
+ Script de linha de comando em Python que lê um arquivo CSV e calcula a média de uma coluna numérica, com filtro opcional por valor de outra coluna.
4
+
5
+ ## Funcionalidades
6
+
7
+ - Calcula a média de qualquer coluna numérica de um CSV com cabeçalho.
8
+ - Filtro opcional `--filtrar-coluna COLUNA=VALOR` para considerar só as linhas que batem com o valor.
9
+ - Ignora células vazias.
10
+ - Lê CSVs gravados pelo Excel (UTF-8 com BOM).
11
+ - Mensagens de erro claras e códigos de saída previsíveis.
12
+ - Usa apenas a biblioteca padrão: nenhuma dependência externa.
13
+
14
+ ## Requisitos
15
+
16
+ - Python 3.10 ou superior.
17
+
18
+ ## Instalação
19
+
20
+ Não há pacotes para instalar. Basta clonar o repositório:
21
+
22
+ ```bash
23
+ git clone <url-do-repositorio>
24
+ cd <pasta-do-repositorio>
25
+ ```
26
+
27
+ ## Instalação via pip
28
+
29
+ O projeto também pode ser instalado como pacote, o que disponibiliza o comando `csv-mean` no terminal. Na raiz do repositório:
30
+
31
+ ```bash
32
+ pip install .
33
+ ```
34
+
35
+ Futuramente, quando o pacote estiver publicado no PyPI:
36
+
37
+ ```bash
38
+ pip install csv-mean-filter
39
+ ```
40
+
41
+ Depois de instalado, use `csv-mean` no lugar de `python src/analise.py`:
42
+
43
+ ```bash
44
+ $ csv-mean dados.csv salario --filtrar-coluna idade=30
45
+ 1500.0
46
+ ```
47
+
48
+ Recomenda-se instalar dentro de um ambiente virtual (`python -m venv .venv`).
49
+
50
+ ## Uso
51
+
52
+ ```bash
53
+ python src/analise.py ARQUIVO COLUNA [--filtrar-coluna COLUNA=VALOR]
54
+ ```
55
+
56
+ ### Exemplos
57
+
58
+ Dado o arquivo `dados.csv`:
59
+
60
+ ```csv
61
+ nome,idade,salario
62
+ Ana,30,1000
63
+ Bia,25,3000
64
+ Caio,30,2000
65
+ ```
66
+
67
+ Média de uma coluna:
68
+
69
+ ```bash
70
+ $ python src/analise.py dados.csv salario
71
+ 2000.0
72
+ ```
73
+
74
+ Média só das linhas em que `idade` é `30`:
75
+
76
+ ```bash
77
+ $ python src/analise.py dados.csv salario --filtrar-coluna idade=30
78
+ 1500.0
79
+ ```
80
+
81
+ ### Regras do filtro
82
+
83
+ - O formato é `coluna=valor`. A divisão acontece no primeiro `=`, então `obs=a=b` filtra a coluna `obs` pelo valor `a=b`.
84
+ - A comparação é exata e diferencia maiúsculas de minúsculas, após remover espaços nas pontas. `30` não bate com `30.0`.
85
+ - `coluna=` (valor vazio) seleciona as linhas em que a coluna está vazia.
86
+ - Valores não numéricos em linhas que não batem com o filtro são ignorados.
87
+
88
+ ### Códigos de saída
89
+
90
+ | Código | Situação |
91
+ |--------|----------|
92
+ | `0` | Sucesso: a média é impressa na saída padrão. |
93
+ | `1` | Arquivo inexistente, coluna inexistente, valor não numérico ou nenhum valor numérico (mensagem `Erro: ...` na saída de erro). |
94
+ | `2` | Argumentos inválidos, como um filtro sem `=` (`use o formato coluna=valor`). |
95
+
96
+ ### Uso como módulo
97
+
98
+ Com a pasta `src/` no `PYTHONPATH` (ou executando a partir dela):
99
+
100
+ ```python
101
+ from analise import calcular_media, interpretar_filtro
102
+
103
+ calcular_media("dados.csv", "salario") # 2000.0
104
+ calcular_media("dados.csv", "salario", ("idade", "30")) # 1500.0
105
+ calcular_media("dados.csv", "salario", interpretar_filtro("idade=30"))
106
+ ```
107
+
108
+ ## Testes
109
+
110
+ Os testes usam apenas o `unittest` da biblioteca padrão. Na raiz do projeto:
111
+
112
+ ```bash
113
+ python -m unittest discover -s tests -v
114
+ ```
115
+
116
+ ## Estrutura
117
+
118
+ ```
119
+ src/analise.py # script e funções
120
+ tests/test_analise.py # testes unitários
121
+ ```
122
+
123
+ ## Licença
124
+
125
+ Distribuído sob a licença MIT. Veja o arquivo [LICENSE](LICENSE).
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "csv-mean-filter"
7
+ version = "0.1.0"
8
+ description = "CLI em Python para calcular a média de colunas CSV com filtros dinâmicos, zero dependências e 36 testes unitários."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [
14
+ {name = "jjuffagtotal-source"}
15
+ ]
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3",
18
+ "Operating System :: OS Independent",
19
+ ]
20
+
21
+ [project.scripts]
22
+ csv-mean = "analise:main"
23
+
24
+ [tool.hatch.build.targets.wheel]
25
+ only-include = ["src/analise.py"]
26
+ sources = ["src"]
@@ -0,0 +1,2 @@
1
+ # Este projeto usa apenas a biblioteca padrão do Python 3.10+.
2
+ # Não há dependências externas para instalar.
@@ -0,0 +1,90 @@
1
+ """Calcula a média de uma coluna numérica de um arquivo CSV."""
2
+
3
+ import argparse
4
+ import csv
5
+ import os
6
+ import sys
7
+
8
+
9
+ def interpretar_filtro(texto: str) -> tuple[str, str]:
10
+ """Converte "coluna=valor" em (coluna, valor), dividindo no primeiro "="."""
11
+ if "=" not in texto:
12
+ raise ValueError(f"Filtro sem '=': {texto!r}")
13
+ nome, valor = (parte.strip() for parte in texto.split("=", 1))
14
+ if not nome:
15
+ raise ValueError(f"Filtro sem nome de coluna: {texto!r}")
16
+ return nome, valor
17
+
18
+
19
+ def _filtro_argparse(texto: str) -> tuple[str, str]:
20
+ """Adapta interpretar_filtro para o type= do argparse."""
21
+ try:
22
+ return interpretar_filtro(texto)
23
+ except ValueError:
24
+ raise argparse.ArgumentTypeError("use o formato coluna=valor") from None
25
+
26
+
27
+ def calcular_media(
28
+ caminho_csv: str, coluna: str, filtro: tuple[str, str] | None = None
29
+ ) -> float:
30
+ """Retorna a média dos valores numéricos da coluna informada.
31
+
32
+ Células vazias são ignoradas. Com filtro (coluna, valor), considera só as
33
+ linhas cuja coluna do filtro é igual ao valor (após strip). Levanta
34
+ FileNotFoundError se o arquivo não existir e ValueError para coluna
35
+ ausente, valor não numérico ou ausência de valores numéricos.
36
+ """
37
+ if not os.path.isfile(caminho_csv):
38
+ raise FileNotFoundError(f"Arquivo não encontrado: {caminho_csv}")
39
+
40
+ with open(caminho_csv, encoding="utf-8-sig", newline="") as arquivo:
41
+ leitor = csv.DictReader(arquivo)
42
+ cabecalho = leitor.fieldnames or []
43
+ for nome in [coluna] + ([filtro[0]] if filtro else []):
44
+ if nome not in cabecalho:
45
+ raise ValueError(f"Coluna '{nome}' não encontrada no cabeçalho")
46
+
47
+ soma = 0.0
48
+ quantidade = 0
49
+ for linha in leitor:
50
+ if filtro and (linha.get(filtro[0]) or "").strip() != filtro[1].strip():
51
+ continue
52
+ bruto = (linha.get(coluna) or "").strip()
53
+ if not bruto:
54
+ continue
55
+ try:
56
+ soma += float(bruto)
57
+ except ValueError:
58
+ raise ValueError(
59
+ f"Valor não numérico '{bruto}' na linha {leitor.line_num}"
60
+ ) from None
61
+ quantidade += 1
62
+
63
+ if quantidade == 0:
64
+ sufixo = f" com o filtro {filtro[0]}={filtro[1]}" if filtro else ""
65
+ raise ValueError(f"Nenhum valor numérico na coluna '{coluna}'{sufixo}")
66
+ return soma / quantidade
67
+
68
+
69
+ def main(argv=None) -> int:
70
+ """Ponto de entrada da CLI; retorna o código de saída."""
71
+ parser = argparse.ArgumentParser(description="Média de uma coluna de um CSV.")
72
+ parser.add_argument("arquivo", help="caminho do arquivo CSV")
73
+ parser.add_argument("coluna", help="nome da coluna")
74
+ parser.add_argument(
75
+ "--filtrar-coluna", type=_filtro_argparse, metavar="COLUNA=VALOR",
76
+ help="considera só as linhas em que COLUNA é igual a VALOR",
77
+ )
78
+ args = parser.parse_args(argv)
79
+
80
+ try:
81
+ media = calcular_media(args.arquivo, args.coluna, args.filtrar_coluna)
82
+ except (FileNotFoundError, ValueError) as erro:
83
+ print(f"Erro: {erro}", file=sys.stderr)
84
+ return 1
85
+ print(media)
86
+ return 0
87
+
88
+
89
+ if __name__ == "__main__":
90
+ sys.exit(main())
@@ -0,0 +1,267 @@
1
+ import contextlib
2
+ import io
3
+ import os
4
+ import sys
5
+ import tempfile
6
+ import unittest
7
+
8
+ sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "src"))
9
+
10
+ import analise # noqa: E402
11
+
12
+
13
+ class BaseCSVTest(unittest.TestCase):
14
+ def setUp(self):
15
+ self._tmp = tempfile.TemporaryDirectory()
16
+ self.addCleanup(self._tmp.cleanup)
17
+
18
+ def criar_csv(self, conteudo, nome="dados.csv", encoding="utf-8"):
19
+ caminho = os.path.join(self._tmp.name, nome)
20
+ with open(caminho, "w", encoding=encoding, newline="") as f:
21
+ f.write(conteudo)
22
+ return caminho
23
+
24
+
25
+ class TestCalcularMedia(BaseCSVTest):
26
+ def test_media_inteiros(self):
27
+ caminho = self.criar_csv("nome,valor\na,10\nb,20\nc,30\n")
28
+ self.assertEqual(analise.calcular_media(caminho, "valor"), 20.0)
29
+
30
+ def test_media_floats(self):
31
+ caminho = self.criar_csv("valor\n1.5\n2.5\n3.5\n")
32
+ self.assertAlmostEqual(analise.calcular_media(caminho, "valor"), 2.5)
33
+
34
+ def test_celulas_em_branco_ignoradas(self):
35
+ caminho = self.criar_csv("nome,valor\na,10\nb,\nc, \nd,20\n")
36
+ self.assertEqual(analise.calcular_media(caminho, "valor"), 15.0)
37
+
38
+ def test_linha_curta_ignorada(self):
39
+ caminho = self.criar_csv("nome,valor\na,10\nb\nc,20\n")
40
+ self.assertEqual(analise.calcular_media(caminho, "valor"), 15.0)
41
+
42
+ def test_arquivo_inexistente(self):
43
+ caminho = os.path.join(self._tmp.name, "nao_existe.csv")
44
+ with self.assertRaises(FileNotFoundError):
45
+ analise.calcular_media(caminho, "valor")
46
+
47
+ def test_coluna_desconhecida(self):
48
+ caminho = self.criar_csv("nome,valor\na,10\n")
49
+ with self.assertRaises(ValueError) as ctx:
50
+ analise.calcular_media(caminho, "preco")
51
+ self.assertIn("preco", str(ctx.exception))
52
+
53
+ def test_arquivo_vazio(self):
54
+ caminho = self.criar_csv("")
55
+ with self.assertRaises(ValueError):
56
+ analise.calcular_media(caminho, "valor")
57
+
58
+ def test_valor_nao_numerico(self):
59
+ caminho = self.criar_csv("nome,valor\na,10\nb,abc\n")
60
+ with self.assertRaises(ValueError) as ctx:
61
+ analise.calcular_media(caminho, "valor")
62
+ self.assertIn("abc", str(ctx.exception))
63
+ self.assertIn("3", str(ctx.exception))
64
+
65
+ def test_coluna_sem_valores_numericos(self):
66
+ caminho = self.criar_csv("nome,valor\na,\nb, \n")
67
+ with self.assertRaises(ValueError) as ctx:
68
+ analise.calcular_media(caminho, "valor")
69
+ self.assertIn("valor", str(ctx.exception))
70
+
71
+
72
+ class TestMain(BaseCSVTest):
73
+ def executar(self, argv):
74
+ out, err = io.StringIO(), io.StringIO()
75
+ with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
76
+ codigo = analise.main(argv)
77
+ return codigo, out.getvalue(), err.getvalue()
78
+
79
+ def test_sucesso_imprime_media(self):
80
+ caminho = self.criar_csv("valor\n10\n\n20\n")
81
+ codigo, out, err = self.executar([caminho, "valor"])
82
+ self.assertEqual(codigo, 0)
83
+ self.assertEqual(out.strip(), "15.0")
84
+ self.assertEqual(err, "")
85
+
86
+ def test_erro_arquivo_inexistente(self):
87
+ caminho = os.path.join(self._tmp.name, "nao_existe.csv")
88
+ codigo, out, err = self.executar([caminho, "valor"])
89
+ self.assertEqual(codigo, 1)
90
+ self.assertEqual(out, "")
91
+ self.assertTrue(err.startswith("Erro:"))
92
+
93
+ def test_erro_coluna_desconhecida(self):
94
+ caminho = self.criar_csv("valor\n1\n")
95
+ codigo, out, err = self.executar([caminho, "outra"])
96
+ self.assertEqual(codigo, 1)
97
+ self.assertIn("Erro:", err)
98
+
99
+ def test_erro_valor_nao_numerico(self):
100
+ caminho = self.criar_csv("valor\nx\n")
101
+ codigo, _, err = self.executar([caminho, "valor"])
102
+ self.assertEqual(codigo, 1)
103
+ self.assertIn("Erro:", err)
104
+
105
+
106
+ CSV_FILTRO = (
107
+ "nome,idade,salario,cidade\n"
108
+ "a,30,1000,SP\n"
109
+ "b, 30 ,2000,\n"
110
+ "c,40,xx,RJ\n"
111
+ "d,30.0,9999,RJ\n"
112
+ "e,50,,X\n"
113
+ )
114
+
115
+
116
+ class TestInterpretarFiltro(unittest.TestCase):
117
+ def test_simples(self):
118
+ self.assertEqual(analise.interpretar_filtro("idade=30"), ("idade", "30"))
119
+
120
+ def test_divide_no_primeiro_igual(self):
121
+ self.assertEqual(analise.interpretar_filtro("obs=a=b"), ("obs", "a=b"))
122
+
123
+ def test_remove_espacos(self):
124
+ self.assertEqual(analise.interpretar_filtro(" x = y "), ("x", "y"))
125
+
126
+ def test_valor_vazio(self):
127
+ self.assertEqual(analise.interpretar_filtro("cidade="), ("cidade", ""))
128
+
129
+ def test_sem_igual(self):
130
+ with self.assertRaises(ValueError):
131
+ analise.interpretar_filtro("idade30")
132
+
133
+ def test_nome_coluna_vazio(self):
134
+ with self.assertRaises(ValueError):
135
+ analise.interpretar_filtro("=1")
136
+ with self.assertRaises(ValueError):
137
+ analise.interpretar_filtro(" =1")
138
+
139
+
140
+ class TestCalcularMediaComFiltro(BaseCSVTest):
141
+ def setUp(self):
142
+ super().setUp()
143
+ self.caminho = self.criar_csv(CSV_FILTRO)
144
+
145
+ def test_media_apenas_linhas_correspondentes(self):
146
+ # " 30 " casa com "30"; "30.0" nao casa; "xx" da linha c e ignorado
147
+ self.assertEqual(
148
+ analise.calcular_media(self.caminho, "salario", ("idade", "30")), 1500.0
149
+ )
150
+
151
+ def test_comparacao_exata_e_sensivel_a_maiusculas(self):
152
+ caminho = self.criar_csv("cidade,valor\nSP,10\nsp,100\nSP ,20\n")
153
+ self.assertEqual(analise.calcular_media(caminho, "valor", ("cidade", "SP")), 15.0)
154
+ self.assertEqual(analise.calcular_media(caminho, "valor", ("cidade", "sp")), 100.0)
155
+
156
+ def test_30_nao_casa_com_30_ponto_0(self):
157
+ self.assertEqual(
158
+ analise.calcular_media(self.caminho, "salario", ("idade", "30.0")), 9999.0
159
+ )
160
+
161
+ def test_valor_vazio_casa_celulas_vazias(self):
162
+ self.assertEqual(
163
+ analise.calcular_media(self.caminho, "salario", ("cidade", "")), 2000.0
164
+ )
165
+
166
+ def test_filtro_none_igual_ao_anterior(self):
167
+ caminho = self.criar_csv("nome,valor\na,10\nb,\nc,20\n")
168
+ self.assertEqual(
169
+ analise.calcular_media(caminho, "valor", None),
170
+ analise.calcular_media(caminho, "valor"),
171
+ )
172
+ self.assertEqual(analise.calcular_media(caminho, "valor", None), 15.0)
173
+
174
+ def test_filtro_none_valor_invalido_ainda_falha(self):
175
+ with self.assertRaises(ValueError):
176
+ analise.calcular_media(self.caminho, "salario", None)
177
+
178
+ def test_coluna_filtro_inexistente(self):
179
+ with self.assertRaises(ValueError) as ctx:
180
+ analise.calcular_media(self.caminho, "salario", ("nada", "1"))
181
+ self.assertIn("nada", str(ctx.exception))
182
+
183
+ def test_nenhuma_linha_corresponde(self):
184
+ with self.assertRaises(ValueError) as ctx:
185
+ analise.calcular_media(self.caminho, "salario", ("idade", "99"))
186
+ self.assertIn("idade=99", str(ctx.exception))
187
+
188
+ def test_correspondentes_somente_vazios(self):
189
+ with self.assertRaises(ValueError):
190
+ analise.calcular_media(self.caminho, "salario", ("idade", "50"))
191
+
192
+ def test_nao_numerico_em_linha_correspondente_falha(self):
193
+ with self.assertRaises(ValueError) as ctx:
194
+ analise.calcular_media(self.caminho, "salario", ("idade", "40"))
195
+ self.assertIn("xx", str(ctx.exception))
196
+ self.assertIn("4", str(ctx.exception))
197
+
198
+
199
+ class TestMainComFiltro(BaseCSVTest):
200
+ executar = TestMain.executar
201
+
202
+ def test_filtro_sucesso(self):
203
+ caminho = self.criar_csv(CSV_FILTRO)
204
+ codigo, out, err = self.executar(
205
+ [caminho, "salario", "--filtrar-coluna", "idade=30"]
206
+ )
207
+ self.assertEqual(codigo, 0)
208
+ self.assertEqual(out.strip(), "1500.0")
209
+ self.assertEqual(err, "")
210
+
211
+ def test_filtro_coluna_inexistente(self):
212
+ caminho = self.criar_csv(CSV_FILTRO)
213
+ codigo, out, err = self.executar(
214
+ [caminho, "salario", "--filtrar-coluna", "nada=1"]
215
+ )
216
+ self.assertEqual(codigo, 1)
217
+ self.assertEqual(out, "")
218
+ self.assertIn("Erro:", err)
219
+
220
+ def test_filtro_mal_formatado_sai_com_2(self):
221
+ caminho = self.criar_csv(CSV_FILTRO)
222
+ for texto in ("idade30", "=1"):
223
+ with self.subTest(texto=texto):
224
+ err = io.StringIO()
225
+ with contextlib.redirect_stderr(err), self.assertRaises(SystemExit) as ctx:
226
+ analise.main([caminho, "salario", "--filtrar-coluna", texto])
227
+ self.assertEqual(ctx.exception.code, 2)
228
+ self.assertIn("--filtrar-coluna", err.getvalue())
229
+
230
+
231
+ class TestMensagemFiltroInvalido(BaseCSVTest):
232
+ def test_mensagem_formato_coluna_valor(self):
233
+ caminho = self.criar_csv(CSV_FILTRO)
234
+ err = io.StringIO()
235
+ with contextlib.redirect_stderr(err), self.assertRaises(SystemExit) as ctx:
236
+ analise.main([caminho, "salario", "--filtrar-coluna", "idade30"])
237
+ self.assertEqual(ctx.exception.code, 2)
238
+ self.assertIn(
239
+ "argument --filtrar-coluna: use o formato coluna=valor", err.getvalue()
240
+ )
241
+
242
+
243
+ class TestArquivoComBOM(BaseCSVTest):
244
+ def setUp(self):
245
+ super().setUp()
246
+ self.caminho = self.criar_csv(
247
+ "idade,salario\n30,1000\n40,2000\n30,3000\n", encoding="utf-8-sig"
248
+ )
249
+ with open(self.caminho, "rb") as f:
250
+ self.assertEqual(f.read(3), b"\xef\xbb\xbf")
251
+
252
+ def test_media_primeira_coluna_sem_filtro(self):
253
+ self.assertAlmostEqual(analise.calcular_media(self.caminho, "idade"), 100 / 3)
254
+
255
+ def test_media_primeira_coluna_com_filtro_na_primeira_coluna(self):
256
+ self.assertEqual(
257
+ analise.calcular_media(self.caminho, "idade", ("idade", "30")), 30.0
258
+ )
259
+
260
+ def test_filtro_na_primeira_coluna_media_de_outra(self):
261
+ self.assertEqual(
262
+ analise.calcular_media(self.caminho, "salario", ("idade", "30")), 2000.0
263
+ )
264
+
265
+
266
+ if __name__ == "__main__":
267
+ unittest.main()