Arquitetura¶
Documentação completa da arquitetura do Global-Data-Finance, padrões de design e estrutura do projeto.
Visão Geral¶
Global-Data-Finance é uma biblioteca Python distribuída via PyPI cuja API pública é deliberadamente estreita — exportando na raiz as classes FundamentalStocksDataCVM e HistoricalQuotesB3, além do contrato ExtractionResultB3. Internamente, cada fonte de dados é implementada em um diretório próprio com layout plano de módulos nomeados por papel.
Esta abordagem privilegia:
- ✅ Leitura: poucos arquivos com nomes claros (CVM:
core.py,client.py,http.py,extract.py,errors.py; B3: módulos por papel —client.py,assets.py,models.py,years.py,processing.py,filesystem.py,errors.py, mais subpacotes pesados) - ✅ Manutenibilidade: estrutura limpa, concisa e orientada de forma pragmática às funcionalidades dos módulos
- ✅ Extensibilidade: adicionar nova fonte = adicionar nova pasta-irmã com o mesmo conjunto plano de módulos
- ✅ Testabilidade: uso de duck typing direto e injeção de dependência para isolar e testar componentes de forma simples
Estrutura do Projeto¶
globaldatafinance/
├── src/globaldatafinance/
│ ├── __init__.py # re-exporta a API pública
│ ├── application/ # FACADE PÚBLICO (top-level)
│ │ ├── b3_docs/
│ │ │ ├── historical_quotes.py # HistoricalQuotesB3
│ │ │ ├── extraction_result_formatter.py
│ │ │ └── result_formatters/
│ │ └── cvm_docs/
│ │ ├── fundamental_stocks_data.py # FundamentalStocksDataCVM
│ │ └── download_result_formatter.py
│ ├── brazil/ # IMPLEMENTAÇÕES POR FONTE
│ │ ├── b3_data/
│ │ │ └── historical_quotes/ # módulos por papel + subpacotes pesados
│ │ │ ├── client.py # ExtractHistoricalQuotesUseCaseB3 + funções de orquestração
│ │ │ ├── assets.py # AvailableAssetsServiceB3 (validação + TPMERC mapping)
│ │ │ ├── models.py # DocsToExtractorB3 (dataclass)
│ │ │ ├── years.py # YearRangeB3 (value object + validators)
│ │ │ ├── processing.py # ProcessingModeEnumB3, _ProcessingModeConfig
│ │ │ ├── filesystem.py # FileSystemServiceB3.validate_directory_path
│ │ │ ├── errors.py # exceções específicas da fonte
│ │ │ ├── zip_reader.py # leitura de COTAHIST ZIP
│ │ │ ├── cotahist_parser.py # parser posicional (preservado isolado)
│ │ │ ├── parquet_writer/ # subpacote: writer Parquet
│ │ │ └── extraction_service/ # subpacote: orquestração streaming/threadpool
│ │ └── cvm/
│ │ └── fundamental_stocks_data/ # layout achatado (5 + 2 helpers)
│ │ ├── core.py # AvailableDocsCVM, AvailableYearsCVM, DictZipsToDownloadCVM, DownloadResultCVM
│ │ ├── client.py # DownloadDocumentsUseCaseCVM, GenerateUrlsUseCaseCVM, VerifyPathsUseCasesCVM
│ │ ├── http.py # AsyncDownloadAdapterCVM (httpx async + retry + integrity)
│ │ ├── extract.py # ParquetExtractorAdapterCVM
│ │ ├── download_validation.py # validate_downloaded_file, validate_parquet_files, find_parquet_files
│ │ ├── download_extraction.py # extract_downloaded_file (orquestra adapter + validation)
│ │ └── errors.py # exceções específicas da fonte
│ ├── core/ # utilidades de infraestrutura
│ ├── macro_infra/ # adapters genéricos compartilhados
│ └── macro_exceptions/ # exceções de base do projeto
├── tests/ # pytest, mirror por fonte
├── docs/ # MkDocs
└── pyproject.toml
Padrão de módulos por fonte¶
Cada brazil/<país>/<fonte>/ segue o mesmo conjunto de papéis. O mapeamento papel ↔ arquivo é fixo na CVM (um arquivo por papel) e mais granular na B3 (papéis “dados puros” e “validação” foram quebrados em módulos por tópico para evitar um único core.py gigante). Adicionar uma nova fonte pode seguir qualquer dos dois caminhos — o critério é legibilidade.
| Papel | CVM | B3 |
|---|---|---|
| Dados puros (types, enums) | core.py |
models.py + years.py + processing.py |
| Validação / domain services | core.py (AvailableDocsCVM.validate_*) |
assets.py (AvailableAssetsServiceB3) + filesystem.py (validate_directory_path) |
| Orquestração / use cases | client.py |
client.py |
| HTTP / download | http.py (AsyncDownloadAdapterCVM) |
(no zip_reader.py + extraction_service/; não há download HTTP) |
| Extração / escrita Parquet | extract.py (ParquetExtractorAdapterCVM) |
parquet_writer/ (subpacote) |
| Parser de formato | — | cotahist_parser.py |
| Helpers de validação | download_validation.py, download_extraction.py |
embutidos em filesystem.py / client.py |
| Exceções | errors.py |
errors.py |
Funções/classes auxiliares são internas ao módulo a menos que sejam usadas em outro arquivo — o ponto de extensibilidade real é a fonte, não o “tipo de objeto”.
Por que B3 não tem
core.pyconsolidado. O conteúdo de B3 vive em módulos por tópico (assets.py,models.py,years.py,processing.py,filesystem.py) em vez de um únicocore.pyporque cada um já tem massa crítica (~100–300 linhas) e diferentes consumidores. Consolidar trocaria 5 arquivos médios por 1 arquivo enorme.
Camadas observáveis¶
A arquitetura do projeto é estruturada ao redor de duas camadas explícitas e bem delineadas:
1. Facade público (application/)¶
Responsabilidade: superfície semver-relevante para usuários da biblioteca.
# src/globaldatafinance/application/cvm_docs/fundamental_stocks_data.py
from ...brazil.cvm.fundamental_stocks_data import (
AsyncDownloadAdapterCVM,
DownloadDocumentsUseCaseCVM,
DownloadResultCVM,
ParquetExtractorAdapterCVM,
# ...
)
class FundamentalStocksDataCVM:
def __init__(self):
self.download_adapter = AsyncDownloadAdapterCVM(...)
self.__download_use_case = DownloadDocumentsUseCaseCVM(self.download_adapter)
def download(self, destination_path, list_docs=None, initial_year=None, last_year=None):
result = self.__download_use_case.execute(
destination_path=destination_path,
list_docs=list_docs,
initial_year=initial_year,
last_year=last_year,
)
self.__result_formatter.print_result(result)
return result
O facade importa diretamente dos módulos planos da fonte (sem passar por re-exports intermediários em brazil/__init__.py).
2. Implementação por fonte (brazil/<país>/<fonte>/)¶
Cada fonte é autocontida em ~5–8 arquivos. Exemplo CVM:
# src/globaldatafinance/brazil/cvm/fundamental_stocks_data/core.py
class AvailableDocsCVM:
DOCS_MAPPING = {
'DFP': 'Demonstração Financeira Padronizada',
'ITR': 'Informação Trimestral',
# ...
}
def validate_docs_name(self, doc_name: str) -> None:
if doc_name not in self.DOCS_MAPPING:
raise InvalidDocumentName(f'Invalid document: {doc_name}')
# src/globaldatafinance/brazil/cvm/fundamental_stocks_data/client.py
class DownloadDocumentsUseCaseCVM:
def __init__(self, repository: AsyncDownloadAdapterCVM) -> None:
self.__repository: AsyncDownloadAdapterCVM = repository
# ...
def execute(self, destination_path, list_docs=None, initial_year=None, last_year=None) -> DownloadResultCVM:
start_time = time.time()
# ... orquestração ...
result = self.__repository.download_docs(tasks)
result.elapsed_time = time.time() - start_time
return result
# src/globaldatafinance/brazil/cvm/fundamental_stocks_data/http.py
class AsyncDownloadAdapterCVM:
def download_docs(self, tasks) -> DownloadResultCVM:
# implementação concreta com httpx async, retry, integrity check
...
O orquestrador interage com AsyncDownloadAdapterCVM através do seu contrato observável de métodos, permitindo que os testes utilizem stubs de duck typing simples sem complexidade adicional (ver “Testes” mais abaixo).
Padrões de design¶
Função (ou classe leve) por operação em client.py¶
A maioria das operações é modelada como função de módulo ou classe objetiva focada na sua responsabilidade específica, acionada diretamente pelo facade. Classes são utilizadas principalmente quando há estado real reutilizável: por exemplo, ExtractHistoricalQuotesUseCaseB3 segura zip_reader + parser + writer + processing_mode entre chamadas e por isso permanece como classe.
# Função simples em client.py
def generate_range_years(initial_year: int | None, last_year: int | None) -> list[int]:
...
# Classe com estado real
class ExtractHistoricalQuotesUseCaseB3:
def __init__(self, zip_reader, parser, writer, processing_mode):
...
def execute(self, paths_of_docs, docs_to_extract):
...
Adapters concretos e diretos¶
Os adapters responsáveis por I/O, como AsyncDownloadAdapterCVM e ParquetExtractorAdapterCVM, são importados e instanciados diretamente nos fluxos principais, garantindo rastreabilidade e simplicidade na navegação da base de código.
Result objects¶
Operações que podem falhar parcialmente retornam um dataclass de resultado com sucessos e erros explícitos, em vez de raise em falhas parciais.
@dataclass
class DownloadResultCVM:
success_count_downloads: int
error_count_downloads: int
successful_downloads: list[str]
failed_downloads: dict[str, str]
elapsed_time: float
def has_errors(self) -> bool:
return self.error_count_downloads > 0
Value objects¶
Tipos imutáveis em core.py que encapsulam validação e construção (DictZipsToDownloadCVM, DocsToExtractorB3, etc.).
Separação de apresentação¶
Saída de console/log de apresentação fica em módulos *_formatter.py dentro de application/. Código de client.py permanece I/O-free (exceto pelas chamadas a adapters concretos).
Defesa de path-traversal preservada como contrato¶
VerifyPathsUseCasesCVM (CVM, em client.py) e o helper FileSystemServiceB3 (B3, em filesystem.py) levantam SecurityError antes de qualquer mkdir, bloqueando escrita em /etc /sys /proc /dev /boot /root. Esses helpers fazem parte do contrato observável de FundamentalStocksDataCVM.download(destination_path=...) e HistoricalQuotesB3.extract(path_of_docs=...) — devem permanecer bit-idênticos em qualquer refactor que mova o código.
Fluxo de dados¶
Download de documentos CVM¶
graph TD
A[FundamentalStocksDataCVM] -->|1. chamar download| B[DownloadDocumentsUseCaseCVM]
B -->|2. validar inputs| C[AvailableDocsCVM / AvailableYearsCVM em core.py]
B -->|3. gerar URLs| D[GenerateUrlsUseCaseCVM em client.py]
B -->|4. verificar paths| E[VerifyPathsUseCasesCVM<br/>raise SecurityError em /etc, /sys, ...]
B -->|5. executar download| F[AsyncDownloadAdapterCVM em http.py]
F -->|6. HTTP requests| G[Servidor CVM]
F -->|7. integrity check + salvar| H[Sistema de arquivos]
F -->|8. retornar resultado| B
B -->|9. retornar resultado| A
A -->|10. formatar saída| I[DownloadResultFormatter]
Extração de cotações B3¶
graph TD
A[HistoricalQuotesB3] -->|1. chamar extract| B[ExtractHistoricalQuotesUseCaseB3]
B -->|2. validar inputs| C[validators em core.py]
B -->|3. validar destino| D[validate_directory_path<br/>raise SecurityError em /etc, /sys, ...]
B -->|4. listar arquivos| E[zip_reader.py]
E -->|5. iterar ZIP entries| F[cotahist_parser.py]
F -->|6. dataframes parciais| G[extraction_service/]
G -->|7. orquestrar threadpool/flush| H[parquet_writer/]
H -->|8. escrever Parquet| I[Sistema de arquivos]
B -->|9. retornar resultado| A
A -->|10. formatar saída| J[ExtractionResultFormatter]
Como adicionar uma nova fonte¶
Para uma nova fonte (ex.: SEC dos EUA), crie uma nova pasta-irmã com módulos nomeados por papel. Para fontes pequenas, o conjunto mínimo da CVM funciona bem; para fontes maiores, espelhe a granularidade de B3 e separe os tópicos antes que o core.py cresça demais.
src/globaldatafinance/usa/sec/fundamental_data/
├── core.py # entidades, value objects, validadores (consolide se ≤ ~300 linhas)
├── client.py # use cases / orquestradores
├── http.py # adapter HTTP concreto
├── extract.py # adapter de extração concreto (se aplicável)
└── errors.py # exceções específicas
Em seguida:
- Re-export interno: adicione
__init__.pyna pasta da fonte (ou deixe vazio se não houver consumidores internos). - Facade público: crie
src/globaldatafinance/application/sec_docs/fundamental_data.pycom uma classeFundamentalDataSECque importa diretamente dos módulos planos:
from ...usa.sec.fundamental_data import (
DownloadAdapterSEC,
DownloadDocumentsUseCaseSEC,
# ...
)
- API pública: re-exporte a nova classe em
src/globaldatafinance/__init__.pyesrc/globaldatafinance/application/__init__.py. Trate como adição semver-relevante. - Testes: crie
tests/usa/sec/fundamental_data/espelhando módulos por tópico (não por camada). Tests devem importar dos módulos planos. - Docs: atualize
AGENTS.md(Repository Map / Architecture Map) e este arquivo. Adicione referência emdocs/reference/.
Testes¶
A árvore de testes espelha as fontes:
tests/
├── brazil/
│ ├── cvm/fundamental_stocks_data/
│ │ ├── application/use_cases/ # tests por tópico (organizacional, não camada)
│ │ ├── domain/ # tests de value objects, validators (core.py)
│ │ ├── infra/adapters/ # tests de adapters concretos (http.py, extract.py)
│ │ ├── exceptions/ # tests das exceções (errors.py)
│ │ └── integration/ # tests integration-marker
│ └── b3_data/historical_quotes/ # layout plano: 21 test_*.py diretamente na pasta
└── application/
├── cvm_docs/ # tests do facade público
└── b3_docs/
└── result_formatters/
Os subdiretórios dentro da fonte CVM são organizacionais (agrupam por tópico para legibilidade), não arquiteturais — qualquer test importa dos módulos via from globaldatafinance.brazil.cvm.fundamental_stocks_data.client import ....
Estratégias de Mock e Stub¶
Para testar orquestradores e adapters de forma limpa e isolada, os testes substituem as dependências através de:
- Stub duck-typed: classes simples focadas nos métodos consumidos no teste, mantendo alta flexibilidade.
monkeypatch.setattr: patcheia o método do adapter real.httpx.MockTransport: para tests do adapter HTTP que precisam respostas determinísticas sem rede.
Exemplo (duck typing):
class MockRepository:
def download_docs(self, tasks):
return DownloadResultCVM(
success_count_downloads=2,
error_count_downloads=0,
successful_downloads=['DFP_2023', 'ITR_2023'],
failed_downloads={},
)
use_case = DownloadDocumentsUseCaseCVM(MockRepository())
result = use_case.execute(destination_path='/tmp/cvm')
assert result.success_count_downloads == 2
Coverage é validado por capability: tests/brazil/<source>/ + tests/application/<facade>/ cobrem cada fonte como unidade isolada.
Próximos Passos¶
- 📖 Referência da API — Documentação completa da API
- 🤝 Como Contribuir — Guia para contribuidores
- 🧪 Testes — Como escrever e executar testes
- 🔧 Uso Avançado — Customização e extensões