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.
- csv_mean_filter-0.1.0/.gitignore +20 -0
- csv_mean_filter-0.1.0/LICENSE +21 -0
- csv_mean_filter-0.1.0/PKG-INFO +137 -0
- csv_mean_filter-0.1.0/README.md +125 -0
- csv_mean_filter-0.1.0/pyproject.toml +26 -0
- csv_mean_filter-0.1.0/requirements.txt +2 -0
- csv_mean_filter-0.1.0/src/analise.py +90 -0
- csv_mean_filter-0.1.0/tests/test_analise.py +267 -0
|
@@ -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,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()
|