services.csv_writer

Módulo csv_writer.py

[PT-BR] Função utilitária para gravar listas de objetos Pokemon em arquivos CSV. O módulo aplica uma limpeza básica nos dados, removendo quebras de linha e espaços em excesso, além de pular linhas vazias ou inválidas.

Também registra no log a quantidade de registros exportados e quaisquer linhas ignoradas por estarem praticamente vazias.

[EN] Utility function to write lists of Pokemon objects to CSV files. The module applies basic data cleaning, removing line breaks and extra spaces, and skips empty or invalid rows.

It also logs the number of exported records and any skipped entries due to being nearly empty.

Uso típico / Typical usage: from services.csv_writer import write_pokemon_csv

written = write_pokemon_csv(pokemon_list, "output/pokemons.csv")
print(f"{written} Pokémon saved.")
 1"""
 2Módulo csv_writer.py
 3=====================
 4
 5[PT-BR]
 6Função utilitária para gravar listas de objetos ``Pokemon`` em arquivos CSV.
 7O módulo aplica uma limpeza básica nos dados, removendo quebras de linha e
 8espaços em excesso, além de pular linhas vazias ou inválidas.
 9
10Também registra no log a quantidade de registros exportados e quaisquer
11linhas ignoradas por estarem praticamente vazias.
12
13[EN]
14Utility function to write lists of ``Pokemon`` objects to CSV files.
15The module applies basic data cleaning, removing line breaks and
16extra spaces, and skips empty or invalid rows.
17
18It also logs the number of exported records and any skipped entries
19due to being nearly empty.
20
21Uso típico / Typical usage:
22    from services.csv_writer import write_pokemon_csv
23
24    written = write_pokemon_csv(pokemon_list, "output/pokemons.csv")
25    print(f"{written} Pokémon saved.")
26"""
27import csv
28import logging
29from typing import Iterable, Protocol, runtime_checkable
30from pathlib import Path
31from contextlib import suppress
32
33@runtime_checkable
34class HasToDict(Protocol):
35    def to_dict(self) -> dict[str, object]: ...
36
37def clean_csv_value(value: object) -> object:
38    """
39    [PT-BR] Limpa valores para escrita em CSV (quebras de linha, espaços).
40    [EN] Cleans values for CSV output (line breaks, extra spaces).
41    """
42    if isinstance(value, str):
43        return value.replace("\n", " ").strip()
44    return value
45
46def is_effectively_empty(row: dict[str, object]) -> bool:
47    """
48    [PT-BR] Verifica se a linha está vazia (ignora apenas None ou strings vazias).
49    [EN] Checks if the row is effectively empty (ignores None or empty strings).
50    """
51    return all(v in (None, "") for v in row.values())
52
53def write_pokemon_csv(pokemons: Iterable[HasToDict], path: str | Path, skip_empty: bool = True) -> int:
54    """
55    [PT-BR] Grava objetos `Pokemon` no CSV, com limpeza básica e validação de linhas.
56    [EN] Writes `Pokemon` objects to CSV, with basic cleaning and row validation.
57
58    Parâmetros:
59        pokemons (Iterable[HasToDict]): lista de objetos com .to_dict().
60        path (str | Path): caminho do arquivo de saída.
61        skip_empty (bool): se True, ignora linhas consideradas vazias.
62
63    Retorna:
64        int: quantidade de linhas efetivamente gravadas.
65    """
66    pokemons = list(pokemons)
67    if not pokemons:
68        logging.warning("Empty Pokémon list: nothing to write.")
69        return 0
70
71    try:
72        fieldnames = sorted({k for p in pokemons for k in p.to_dict().keys()})
73        written = 0
74
75        with open(path, "w", encoding="utf-8", newline="") as csvfile:
76            writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
77            writer.writeheader()
78
79            for p in pokemons:
80                row = {k: clean_csv_value(v) for k, v in p.to_dict().items()}
81
82                if skip_empty and is_effectively_empty(row):
83                    logging.warning("Row skipped—effectively empty: %s", row)
84                    continue
85
86                writer.writerow(row)
87                written += 1
88
89        logging.info("%d Pokémon exported to '%s'.", written, path)
90        return written
91
92    except (IOError, OSError) as e:
93        logging.error("Failed to write CSV file '%s': %s", path, str(e), exc_info=True)
94        return 0
@runtime_checkable
class HasToDict(typing.Protocol):
34@runtime_checkable
35class HasToDict(Protocol):
36    def to_dict(self) -> dict[str, object]: ...

Base class for protocol classes.

Protocol classes are defined as::

class Proto(Protocol):
    def meth(self) -> int:
        ...

Such classes are primarily used with static type checkers that recognize structural subtyping (static duck-typing), for example::

class C:
    def meth(self) -> int:
        return 0

def func(x: Proto) -> int:
    return x.meth()

func(C())  # Passes static type check

See PEP 544 for details. Protocol classes decorated with @typing.runtime_checkable act as simple-minded runtime protocols that check only the presence of given attributes, ignoring their type signatures. Protocol classes can be generic, they are defined as::

class GenProto(Protocol[T]):
    def meth(self) -> T:
        ...
HasToDict(*args, **kwargs)
1431def _no_init_or_replace_init(self, *args, **kwargs):
1432    cls = type(self)
1433
1434    if cls._is_protocol:
1435        raise TypeError('Protocols cannot be instantiated')
1436
1437    # Already using a custom `__init__`. No need to calculate correct
1438    # `__init__` to call. This can lead to RecursionError. See bpo-45121.
1439    if cls.__init__ is not _no_init_or_replace_init:
1440        return
1441
1442    # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`.
1443    # The first instantiation of the subclass will call `_no_init_or_replace_init` which
1444    # searches for a proper new `__init__` in the MRO. The new `__init__`
1445    # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent
1446    # instantiation of the protocol subclass will thus use the new
1447    # `__init__` and no longer call `_no_init_or_replace_init`.
1448    for base in cls.__mro__:
1449        init = base.__dict__.get('__init__', _no_init_or_replace_init)
1450        if init is not _no_init_or_replace_init:
1451            cls.__init__ = init
1452            break
1453    else:
1454        # should not happen
1455        cls.__init__ = object.__init__
1456
1457    cls.__init__(self, *args, **kwargs)
def to_dict(self) -> dict[str, object]:
36    def to_dict(self) -> dict[str, object]: ...
def clean_csv_value(value: object) -> object:
38def clean_csv_value(value: object) -> object:
39    """
40    [PT-BR] Limpa valores para escrita em CSV (quebras de linha, espaços).
41    [EN] Cleans values for CSV output (line breaks, extra spaces).
42    """
43    if isinstance(value, str):
44        return value.replace("\n", " ").strip()
45    return value

[PT-BR] Limpa valores para escrita em CSV (quebras de linha, espaços). [EN] Cleans values for CSV output (line breaks, extra spaces).

def is_effectively_empty(row: dict[str, object]) -> bool:
47def is_effectively_empty(row: dict[str, object]) -> bool:
48    """
49    [PT-BR] Verifica se a linha está vazia (ignora apenas None ou strings vazias).
50    [EN] Checks if the row is effectively empty (ignores None or empty strings).
51    """
52    return all(v in (None, "") for v in row.values())

[PT-BR] Verifica se a linha está vazia (ignora apenas None ou strings vazias). [EN] Checks if the row is effectively empty (ignores None or empty strings).

def write_pokemon_csv( pokemons: Iterable[HasToDict], path: str | pathlib.Path, skip_empty: bool = True) -> int:
54def write_pokemon_csv(pokemons: Iterable[HasToDict], path: str | Path, skip_empty: bool = True) -> int:
55    """
56    [PT-BR] Grava objetos `Pokemon` no CSV, com limpeza básica e validação de linhas.
57    [EN] Writes `Pokemon` objects to CSV, with basic cleaning and row validation.
58
59    Parâmetros:
60        pokemons (Iterable[HasToDict]): lista de objetos com .to_dict().
61        path (str | Path): caminho do arquivo de saída.
62        skip_empty (bool): se True, ignora linhas consideradas vazias.
63
64    Retorna:
65        int: quantidade de linhas efetivamente gravadas.
66    """
67    pokemons = list(pokemons)
68    if not pokemons:
69        logging.warning("Empty Pokémon list: nothing to write.")
70        return 0
71
72    try:
73        fieldnames = sorted({k for p in pokemons for k in p.to_dict().keys()})
74        written = 0
75
76        with open(path, "w", encoding="utf-8", newline="") as csvfile:
77            writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
78            writer.writeheader()
79
80            for p in pokemons:
81                row = {k: clean_csv_value(v) for k, v in p.to_dict().items()}
82
83                if skip_empty and is_effectively_empty(row):
84                    logging.warning("Row skipped—effectively empty: %s", row)
85                    continue
86
87                writer.writerow(row)
88                written += 1
89
90        logging.info("%d Pokémon exported to '%s'.", written, path)
91        return written
92
93    except (IOError, OSError) as e:
94        logging.error("Failed to write CSV file '%s': %s", path, str(e), exc_info=True)
95        return 0

[PT-BR] Grava objetos Pokemon no CSV, com limpeza básica e validação de linhas. [EN] Writes Pokemon objects to CSV, with basic cleaning and row validation.

Parâmetros: pokemons (Iterable[HasToDict]): lista de objetos com .to_dict(). path (str | Path): caminho do arquivo de saída. skip_empty (bool): se True, ignora linhas consideradas vazias.

Retorna: int: quantidade de linhas efetivamente gravadas.