Full Code of dude333/rapina for AI

master d0d1de8ad991 cached
79 files
286.7 KB
99.8k tokens
407 symbols
1 requests
Download .txt
Showing preview only (307K chars total). Download the full file or copy to clipboard to get everything.
Repository: dude333/rapina
Branch: master
Commit: d0d1de8ad991
Files: 79
Total size: 286.7 KB

Directory structure:
gitextract_5rc_9ybs/

├── .githooks/
│   └── pre-commit
├── .github/
│   └── workflows/
│       └── test-lint-release.yml
├── .gitignore
├── LICENSE
├── Makefile
├── NOTICE
├── README.md
├── README_en.md
├── cmd/
│   └── rapina/
│       ├── cmdutils.go
│       ├── cmdutils_test.go
│       ├── fii.go
│       ├── fii_dividends.go
│       ├── fii_monthly.go
│       ├── flags.go
│       ├── list.go
│       ├── main.go
│       ├── report.go
│       ├── server.go
│       └── update.go
├── common.go
├── common_test.go
├── errors.go
├── fetch/
│   ├── fetch.go
│   ├── fetch_fii.go
│   ├── fetch_fii_test.go
│   ├── fetch_http.go
│   ├── fetch_http_test.go
│   ├── fetch_stock.go
│   ├── fetch_test.go
│   └── unzip.go
├── fii.go
├── go.mod
├── go.sum
├── logger.go
├── parsers/
│   ├── codeaccounts.go
│   ├── companies.go
│   ├── fii.go
│   ├── fiidb.go
│   ├── financial.go
│   ├── financial_test.go
│   ├── fre.go
│   ├── fuzzy.go
│   ├── fuzzy_test.go
│   ├── md5.go
│   ├── md5_test.go
│   ├── meta/
│   │   ├── meta_bpa_cia_aberta.txt
│   │   ├── meta_bpp_cia_aberta.txt
│   │   ├── meta_dfc_md_cia_aberta.txt
│   │   ├── meta_dfc_mi_cia_aberta.txt
│   │   └── meta_dre_cia_aberta.txt
│   ├── sectors.go
│   ├── sectors_test.go
│   ├── stock.go
│   ├── stock_test.go
│   ├── tables.go
│   └── transform.go
├── progress/
│   ├── cmd/
│   │   └── main.go
│   └── progress.go
├── reports/
│   ├── db.go
│   ├── db_test.go
│   ├── excel.go
│   ├── format.go
│   ├── format_test.go
│   ├── list.go
│   ├── logger.go
│   ├── logger_test.go
│   ├── reports.go
│   ├── reports_fii.go
│   ├── reports_html.go
│   └── reports_test.go
├── server/
│   ├── fs_dev.go
│   ├── fs_prod.go
│   ├── payload.go
│   ├── server.go
│   └── templates/
│       ├── fii.html
│       ├── financials.html
│       ├── index.html
│       └── layout.html
└── stock.go

================================================
FILE CONTENTS
================================================

================================================
FILE: .githooks/pre-commit
================================================
#!/bin/sh

# git config core.hooksPath .githooks

echo "Running pre-commit checks at `pwd`"

{
  echo "golangci-lint run ./..."
	golangci-lint run ./...
} || {
	exitStatus=$?

	if [ $exitStatus ]; then
		printf "\nLint errors in your code, please fix them and try again."
		exit 1
	fi
}

{
  echo "go test ./..."
	go test ./...
} || {
	exitStatus=$?

	if [ $exitStatus ]; then
		printf "\nTest errors in your code, please fix them and try again."
		exit 1
	fi
}



================================================
FILE: .github/workflows/test-lint-release.yml
================================================
name: Test, Lint & Release

on: [ push, pull_request ]

jobs:
  go-test:
    strategy:
      fail-fast: false
      matrix:
        go: ['1.21.1']
        platform: [ubuntu-latest, windows-latest]
    runs-on: ${{ matrix.platform }}
    steps:
      - if: github.actor == 'nektos/act'
        name: act workaround
        run: apt update && apt install -y zstd gcc git
      - name: Set up Go
        uses: actions/setup-go@v2
        with:
          go-version: ${{ matrix.go }}
      - name: Show go version
        run: go version
      - name: Checkout
        uses: actions/checkout@v2
      - name: go mod package cache
        uses: actions/cache@v2
        with:
          # In order:
          # * Module download cache
          # * Build cache (Linux)
          # * Build cache (Mac)
          # * Build cache (Windows)
          path: |
            ~/go/pkg/mod
            ~/.cache/go-build
            ~/Library/Caches/go-build
            %LocalAppData%\go-build
          key: ${{ runner.os }}-go-${{ matrix.go }}-${{ hashFiles('**/go.mod') }}
          restore-keys: |
            ${{ runner.os }}-go-${{ matrix.go }}
      - name: Run tests
        run: go test -short -cover ./...

  go-lint:
    name: lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: golangci-lint
        uses: golangci/golangci-lint-action@v2
        with:
          version: latest

  xgo:
    if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
    needs: [go-test, go-lint]
    
    strategy:
      fail-fast: false
      matrix:
        go_version: [ 1.21.x ]

    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: Get current date
        id: date
        run: echo "::set-output name=date::$(date +'%F')"
      - name: Get current git tag or commit
        id: tag
        run: echo "::set-output name=tag::$(git describe --tags --always)"
      - name: Build with xgo
        uses: crazy-max/ghaction-xgo@v1
        with:
          xgo_version: latest
          go_version: ${{ matrix.go_version }}
          pkg: cmd/rapina
          dest: build
          prefix: rapina-${{ steps.tag.outputs.tag }}
          targets: windows/386,windows/amd64,linux/386,linux/amd64,darwin/386,darwin/amd64
          v: false
          x: false
          race: false
          ldflags: -s -w -X main.build=${{ steps.date.outputs.date }} -X main.version=${{ steps.tag.outputs.tag }}
          buildmode: default
      - name: Run UPX
        uses: gacts/upx@master
        with:
          dir: 'build'
          upx_args: '-9'
      - name: Checksum
        run: |
          cd build
          sha1sum rapina* > sha1sum.txt
      - name: Generate changelog
        id: changelog
        uses: metcalfc/changelog-generator@v1.0.0
        with:
          myToken: ${{ secrets.GITHUB_TOKEN }}
      - name: Release
        uses: softprops/action-gh-release@v1
        with:
          files: build/*
          body: ${{ steps.changelog.outputs.changelog }}
          draft: false
          prerelease: false
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .gitignore
================================================
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
debug
*.db-journal
.vscode
bin/**

# Temporary and data files
*.zip
**/.data
*.csv
*.xls*
*.db
*.old
.vscode/*
*.sql
*_string.go
*.yaml
*.yml
!.travis.yml
wiki/**
.DS_Store

# Test binary, build with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Skip these (keep this at the end)
!.github/**

# Dependency Analytics
target/**


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright © 2018 Adriano P

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

================================================
FILE: Makefile
================================================
BUILDDIR=./cmd/...
SOURCEDIR=.
SOURCES := $(shell find $(SOURCEDIR) -name '*.go')

BINARYDIR=./bin/
BINARY=bin/rapina
WINBINARY=bin/rapina.exe
OSXBINARY=bin/rapina-osx

VERSION=`git describe --tags --always`
BUILD_TIME=`date +%F`

export GO111MODULE=on

# Setup the -ldflags option for go build here, interpolate the variable values
LDFLAGS=-ldflags "-w -s -X main.version=${VERSION} -X main.build=${BUILD_TIME}"

.DEFAULT_GOAL: $(BINARY)

$(BINARY): $(SOURCES) $(wildcard ../*.go) $(wildcard ../parsers/*.go) $(wildcard ../reports/*.go)
	CGO_CFLAGS="-O2 -Wno-return-local-addr" go build ${LDFLAGS} -o $(BINARYDIR) $(BUILDDIR)

win: $(SOURCES)
	# go get -v -d ../...
	GOOS=windows GOARCH=386 CGO_ENABLED=1 CC=i686-w64-mingw32-gcc CXX=i686-w64-mingw32-g++ CGO_CFLAGS="-O2 -Wno-return-local-addr" CGO_LDFLAGS="-lssp -w" go build ${LDFLAGS} -o ${BINARYDIR} $(BUILDDIR)

osx:  $(SOURCES)
	# go get -v -d ../...
	GOOS=darwin GOARCH=amd64 CGO_ENABLED=1 CC=o64-clang CXX=o64-clang++ CGO_CFLAGS="-O2 -Wno-return-local-addr" CGO_LDFLAGS="-w" go build ${LDFLAGS} -o ${BINARYDIR} $(BUILDDIR)

.PHONY: install
install:
	go install ${LDFLAGS} ./...

.PHONY: clean
clean:
	if [ -f ${BINARY} ] ; then rm ${BINARY} ; fi

.PHONY: list
list:
	cd .. && go list -f '{{ join .Imports "\n" }}'


================================================
FILE: NOTICE
================================================
================================================================================
| Open Database License (ODbL) |

Contains information from "Portal Dados Abertos CVM", which is made available
here under the Open Database License (ODbL).

  https://www.opendatacommons.org/licenses/odbl/1.0/


================================================================================
| BSD-2-Clause |

==> github.com/pkg/errors: Copyright (c) 2015, Dave Cheney <dave@cheney.net>.
All rights reserved.

  https://opensource.org/licenses/BSD-2-Clause


================================================================================
| BSD 3-Clause License |

==> github.com/360EntSecGroup-Skylar/excelize:
Copyright (c) 2016 - 2018 360 Enterprise Security Group, Endpoint Security, inc.
All rights reserved.

==> github.com/manifoldco/promptui: Copyright (c) 2017, Arigato Machine Inc.
All rights reserved.

  https://opensource.org/licenses/BSD-3-Clause


================================================================================
| Apache License, Version 2.0 |

==> github.com/spf13/cobra: Copyright © 2013 Steve Francia <spf@spf13.com>.

  http://www.apache.org/licenses/LICENSE-2.0


================================================================================
| The MIT License (MIT) |

==> github.com/mattn/go-sqlite3: Copyright (c) 2014 Yasuhiro Matsumoto.

  https://opensource.org/licenses/MIT

================================================
FILE: README.md
================================================
# 𝚛𝚊𝚙𝚒𝚗𝚊

Download e processamento de dados<sup>[1](#disclaimer)</sup> financeiros de empresas brasileiras diretamente da [CVM](http://dados.cvm.gov.br/dados/CIA_ABERTA/DOC/DFP/).

[![GitHub release](https://img.shields.io/github/tag/dude333/rapina.svg?label=latest)](https://github.com/dude333/rapina/releases)
[![Travis](https://img.shields.io/travis/dude333/rapina/master.svg)](https://travis-ci.org/dude333/rapina)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)

Este programa baixa e processa os arquivos CSV do site da CVM e os armazena em um banco de dados local (sqlite), onde são extraídos os dados **consolidados** do balanço patrimonial, fluxo de caixa, DRE (demonstração de resultado), DVA (demonstração de valor adicionado).

São coletados vários arquivos CSV desde 2010. Cada um destes arquivos contém informações do ano corrente e também do ano anterior, dessa forma foi possível extrair também os dados de 2009.

Com base nestes dados, são criados os relatórios por empresa, com um comparativo de outras empresas do mesmo setor. A classificação dos setores é baixada do site da Bovespa e armazenada no arquivo setores.yml (no formato [YAML](https://medium.com/@akio.miyake/introdu%C3%A7%C3%A3o-b%C3%A1sica-ao-yaml-para-ansiosos-2ac4f91a4443)), que pode ser editado para se adequar aos seus critérios, caso necessário.

A partir do release v0.11.0, passou-se a usar os dados trimestrais para compor os valores do ano corrente, usando-se para isso os últimos 4 trimestre ([TTM](#ttm-calc)), ou seja, a soma dos dados trimestrais do ano corrente com alguns do ano anterior, mantendo-se assim uma mesma base de comparação com os anos anteriores. 

| :memo:        | **NOTA**: Desenvolvi a [versão 2 do rapina](https://github.com/dude333/rapinav2) com o intuito de criar relatórios trimestrais. Pretendo integrar essa nova funcionalidade a este repositório no futuro.  |
|---------------|:------------------------|

# 1. Instalação

Não é necessário instalar, basta baixar o executável da [página de release](https://github.com/dude333/rapina/releases) e renomeie o executável para `rapina.exe` (no caso do Windows) ou `rapina` (para o Linux ou macOS).

Abra o terminal ([CMD](https://superuser.com/a/340051/61616) no Windows) e rode os comandos listados abaixo.

# 2. Uso

Na primeira vez, rodar o seguinte comando para baixar e processar os arquivos do site da CVM:

    ./rapina update

Depois, para obter o relatório de uma determinada empresa, com o resumo das empresas do mesmo setor:

    ./rapina report <empresa>

_Eventualmente, as empresas corrigem algum dado e enviam um novo arquivo à CVM, então é recomendável rodar o `rapina update` periodicamente._

# 3. Detalhe dos Comandos

## 3.1. update

**Download e armazenamento de dados financeiros no banco de dados local.**

    ./rapina update [-s]

Baixa todos os arquivos disponíveis no servidor da CVM, processa o conteúdo e o armazena num banco de dados sqlite em `.data/rapina.db`.

Este comando deve ser executado **pelo menos uma vez** antes dos outros comandos.

### 3.1.1 Opção

```
  -s, --sectors   Baixa a classificação setorial das empresas e fundos negociados na B3
```

Usado para obter apenas o arquivo de classificação setorial atualizado.

## 3.2. list

**Listas**

    ./rapina list

### 3.2.1 Lista todas as empresas disponíveis

```
  -e, --empresas               Lista todas as empresas disponíveis
```

### 3.2.2 Lista as empresas do mesmo setor

```
  -s, --setor string           Lista todas as empresas do mesmo setor
```

Por exemplo, para listar todas as empras do mesmo setor do Itaú: `./rapina lista -s itau`

O resultado mostra a lista das empresas do mesmo setor contidos no banco de dados e no arquivo **setores.yml**, que você pode editar caso queira realocar os setores das empresas.

### 3.2.3 Lista empresas com critério de lucro líquido

```
  -l, --lucroLiquido número   Lista empresas com lucros lucros positivos e com a taxa de crescimento definida
```

Lista as empresas com lucros líquidos positivos e com uma taxa de crescimento definida em relação ao mês anterior. 
Por exemplo:
* Para listar as empresas com crescimento mínimo de 10% em relação ao ano anterior: `./rapina list -l 0.1`
* Para listar as empresas com variação no lucro de maiores que -5% em relação ao ano anterior: `./rapina list -l -0.05`


## 3.3. report

**Cria uma planilha com os dados financeiros de uma empresa.**

    ./rapina report [opções] empresa

Será criada uma planilha com os dados financeiros (BP, DRE, DFC) e, em outra aba, o resumo de todas as empresas do mesmo setor.

A lista setorial é obtida da B3 e salva no arquivo `setor.yml` (via comando `update -s`). Caso deseje alterar o agrupamento setorial, basta editar este arquivo. Mas lembre-se que ao rodar o `update -s` o arquivo será sobrescrito.

No **Linux** ou **macOS**, use as setas para navegar na lista das empresas. No **Windows**, use <kbd>j</kbd> e <kbd>k</kbd>.

### 3.3.1. Opções

```
  -a, --all                Mostra todos os indicadores
  -x, --extraRatios        Reporte de índices extras
  -F, --fleuriet           Capital de giro no modelo Fleuriet
  -o, --omitSector         Omite o relatório das empresas do mesmo setor
  -d, --outputDir string   Diretório onde o relatório será salvo (default "reports")
  -s, --scriptMode         Para modo script (escolhe a empresa com nome mais próximo)
  -f, --showShares         Mostra o número de ações e free float

```


### 3.3.2. Exemplos

    ./rapina report WEG

A planilha será salva em `./reports`

    ./rapina report "TEC TOY" -s -d /tmp/output

A planilha será salva em `/tmp/output`

# 4. Nova funções

## 4.1. fii

**Relatórios relacionados aos Fundos de Investimento Imobiliários**

### 4.1.1. rendimentos

    ./rapina fii rendimentos [-n] ABCD11 EFGH11...

Onde `-n` é o número de meses a serem apresentados.

E como parâmetros, passe uma lista de FIIs separados por espaço.

#### 4.1.1.1 Exemplo

    ./rapina fii rendimentos -n 2 knip11 hfof11

```
-------------------------------------------------------------------
KNIP11
-------------------------------------------------------------------
  DATA COM       RENDIMENTO     COTAÇÃO       YELD      YELD a.a.
  ----------     ----------     ----------    ------    ---------
  2021-04-30     R$    1,00     R$  113,00     0,88%       11,15%
  2021-03-31     R$    1,02     R$  115,95     0,88%       11,08%
-------------------------------------------------------------------
HFOF11
-------------------------------------------------------------------
  DATA COM       RENDIMENTO     COTAÇÃO       YELD      YELD a.a.
  ----------     ----------     ----------    ------    ---------
  2021-04-30     R$    0,60     R$   99,75     0,60%        7,46%
  2021-03-31     R$    0,56     R$  100,70     0,56%        6,88%
-------------------------------------------------------------------

```

# 4.2. server

**Web server para visualização dos relatórios no browser**

## 4.2.1. Exemplo

    ./rapina server

    2021/05/11 19:23:15 Listening on :3000...

Para visualizar a página, abrir o link http://localhost:3000

**NOTA:** Por hora só está disponível o relatório de rendimentos de FIIs.


# 5. Possíveis problemas

Algumas distribuições Linux (Fedora 34, por exemplo) podem encontrar problemas com as autoridades certificadores (Global Sign) presentes nos certificados SSL dos websites da B3. Em caso de erro `x509: certificate signed by unknown authority`, deve-se importar manualmente o Root CA para o trusted database do sistemas operacional:

**Fedora 34 / CentOS** 

1. Realizar o download do Issuer Root Cert

    `curl http://secure.globalsign.com/cacert/gsrsaovsslca2018.crt > /tmp/global-signer.der`

2. Converter de .der para .pem

    `openssl x509 -inform der -in /tmp/global-signer.der -out /tmp/globalsignroot.pem`

3. Importar .pem arquivo para pasta de anchors

    `sudo cp /tmp/globalsignroot.pem /usr/share/pki/ca-trust-source/anchors/`

4. Atualizar base de trusted certificates

    `sudo update-ca-trust`

**Ubuntu** 

1. Realizar o download do Issuer Root Cert

    `curl https://secure.globalsign.net/cacert/Root-R1.crt > /tmp/GlobalSign_Root_CA.crt`
    `curl https://secure.globalsign.net/cacert/Root-R2.crt > /tmp/GlobalSign_Root_CA_R2.crt`

2. Importar .crt arquivos para pasta de certificados

    `sudo cp /tmp/GlobalSign_Root_CA.crt /usr/local/share/ca-certificates/`
    `sudo cp /tmp/GlobalSign_Root_CA_R2.crt /usr/local/share/ca-certificates/`

3. Atualizar base de trusted certificates

    `sudo update-ca-trust`

# 6. Como compilar

Se quiser compilar seu próprio executável, primeiro [baixe e instale](https://golang.org/dl/) o compilador Go (v1.16 ou maior). Depois execute estes passos:

1. `git clone github.com/dude333/rapina`
2. `cd rapina`
3. `make`

O executável será criado na pasta `bin`. Você pode movê-lo para outro local. Ao rodar a primeira vez, apenar o executável é necessário, mas após rodá-lo, será criado um diretório `.data` que deverá ser movido junto com o executável, caso queira trazer o dados.

IMPORTANTE: para compilar a biblioteca do sqlite, é necessário ter um compilador C instalado na máquina (para o Windows, mais detalhes [aqui](https://github.com/mattn/go-sqlite3#windows)).

# 7. Contribua

1. Faça um fork deste projeto no [github.com](github.com/dude333/rapina)
2. `git clone https://github.com/`*your_username*`/rapina && cd rapina`
3. `git checkout -b `*my-new-feature*
4. Faça as modificações
5. `git add .`
6. `git commit -m 'Add some feature'`
7. `git push origin my-new-feature`
8. Crie um _pull request_

# 8. Screenshot

![WEG](https://i.imgur.com/czPhPkH.png)


# 9. License

MIT




<br />
<br />
<br />
<a name="disclaimer">1</a>: *Os dados são fornecidos "no estado em que se encontram" e somente para fins informativos, não para fins comerciais ou de consultoria.*


================================================
FILE: README_en.md
================================================
# 𝚛𝚊𝚙𝚒𝚗𝚊

Download and process Brazilian companies' financial data directly from [CVM](http://dados.cvm.gov.br/dados/CIA_ABERTA/DOC/DFP/). [[Em português](./README.md)]

[![GitHub release](https://img.shields.io/github/tag/dude333/rapina.svg?label=latest)](https://github.com/dude333/rapina/releases)
[![Travis](https://img.shields.io/travis/dude333/rapina/master.svg)](https://travis-ci.org/dude333/rapina)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)

# 1. Installation

No installation required, just download the [latest released executable](https://github.com/dude333/rapina/releases). Then open a terminal ([CMD](https://superuser.com/a/340051/61616) on Windows) and run the commands shown below.

# 2. Commands

For the first time, run the following command:

    ./rapina get

Then, to get a company report, together with a summary for the companies from the same sector:

    ./rapina report <company>

## 2.1. `get`| Download and store financial data into the local database

    ./rapina get [-s]

It downloads all files from CVM web server, parses their contents and stores on a sqlite database at `.data/rapina.db`.

This command must be run **at least once** before you run the other commands.

### 2.1.1 Option

```
  -s, --sectors   Download and sector classification for companies listed at B3
```

Used to get only a summary for the other companies from the same sector.

[![asciicast](https://asciinema.org/a/656x2hrtCFFZLVLa9fGGcetw7.svg)](https://asciinema.org/a/656x2hrtCFFZLVLa9fGGcetw7?speed=4&autoplay=1&loop=1)

## 2.2. `list`| List all companies

    ./rapina list

[![asciicast](https://asciinema.org/a/TbJyGaOodJUxEzjDySQu3MaEW.svg)](https://asciinema.org/a/TbJyGaOodJUxEzjDySQu3MaEW?autoplay=1&loop=1)

## 2.3. `report`| Create a spreadsheet with a company financial data

    ./rapina report [flags] company_name

A spreadsheet with the financial data will be created and, on another sheet, the summary of all companies in the same sector.

The sector list is obtained from B3 and saved in the `sector.yml` file (via `get -s` command). If you want to change the sector grouping, just edit this file.

### 2.3.1. Options

```
  -d, --outputDir string   Output directory [default: ./reports]
  -s, --scriptMode         Does not show companies list; uses the most similar
                           company name
```

On **Linux** or **macOS**, use the arrow keys to navigate through the companies list. On **Windows**, use <kbd>j</kbd> and <kbd>k</kbd>.

[![asciicast](https://asciinema.org/a/jhmHxzgROtc8EBh3tkSwYTaa9.svg)](https://asciinema.org/a/jhmHxzgROtc8EBh3tkSwYTaa9?autoplay=1&loop=1)

### 2.3.2. Examples

    ./rapina report WEG

The spreadsheet will be saved at `./reports`

    ./rapina report "TEC TOY" -s -d /tmp/output

The spreadsheet will be saved at `/tmp/output`

# 3. Troubleshooting

Some Linux distributions (e.g. Fedora 34) might face some issues regarding the signer authority (Global Sign) that B3 is using on its SSL certificates. In case of `x509: certificate signed by unknown authority` error, one should manually import the Root CA certificate into the O.S. trusted database:

**Fedora 34 / CentOS** 

1. Download the Issuer Root Cert

    `curl http://secure.globalsign.com/cacert/gsrsaovsslca2018.crt > /tmp/global-signer.der`

2. Convert from .der to .pem

    `openssl x509 -inform der -in /tmp/global-signer.der -out /tmp/globalsignroot.pem`

3. Move the .pem file to the anchors folder

    `sudo cp /tmp/globalsignroot.pem /usr/share/pki/ca-trust-source/anchors/`

4. Update the trusted certificates database

    `sudo update-ca-trust`

**Ubuntu** 

1. Download the Issuer Root Cert

    `curl https://secure.globalsign.net/cacert/Root-R1.crt > /tmp/GlobalSign_Root_CA.crt`
    `curl https://secure.globalsign.net/cacert/Root-R2.crt > /tmp/GlobalSign_Root_CA_R2.crt`

2. Move the .crt files to the certificates folder

    `sudo cp /tmp/GlobalSign_Root_CA.crt /usr/local/share/ca-certificates/`
    `sudo cp /tmp/GlobalSign_Root_CA_R2.crt /usr/local/share/ca-certificates/`

3. Update the trusted certificates database

    `sudo update-ca-trust`


# 4. How to compile

If you want to compile your own executable, you need first to [download and install](https://golang.org/dl/) the Go compiler. Then follow these steps:

1. `go get github.com/dude333/rapina`
2. `cd $GOPATH/src/github.com/dude333/rapina`
3. Change to the cli directory (`cd cli`)
4. Compile using the Makefile (`make`). _To cross compile for Windows on Linux, use `make win`_.

# 5. Contributing

1. Fork it
2. `cd $GOPATH/src/github.com/your_username`
3. Download your fork to your PC (`git clone https://github.com/your_username/rapina && cd rapina`)
4. Create your feature branch (`git checkout -b my-new-feature`)
5. Make changes and add them (`git add .`)
6. Commit your changes (`git commit -m 'Add some feature'`)
7. Push to the branch (`git push origin my-new-feature`)
8. Create new pull request

# 6. Screenshot

![WEG](https://i.imgur.com/czPhPkH.png)

# 7. License

MIT


================================================
FILE: cmd/rapina/cmdutils.go
================================================
package main

import (
	"database/sql"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/manifoldco/promptui"
	"github.com/pkg/errors"
)

// Directory where the DB and downloaded files are stored
const dataDir = ".data"
const yamlFile = "./setores.yml"

// Parms holds the input parameters
type Parms struct {
	// Company name to be processed
	Company string
	// SpcfctnCd to indentify the ticker
	SpcfctnCd string
	// Report format (xlsx/stdout)
	Format string
	// OutputDir: path of the output xlsx
	OutputDir string
	// YamlFile: file with the companies' sectors
	YamlFile string
	// Reports is a map with the reports and reports items to be printed
	Reports map[string]bool
}

//
// openDatabase to be used by parsers and reporting
//
func openDatabase() (db *sql.DB, err error) {
	if err := os.MkdirAll(dataDir, os.ModePerm); err != nil {
		return nil, err
	}
	connStr := "file:" + dataDir + "/rapina.db?cache=shared&mode=rwc&_journal_mode=WAL&_busy_timeout=5000"
	db, err = sql.Open("sqlite3", connStr)
	if err != nil {
		return db, errors.Wrap(err, "database open failed")
	}
	db.SetMaxOpenConns(1)

	return
}

//
// promptUser presents a navigable list to be selected on CLI
//
func promptUser(list []string, label string) (result string) {
	if label == "" {
		label = "Selecione a Empresa"
	}
	templates := &promptui.SelectTemplates{
		Help: `{{ "Use estas teclas para navegar:" | faint }} {{ .NextKey | faint }} ` +
			`{{ .PrevKey | faint }} {{ .PageDownKey | faint }} {{ .PageUpKey | faint }} ` +
			`{{ if .Search }} {{ "and" | faint }} {{ .SearchKey | faint }} {{ "toggles search" | faint }}{{ end }}`,
	}

	prompt := promptui.Select{
		Label:     label,
		Items:     list,
		Templates: templates,
	}

	_, result, err := prompt.Run()

	if err != nil {
		fmt.Printf("Prompt failed %v\n", err)
		return
	}

	return
}

//
// filename cleans up the filename and returns the path/filename
func filename(path, name string) (fpath string, err error) {
	clean := func(r rune) rune {
		switch r {
		case ' ', ',', '/', '\\':
			return '_'
		}
		return r
	}
	path = strings.TrimSuffix(path, "/")
	name = strings.TrimSuffix(name, ".")
	name = strings.Map(clean, name)
	fpath = filepath.FromSlash(path + "/" + name + ".xlsx")

	const max = 50
	var x int
	for x = 1; x <= max; x++ {
		_, err = os.Stat(fpath)
		if err == nil {
			// File exists, try again with another name
			fpath = fmt.Sprintf("%s/%s(%d).xlsx", path, name, x)
		} else if os.IsNotExist(err) {
			err = nil // reset error
			break
		} else {
			err = fmt.Errorf("file %s stat error: %v", fpath, err)
			return
		}
	}

	if x > max {
		err = fmt.Errorf("remova o arquivo %s/%s.xlsx antes de continuar", path, name)
		return
	}

	// Create directory
	_ = os.Mkdir(path, os.ModePerm)

	// Check if the directory was created
	if _, err := os.Stat(path); os.IsNotExist(err) {
		return "", errors.Wrap(err, "diretório não pode ser criado")
	}

	return
}


================================================
FILE: cmd/rapina/cmdutils_test.go
================================================
package main

import (
	"os"
	"path/filepath"
	"testing"
)

func TestFilename(t *testing.T) {
	tempDir, _ := os.MkdirTemp("", "rapina-test")

	table := []struct {
		path     string
		name     string
		expected string
	}{
		{tempDir + "/test", "sample", tempDir + "/test/sample.xlsx"},
		{tempDir, "File 100", tempDir + "/File_100.xlsx"},
		{tempDir, "An,odd/file\\name", tempDir + "/An_odd_file_name.xlsx"},
	}

	for _, x := range table {
		returned, err := filename(x.path, x.name)
		expected := filepath.FromSlash(x.expected)
		if err != nil {
			t.Errorf("filename returned an error %v.", err)
		} else if returned != expected {
			t.Errorf("filename got: %s, want: %s.", returned, expected)
		}
	}

}


================================================
FILE: cmd/rapina/fii.go
================================================
/*
Copyright © 2021 Adriano P <dev@dude333.com>
Distributed under the MIT License.
*/
package main

import (
	"fmt"
	"os"
	"path/filepath"

	"github.com/spf13/cobra"
)

type fiiFlags struct {
	num       int // number of months since current
	dividends fiiDividendsFlags
	monthly   fiiMonthlyFlags
}

// fiiCmd represents the fii command
var fiiCmd = &cobra.Command{
	Use:   "fii",
	Short: "Comando relacionados aos FIIs",
	Long:  `Comando relacionado aos Fundos de Investiment Imobiliários (FII).`,
	Run: func(cmd *cobra.Command, args []string) {
		_ = cmd.Help()
	},
	Example: func() string {
		return fmt.Sprintf("%s fii rendimentos KNIP11 KNCR11 HGLG11 -n 4", filepath.Base(os.Args[0]))
	}(),
}

func init() {
	rootCmd.AddCommand(fiiCmd)
	fiiCmd.PersistentFlags().IntVarP(&flags.fii.num,
		Fnum, "n", 1, "número de meses desde o último disponível")
}


================================================
FILE: cmd/rapina/fii_dividends.go
================================================
/*
Copyright © 2021 Adriano P <dev@dude333.com>
Distributed under the MIT License.
*/
package main

import (
	"fmt"
	"log"
	"os"
	"path/filepath"
	"strings"

	"github.com/dude333/rapina/reports"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

type fiiDividendsFlags struct {
	format string // output format of the report
}

// fiiDividendsCmd represents the rendimentos command
var fiiDividendsCmd = &cobra.Command{
	Use:     "rendimentos",
	Aliases: []string{"rend", "dividendos", "dividends", "div"},
	Args:    cobra.MinimumNArgs(1),
	Short:   "Lista os rendimentos de um FII",
	Long:    `Lista os rendimentos de um Fundos de Investiment Imobiliários (FII).`,
	Run: func(cmd *cobra.Command, args []string) {
		// Number of reports
		n := flags.fii.num
		if n <= 0 {
			n = 1
		}

		parms := make(map[string]string)
		// Verbose
		if flags.verbose {
			parms[Fverbose] = "true"
		}
		// Report format
		parms[Fformat] = flags.fii.dividends.format

		if err := FIIDividends(parms, args, n); err != nil {
			log.Println(err)
		}

	},
	Example: func() string {
		return fmt.Sprintf("%s fii rendimentos KNIP11 KNCR11 HGLG11 -n 4", filepath.Base(os.Args[0]))
	}(),
}

func init() {
	fiiCmd.AddCommand(fiiDividendsCmd)
	fiiDividendsCmd.Flags().StringVarP(&flags.fii.dividends.format, Fformat,
		"f", "tabela", "formato do relatório: tabela|csv|csvrend")
}

// FIIDividends prints the dividends from 'code' for 'n' months,
// starting from latest.
func FIIDividends(parms map[string]string, codes []string, n int) error {
	for i := 0; i < len(codes); i++ {
		codes[i] = strings.ToUpper(codes[i])
	}

	db, err := openDatabase()
	if err != nil {
		return err
	}

	opts := reports.FIITerminalOptions{
		APIKey:  viper.GetString("apikey"),
		DataDir: dataDir,
	}

	r, err := reports.NewFIITerminal(db, opts)
	if err != nil {
		return err
	}

	r.SetParms(parms)

	err = r.Dividends(codes, n)
	if err != nil {
		return err
	}

	return nil
}


================================================
FILE: cmd/rapina/fii_monthly.go
================================================
/*
Copyright © 2021 Adriano P <dev@dude333.com>
Distributed under the MIT License.
*/
package main

import (
	"log"
	"strings"

	"github.com/dude333/rapina/reports"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

type fiiMonthlyFlags struct {
	format string // output format of the report
}

// fiiMonthlyCmd represents the rendimentos command
var fiiMonthlyCmd = &cobra.Command{
	Hidden:  true,
	Use:     "mensal",
	Aliases: []string{"monthly"},
	Args:    cobra.MinimumNArgs(1),
	Short:   "Lista os informes mensais de um FII",
	Long:    `Lista os informes mensais de um Fundos de Investiment Imobiliários (FII).`,
	Run: func(cmd *cobra.Command, args []string) {
		// Number of reports
		n := flags.fii.num

		parms := make(map[string]string)
		// Verbose
		if flags.verbose {
			parms[Fverbose] = "true"
		}
		// Report format
		parms[Fformat] = flags.fii.monthly.format

		if err := FIIMonthly(parms, args, n); err != nil {
			log.Println(err)
		}

	},
}

func init() {
	fiiCmd.AddCommand(fiiMonthlyCmd)
	fiiMonthlyCmd.Flags().StringVarP(&flags.fii.monthly.format, Fformat,
		"f", "tabela", "formato do relatório: tabela|csv|csvrend")
}

// FIIMonthly prints the monthly reports from 'code' for 'n' months,
// starting from latest.
func FIIMonthly(parms map[string]string, codes []string, n int) error {
	for i := 0; i < len(codes); i++ {
		codes[i] = strings.ToUpper(codes[i])
	}

	db, err := openDatabase()
	if err != nil {
		return err
	}

	opts := reports.FIITerminalOptions{
		APIKey:  viper.GetString("apikey"),
		DataDir: dataDir,
	}

	r, err := reports.NewFIITerminal(db, opts)
	if err != nil {
		return err
	}

	r.SetParms(parms)

	err = r.Monthly(codes, n)
	if err != nil {
		return err
	}

	return nil
}


================================================
FILE: cmd/rapina/flags.go
================================================
package main

// Flags constants
const (
	// Root persistent
	Fverbose = "verbose"

	// fiiCmd persistent
	Fnum = "num"

	// fiiDividendsCmd
	Fformat = "format"
)


================================================
FILE: cmd/rapina/list.go
================================================
// Copyright © 2018 Adriano P
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package main

import (
	"fmt"

	"github.com/dude333/rapina/reports"
	"github.com/pkg/errors"
	"github.com/spf13/cobra"
)

// listCmd represents the list command
var listCmd = &cobra.Command{
	Use:   "list",
	Short: "Lista informações armazenadas no banco de dados",
}

func init() {
	var (
		listCompanies bool
		sector        string
		netProfitRate float32
	)

	rootCmd.AddCommand(listCmd)

	listCmd.Flags().BoolVarP(&listCompanies, "empresas", "e", false, "Lista todas as empresas disponíveis")
	listCmd.Flags().StringVarP(&sector, "setor", "s", "", "Lista todas as empresas do mesmo setor")
	listCmd.Flags().Float32VarP(&netProfitRate, "lucroLiquido", "l", -0.8, "Lista empresas com lucros lucros positivos e com a taxa de crescimento definida")

	listCmd.Run = func(cmd *cobra.Command, args []string) {
		var err error

		if listCmd.Flags().NFlag() == 0 {
			_ = listCmd.Help()
			return
		}

		if listCompanies {
			err = ListCompanies()
		} else if sector != "" {
			err = ListSector(sector, yamlFile)
		} else if listCmd.Flags().Changed("lucroLiquido") {
			err = ListCompaniesProfits(netProfitRate)
		}
		if err != nil {
			fmt.Println("[x]", err)
		}
	}

}

//
// ListCompanies a company from DB to Excel
//
func ListCompanies() (err error) {
	db, err := openDatabase()
	if err != nil {
		return errors.Wrap(err, "fail to open db")
	}

	com, err := reports.ListCompanies(db)
	if err != nil {
		return errors.Wrap(err, "erro ao listar empresas")
	}
	for _, c := range com {
		fmt.Println(c)
	}

	return
}

//
// ListSector shows all companies from the same sector as 'company'
//
func ListSector(company, yamlFile string) (err error) {
	db, err := openDatabase()
	if err != nil {
		return errors.Wrap(err, "fail to open db")
	}

	err = reports.ListSector(db, company, yamlFile)
	if err != nil {
		return errors.Wrap(err, "erro ao listar empresas")
	}

	return
}

//
// ListCompaniesProfits lists companies profits
//
func ListCompaniesProfits(rate float32) (err error) {
	db, err := openDatabase()
	if err != nil {
		return errors.Wrap(err, "fail to open db")
	}

	err = reports.ListCompaniesProfits(db, rate)
	if err != nil {
		return errors.Wrap(err, "erro ao listar lucros")
	}

	return
}


================================================
FILE: cmd/rapina/main.go
================================================
// Copyright © 2018 Adriano P
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package main

import (
	"fmt"
	"os"
	"os/signal"

	"github.com/dude333/rapina/progress"
	homedir "github.com/mitchellh/go-homedir"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

var flags = struct {
	verbose bool
	fii     fiiFlags
	server  serverFlags
}{}

var cfgFile string

// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
	Use:   "rapina",
	Short: "Dados Financeiros de Empresas via CVM.",
	Long: `
Este  programa  coleta  informações sobre os dados financeiros  do
site da CVM e os exporta para uma planilha. Dados usados:  balanço
patrimonial ativo e passivo, e também o demonstrativo de resultado
do exercício (DRE).`,
	// Uncomment the following line if your bare application
	// has an action associated with it:
	//	Run: func(cmd *cobra.Command, args []string) { },
}

// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() int {
	if err := rootCmd.Execute(); err != nil {
		fmt.Println(err)
		return 1
	}
	return 0
}

func init() {
	cobra.OnInitialize(initConfig)

	// Here you will define your flags and configuration settings.
	// Cobra supports persistent flags, which, if defined here,
	// will be global for your application.
	// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cli.yaml)")

	// Cobra also supports local flags, which will only run
	// when this action is called directly.
	rootCmd.PersistentFlags().BoolVarP(&flags.verbose, Fverbose, "v", false, "Mostrar mensagens de execução")

	str := `Uso:{{if .Runnable}}
  {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}
  {{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}}

Aliases:
  {{.NameAndAliases}}{{end}}{{if .HasExample}}

Exemplos:
  {{.Example}}{{end}}{{if .HasAvailableSubCommands}}

Comandos Disponíveis:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
  {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}

Flags:
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}

Global Flags:
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}

Tópicos de ajuda opcionais:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
  {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}

Use "{{.CommandPath}} [command] --help" para mais informações sobre um comando.{{end}}
`
	rootCmd.SetUsageTemplate(str)
}

// initConfig reads in config file and ENV variables if set.
func initConfig() {
	if cfgFile != "" {
		// Use config file from the flag.
		viper.SetConfigFile(cfgFile)
	} else {
		// Find home directory.
		home, err := homedir.Dir()
		if err != nil {
			fmt.Println(err)
			os.Exit(1)
		}

		// Search config in home directory with name ".rapina" (without extension).
		viper.AddConfigPath(home)
		viper.AddConfigPath(".")
		viper.SetConfigName("config")
	}

	viper.AutomaticEnv() // read in environment variables that match

	// If a config file is found, read it in.
	if err := viper.ReadInConfig(); err == nil {
		fmt.Fprintf(os.Stderr, "[INFO]  Usando arquivo de configuração %s\n\n", viper.ConfigFileUsed())
	}
}

var (
	version string
	build   string
)

func main() {
	fmt.Fprint(os.Stderr, "Rapina - Dados Financeiros de Empresas Brasileiras - ")
	fmt.Fprintf(os.Stderr, "%s-%s\n", version, build)
	fmt.Fprint(os.Stderr, "(2018-2020) github.com/dude333/rapina\n\n")

	progress.Cursor(false)
	defer func() {
		progress.Cursor(true)
		if err := recover(); err != nil { //catch
			fmt.Fprintf(os.Stderr, "Exception: %v\n", err)
			os.Exit(1)
		}
	}()

	// Handle Ctrl+C
	c := make(chan os.Signal, 1)
	signal.Notify(c, os.Interrupt)
	go func() {
		<-c
		progress.Cursor(true)
		os.Exit(0)
	}()

	ret := Execute()
	progress.Cursor(true)
	os.Exit(ret)
}


================================================
FILE: cmd/rapina/report.go
================================================
/// Copyright © 2018 Adriano P
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package main

import (
	"fmt"
	"sort"
	"strings"

	"github.com/dude333/rapina/reports"
	"github.com/lithammer/fuzzysearch/fuzzy"
	"github.com/pkg/errors"
	"github.com/spf13/cobra"
)

// Flags
var scriptMode bool
var all bool
var showShares bool
var extraRatios bool
var fleuriet bool
var omitSector bool
var outputDir = "reports"
var format string // output format of the report

// reportCmd represents the report command
var reportCmd = &cobra.Command{
	Use:   "report [-s] nome_empresa",
	Short: "Cria planilha com dados da companhia escolhida",
	Long:  "Cria planilha com dados da companhia escolhida",
	Args:  cobra.MinimumNArgs(1),
	Run: func(cmd *cobra.Command, args []string) {
		report(args[0])
	},
}

func init() {
	rootCmd.AddCommand(reportCmd)

	reportCmd.Flags().BoolVarP(&scriptMode, "scriptMode", "s", false, "Para modo script (escolhe a empresa com nome mais próximo)")
	reportCmd.Flags().BoolVarP(&all, "all", "a", false, "Mostra todos os indicadores")
	reportCmd.Flags().BoolVarP(&showShares, "showShares", "f", false, "Mostra o número de ações e free float")
	reportCmd.Flags().BoolVarP(&extraRatios, "extraRatios", "x", false, "Reporte de índices extras")
	reportCmd.Flags().BoolVarP(&fleuriet, "fleuriet", "F", false, "Capital de giro no modelo Fleuriet")
	reportCmd.Flags().BoolVarP(&omitSector, "omitSector", "o", false, "Omite o relatório das empresas do mesmo setor")
	reportCmd.Flags().StringVarP(&outputDir, "outputDir", "d", "reports", "Diretório onde o relatório será salvo")
	reportCmd.Flags().StringVarP(&format, "format", "r", "xlsx", "Formato do relatório: xlsx|stdout")
}

func report(company string) {
	var spcfctnCd string = "ON"
	company = SelectCompany(company, scriptMode)
	if company == "" {
		fmt.Println("[x] Empresa não encontrada")
		return
	}
	if strings.Contains(company, "@#") {
		companyWithTicker := strings.Split(company, "@#")
		company = companyWithTicker[0]
		spcfctnCd = companyWithTicker[1]
	}
	fmt.Println()
	fmt.Printf("[√] Criando relatório para %s ========\n", company)

	if all {
		extraRatios = true
		showShares = true
		fleuriet = true
	}

	r := make(map[string]bool)
	r["ExtraRatios"] = extraRatios
	r["ShowShares"] = showShares
	r["Fleuriet"] = fleuriet
	r["PrintSector"] = !omitSector

	parms := Parms{
		Company:   company,
		SpcfctnCd: spcfctnCd,
		Format:    format,
		OutputDir: outputDir,
		YamlFile:  yamlFile,
		Reports:   r,
	}
	err := Report(parms)
	if err != nil {
		fmt.Println("[x]", err)
	}
}

//
// SelectCompany returns the company name compared to the names
// stored in the DB
//
func SelectCompany(company string, scriptMode bool) string {
	db, err := openDatabase()
	if err != nil {
		fmt.Println("[x]", err)
		return ""
	}

	companies, err := reports.ListCompanies(db)
	if err != nil {
		fmt.Println("[x]", err)
		return ""
	}

	// Do a fuzzy match on the company name against
	// all companies listed on the DB
	matches := make([]string, 0, 10)
	for _, c := range companies {
		if fuzzy.MatchNormalizedFold(company, c) {
			matches = append(matches, c)
		}
	}

	// Script mode
	if len(matches) >= 1 && scriptMode {
		rank := fuzzy.RankFindNormalizedFold(company, matches)
		if len(rank) <= 0 {
			return ""
		}
		sort.Sort(rank)
		return rank[0].Target
	}

	// Interactive menu
	if len(matches) >= 1 {
		result := promptUser(matches, "Selecione a Empresa")

		tickers, err := reports.ListTickers(db, result)
		if err != nil {
			fmt.Println("[x] Recuperando lista de tickers ", err)
			return result
		}

		// Interactive menu
		if len(tickers) > 0 {
			ticker := promptUser(tickers, "Selecione o ticker")
			resultWithTicker := fmt.Sprintf("%s@#%s", result, reports.GetSpcfctnCd(db, result, ticker))
			return resultWithTicker
		}

		return result
	}

	return ""
}

//
// Report a company from DB to Excel
//
func Report(p Parms) (err error) {

	db, err := openDatabase()
	if err != nil {
		return errors.Wrap(err, "fail to open db")
	}

	if p.OutputDir == "" {
		p.OutputDir = outputDir
	}

	file, err := filename(p.OutputDir, p.Company)
	if err != nil {
		return err
	}

	parms := map[string]interface{}{
		"db":        db,
		"dataDir":   dataDir,
		"company":   p.Company,
		"SpcfctnCd": p.SpcfctnCd,
		"format":    p.Format,
		"filename":  file,
		"yamlFile":  p.YamlFile,
		"reports":   p.Reports,
	}

	if p.Format == "stdout" {
		return reports.ReportToStdout(parms)
	}

	return reports.ReportToXlsx(parms)
}


================================================
FILE: cmd/rapina/server.go
================================================
/*
Copyright © 2021 Adriano P <dev@dude333.com>
Distributed under the MIT License.
*/
package main

import (
	"log"

	"github.com/dude333/rapina/server"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

type serverFlags struct {
}

// serverCmd represents the server command
var serverCmd = &cobra.Command{
	Use:   "server",
	Short: "Inicia o servidor web",
	Long:  `Comando para iniciar o servidor para a exibição dos dados via web browser.`,
	Run: func(cmd *cobra.Command, args []string) {
		parms := make(map[string]string)
		// Verbose
		if flags.verbose {
			parms[Fverbose] = "true"
		}

		err := serve(parms)
		if err != nil {
			log.Println(err)
		}
	},
}

func init() {
	rootCmd.AddCommand(serverCmd)
	// serverCmd.Flags().IntVarP(&flags.server.num,
	// 	Fnum, "n", 1, "número de meses desde o último disponível")
}

func serve(parms map[string]string) error {

	db, err := openDatabase()
	if err != nil {
		return err
	}

	v := parms[Fverbose] == "true"

	server.HTML(
		server.WithDB(db),
		server.WithAPIKey(viper.GetString("apikey")),
		server.WithDataDir(dataDir),
		server.Verbose(v))

	return nil
}


================================================
FILE: cmd/rapina/update.go
================================================
// Copyright © 2018 Adriano P
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package main

import (
	"errors"
	"fmt"
	"os"

	"github.com/dude333/rapina"
	"github.com/dude333/rapina/fetch"
	"github.com/dude333/rapina/reports"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

var sectors bool

// getUpdate represents the get command
var getUpdate = &cobra.Command{
	Use:     "update",
	Aliases: []string{"get"},
	Short:   "Baixa os arquivos da CVM e atualiza o bando de dados",
	Long:    `Baixa os arquivos do site da CVM, processa e os armazena no bando de dados.`,
	Run: func(cmd *cobra.Command, args []string) {
		db, err := openDatabase()
		if err != nil {
			fmt.Println("[x]", err)
			return
		}

		fmt.Println("[√] Coletando dados ===========")
		err = fetch.Sectors(yamlFile)
		if err != nil && !errors.Is(err, rapina.ErrFileNotUpdated) {
			fmt.Println("[x]", err)
			return
		}
		if err == nil {
			fmt.Println("[√] Arquivo salvo:", yamlFile)
		}
		//
		fmt.Println()
		//
		if sectors { // skip if -s flag is selected (dowload only the sectors)
			return
		}

		err = fetch.CVM(db, dataDir)
		if err != nil {
			fmt.Println("[x]", err)
			return
		}

		// Stock codes
		log := reports.NewLogger(os.Stderr)
		stock, err := fetch.NewStock(db, log, viper.GetString("apikey"), dataDir)
		if err != nil {
			log.Error(err.Error())
			return
		}
		_ = stock.UpdateStockCodes()
	},
}

func init() {
	rootCmd.AddCommand(getUpdate)

	getUpdate.Flags().BoolVarP(&sectors, "sectors", "s", false, "Baixa a classificação setorial das empresas e fundos negociados na B3")
}


================================================
FILE: common.go
================================================
package rapina

import (
	"fmt"
	"net/url"
	"path"
	"strconv"
	"strings"
	"time"
)

// IsDate checks if date is in format YYYY-MM-DD.
func IsDate(date string) bool {
	if len(date) != len("2021-04-26") || strings.Count(date, "-") != 2 {
		return false
	}

	y, errY := strconv.Atoi(date[0:4])
	m, errM := strconv.Atoi(date[5:7])
	d, errD := strconv.Atoi(date[8:10])
	if errY != nil || errM != nil || errD != nil {
		return false
	}

	// Ok, we'll still be using this in 2200 :)
	if y < 1970 || y > 2200 {
		return false
	}
	if m < 1 || m > 12 {
		return false
	}
	nDays := [13]int{0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
	if d < 1 || d > nDays[m] {
		return false
	}
	return true
}

// IsURL returns true if 'str' is a valid URL.
func IsURL(str string) bool {
	u, err := url.Parse(str)
	return err == nil && u.Scheme != "" && u.Host != ""
}

// JoinURL joins strings as URL paths
func JoinURL(base string, paths ...string) string {
	p := path.Join(paths...)
	return fmt.Sprintf("%s/%s", strings.TrimRight(base, "/"), strings.TrimLeft(p, "/"))
}

var _timeNow = time.Now

// MonthsFromToday returns a list of months including the current.
// Date formatted as YYYY-MM.
func MonthsFromToday(n int) []string {
	if n < 1 {
		n = 1
	}
	if n > 100 {
		n = 100
	}

	now := _timeNow()
	now = time.Date(now.Year(), now.Month(), 15, 12, 0, 0, 0, time.UTC)

	var monthYears []string
	for ; n > 0; n-- {
		monthYears = append(monthYears, now.Format("2006-01"))
		now = now.AddDate(0, -1, 0)
	}

	return monthYears
}

// LastBusinessDayOfYear returns the last business day of the 'year' (the business
// day before Dec 30). If current year, returns last business day before today.
// Returns date as YYYY-MM-DD.
func LastBusinessDayOfYear(year int) string {
	today := time.Now()
	if year == today.Year() {
		return LastBusinessDay(1)
	}

	date := time.Date(year, time.December, 29, 12, 0, 0, 0, time.UTC)

	if date.Weekday() == time.Saturday {
		date = date.AddDate(0, 0, -1)
	}
	if date.Weekday() == time.Sunday {
		date = date.AddDate(0, 0, -2)
	}

	return date.Format("2006-01-02")
}

// LastBusinessDay returns the most recent business day 'n' days before today.
// Returns date as YYYY-MM-DD.
func LastBusinessDay(n int) string {
	date := time.Now()
	if n > 0 {
		date = date.AddDate(0, 0, -n)
	}

	if date.Weekday() == time.Saturday {
		date = date.AddDate(0, 0, -1)
	}
	if date.Weekday() == time.Sunday {
		date = date.AddDate(0, 0, -2)
	}

	return date.Format("2006-01-02")
}


================================================
FILE: common_test.go
================================================
package rapina

import (
	"reflect"
	"testing"
	"time"
)

func TestIsDate(t *testing.T) {
	type args struct {
		date string
	}
	tests := []struct {
		name string
		args args
		want bool
	}{
		{
			name: "should be true",
			args: args{date: "2021-04-26"},
			want: true,
		},
		{
			name: "should be true too",
			args: args{date: "2030-12-31"},
			want: true,
		},
		{
			name: "should be false",
			args: args{date: "2021-04-31"},
			want: false,
		},
		{
			name: "should be false too",
			args: args{date: "20/12/2000"},
			want: false,
		},
		{
			name: "should be false three",
			args: args{date: "2021-07-32"},
			want: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := IsDate(tt.args.date); got != tt.want {
				t.Errorf("IsDate() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestIsUrl(t *testing.T) {
	type args struct {
		str string
	}
	tests := []struct {
		name string
		args args
		want bool
	}{
		{
			name: "should be true",
			args: args{str: "http://example.com/path"},
			want: true,
		},
		{
			name: "should be false",
			args: args{str: "example.com/path"},
			want: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := IsURL(tt.args.str); got != tt.want {
				t.Errorf("IsUrl() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestMonthsFromToday(t *testing.T) {
	timeNow1 := func() time.Time {
		return time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
	}
	timeNow2 := func() time.Time {
		return time.Date(2009, time.March, 31, 23, 0, 0, 0, time.UTC)
	}

	type args struct {
		n int
	}
	tests := []struct {
		name    string
		args    args
		timeNow func() time.Time
		want    []string
	}{
		{
			name:    "should show 3 months",
			args:    args{n: 3},
			timeNow: timeNow1,
			want:    []string{"2009-11", "2009-10", "2009-09"},
		},
		{
			name:    "should show 2 months",
			args:    args{n: 2},
			timeNow: timeNow2,
			want:    []string{"2009-03", "2009-02"},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			_timeNow = tt.timeNow
			if got := MonthsFromToday(tt.args.n); !reflect.DeepEqual(got, tt.want) {
				t.Errorf("MonthsFromToday() = %#v, want %v", got, tt.want)
			}
		})
	}
}

func TestLastBusinessDayOfYear(t *testing.T) {
	type args struct {
		year int
	}
	tests := []struct {
		name string
		args args
		want string
	}{
		{
			name: "2022",
			args: args{2022},
			want: "2022-12-29",
		},
		{
			name: "2020",
			args: args{2020},
			want: "2020-12-29",
		},
		{
			name: "2017",
			args: args{2017},
			want: "2017-12-29",
		},
		{
			name: "2016",
			args: args{2016},
			want: "2016-12-29",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := LastBusinessDayOfYear(tt.args.year); got != tt.want {
				t.Errorf("LastBusinessDayOfYear() = %v, want %v", got, tt.want)
			}
		})
	}
}


================================================
FILE: errors.go
================================================
package rapina

import "errors"

// Error codes
var (
	ErrRecordExists   = errors.New("insert ignored, register already exists")
	ErrFileNotUpdated = errors.New("file not updated")
	ErrInvalidAPIKey  = errors.New("apiKey inválida, configure uma chave em" +
		" https://www.alphavantage.co/support/#api-key e adicione no arquivo" +
		" config.yml")
	ErrInvalidDate = errors.New("invalid date format")
)


================================================
FILE: fetch/fetch.go
================================================
// Copyright © 2018 Adriano P
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package fetch

import (
	"database/sql"
	"fmt"
	"io"
	"net/http"
	"os"
	"path"
	"path/filepath"
	"strings"
	"time"

	"github.com/dude333/rapina/parsers"
	"github.com/dustin/go-humanize"
	_ "github.com/mattn/go-sqlite3" // requires CGO_ENABLED=1 and gcc
	"github.com/pkg/errors"
)

var (
	// ErrFileNotFound error
	ErrFileNotFound = errors.New("file not found")
	// ErrItemNotFound for string not found on []string
	ErrItemNotFound = errors.New("item not found")
)

//
// CVM fetches all statements from a range
// of years
//
func CVM(db *sql.DB, dataDir string) error {
	now := time.Now().Year()
	try(processQuarterlyReport, db, dataDir, "Arquivo ITR não encontrado", now, now-1, 2)
	try(processAnnualReport, db, dataDir, "Arquivo DFP não encontrado", now-1, 2010, 2)
	try(processFREReport, db, dataDir, "Arquivo FRE não encontrado", now-1, 2010, 2)

	return nil
}

type fn func(*sql.DB, string, int) error

// try to run the function 'f' 'n' times, in case there are network errors.
func try(f fn, db *sql.DB, dataDir, errMsg string, now, limit, n int) {
	tries := n
	var err error

	for year := now; tries > 0 && year >= limit; year-- {
		fmt.Printf("[>] %d ---------------------\n", year)
		err = f(db, dataDir, year)
		if err == ErrFileNotFound {
			fmt.Printf("[x] %s\n", errMsg)
			tries--
			continue
		} else if err != nil {
			fmt.Printf("[x] Erro ao processar arquivo de %d: %v\n", year, err)
			tries--
		} else {
			tries = n
		}
	}
}

// processAnnualReport will get data from .zip files downloaded
// directly from CVM and insert its data into the DB
func processAnnualReport(db *sql.DB, dataDir string, year int) error {

	url := fmt.Sprintf("http://dados.cvm.gov.br/dados/CIA_ABERTA/DOC/DFP/DADOS/dfp_cia_aberta_%d.zip", year)
	zipfile := fmt.Sprintf("%s/dfp_%d.zip", dataDir, year)

	// Download files from CVM server
	fmt.Print("[          ] Download do arquivo DFP")
	files, err := fetchFiles(url, dataDir, zipfile)
	if err != nil {
		return err
	}

	dataTypes := []string{"BPA", "BPP", "DRE", "DFC_MD", "DFC_MI", "DVA"}

	for _, dt := range dataTypes {
		pattern := fmt.Sprintf("dfp_cia_aberta_%s_con_%d.csv", dt, year)
		reqFile, err := findFile(files, pattern)
		if err == ErrItemNotFound {
			filesCleanup(files)
			return fmt.Errorf("arquivo %s não encontrado", reqFile)
		}

		// Import file into DB
		if err = parsers.ImportCsv(db, dt, reqFile); err != nil {
			return err
		}
	}

	filesCleanup(files) // remove remaining (unused) files

	return nil
}

//
// processQuarterlyReport download quarter files from CVM and store them on DB
//
func processQuarterlyReport(db *sql.DB, dataDir string, year int) error {

	url := fmt.Sprintf("http://dados.cvm.gov.br/dados/CIA_ABERTA/DOC/ITR/DADOS/ITR_CIA_ABERTA_%d.zip", year)
	zipfile := fmt.Sprintf("%s/itr_%d.zip", dataDir, year)

	// Download files from CVM server
	fmt.Print("[          ] Download do arquivo ITR")
	files, err := fetchFiles(url, dataDir, zipfile)
	if err != nil {
		return err
	}

	dataTypes := []string{"BPA", "BPP", "DRE", "DFC_MD", "DFC_MI", "DVA"}

	for _, dt := range dataTypes {
		pattern := fmt.Sprintf("ITR_CIA_ABERTA_%s_con_%d.csv", dt, year)
		reqFile, err := findFile(files, pattern)
		if err == ErrItemNotFound {
			filesCleanup(files)
			return fmt.Errorf("arquivo %s não encontrado", reqFile)
		}

		// Import file into DB (the trick is to add ITR to the data type so the
		// ImportCSV loads that into the ITR table)
		if err = parsers.ImportCsv(db, dt+"_ITR", reqFile); err != nil {
			return err
		}
	}

	filesCleanup(files) // remove remaining (unused) files

	return nil
}

//
// processFREReport download FRE (Reference Form) files from CVM and store
// them on DB.
//
func processFREReport(db *sql.DB, dataDir string, year int) error {
	url := fmt.Sprintf("http://dados.cvm.gov.br/dados/CIA_ABERTA/DOC/FRE/DADOS/fre_cia_aberta_%d.zip", year)
	zipfile := fmt.Sprintf("%s/fre_%d.zip", dataDir, year)

	// Download files from CVM server
	fmt.Print("[          ] Download do arquivo FRE")
	files, err := fetchFiles(url, dataDir, zipfile)
	if err != nil {
		return err
	}

	patterns := []string{"fre_cia_aberta_distribuicao_capital_%d.csv"}

	for _, p := range patterns {
		pattern := fmt.Sprintf(p, year)
		reqFile, err := findFile(files, pattern)
		if err == ErrItemNotFound {
			filesCleanup(files)
			return fmt.Errorf("arquivo %s não encontrado", reqFile)
		}

		if err = parsers.ImportCsv(db, "FRE", reqFile); err != nil {
			return err
		}

	}

	filesCleanup(files) // remove remaining (unused) files

	return nil
}

//
// fetchFiles from web verbosely.
//
func fetchFiles(url, dataDir string, zipfile string) ([]string, error) {
	return fetchFilesVerbosity(url, dataDir, zipfile, true)
}

//
// fetchFilesVerbosity from web.
//
func fetchFilesVerbosity(url, dataDir string, zipfile string, verbose bool) ([]string, error) {

	// Download file from web
	err := downloadFile(url, zipfile, verbose)
	if verbose {
		fmt.Println()
	}
	if err != nil {
		return nil, ErrFileNotFound
	}

	// Unzip and list files
	files, err := Unzip(zipfile, dataDir, verbose)
	os.Remove(zipfile)
	if err != nil {
		return nil, errors.Wrap(err, "could not unzip file")
	}

	return files, nil
}

// WriteCounter counts the number of bytes written the io.Writer.
// source: https://golangcode.com/download-a-file-with-progress/
type WriteCounter struct {
	Total uint64
}

// Write implements the io.Writer interface and will be passed to io.TeeReader().
func (wc *WriteCounter) Write(p []byte) (int, error) {
	n := len(p)
	wc.Total += uint64(n)
	wc.printProgress()
	return n, nil
}

func (wc WriteCounter) printProgress() {
	fmt.Printf("\r[  %7s", humanize.Bytes(wc.Total))
}

//
// downloadFile source: https://stackoverflow.com/a/33853856/276311
//
func downloadFile(url, filepath string, verbose bool) (err error) {
	// Create dir if necessary
	basepath := path.Dir(filepath)
	if err = os.MkdirAll(basepath, os.ModePerm); err != nil {
		return err
	}

	// Create the file
	out, err := os.Create(filepath)
	if err != nil {
		return err
	}

	// https://www.joeshaw.org/dont-defer-close-on-writable-files/
	defer func() {
		cerr := out.Close()
		if err == nil {
			err = cerr
		}
	}()

	// Get the data
	resp, err := http.Get(url)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	// Check server response
	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("bad status: %s", resp.Status)
	}

	// Write the body to file
	counter := io.Discard
	if verbose {
		counter = &WriteCounter{}
	}
	_, err = io.Copy(out, io.TeeReader(resp.Body, counter))
	if err != nil {
		return err
	}

	return
}

//
// Sectors checks if the configuration file is already populated.
// If 'force' is set or if the config is empty, it retrieves data from B3,
// unzip and extract a spreadsheet containing a list of companies divided by
// sector, subsector, and segment; then this info is set into the config file.
//
func Sectors(yamlFile string) (err error) {
	err = parsers.SectorsToYaml(yamlFile)

	return
}

//
// filesCleanup
//
func filesCleanup(files []string) {
	// Clean up
	for _, f := range files {
		if err := os.Remove(f); err != nil {
			fmt.Println("could not delete file", f)
		}
	}
}

//
// findFile finds an item on list that matches pattern (case insensitive)
//
func findFile(list []string, pattern string) (string, error) {

	for i := range list {
		f := filepath.Base(list[i])
		if strings.EqualFold(f, pattern) {
			return list[i], nil
		}
	}

	return "", ErrItemNotFound
}


================================================
FILE: fetch/fetch_fii.go
================================================
package fetch

/*
	URL List:

	Fundos.NET: where the report IDs are obtained.
	=> https://fnet.bmfbovespa.com.br/fnet/publico/pesquisarGerenciadorDocumentosCVM?paginaCertificados=false&tipoFundo=1
	=> GET
	https://fnet.bmfbovespa.com.br/fnet/publico/pesquisarGerenciadorDocumentosDados?d=3&s=0&l=10&o[0][dataEntrega]=desc&tipoFundo=1&idCategoriaDocumento=14&idTipoDocumento=41&idEspecieDocumento=0&situacao=A&cnpj=28737771000185&dataInicial=01/02/2021&dataFinal=28/02/2021&_=1619467786288
*/

import (
	"bytes"
	"crypto/tls"
	"database/sql"
	"encoding/base64"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

	"github.com/dude333/rapina"
	"github.com/dude333/rapina/parsers"
	"github.com/dude333/rapina/progress"
	"github.com/gocolly/colly/v2"
	"github.com/pkg/errors"
	"golang.org/x/net/html"
)

const MAX_N = 100

// FII holds the infrastructure data.
type FII struct {
	storage rapina.FIIStorage
}

// NewFII creates a new instace of FII.
func NewFII(db *sql.DB, log rapina.Logger) (*FII, error) {
	storage, err := parsers.NewFII(db, log)
	if err != nil {
		return nil, err
	}

	fii := &FII{
		storage: storage,
	}
	return fii, nil
}

type id int

// Report holds the result of all documents filtered by a criteria defined by a
// http.Get on the B3 server.
type Report struct {
	Data []docID `json:"data"`
}
type docID struct {
	ID          id     `json:"id"`
	Description string `json:"descricaoFundo"`
	DocType     string `json:"tipoDocumento"`
	Status      string `json:"situacaoDocumento"`
}

// Dividends gets the report IDs for one company ('cnpj') and then the
// yeld montlhy report for 'n' months, starting from the latest released.
func (fii FII) Dividends(code string, n int) (*[]rapina.Dividend, error) {
	dividends, months, err := fii.dividendsFromDB(code, n)
	if err == nil {
		if months >= n {
			return dividends, err
		}
	}

	dividends, err = fii.dividendsFromServer(code, n)
	if err != nil {
		return nil, err
	}
	for _, d := range *dividends {
		err := fii.storage.SaveDividend(d) // Save dividends to DB
		if err != nil {
			progress.ErrorMsg("Erro ao salvar dividendos no banco de dados: %s - %v", err, d)
		}
	}

	// Load dividends from DB to filter results
	dividends, _, err = fii.dividendsFromDB(code, n)
	return dividends, err
}

func (fii FII) dividendsFromDB(code string, n int) (*[]rapina.Dividend, int, error) {
	var dividends []rapina.Dividend
	var months int
	for _, monthYear := range rapina.MonthsFromToday(n + 2) {
		d, err := fii.storage.Dividends(code, monthYear)
		if err == nil { // ignore errors
			dividends = append(dividends, *d...)
			months++
		}
		if months == n {
			break
		}
	}

	if len(dividends) == 0 {
		return nil, 0, errors.New("dividendos não encontrados")
	}

	return &dividends, months, nil
}

// Dividends gets the report IDs for one company ('cnpj') and then the
// yeld montlhy report for 'n' months, starting from the latest released.
//
// If the number of reports does not match n, it'll retry with a bigger n as
// sometimes reports from follow-on offerings (FPO).
func (fii *FII) dividendsFromServer(code string, n int) (*[]rapina.Dividend, error) {
	n = int(float64(n) * 1.25)
	if n > MAX_N {
		n = MAX_N
	}

	ids, err := fii.reportIDs(repDividends, code, n)
	if err != nil {
		return nil, err
	}
	progress.Debug("Report IDs: %v", ids)

	progress.Status("Relatórios de dividendos: %s", code)
	dividends, err := fii.dividendReport(code, ids)
	if err != nil {
		return nil, err
	}

	return dividends, nil
}

// dividendReport parses the dividend reports and returns their dividends.
func (fii *FII) dividendReport(code string, ids []id) (*[]rapina.Dividend, error) {
	var dividends []rapina.Dividend

	// HTTP client setup
	client := &http.Client{
		Transport: &http.Transport{
			TLSClientConfig: &tls.Config{
				InsecureSkipVerify: true,
			},
			MaxIdleConnsPerHost: 10,
		},
	}

	for _, id := range ids {
		url := fmt.Sprintf("https://fnet.bmfbovespa.com.br/fnet/publico/exibirDocumento?id=%d&cvm=true", id)
		progress.Debug("GET %s", url)

		// Make HTTP request
		req, err := http.NewRequest("GET", url, nil)
		if err != nil {
			return nil, err
		}

		// Reuse the same client for subsequent requests
		resp, err := client.Do(req)
		if err != nil {
			return nil, err
		}

		if resp.StatusCode != http.StatusOK {
			resp.Body.Close()
			return nil, errors.Wrapf(err, "unexpected status code: %d", resp.StatusCode)
		}

		// Read response body
		body, err := io.ReadAll(resp.Body)
		resp.Body.Close()
		if err != nil {
			return nil, err
		}

		// Decode base64 encoded body
		decodedBody, err := base64.StdEncoding.DecodeString(strings.Trim(string(body), `"`))
		if err != nil {
			return nil, err
		}

		doc, err := html.Parse(bytes.NewReader(decodedBody))
		if err != nil {
			return nil, errors.Wrap(err, "error parsing HTML: %s")
		}

		var data []string
		var extractData func(*html.Node)
		extractData = func(n *html.Node) {
			if n.Type == html.ElementNode && n.Data == "td" {
				text := getTextContent(n)
				data = append(data, text)
			}
			for c := n.FirstChild; c != nil; c = c.NextSibling {
				extractData(c)
			}
		}
		extractData(doc)

		// Store dividend
		if d, ok := parseData(data); ok {
			dividends = append(dividends, d)
		}
	}

	return &dividends, nil
}

func parseData(data []string) (rapina.Dividend, bool) {
	dividend := rapina.Dividend{}
	fieldName := ""
	count := 0
	for _, str := range data {
		if fieldName == "" {
			if str != "" {
				fieldName = str
			}
			continue
		}
		if strings.Contains(fieldName, "Código de negociação") {
			dividend.Code = str
			count++
		} else if strings.Contains(fieldName, "Data-base") {
			dividend.Date = fixDate(str)
			count++
		} else if strings.Contains(fieldName, "Data do pagamento") {
			dividend.PaymentDate = fixDate(str)
			count++
		} else if strings.Contains(fieldName, "Valor do provento") {
			dividend.Val = comma2dot(str)
			count++
		}
		fieldName = ""
	}

	return dividend, count == 4 // false if not all fields are filled
}

func comma2dot(val string) float64 {
	a := strings.ReplaceAll(val, ".", "")
	b := strings.ReplaceAll(a, ",", ".")
	n, _ := strconv.ParseFloat(b, 64)
	return n
}

// fixDate converts dates from DD/MM/YYYY to YYYY-MM-DD.
func fixDate(date string) string {
	if len(date) != len("26/04/2021") || strings.Count(date, "/") != 2 {
		return date
	}

	return date[6:10] + "-" + date[3:5] + "-" + date[0:2]
}

func getTextContent(n *html.Node) string {
	textContent := ""
	if n == nil {
		return ""
	}
	if n.Type == html.TextNode {
		return n.Data
	}
	for c := n.FirstChild; c != nil; c = c.NextSibling {
		textContent += getTextContent(c)
	}
	return strings.TrimSpace(textContent)
}

func (fii *FII) MonthlyReportIDs(code string, n int) ([]id, error) {
	ids, err := fii.reportIDs(repMonthly, code, n)
	if err != nil {
		return []id{}, err
	}
	_, err = fii.monthlyReport(code, ids)
	if err != nil {
		return []id{}, err
	}

	return ids, nil
}

// monthlyReport parses the FII monthly reports.
func (fii *FII) monthlyReport(code string, ids []id) (*[]rapina.Monthly, error) {
	yeld := make(map[string]string, len(ids))

	c := colly.NewCollector()
	c.WithTransport(&http.Transport{
		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
	})

	c.OnRequest(func(r *colly.Request) {
		r.Headers.Set("Accept", "text/html")
	})

	c.OnError(func(r *colly.Response, err error) {
		progress.ErrorMsg("Request URL: %v failed with response: %v\nError: %v", r.Request.URL, string(r.Body), err)
	})

	// Handles the html report
	c.OnHTML("tr", func(e *colly.HTMLElement) {
		var fieldName string
		e.ForEach("td", func(_ int, el *colly.HTMLElement) {
			v := strings.Trim(el.Text, " \r\n")
			progress.Debug("%q", v)
			if v != "" {
				if fieldName == "" {
					if v[0] < '0' || v[0] > '9' { // Ignore fields starting with number
						fieldName = v
					}
				} else {
					fmt.Printf("%-30s => %s\n", fieldName, v)
					yeld[fieldName] = v
					fieldName = ""
				}
			}
		})
		progress.Status("----------------------")
	})

	// Get the yeld monthly report given the list of 'report IDs' -- returns HTML
	monthly := make([]rapina.Monthly, 0, len(ids))
	for _, id := range ids {
		u := fmt.Sprintf("https://fnet.bmfbovespa.com.br/fnet/publico/exibirDocumento?id=%d&cvm=true", id)
		progress.Debug(u)
		if err := c.Visit(u); err != nil {
			return nil, err
		}
		// d, err := fii.storage.SaveDividend(yeld)
		// if err != nil {
		// 	fii.log.Error("%v", err)
		// 	continue
		// }
		// // fmt.Println("from server", d.Code, d.Date, d.Val)
		// if d.Code == code {
		// 	monthly = append(monthly, *d)
		// }
	}

	return &monthly, nil
}

// Details returns the FII Details from DB. If not found:
// fetches from server, stores it in the DB and returns the Details.
func (fii *FII) Details(fiiCode string) (*rapina.FIIDetails, error) {
	if len(fiiCode) != 4 && len(fiiCode) != 6 {
		return nil, fmt.Errorf("wrong code '%s'", fiiCode)
	}

	details, err := fii.storage.Details(fiiCode)
	if err == nil && details.DetailFund.CNPJ != "" {
		return details, nil
	}

	progress.Warning("Detalhes do %s não encontrado no bd. Consultando web...", fiiCode)

	// Fetch from server if not found in the database
	data := fmt.Sprintf(`{"typeFund":7,"cnpj":"0","identifierFund":"%s"}`, fiiCode[0:4])
	enc := base64.URLEncoding.EncodeToString([]byte(data))
	fundDetailURL := rapina.JoinURL(
		`https://sistemaswebb3-listados.b3.com.br/fundsProxy/fundsCall/GetDetailFundSIG/`,
		enc,
	)

	tr := &http.Transport{
		DisableCompression: true,
		IdleConnTimeout:    _http_timeout,
		TLSClientConfig:    &tls.Config{InsecureSkipVerify: true},
	}
	client := &http.Client{Transport: tr}

	resp, err := client.Get(fundDetailURL)
	if err != nil {
		return details, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		return details, fmt.Errorf("%s: %s", resp.Status, fundDetailURL)
	}

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, errors.Wrapf(err, "FII Details(%s): reading body", fiiCode)
	}

	err = fii.storage.SaveDetails(body)
	if err != nil {
		return details, errors.Wrap(err, "armazenando detalhes do FII")
	}

	return fii.storage.Details(fiiCode)
}

// Report type
type repType int

const (
	repMonthly repType = iota + 1
	repDividends
)

func (fii *FII) reportIDs(rt repType, code string, n int) ([]id, error) {
	n = minmax(n, 1, MAX_N)

	// Parameters to list the report IDs for the last 'n' dividend reports
	timestamp := strconv.FormatInt(int64(time.Now().UnixNano()/1e6), 10)
	nMonthAgo := time.Now()
	nMonthAgo = nMonthAgo.AddDate(0, -n, -nMonthAgo.Day()+1)
	det, err := fii.Details(code)
	if err != nil {
		return nil, err
	}
	cnpj := det.DetailFund.CNPJ

	var idTipoDocumento, idCategoriaDocumento, d string
	if rt == repMonthly {
		idTipoDocumento = "40"
		idCategoriaDocumento = "6"
		d = "0"
	} else if rt == repDividends {
		idTipoDocumento = "41"
		idCategoriaDocumento = "14"
		d = "2"
	} else {
		return []id{}, errors.New("invalid report type")
	}

	v := url.Values{
		"tipoFundo":            []string{"1"},
		"cnpjFundo":            []string{cnpj},
		"idTipoDocumento":      []string{idTipoDocumento},
		"idCategoriaDocumento": []string{idCategoriaDocumento},
		"d":                    []string{d},
		"idEspecieDocumento":   []string{"0"},
		"situacao":             []string{"A"},
		"s":                    []string{"0"},
		"l":                    []string{"200"}, // 'n*2' latest reports as other codes may appear (e.g.:ABCD11, ABCD12, ABCD13...)
		"dataFinal":            []string{time.Now().Format("02/01/2006")},
		"dataInicial":          []string{nMonthAgo.Format("02/01/2006")},
		"o[0][dataReferencia]": []string{"asc"},
		"_":                    []string{timestamp},
	}

	// Get the 'report IDs' for a given company (CNPJ) -- returns JSON
	var report Report
	u := "https://fnet.bmfbovespa.com.br/fnet/publico/pesquisarGerenciadorDocumentosDados?" +
		v.Encode()
	progress.Debug("* Report IDs: %s", u)
	if err := getJSON(u, &report); err != nil {
		return nil, err
	}

	var ids []id
	for _, d := range report.Data {
		if d.Status == "A" {
			ids = append(ids, d.ID)
		}
	}

	return ids, nil
}

// minmax returns n limited to [min, max]
func minmax(n, min, max int) int {
	if n < min {
		n = min
	}
	if n > max {
		n = MAX_N
	}
	return n
}


================================================
FILE: fetch/fetch_fii_test.go
================================================
package fetch

import (
	"testing"
)

func Test_comma2dot(t *testing.T) {
	type args struct {
		val string
	}
	tests := []struct {
		name string
		args args
		want float64
	}{
		{
			name: "should work",
			args: args{val: "1.230,56"},
			want: 1230.56,
		},
		{
			name: "should return 0",
			args: args{val: "shouldbeanum"},
			want: 0,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := comma2dot(tt.args.val); got != tt.want {
				t.Errorf("comma2dot() = %v, want %v", got, tt.want)
			}
		})
	}
}

func Test_FixDate(t *testing.T) {
	type args struct {
		date string
	}
	tests := []struct {
		name string
		args args
		want string
	}{
		{
			name: "should work",
			args: args{date: "01/02/2021"},
			want: "2021-02-01",
		},
		{
			name: "should return the input",
			args: args{date: "wrong/date"},
			want: "wrong/date",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := fixDate(tt.args.date); got != tt.want {
				t.Errorf("fixDate() = %v, want %v", got, tt.want)
			}
		})
	}
}


================================================
FILE: fetch/fetch_http.go
================================================
package fetch

import (
	"crypto/tls"
	"encoding/json"
	"fmt"
	"net/http"
	"time"

	"github.com/dude333/rapina/progress"
)

const _http_timeout = 30 * time.Second

// HTTPFetch implements a generic HTTP fetcher.
type HTTPFetch struct {
	client *http.Client
}

// NewHTTP creates a new HTTPFetch instance.
func NewHTTP() *HTTPFetch {
	c := &http.Client{Timeout: _http_timeout}
	return &HTTPFetch{client: c}
}

// JSON handles json responses.
func (h HTTPFetch) JSON(url string, target interface{}) error {
	r, err := h.client.Get(url)
	if err != nil {
		return err
	}
	defer r.Body.Close()

	// for _, c := range r.Cookies() {
	// 	fmt.Printf("COOKIE: %+v\n", c)
	// }

	return json.NewDecoder(r.Body).Decode(target)
}

func getJSON(url string, target interface{}) error {
	c := &http.Client{
		Timeout: _http_timeout,
		Transport: &http.Transport{
			DisableCompression: true,
			IdleConnTimeout:    _http_timeout,
			TLSClientConfig:    &tls.Config{InsecureSkipVerify: true},
		},
	}

	r, err := c.Get(url)
	if err != nil {
		return err
	}
	if r.StatusCode < 200 || r.StatusCode >= 300 {
		return fmt.Errorf("unexpected status code: %d", r.StatusCode)
	}

	defer func() {
		if err := r.Body.Close(); err != nil {
			progress.ErrorMsg("Failed to close response body: %v", err)
		}
	}()

	return json.NewDecoder(r.Body).Decode(target)
}


================================================
FILE: fetch/fetch_http_test.go
================================================
package fetch

import (
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/stretchr/testify/assert"
)

var ts *httptest.Server

func init() {
	handler := http.NewServeMux()
	handler.HandleFunc("/server/api/v1/json", jsonsMock)

	ts = httptest.NewServer(handler)
}

func jsonsMock(w http.ResponseWriter, r *http.Request) {
	_, _ = w.Write([]byte(`{"text": "mock"}`))
}

type jsonData struct {
	Text string `json:"text"`
}

func TestHTTPFetch_JSON(t *testing.T) {
	h := NewHTTP()

	var got jsonData

	err := h.JSON(ts.URL+"/server/api/v1/json", &got)

	assert.Equal(t, jsonData{Text: "mock"}, got)
	assert.Nil(t, err)
}


================================================
FILE: fetch/fetch_stock.go
================================================
package fetch

import (
	"crypto/tls"
	"database/sql"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"time"

	"github.com/dude333/rapina"
	"github.com/dude333/rapina/parsers"
	"github.com/dude333/rapina/progress"
	"github.com/pkg/errors"
	"golang.org/x/text/encoding/charmap"
	"golang.org/x/text/transform"
)

// API providers
const (
	APInone = iota
	APIalphavantage
	APIyahoo
)

// Stock implements a fetcher for stock info.
type Stock struct {
	apiKey  string // API key for Alpha Vantage API server
	store   rapina.StockStorage
	cache   map[string]int // Cache to avoid duplicated fetch on Alpha Vantage server
	dataDir string         // working directory where files will be stored to be parsed
	log     rapina.Logger
}

//
// NewStock returns a new instance of *Stock
//
func NewStock(db *sql.DB, log rapina.Logger, apiKey, dataDir string) (*Stock, error) {
	store, err := parsers.NewStock(db, log)
	if err != nil {
		return nil, err
	}

	return &Stock{
		apiKey:  apiKey,
		store:   store,
		cache:   make(map[string]int),
		dataDir: dataDir,
		log:     log,
	}, nil
}

// Quote returns the quote for 'code' on 'date'.
// Date format: YYYY-MM-DD.
func (s *Stock) Quote(code, date string) (float64, error) {
	if len(code) < len("CODE3") {
		return 0, fmt.Errorf("código inválido: %q", code)
	}
	if !rapina.IsDate(date) {
		return 0, fmt.Errorf("data inválida: %q", date)
	}

	val, err := s.store.Quote(code, date)
	if err == nil {
		return val, nil // returning data found on db
	}

	// Load quotes from B3
	if err := s.stockQuoteFromB3(date); ifNot(err) {
		if val, err = s.store.Quote(code, date); ifNot(err) {
			return val, nil // returning data found on B3
		}
	}

	// Fallback to Yahoo Finance if not found on B3
	if err := s.stockQuoteFromAPIServer(code, date, APIyahoo); ifNot(err) {
		if val, err = s.store.Quote(code, date); ifNot(err) {
			return val, nil // returning data found on Yahoo
		}
	}

	errNoProvider := errors.New("cotação não encontrada em nenhum provedor (B3, Yahoo e Alpha Vantage)")
	if s.apiKey == "" {
		errNoProvider = errors.New("cotação não encontrada em nenhum provedor (B3 e Yahoo)")
	}

	// Fallback to Alpha Vantage if not found on B3 and Yahoo
	if s.apiKey == "" {
		return 0, errNoProvider
	}
	if err := s.stockQuoteFromAPIServer(code, date, APIalphavantage); err != nil {
		return 0, errNoProvider
	}
	// Last try: return quote loaded by Alpha Vantage
	val, err = s.store.Quote(code, date)
	if err != nil {
		return 0, errNoProvider
	}

	return val, nil
}

//
// stockQuoteFromB3 downloads the quotes for all companies for the given date,
// where 'date' format is YYYY-MM-DD.
//
func (s *Stock) stockQuoteFromB3(date string) error {
	// Convert date string from YYYY-MM-DD to DDMMYYYY
	if len(date) != len("2021-05-03") {
		return fmt.Errorf("data com formato inválido: %s", date)
	}
	conv := date[8:10] + date[5:7] + date[0:4]
	url := fmt.Sprintf(`http://bvmf.bmfbovespa.com.br/InstDados/SerHist/COTAHIST_D%s.ZIP`,
		conv)
	// Download ZIP file and unzips its files
	zip := fmt.Sprintf("%s/COTAHIST_D%s.ZIP", s.dataDir, conv)
	files, err := fetchFilesVerbosity(url, s.dataDir, zip, false)
	if err != nil {
		return err
	}

	// Delete files on return
	defer filesCleanup(files)

	// Parse and store files content
	for _, f := range files {
		fh, err := os.Open(f)
		if err != nil {
			return errors.Wrapf(err, "abrindo arquivo %s", f)
		}
		defer fh.Close()

		dec := transform.NewReader(fh, charmap.ISO8859_1.NewDecoder())
		if _, err := s.store.Save(dec, ""); err != nil {
			return err
		}
	}

	return nil
}

//
// stockQuoteFromAPIServer fetches the daily time series (date, daily open, daily high,
// daily low, daily close, daily volume) of the global equity specified,
// covering 20+ years of historical data.
//
func (s *Stock) stockQuoteFromAPIServer(code, date string, apiProvider int) error {
	if v := s.cache[code]; v == APIalphavantage && apiProvider == APIalphavantage {
		return nil // silent return if this fetch has been run already
	}

	// Download quote for 'code'
	tr := &http.Transport{
		DisableCompression: true,
		IdleConnTimeout:    _http_timeout,
		TLSClientConfig:    &tls.Config{InsecureSkipVerify: true},
	}
	client := &http.Client{Transport: tr}
	u := apiURL(apiProvider, s.apiKey, code, date)
	if u == "" {
		return errors.New("URL do API server")
	}
	resp, err := client.Get(u)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("%s", resp.Status)
	}

	s.cache[code] = apiProvider // mark map to avoid unnecessary downloads

	// JSON means error response
	if resp.Header.Get("Content-Type") == "application/json" {
		jsonMap := make(map[string]interface{})
		err := json.NewDecoder(resp.Body).Decode(&jsonMap)
		if err != nil {
			return err
		}
		return errors.New(map2str(jsonMap))
	}

	progress.Running("Armazendo cotações no banco de dados...")
	_, err = s.store.Save(resp.Body, code)
	if err != nil {
		progress.RunFail()
		return errors.Wrapf(err, "armazenando cotações de %s", code)
	}
	progress.RunOK()

	return err
}

func (s *Stock) Code(companyName, stockType string) (string, error) {
	if val, err := s.store.Code(companyName, stockType); err == nil {
		return val, nil // returning data found on db
	}

	if err := s.UpdateStockCodes(); err != nil {
		return "", err
	}

	return s.store.Code(companyName, stockType)
}

type b3CodesFile struct {
	RedirectURL string `json:"redirectUrl"`
	Token       string `json:"token"`
	File        struct {
		Name      string `json:"name"`
		Extension string `json:"extension"`
	} `json:"file"`
}

//
// UpdateStockCodes get the most recent file from B3.com.br with the stock trading code and
// saves them on the storage.
//
func (s *Stock) UpdateStockCodes() error {
	// Get file url
	var f b3CodesFile
	url := `https://arquivos.b3.com.br/api/download/requestname?fileName=InstrumentsConsolidated&date=`
	url += rapina.LastBusinessDay(2)
	h := NewHTTP()
	err := h.JSON(url, &f)
	if err != nil {
		return err
	}

	// Download file
	fp := fmt.Sprintf("%s/codes.csv", s.dataDir)
	tries := 3
	for {
		url = fmt.Sprintf(`https://arquivos.b3.com.br/api/download/?token=%s`, f.Token)
		progress.Download("Download do arquivo de códigos")
		err = downloadFile(url, fp, false)
		if err != nil {
			tries--
			if tries <= 0 {
				return err
			}
			time.Sleep(2 * time.Second)
			continue
		}
		// Delete files on return
		defer filesCleanup([]string{fp})
		break
	}

	// Parse and store files content
	fh, err := os.Open(fp)
	if err != nil {
		return errors.Wrapf(err, "abrindo arquivo %s", fp)
	}
	defer fh.Close()

	dec := transform.NewReader(fh, charmap.ISO8859_1.NewDecoder())
	_, err = s.store.Save(dec, "")

	return err
}

/* --- UTILS --- */

func apiURL(provider int, apiKey, code, date string) string {
	v := url.Values{}
	switch provider {
	case APIalphavantage:
		v.Set("function", "TIME_SERIES_DAILY")
		v.Add("symbol", code+".SA")
		v.Add("apikey", apiKey)
		v.Add("outputsize", "full")
		v.Add("datatype", "csv")
		return "https://www.alphavantage.co/query?" + v.Encode()

	case APIyahoo:
		const layout = "2006-01-02 15:04:05 -0700 MST"
		t1, err1 := time.Parse(layout, date+" 00:00:00 -0300 GMT")
		t2, err2 := time.Parse(layout, date+" 23:59:59 -0300 GMT")
		if err1 != nil || err2 != nil {
			return ""
		}
		v.Set("period1", fmt.Sprint(t1.Unix()))
		v.Add("period2", fmt.Sprint(t2.Unix()))
		v.Add("interval", "1d")
		v.Add("events", "history")
		v.Add("includeAdjustedClose", "true")
		return fmt.Sprintf("https://query1.finance.yahoo.com/v7/finance/download/%s.SA?%s",
			code, v.Encode())
	}

	return ""
}

func map2str(data map[string]interface{}) string {
	var buf string
	for k, v := range data {
		buf += fmt.Sprintln(k+":", v)
	}
	return buf
}

// ifNot returns true if no error is found.
func ifNot(err error) bool {
	return err == nil
}


================================================
FILE: fetch/fetch_test.go
================================================
package fetch

import (
	"testing"

	_ "github.com/mattn/go-sqlite3"
)

func Test_findFile(t *testing.T) {
	type args struct {
		list    []string
		pattern string
	}
	tests := []struct {
		name    string
		args    args
		want    string
		wantErr bool
	}{
		{
			name:    "should find item",
			args:    args{[]string{"aaa", "aaa bbb CCC ddd"}, "aaa bbb CCC ddd"},
			want:    "aaa bbb CCC ddd",
			wantErr: false,
		},
		{
			name:    "should not find item",
			args:    args{[]string{"aaa", "aaa bbb CCC ddd"}, "aaa bbb xCC ddd"},
			want:    "",
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := findFile(tt.args.list, tt.args.pattern)
			if (err != nil) != tt.wantErr {
				t.Errorf("findFiles() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if got != tt.want {
				t.Errorf("findFiles() = %v, want %v", got, tt.want)
			}
		})
	}
}


================================================
FILE: fetch/unzip.go
================================================
package fetch

import (
	"archive/zip"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
)

//
// UnzipVerbosity will decompress a zip archive, moving all files and folders
// within the zip file (parameter 1) to an output directory (parameter 2).
// Source: https://golangcode.com/unzip-files-in-go/
//
func Unzip(src string, dest string, verbose bool) ([]string, error) {

	var filenames []string

	r, err := zip.OpenReader(src)
	if err != nil {
		return filenames, err
	}
	defer r.Close()

	for _, f := range r.File {

		if !valid(f.Name) {
			continue
		}

		rc, err := f.Open()
		if err != nil {
			return filenames, err
		}
		defer rc.Close()

		// Store filename/path for returning and using later on
		fpath := filepath.Join(dest, f.Name)

		// Check for ZipSlip. More Info: http://bit.ly/2MsjAWE
		if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) {
			return filenames, fmt.Errorf("%s: illegal file path", fpath)
		}

		filenames = append(filenames, fpath)

		if f.FileInfo().IsDir() {

			// Make Folder
			if err = os.MkdirAll(fpath, os.ModePerm); err != nil {
				return nil, err
			}

		} else {

			// Make File
			if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
				return filenames, err
			}

			outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
			if err != nil {
				return filenames, err
			}

			counter := io.Discard
			if verbose {
				fmt.Printf("[          ] Unziping %s", fpath)
				counter = &WriteCounter{}
			}
			_, err = io.Copy(outFile, io.TeeReader(rc, counter))
			if verbose {
				fmt.Println()
			}

			// Close the file without defer to close before next iteration of loop
			outFile.Close()

			if err != nil {
				return filenames, err
			}

		}
	}
	return filenames, nil
}

func valid(filename string) bool {
	n := strings.ToLower(filename)

	if strings.Contains(n, "_ind_") {
		return false
	}

	list := []string{"_bpa_", "_bpp_", "_dfc_", "_dre_", "_dva_", "fre_", "cotahist_"}

	for _, item := range list {
		if strings.Contains(n, item) {
			return true
		}
	}

	return false
}


================================================
FILE: fii.go
================================================
package rapina

// Dividend contains the stock 'Code', and the 'Date' for the stock dividend 'Val'.
type Dividend struct {
	Code        string
	Date        string
	PaymentDate string
	Val         float64
}

// Monthly contains the FII monthly report fields
type Monthly struct {
}

// FIIDetails details (ID field: DetailFund.CNPJ)
type FIIDetails struct {
	DetailFund struct {
		Acronym               string      `json:"acronym"`
		TradingName           string      `json:"tradingName"`
		TradingCode           string      `json:"tradingCode"`
		TradingCodeOthers     string      `json:"tradingCodeOthers"`
		CNPJ                  string      `json:"cnpj"`
		Classification        string      `json:"classification"`
		WebSite               string      `json:"webSite"`
		FundAddress           string      `json:"fundAddress"`
		FundPhoneNumberDDD    string      `json:"fundPhoneNumberDDD"`
		FundPhoneNumber       string      `json:"fundPhoneNumber"`
		FundPhoneNumberFax    string      `json:"fundPhoneNumberFax"`
		PositionManager       string      `json:"positionManager"`
		ManagerName           string      `json:"managerName"`
		CompanyAddress        string      `json:"companyAddress"`
		CompanyPhoneNumberDDD string      `json:"companyPhoneNumberDDD"`
		CompanyPhoneNumber    string      `json:"companyPhoneNumber"`
		CompanyPhoneNumberFax string      `json:"companyPhoneNumberFax"`
		CompanyEmail          string      `json:"companyEmail"`
		CompanyName           string      `json:"companyName"`
		QuotaCount            string      `json:"quotaCount"`
		QuotaDateApproved     string      `json:"quotaDateApproved"`
		Codes                 []string    `json:"codes"`
		CodesOther            interface{} `json:"codesOther"`
		Segment               interface{} `json:"segment"`
	} `json:"detailFund"`
	ShareHolder struct {
		ShareHolderName           string `json:"shareHolderName"`
		ShareHolderAddress        string `json:"shareHolderAddress"`
		ShareHolderPhoneNumberDDD string `json:"shareHolderPhoneNumberDDD"`
		ShareHolderPhoneNumber    string `json:"shareHolderPhoneNumber"`
		ShareHolderFaxNumber      string `json:"shareHolderFaxNumber"`
		ShareHolderEmail          string `json:"shareHolderEmail"`
	} `json:"shareHolder"`
}

// FIIStorage is the interface that contains the methods needed to parse, save and
// retrieve FII data to/from a storage.
type FIIStorage interface {
	Details(code string) (*FIIDetails, error)
	SaveDetails(stream []byte) error

	Dividends(code, monthYear string) (*[]Dividend, error)
	SaveDividend(dividend Dividend) error
}


================================================
FILE: go.mod
================================================
module github.com/dude333/rapina

require (
	github.com/360EntSecGroup-Skylar/excelize v1.4.1
	github.com/PuerkitoBio/goquery v1.8.1
	github.com/andybalholm/cascadia v1.3.2 // indirect
	github.com/antchfx/htmlquery v1.3.0 // indirect
	github.com/antchfx/xmlquery v1.3.18 // indirect
	github.com/antchfx/xpath v1.2.5 // indirect
	github.com/dustin/go-humanize v1.0.0
	github.com/gocolly/colly/v2 v2.1.0
	github.com/golang/protobuf v1.5.3 // indirect
	github.com/lithammer/fuzzysearch v1.1.0
	github.com/manifoldco/promptui v0.6.0
	github.com/mattn/go-sqlite3 v2.0.1+incompatible
	github.com/mitchellh/go-homedir v1.1.0
	github.com/pkg/errors v0.8.1
	github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
	github.com/spf13/cobra v0.0.5
	github.com/spf13/viper v1.6.1
	github.com/stretchr/testify v1.4.0
	github.com/temoto/robotstxt v1.1.2 // indirect
	golang.org/x/net v0.20.0
	golang.org/x/text v0.14.0
	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
	google.golang.org/appengine v1.6.8 // indirect
	google.golang.org/protobuf v1.32.0 // indirect
	gopkg.in/yaml.v2 v2.4.0
)

go 1.16


================================================
FILE: go.sum
================================================
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/360EntSecGroup-Skylar/excelize v1.4.1 h1:l55mJb6rkkaUzOpSsgEeKYtS6/0gHwBYyfo5Jcjv/Ks=
github.com/360EntSecGroup-Skylar/excelize v1.4.1/go.mod h1:vnax29X2usfl7HHkBrX5EvSCJcmH3dT9luvxzu8iGAE=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
github.com/alecthomas/gometalinter v3.0.0+incompatible h1:e9Zfvfytsw/e6Kd/PYd75wggK+/kX5Xn8IYDUKyc5fU=
github.com/alecthomas/gometalinter v3.0.0+incompatible/go.mod h1:qfIpQGGz3d+NmgyPBqv+LSh50emm1pt72EtcX2vKYQk=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/antchfx/htmlquery v1.2.3/go.mod h1:B0ABL+F5irhhMWg54ymEZinzMSi0Kt3I2if0BLYa3V0=
github.com/antchfx/htmlquery v1.3.0 h1:5I5yNFOVI+egyia5F2s/5Do2nFWxJz41Tr3DyfKD25E=
github.com/antchfx/htmlquery v1.3.0/go.mod h1:zKPDVTMhfOmcwxheXUsx4rKJy8KEY/PU6eXr/2SebQ8=
github.com/antchfx/xmlquery v1.2.4/go.mod h1:KQQuESaxSlqugE2ZBcM/qn+ebIpt+d+4Xx7YcSGAIrM=
github.com/antchfx/xmlquery v1.3.18 h1:FSQ3wMuphnPPGJOFhvc+cRQ2CT/rUj4cyQXkJcjOwz0=
github.com/antchfx/xmlquery v1.3.18/go.mod h1:Afkq4JIeXut75taLSuI31ISJ/zeq+3jG7TunF7noreA=
github.com/antchfx/xpath v1.1.6/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk=
github.com/antchfx/xpath v1.1.8/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk=
github.com/antchfx/xpath v1.2.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/antchfx/xpath v1.2.4/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/antchfx/xpath v1.2.5 h1:hqZ+wtQ+KIOV/S3bGZcIhpgYC26um2bZYP2KVGcR7VY=
github.com/antchfx/xpath v1.2.5/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gocolly/colly v1.2.0 h1:qRz9YAn8FIH0qzgNUw+HT9UN7wm1oF9OBAilwEWpyrI=
github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA=
github.com/gocolly/colly/v2 v2.1.0 h1:k0DuZkDoCsx51bKpRJNEmcxcp+W5N8ziuwGaSDuFoGs=
github.com/gocolly/colly/v2 v2.1.0/go.mod h1:I2MuhsLjQ+Ex+IzK3afNS8/1qP3AedHOusRPcRdC5o0=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf h1:7+FW5aGwISbqUtkfmIpZJGRgNFg2ioYPvFaUxdqpDsg=
github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gordonklaus/ineffassign v0.0.0-20180909121442-1003c8bd00dc h1:cJlkeAx1QYgO5N80aF5xRGstVsRQwgLR7uA2FnP1ZjY=
github.com/gordonklaus/ineffassign v0.0.0-20180909121442-1003c8bd00dc/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jawher/mow.cli v1.1.0/go.mod h1:aNaQlc7ozF3vw6IJ2dHjp2ZFiA4ozMIYY6PyuRJwlUg=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU=
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o=
github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lithammer/fuzzysearch v1.1.0 h1:go9v8tLCrNTTlH42OAaq4eHFe81TDHEnlrMEb6R4f+A=
github.com/lithammer/fuzzysearch v1.1.0/go.mod h1:Bqx4wo8lTOFcJr3ckpY6HA9lEIOO0H5HrkJ5CsN56HQ=
github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw=
github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/manifoldco/promptui v0.6.0 h1:GuXmIdl5lhlamnWf3NbsKWYlaWyHABeStbD1LLsQMuA=
github.com/manifoldco/promptui v0.6.0/go.mod h1:o9/C5VV8IPXxjxpl9au84MtQGIi5dwn7eldAgEdePPs=
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-sqlite3 v2.0.1+incompatible h1:xQ15muvnzGBHpIpdrNi1DA5x0+TcBZzsIDwmw9uTHzw=
github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nicksnyder/go-i18n v1.10.1 h1:isfg77E/aCD7+0lD/D00ebR2MV5vgeQ276WYyDaCRQc=
github.com/nicksnyder/go-i18n v1.10.1/go.mod h1:e4Di5xjP9oTVrC6y3C7C0HoSYXjSbhh/dU0eUV32nB4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/spf13/viper v1.6.1 h1:VPZzIkznI1YhVMRi6vNFLHSwhnhReBfgTxIPccpfdZk=
github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.3-0.20181224173747-660f15d67dbb/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/temoto/robotstxt v1.1.1/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo=
github.com/temoto/robotstxt v1.1.2 h1:W2pOjSJ6SWvldyEuiFXNxz3xZ8aiWX5LbfDiOFd7Fxg=
github.com/temoto/robotstxt v1.1.2/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tsenart/deadcode v0.0.0-20160724212837-210d2dc333e9 h1:vY5WqiEon0ZSTGM3ayVVi+twaHKHDFUVloaQ/wug9/c=
github.com/tsenart/deadcode v0.0.0-20160724212837-210d2dc333e9/go.mod h1:q+QjxYvZ+fpjMXqs+XEriussHjSYqeXVnAdSV1tkMYk=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3 h1:XQyxROzUlZH+WIQwySDgnISgOivlhjIEwaQaJEJrrN0=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181122213734-04b5d21e00f1/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20171010053543-63abe20a23e2 h1:5zOHKFi4LqGWG+3d+isqpbPrN/2yhDJnlO+BhRiuR6U=
gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20171010053543-63abe20a23e2/go.mod h1:3HH7i1SgMqlzxCcBmUHW657sD4Kvv9sC3HpL3YukzwA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=


================================================
FILE: logger.go
================================================
package rapina

import "io"

// Logger interface contains the methods needed to poperly display log messages.
type Logger interface {
	Run(format string, v ...interface{})
	Ok()
	Nok()
	Printf(format string, v ...interface{})
	Trace(format string, v ...interface{})
	Debug(format string, v ...interface{})
	Info(format string, v ...interface{})
	Warn(format string, v ...interface{})
	Error(format string, v ...interface{})
	SetOut(out io.Writer)
}


================================================
FILE: parsers/codeaccounts.go
================================================
package parsers

import (
	"strings"
)

// Bookkeeping account codes
// If you add new const values, run 'go generate'
// to update the generated code
const (
	UNDEF uint32 = iota
	SPACE

	// Balance Sheet
	Caixa
	AplicFinanceiras
	Estoque
	Equity
	ContasARecebCirc
	ContasARecebNCirc
	AtivoCirc
	AtivoNCirc
	AtivoTotal
	PassivoCirc
	PassivoNCirc
	PassivoTotal
	DividaCirc
	DividaNCirc
	DividendosJCP
	DividendosMin

	// Income Statement
	Vendas
	CustoVendas
	DespesasOp
	EBIT
	ResulFinanc
	ResulOpDescont
	LucLiq

	// DFC
	FCO
	FCI
	FCF

	// Value Added Statement
	Deprec
	JurosCapProp
	Dividendos

	// Values stored on table 'fre'
	Shares
	FreeFloat

	// Financial ratios
	EstoqueMedio
	EquityAvg

	// Financial scale (unit, thousand)
	Escala

	// Stock quote from last day of year
	Quote
)

// account code, description and bookkeeping code
type account struct {
	cdAccount string
	dsAccount string
	code      uint32
}

var _accountsTable = []account{
	// BPA
	{"1", "Ativo Total", AtivoTotal},
	{"1.01", "Ativo Circulante", AtivoCirc},
	{"1.02", "Ativo Não Circulante", AtivoNCirc},
	{"1.01.01", "Caixa e Equivalentes de Caixa", Caixa},
	{"1.01.02", "Aplicações Financeiras", AplicFinanceiras},
	{"1.01.04", "Estoques", Estoque}, // or "Títulos e Créditos a Receber" for security companies
	{"1.01.03", "Contas a Receber", ContasARecebCirc},
	{"1.02.01.03", "Contas a Receber", ContasARecebNCirc},
	{"1.02.01.04", "Contas a Receber", ContasARecebNCirc},

	// BPP
	{"2", "Passivo Total", PassivoTotal},
	{"2.01", "Passivo Circulante", PassivoCirc},
	{"2.02", "Passivo Não Circulante", PassivoNCirc},
	{"2.*", "Patrimônio Líquido Consolidado", Equity},
	{"2.01.04", "Empréstimos e Financiamentos", DividaCirc},
	{"2.02.01", "Empréstimos e Financiamentos", DividaNCirc},
	{"2.01.05.02.01", "Dividendos e JCP a Pagar", DividendosJCP},
	{"2.01.05.02.02", "Dividendo Mínimo Obrigatório a Pagar", DividendosMin},

	// DRE
	{"3.01", "", Vendas},
	{"3.02", "", CustoVendas},
	{"3.04", "", DespesasOp},
	{"3.*", "Resultado Antes do Resultado Financeiro e dos Tributos", EBIT},
	{"3.06", "Resultado Financeiro", ResulFinanc},
	{"3.07", "Resultado Financeiro", ResulFinanc},
	{"3.08", "Resultado Financeiro", ResulFinanc},
	{"3.10", "Resultado Líquido de Operações Descontinuadas", ResulOpDescont},
	{"3.11", "Resultado Líquido de Operações Descontinuadas", ResulOpDescont},
	{"3.12", "Resultado Líquido de Operações Descontinuadas", ResulOpDescont},
	{"3.*", "Lucro/Prejuízo Consolidado do Período", LucLiq},
	{"3.*", "Lucro/Prejuízo do Período", LucLiq},

	// DFC
	{"6.01", "", FCO},
	{"6.02", "", FCI},
	{"6.03", "", FCF},

	// DVA
	{"7.*", "Depreciação, Amortização e Exaustão", Deprec},
	{"7.*", "Juros sobre o Capital Próprio", JurosCapProp},
	{"7.*", "Dividendos", Dividendos},
}

// acctCode returns the code based on the account code and
// account description; if the code is not found in the table
// returns the hash.
func acctCode(cdAccount, dsAccount string) uint32 {
	dsAccount = strings.ToLower(dsAccount)

	for _, acc := range _accountsTable {
		descr := strings.ToLower(acc.dsAccount)
		l := len(acc.cdAccount)
		code := ""
		if l > 1 && acc.cdAccount[l-1] == '*' {
			code = acc.cdAccount[:l-1] // remove the '*'
		}

		if code != "" && strings.HasPrefix(cdAccount, code) {
			if descr == "" || descr == dsAccount {
				return acc.code
			}
		} else if acc.cdAccount == "" || acc.cdAccount == cdAccount {
			if descr == "" || descr == dsAccount {
				return acc.code
			}
		}
	}

	return Hash(cdAccount + dsAccount)
}


================================================
FILE: parsers/companies.go
================================================
package parsers

import (
	"database/sql"

	"github.com/pkg/errors"
)

type company struct {
	id   int
	name string
}

func loadCompanies(db *sql.DB) (map[string]company, error) {
	companies := make(map[string]company)

	selectCompanies := `SELECT ID, CNPJ, NAME from companies`
	rows, err := db.Query(selectCompanies)
	if err != nil {
		return companies, errors.Wrap(err, "falha ao ler banco de dados")
	}

	var id int
	var cnpj, name string

	defer rows.Close()
	for rows.Next() {
		err := rows.Scan(&id, &cnpj, &name)
		if err != nil && err != sql.ErrNoRows {
			return nil, err
		}
		companies[cnpj] = company{id, name}
	}

	return companies, nil
}

func saveCompanies(db *sql.DB, companies map[string]company) error {
	insert := `INSERT OR IGNORE INTO companies (ID,CNPJ,NAME) VALUES (?,?,?);`
	stmt, err := db.Prepare(insert)
	if err != nil {
		return errors.Wrap(err, "erro ao preparar insert da lista de empresas")
	}
	defer stmt.Close()

	for cnpj, value := range companies {
		_, err := stmt.Exec(value.id, cnpj, value.name)
		if err != nil {
			return errors.Wrap(err, "falha ao inserir empresa")
		}
	}

	return nil
}

//
// updateCompanies inserts a new company to the map
//
func updateCompanies(companies map[string]company, cnpj, name string) {
	if _, exists := companies[cnpj]; !exists {
		companies[cnpj] = company{
			len(companies) + 100,
			name,
		}
	}
}


================================================
FILE: parsers/fii.go
================================================
package parsers

/*
//
// FetchFIIs downloads the list of FIIs to get their code (e.g. 'HGLG'),
// then it uses this code to retrieve its details to get the CNPJ.
// Original baseURL: https://sistemaswebb3-listados.b3.com.br.
//
func FetchFIIList(baseURL string) ([]string, error) {
	listFundsURL := JoinURL(baseURL, `/fundsProxy/fundsCall/GetListFundDownload/eyJ0eXBlRnVuZCI6NywicGFnZU51bWJlciI6MSwicGFnZVNpemUiOjIwfQ==`)
	// fundsDetailsURL := `https://sistemaswebb3-listados.b3.com.br/fundsProxy/fundsCall/GetDetailFundSIG`

	tr := &http.Transport{
		DisableCompression: true,
		IdleConnTimeout:    30 * time.Second,
		TLSClientConfig:    &tls.Config{InsecureSkipVerify: true},
	}
	client := &http.Client{Transport: tr}

	resp, err := client.Get(listFundsURL)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != 200 {
		return nil, errors.New(resp.Status)
	}

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	unq, err := strconv.Unquote(string(body))
	if err != nil {
		return nil, err
	}
	txt, err := base64.StdEncoding.DecodeString(unq)
	if err != nil {
		return nil, err
	}

	var codes []string

	for _, line := range strings.Split(string(txt), "\n") {
		p := strings.Split(line, ";")
		if len(p) > 3 && len(p[3]) == 4 {
			codes = append(codes, p[3])
		}
	}

	return codes, nil
}


*/


================================================
FILE: parsers/fiidb.go
================================================
package parsers

import (
	"database/sql"
	"encoding/json"
	"fmt"
	"strings"
	"sync"

	"github.com/dude333/rapina"
	"github.com/dude333/rapina/progress"
	"github.com/pkg/errors"
)

// Error codes
var (
	ErrDBUnset  = errors.New("database not set")
	ErrNotFound = errors.New("not found")
)

// FIIParser implements sqlite storage for a rapina.FIIParser object.
type FIIParser struct {
	db  *sql.DB
	log rapina.Logger
	mu  sync.Mutex // ensures atomic writes on db
}

// NewFII creates a new instace of FII.
func NewFII(db *sql.DB, log rapina.Logger) (*FIIParser, error) {
	err := createAllTables(db)
	return &FIIParser{
		db:  db,
		log: log,
	}, err
}

// StoreFIIDetails parses the stream data into FIIDetails and returns
// the *FIIDetails.
func (fii *FIIParser) SaveDetails(stream []byte) error {
	fii.mu.Lock()
	defer fii.mu.Unlock()

	if !hasTable(fii.db, "fii_details") {
		if err := createTable(fii.db, "fii_details"); err != nil {
			return err
		}
	}

	var fiiDetails rapina.FIIDetails
	if err := json.Unmarshal(stream, &fiiDetails); err != nil {
		return errors.Wrap(err, "json unmarshal")
	}

	trimFIIDetails(&fiiDetails)

	x := fiiDetails.DetailFund
	if x.CNPJ == "" {
		return errors.New("CNPJ não encontrado")
	}

	insert := `INSERT OR IGNORE INTO fii_details 
		(cnpj, acronym, trading_code, json) 
		VALUES (?,?,?,?);`
	_, err := fii.db.Exec(insert,
		x.CNPJ, x.Acronym, x.TradingCode, stream)

	return err
}

// Details returns the FII Details for the 'code' or
// an empty string if not found in the db.
func (fii *FIIParser) Details(code string) (*rapina.FIIDetails, error) {
	details := rapina.FIIDetails{}

	if fii.db == nil {
		return nil, ErrDBUnset
	}

	var query string
	if len(code) == 4 {
		query = `SELECT json FROM fii_details WHERE acronym=?`
	} else if len(code) == 6 {
		query = `SELECT json FROM fii_details WHERE trading_code=?`
	} else {
		return nil, fmt.Errorf("invalid code '%s'", code)
	}

	var jsonStr []byte
	row := fii.db.QueryRow(query, code)
	err := row.Scan(&jsonStr)
	if err != nil {
		return nil, err
	}

	if err := json.Unmarshal(jsonStr, &details); err != nil {
		progress.ErrorMsg("FII details [%v]: %s\n", err, string(jsonStr))
		return nil, errors.Wrap(err, "json unmarshal")
	}

	return &details, nil
}

// Dividends returns the dividend from the db.
func (fii *FIIParser) Dividends(code, monthYear string) (*[]rapina.Dividend, error) {
	fii.mu.Lock()
	defer fii.mu.Unlock()

	const s = `SELECT trading_code, base_date, value
	FROM fii_dividends 
	WHERE trading_code=$1 
	AND base_date LIKE $2;`
	rows, err := fii.db.Query(s, code, monthYear+"%")
	if err != nil {
		return nil, errors.Wrap(err, "lendo dividendos do bd")
	}
	defer rows.Close()

	dividends := []rapina.Dividend{}
	var (
		tradingCode, baseDate string
		value                 float64
	)
	for rows.Next() {
		err := rows.Scan(&tradingCode, &baseDate, &value)
		if err != nil {
			return nil, err
		}

		// fii.log.Debug("reading: %v %v %v", tradingCode, baseDate, value)

		dividends = append(dividends, rapina.Dividend{
			Code: tradingCode,
			Date: baseDate,
			Val:  value,
		})
	}

	if err := rows.Err(); err != nil {
		return nil, err
	}

	if len(dividends) == 0 {
		return nil, errors.New("dividendos não encontrados")
	}

	return &dividends, nil
}

// SaveDividend parses and stores the map in the db. Returns the parsed stream.
func (fii *FIIParser) SaveDividend(dividend rapina.Dividend) error {
	fii.mu.Lock()
	defer fii.mu.Unlock()

	if err := createTable(fii.db, "fii_dividends"); err != nil {
		return err
	}

	const insert = `INSERT OR IGNORE INTO fii_dividends 
	(trading_code, base_date, payment_date, value) VALUES (?,?,?,?)`
	_, err := fii.db.Exec(insert, dividend.Code, dividend.Date, dividend.PaymentDate, dividend.Val)

	return errors.Wrap(err, "inserting data on fii_dividends")
}

func (fii *FIIParser) SelectFIIDetails(code string) (*rapina.FIIDetails, error) {
	if fii.db == nil {
		return nil, ErrDBUnset
	}

	var query string
	if len(code) == 4 {
		query = `SELECT cnpj, acronym, trading_code FROM fii_details WHERE acronym=?`
	} else if len(code) == 6 {
		query = `SELECT cnpj, acronym, trading_code FROM fii_details WHERE trading_code=?`
	} else {
		return nil, fmt.Errorf("invalid code '%s'", code)
	}

	var cnpj, acronym, tradingCode string
	row := fii.db.QueryRow(query, code)
	err := row.Scan(&cnpj, &acronym, &tradingCode)
	if err != nil {
		return nil, err
	}

	var fiiDetail rapina.FIIDetails
	fiiDetail.DetailFund.CNPJ = cnpj
	fiiDetail.DetailFund.Acronym = acronym
	fiiDetail.DetailFund.TradingCode = tradingCode

	return &fiiDetail, nil
}

/* -------- Utils ----------- */

func trimFIIDetails(f *rapina.FIIDetails) {
	f.DetailFund.CNPJ = strings.TrimSpace(f.DetailFund.CNPJ)
	f.DetailFund.Acronym = strings.TrimSpace(f.DetailFund.Acronym)
	tradingCodes := strings.Split(
		strings.TrimSpace(f.DetailFund.TradingCode), " ")
	f.DetailFund.TradingCode = tradingCodes[0]
}


================================================
FILE: parsers/financial.go
================================================
// financial.go
// Parses data from csv files containing financial statements

package parsers

import (
	"bufio"
	"database/sql"
	"fmt"
	"os"
	"strings"
	"time"

	"github.com/pkg/errors"
	"golang.org/x/text/encoding/charmap"
	"golang.org/x/text/transform"
)

var (
	// ErrAccumITR error for accumulatd quarterly results
	ErrAccumITR = fmt.Errorf("accumulated quarterly results")
)

//
// ImportCsv start the data import process, including the database creation
// if necessary
//
func ImportCsv(db *sql.DB, dataType string, file string) (err error) {

	// Create status table
	if err = createTable(db, "STATUS"); err != nil {
		return err
	}

	// Create companies table
	if err = createTable(db, "COMPANIES"); err != nil {
		return err
	}

	// Check table version, wipe it if version differs from current version, and
	// (re)create the table
	for _, t := range []string{dataType, "MD5"} {
		if v, table := dbVersion(db, t); v != currentDbVersion {
			if v > 0 {
				fmt.Printf("[i] Apagando tabela %s versão %d (versão atual: %d)\n", table, v, currentDbVersion)
			}
			if err := wipeDB(db, t); err != nil {
				return err
			}
		}
		if err := createTable(db, t); err != nil {
			return err
		}

	}

	isNew, err := isNewFile(db, file)
	if !isNew && err == nil { // if error, process file
		fmt.Printf("[ ] %s já processado anteriormente\n", dataType)
		return
	}

	var count int
	if dataType == "FRE" {
		count, err = populateFRE(db, file)
	} else {
		count, err = populateTable(db, dataType, file)
	}
	if err == nil {
		fmt.Printf("\r[√] %-7s %7d linhas processadas", dataType+":", count)
		storeFile(db, file)
	} else {
		fmt.Print("\r[x")
	}
	fmt.Println()

	return err
}

//
// populateTable loop thru file and insert its lines into DB
// and returns the number os lines inserted.
//
func populateTable(db *sql.DB, dataType, file string) (int, error) {
	progress := []string{"/", "-", "\\", "|", "-", "\\"}
	p := 0

	table, err := whatTable(dataType)
	if err != nil {
		return 0, err
	}

	companies, _ := loadCompanies(db)

	fh, err := os.Open(file)
	if err != nil {
		return 0, errors.Wrapf(err, "erro ao abrir arquivo %s", file)
	}
	defer fh.Close()

	dec := transform.NewReader(fh, charmap.ISO8859_1.NewDecoder())

	// BEGIN TRANSACTION
	tx, err := db.Begin()
	if err != nil {
		return 0, errors.Wrap(err, "Failed to begin transaction")
	}

	// Data used inside loop
	header := make(map[string]int) // stores the header item position (e.g., DT_FIM_EXERC:9)
	scanner := bufio.NewScanner(dec)
	count := 0
	insert := ""
	var stmt *sql.Stmt

	// Loop thru file, line by line
	fmt.Print("[ ] Processando arquivo ", dataType)
	for scanner.Scan() {
		line := scanner.Text()
		if len(line) == 0 {
			continue
		}
		fields := strings.Split(line, ";")

		if len(header) == 0 { // HEADER
			// Get header positioning
			for i, h := range fields {
				header[h] = i
			}
			// Prepare insert statement
			insert = fmt.Sprintf(`INSERT OR IGNORE INTO %s (
				ID, ID_CIA, CODE, YEAR, DATA_TYPE,
				VERSAO,
				MOEDA, ESCALA_MOEDA, 
				DT_FIM_EXERC,
				CD_CONTA, DS_CONTA, VL_CONTA
			) VALUES (
				?, ?, ?, ?, "%s",
				?,
				?, ?,
				?,
				?, ?, ?
				);`, table, dataType)
			stmt, err = tx.Prepare(insert)
			if err != nil {
				err = errors.Wrapf(err, "erro ao preparar insert (verificar cabeçalho do arquivo %s)", file)
				return count, err
			}
			defer stmt.Close()

		} else { // VALUES

			if len(fields) <= 12 {
				continue
			}

			// Only use penultimate for 2010 file, that's the last year published,
			// to get data from 2009
			if fields[header["ORDEM_EXERC"]] == "PENÚLTIMO" {
				dt := fields[header["DT_FIM_EXERC"]]
				if len(dt) < 4 || dt[:4] != "2009" {
					continue
				}
			}

			// UPDATE COMPANIES
			n1, ok1 := header["CNPJ_CIA"]
			n2, ok2 := header["DENOM_CIA"]
			if ok1 && ok2 && n1 >= 0 && n1 < len(fields) && n2 >= 0 && n2 < len(fields) {
				updateCompanies(companies, fields[header["CNPJ_CIA"]], fields[header["DENOM_CIA"]])
			}

			// INSERT
			f, err := prepareFields(dataType, header, fields, companies)
			if err == ErrAccumITR {
				continue // ignore accumulated ITR data
			}
			if err != nil {
				return count, errors.Wrap(err, "falha ao preparar registro")
			}
			_, err = stmt.Exec(f...)
			if err != nil {
				return count, errors.Wrap(err, "falha ao inserir registro")
			}
		}

		// fmt.Println("-------------------------------")
		if count++; count%1000 == 0 {
			fmt.Printf("\r[%s", progress[p%6])
			p++
		}
	}

	fmt.Print("\r[*")

	// END TRANSACTION
	err = tx.Commit()
	if err != nil {
		return count, errors.Wrap(err, "Failed to commit transaction")
	}

	if err := scanner.Err(); err != nil {
		return count, errors.Wrapf(err, "erro ao ler arquivo %s", file)
	}

	err = saveCompanies(db, companies)

	return count, err
}

// Cache (optimization)
var unixTime = make(map[string]int64)

//
// prepareFields prepares all fields (columns) to be inserted on the DB.
//
// Returns:
// ID, ID_CIA, CODE, YEAR,
// VERSAO,
// MOEDA, ESCALA_MOEDA,
// DT_FIM_EXERC,
// CD_CONTA, DS_CONTA, VL_CONTA
//
// Tip: to convert Unix timestamp to date on sqlite: strftime('%Y-%m-%d', DT_REFER, 'unixepoch')
//
func prepareFields(dataType string, header map[string]int, fields []string, companies map[string]company) ([]interface{}, error) {
	// AUX FUNCTIONS
	val := func(key string) string {
		v, ok := header[key]
		if !ok {
			return ""
		}
		return fields[v]
	}

	// Convert date string (YYYY-MM-DD) into Unix timestamp
	tim := func(key string) int64 {
		v, ok := header[key]
		if !ok {
			return 0
		}
		f := fields[v]
		if ut, ok := unixTime[f]; ok {
			return ut
		}
		t, err := time.Parse("2006-01-02", f)
		if err != nil {
			return 0
		}
		unixTime[f] = t.Unix()
		return unixTime[f]
	}

	// REFERENCE DATE
	v, ok := header["DT_FIM_EXERC"]
	if !ok {
		return nil, fmt.Errorf("DT_FIM_EXERC não encontrado")
	}
	if len(fields[v]) < 4 || tim("DT_FIM_EXERC") == 0 {
		return nil, fmt.Errorf("DT_FIM_EXERC incorreto: %v", fields[v])
	}
	// Check if quarterly data contains data from 90 days, except for "BPA_ITR" and "BPP_ITR"
	if dataType != "BPA_ITR" && dataType != "BPP_ITR" && strings.HasSuffix(dataType, "_ITR") {
		t1 := tim("DT_INI_EXERC")
		t2 := tim("DT_FIM_EXERC")
		days := (t2 - t1) / 60 / 60 / 24
		if days < 80 || days > 100 {
			return nil, ErrAccumITR
		}
	}
	year := fields[v][:4]

	// CNPJ_CIA and DENOM_CIA are replaced by company id
	cnpj := val("CNPJ_CIA")
	c, ok := companies[cnpj]
	if !ok {
		return nil, fmt.Errorf("CNPJ %s não encontrado", cnpj)
	}
	companyID := c.id

	// Unique value to be used as PRIMARY KEY
	hash := Hash(cnpj + val("GRUPO_DFP") + val("DT_FIM_EXERC") + val("VERSAO") + val("CD_CONTA") + val("VL_CONTA"))

	// Output -- need to follow INSERT sequence
	f := make([]interface{}, 11)
	f[0] = hash                                                             // ID
	f[1] = companyID                                                        // ID_CIA
	f[2] = acctCode(fields[header["CD_CONTA"]], fields[header["DS_CONTA"]]) // CODE
	f[3] = year                                                             // YEAR
	f[4] = val("VERSAO")
	f[5] = val("MOEDA")
	f[6] = val("ESCALA_MOEDA")
	f[7] = tim("DT_FIM_EXERC")
	f[8] = val("CD_CONTA")
	f[9] = val("DS_CONTA")
	f[10] = val("VL_CONTA")

	return f, nil
}


================================================
FILE: parsers/financial_test.go
================================================
package parsers

import (
	"database/sql"
	"os"
	"testing"

	_ "github.com/mattn/go-sqlite3"
)

func tempFilename(t *testing.T) string {
	f, err := os.CreateTemp("", "rapina-test-")
	if err != nil {
		t.Fatal(err)
	}
	f.Close()
	return f.Name()
}

func samples(filename string) error {
	bpa := []byte(`
CNPJ_CIA;DT_REFER;VERSAO;DENOM_CIA;CD_CVM;GRUPO_DFP;MOEDA;ESCALA_MOEDA;ORDEM_EXERC;DT_FIM_EXERC;CD_CONTA;DS_CONTA;VL_CONTA
00.000.000/0001-91;2013-12-31;4;BANCO DO BRASIL S.A.;1023;DF Consolidado - Balan�o Patrimonial Ativo;REAL;MILHAR;�LTIMO;2013-12-31;1;Ativo Total;1162167882.00
00.000.000/0001-91;2013-12-31;4;BANCO DO BRASIL S.A.;1023;DF Consolidado - Balan�o Patrimonial Ativo;REAL;MILHAR;�LTIMO;2013-12-31;1.01;Caixa e Equivalentes de Caixa;68841638.00
00.000.000/0001-91;2013-12-31;4;BANCO DO BRASIL S.A.;1023;DF Consolidado - Balan�o Patrimonial Ativo;REAL;MILHAR;�LTIMO;2013-12-31;1.02;Aplica��es Financeiras;110019404.00
00.000.000/0001-91;2013-12-31;4;BANCO DO BRASIL S.A.;1023;DF Consolidado - Balan�o Patrimonial Ativo;REAL;MILHAR;�LTIMO;2013-12-31;1.02.01;Aplica��es Financeiras Avaliadas a Valor Justo;109376121.00
00.000.000/0001-91;2013-12-31;4;BANCO DO BRASIL S.A.;1023;DF Consolidado - Balan�o Patrimonial Ativo;REAL;MILHAR;�LTIMO;2013-12-31;1.02.01.01;T�tulos para Negocia��o;18991047.00
00.000.000/0001-91;2013-12-31;4;BANCO DO BRASIL S.A.;1023;DF Consolidado - Balan�o Patrimonial Ativo;REAL;MILHAR;�LTIMO;2013-12-31;1.02.01.02;T�tulos Dispon�veis para Venda;90385074.00
00.000.000/0001-91;2013-12-31;4;BANCO DO BRASIL S.A.;1023;DF Consolidado - Balan�o Patrimonial Ativo;REAL;MILHAR;�LTIMO;2013-12-31;1.02.02;Aplica��es Financeiras Avaliadas ao Custo Amortizado;643283.00
00.000.000/0001-91;2013-12-31;4;BANCO DO BRASIL S.A.;1023;DF Consolidado - Balan�o Patrimonial Ativo;REAL;MILHAR;�LTIMO;2013-12-31;1.02.02.01;T�tulos Mantidos at� o Vencimento;643283.00
00.000.000/0001-91;2013-12-31;4;BANCO DO BRASIL S.A.;1023;DF Consolidado - Balan�o Patrimonial Ativo;REAL;MILHAR;�LTIMO;2013-12-31;1.03;Empr�stimos e Receb�veis;755821983.00
00.000.000/0001-91;2013-12-31;4;BANCO DO BRASIL S.A.;1023;DF Consolidado - Balan�o Patrimonial Ativo;REAL;MILHAR;�LTIMO;2013-12-31;1.04;Tributos Diferidos;21954460.00
	`)
	err := os.WriteFile(filename, bpa, 0600)

	return err
}

func TestImportCsv(t *testing.T) {
	var db *sql.DB
	var err error
	fileBPA := tempFilename(t)
	defer os.Remove(fileBPA)
	fileDB := tempFilename(t)
	defer os.Remove(fileDB)

	if db, err = sql.Open("sqlite3", fileDB); err != nil {
		t.Errorf("Fail to open db: %v", err)
	}
	defer db.Close()

	if err = samples(fileBPA); err != nil {
		t.Errorf("Fail to create samples: %v", err)
	}

	if err = ImportCsv(db, "BPA", fileBPA); err != nil {
		t.Errorf("Fail to parse: %v", err)
	}

	for _, tp := range []string{"BPA", "MD5"} {
		if v, table := dbVersion(db, tp); v != currentDbVersion {
			t.Errorf("Expecting table %s on version %d, received %d", table, currentDbVersion, v)
		}
	}

	isNew, err := isNewFile(db, fileBPA)
	if isNew && err == nil {
		t.Errorf("Expecting processed file, got new file")
	}

}

func TestGetHash(t *testing.T) {
	table := []struct {
		s string
		h uint32
	}{
		{"test1", 2569220284},
		{"random data", 1626193638},
		{"excel", 1973829744},
		{"One More...12345!", 2258028052},
	}
	for _, x := range table {
		h := Hash(x.s)
		if h != x.h {
			t.Errorf("Hash was incorrect, got: %d, want: %d.", h, x.h)
		}
	}
}

func TestRemoveDiacritics(t *testing.T) {
	list := []struct {
		str string
		exp string
	}{
		{"ITAÚ", "ITAU"},
		{"SÃO", "SAO"},
		{"São Paulo", "Sao Paulo"},
		{"ÁÉÍÓÚáéíóúÀàÃÕãõÇç", "AEIOUaeiouAaAOaoCc"},
	}

	for _, l := range list {
		if RemoveDiacritics(l.str) != l.exp {
			t.Errorf("Expecting %s, received %s", l.exp, RemoveDiacritics(l.str))
		}
	}
}

func Test_prepareFields(t *testing.T) {
	companies := make(map[string]company)
	companies["54321"] = company{1, "A"}

	type args struct {
		dataType  string
		header    map[string]int
		fields    []string
		companies map[string]company
	}
	tests := []struct {
		name    string
		args    args
		wantErr bool
	}{
		{
			"dt_refer not found",
			args{
				"BPA",
				map[string]int{"a": 0, "b": 1},
				[]string{"a", "b"},
				companies,
			},
			true,
		}, {
			"should work",
			args{
				"BPA",
				map[string]int{"x": 0, "y": 1, "DT_FIM_EXERC": 2, "CNPJ_CIA": 3},
				[]string{"X", "Y", "2020-02-25", "54321"},
				companies,
			},
			false,
		}, {
			"cnpj not found",
			args{
				"BPA",
				map[string]int{"x": 0, "y": 2, "DT_FIM_EXERC": 1},
				[]string{"X", "2020-02-25", "Y"},
				companies,
			},
			true,
		}, {
			"DT_FIM_EXERC not found",
			args{
				"BPA",
				map[string]int{"x": 0, "y": 2, "DT_FIM_EXERC": 1},
				[]string{"X", "202", "Y"},
				companies,
			},
			true,
		}, {
			"itr should work",
			args{
				"BPA_ITR",
				map[string]int{"x": 0, "y": 1, "DT_INI_EXERC": 2, "DT_FIM_EXERC": 3, "CNPJ_CIA": 4},
				[]string{"X", "Y", "2020-01-01", "2020-06-30", "54321"},
				companies,
			},
			false,
		}, {
			"itr should fail",
			args{
				"DRE_ITR",
				map[string]int{"x": 0, "y": 1, "DT_INI_EXERC": 2, "DT_FIM_EXERC": 3, "CNPJ_CIA": 4},
				[]string{"X", "Y", "2020-01-01", "2020-06-30", "54321"},
				companies,
			},
			true,
		}, {
			"itr should pass",
			args{
				"DRE_ITR",
				map[string]int{"x": 0, "y": 1, "DT_INI_EXERC": 2, "DT_FIM_EXERC": 3, "CNPJ_CIA": 4},
				[]string{"X", "Y", "2020-01-01", "2020-03-30", "54321"},
				companies,
			},
			false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			_, err := prepareFields(tt.args.dataType, tt.args.header, tt.args.fields, tt.args.companies)
			if (err != nil) != tt.wantErr {
				t.Errorf("prepareFields() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
		})
	}
}

func BenchmarkPrepareFields(b *testing.B) {
	companies := make(map[string]company)
	companies["54321"] = company{1, "A"}

	h := map[string]int{"x": 0, "y": 1, "DT_FIM_EXERC": 2, "CNPJ_CIA": 3}

	f := []string{"X", "Y", "2020-02-25", "54321"}

	// run the prepareFields function b.N times
	for n := 0; n < b.N; n++ {
		_, err := prepareFields("BPA", h, f, companies)
		if err != nil {
			b.Errorf("error: %v", err)
			return
		}
	}
}


================================================
FILE: parsers/fre.go
================================================
package parsers

import (
	"bufio"
	"database/sql"
	"fmt"
	"os"
	"strconv"
	"strings"

	"github.com/pkg/errors"
	"golang.org/x/text/encoding/charmap"
	"golang.org/x/text/transform"
)

var (
	// ErrCNPJNotFound error
	ErrCNPJNotFound = fmt.Errorf("CNPJ not found")
)

func populateFRE(db *sql.DB, file string) (int, error) {
	progress := []string{"/", "-", "\\", "|", "-", "\\"}
	p := 0
	var err error

	table, err := whatTable("FRE")
	if err != nil {
		return 0, err
	}

	companies, _ := loadCompanies(db)

	fh, err := os.Open(file)
	if err != nil {
		return 0, errors.Wrapf(err, "erro ao abrir arquivo %s", file)
	}
	defer fh.Close()

	dec := transform.NewReader(fh, charmap.ISO8859_1.NewDecoder())

	// BEGIN TRANSACTION
	tx, err := db.Begin()
	if err != nil {
		return 0, errors.Wrap(err, "Failed to begin transaction")
	}

	// Data used inside loop
	sep := func(r rune) bool {
		return r == ';'
	}
	header := make(map[string]int) // stores the header item position (e.g., DT_FIM_EXERC:9)
	scanner := bufio.NewScanner(dec)
	count := 0
	insert := ""
	var stmt *sql.Stmt

	// Loop thru file, line by line
	fmt.Print("[ ] Processando arquivo FRE")
	for scanner.Scan() {
		line := scanner.Text()
		if len(line) == 0 {
			continue
		}
		fields := strings.FieldsFunc(line, sep)

		if len(header) == 0 { // HEADER
			// Get header positioning
			for i, h := range fields {
				header[h] = i
			}
			// Prepare insert statement
			insert = fmt.Sprintf(`INSERT OR IGNORE INTO %s (
				ID, ID_CIA, YEAR, 
				Versao,
				Quantidade_Total_Acoes_Circulacao, 
				Percentual_Total_Acoes_Circulacao 
			) VALUES (
				?, ?, ?,
				?,
				?,
				?
				);`, table)
			stmt, err = tx.Prepare(insert)
			if err != nil {
				err = errors.Wrapf(err, "erro ao preparar insert (verificar cabeçalho do arquivo %s)", file)
				return count, err
			}
			defer stmt.Close()

		} else { // VALUES

			if len(fields) <= 12 {
				continue
			}
			// INSERT
			f, err := prepareFREFields(header, fields, companies)
			if err == ErrCNPJNotFound {
				continue
			}
			if err != nil {
				fmt.Println(line)
				fmt.Printf("\r[x] Falha ao preparar registro: %v\n", err)
				fmt.Print("[ ] Processando arquivo FRE")
				continue
			}
			_, err = stmt.Exec(f...)
			if err != nil {
				return count, errors.Wrap(err, "falha ao inserir registro")
			}
		}

		if count++; count%60 == 0 {
			fmt.Printf("\r[%s", progress[p%6])
			p++
		}
	}

	fmt.Print("\r[*")

	// END TRANSACTION
	err = tx.Commit()
	if err != nil {
		return count, errors.Wrap(err, "Failed to commit transaction")
	}

	if err := scanner.Err(); err != nil {
		return count, errors.Wrapf(err, "erro ao ler arquivo %s", file)
	}

	return count, nil
}

func prepareFREFields(header map[string]int, fields []string, companies map[string]company) ([]interface{}, error) {
	if len(fields) < len(header)-1 {
		return nil, fmt.Errorf("len(fields)=%d != len(header)=%d", len(fields), len(header))
	}

	// val checks and gets the value from a map
	val := func(key string) string {
		v, ok := header[key]
		if !ok {
			return ""
		}
		return fields[v]
	}

	// YEAR
	v, ok := header["Data_Referencia"]
	if !ok {
		return nil, fmt.Errorf("Data_Referencia não encontrado")
	}
	if len(fields[v]) != 10 {
		return nil, fmt.Errorf("DT_FIM_EXERC incorreto: %v", fields[v])
	}
	year := fields[v][:4]

	// CNPJ_Companhia is replaced by company id
	cnpj := val("CNPJ_Companhia")
	c, ok := companies[cnpj]
	if !ok {
		return nil, ErrCNPJNotFound
	}
	companyID := c.id

	// Free float
	ff := val("Percentual_Total_Acoes_Circulacao")
	var freeFloat float32
	if ff != "" {
		if f, err := strconv.ParseFloat(ff, 32); err == nil {
			freeFloat = float32(f / 100)
		}
	}

	// Total shares considering the free float
	shares := val("Quantidade_Total_Acoes_Circulacao")
	var totalShares float32
	if shares != "" {
		if f, err := strconv.ParseFloat(shares, 32); err == nil {
			if freeFloat > 0 {
				totalShares = float32(f) / freeFloat
			}
		}
	}

	// Unique value to be used as PRIMARY KEY
	hash := Hash(cnpj + val("Data_Referencia") + val("Versao") + val("ID_Documento") + val("Quantidade_Total_Acoes_Circulacao"))

	// Output -- need to match INSERT sequence
	var f []interface{}
	f = append(f, hash)      // ID
	f = append(f, companyID) // ID_CIA
	f = append(f, year)      // YEAR

	f = append(f, val("Versao"))
	f = append(f, totalShares)
	f = append(f, freeFloat)

	return f, nil
}


================================================
FILE: parsers/fuzzy.go
================================================
package parsers

import (
	"strings"

	"github.com/lithammer/fuzzysearch/fuzzy"
)

//
// FuzzyMatch measures the Levenshtein distance between
// the source and the list, returning true if the distance
// is less or equal the 'distance'.
// Diacritics are removed from 'src' and 'list'.
//
func FuzzyMatch(src string, list []string, distance int) bool {
	return FuzzyFind(src, list, distance) != ""
}

//
// FuzzyFind returns the most approximate string inside 'list' that
// matches the 'src' string within a maximum 'distance'.
//
func FuzzyFind(source string, targets []string, maxDistance int) (found string) {
	for _, target := range targets {
		src := fix(source)
		trg := fix(target)
		if strings.HasPrefix(src, trg) || strings.HasPrefix(trg, src) {
			return target
		}
		distance := fuzzy.LevenshteinDistance(src, trg)
		if distance <= maxDistance {
			maxDistance = distance
			found = target
		}
	}

	if found == "" {
		for _, target := range targets {
			src := strings.Split(fix(source), " ")
			trg := strings.Split(fix(target), " ")

			if len(src) > 2 && len(trg) > 2 {
				if src[0] == trg[0] && src[1] == trg[1] {
					return target
				}
			}
		}
	}

	return
}

func fix(txt string) string {
	txt = strings.ToUpper(txt)
	txt = strings.Replace(txt, "BCO ", "BANCO ", 1)
	return RemoveDiacritics(txt)
}


================================================
FILE: parsers/fuzzy_test.go
================================================
package parsers

import "testing"

func TestFuzzyFind(t *testing.T) {
	list := []struct {
		src      string
		trg      []string
		maxDist  int
		expected string
	}{
		{"ABCD", []string{"ABC", "ACD"}, 2, "ABC"},
		{"ABCD", []string{"XYZ", "ACD"}, 1, "ACD"},
		{"ABCDÉ", []string{"XYZ", "ACD", "ABCDE"}, 0, "ABCDE"},
		{"ABCDÉ FGH", []string{"XYZ", "ACD", "FGH"}, 6, "FGH"},
		{"BCO ABC", []string{"XYZ", "BANCO ABC", "FGH"}, 0, "BANCO ABC"},
	}

	for _, l := range list {
		r := FuzzyFind(l.src, l.trg, l.maxDist)
		if r != l.expected {
			t.Errorf("Expected: %s, got: %s", l.expected, r)
		}
	}
}


================================================
FILE: parsers/md5.go
================================================
package parsers

import (
	"crypto/md5"
	"database/sql"
	"fmt"
	"io"
	"os"
)

//
// isNewFile checks the database to see if this file has been
// processed already
//
func isNewFile(db *sql.DB, filename string) (isNew bool, err error) {
	isNew = true

	md5, err := md5FromFile(filename)
	if err != nil {
		return
	}

	sqlStmt := `SELECT md5 FROM md5 WHERE md5 = ?`
	err = db.QueryRow(sqlStmt, md5).Scan(&md5)
	if err != nil {
		return
	}

	isNew = false
	return
}

//
// storeFile into md5 table (only successfully processed files)
//
func storeFile(db *sql.DB, filename string) (md5 string) {
	md5, err := md5FromFile(filename)
	if err != nil {
		return ""
	}
	insert := fmt.Sprintf(`INSERT OR IGNORE INTO md5 (md5) VALUES ("%s")`, md5)
	_, err = db.Exec(insert)
	if err != nil {
		return ""
	}
	return md5
}

//
// md5FromFile
//
func md5FromFile(filename string) (string, error) {
	f, err := os.Open(filename)
	if err != nil {
		return "", err
	}
	defer f.Close()

	h := md5.New()
	if _, err = io.Copy(h, f); err != nil {
		return "", err
	}

	return fmt.Sprintf("%x", h.Sum(nil)), nil
}


================================================
FILE: parsers/md5_test.go
================================================
package parsers

import (
	"database/sql"
	"os"
	"testing"

	_ "github.com/mattn/go-sqlite3"
	"github.com/pkg/errors"
)

func TestIsNewFile(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping testing in short mode") // used in CI
	}

	db, err := openDatabase()
	if err != nil {
		t.Errorf("cannot open db: %v", err)
		return
	}

	if err := createTable(db, "MD5"); err != nil {
		t.Errorf("could not create table: %v", err)
	}

	file := "../cli/.data/bpa_cia_aberta_con_2017.csv"
	isNew, err := isNewFile(db, file)
	expected := false
	if _, err := os.Stat(file); !os.IsNotExist(err) {
		expected = true
	}

	if isNew == expected {
		t.Errorf("isNewFile returned %v. If 'rapina get' has run before it should've returned false.\nError: [%v]", expected, err)
	}
}

func openDatabase() (db *sql.DB, err error) {

	db, err = sql.Open("sqlite3", "../bin/.data/rapina.db")
	if err != nil {
		return db, errors.Wrap(err, "database open failed")
	}

	return
}


================================================
FILE: parsers/meta/meta_bpa_cia_aberta.txt
================================================
-----------------------
Campo: CNPJ_CIA
-----------------------
   Descrição: CNPJ da companhia
   Domínio: Alfanumérico
   Tipo dados: varchar
   Precisão: 20
   Scale: 0
-----------------------
Campo: DT_REFER
-----------------------
   Descrição: Data de referência do documento
   Domínio: AAAA-MM-DD
   Tipo dados: date
   Precisão: 10
   Scale: 0
-----------------------
Campo: VERSAO
-----------------------
   Descrição: Versão do documento
   Domínio: Numérico
   Tipo dados: smallint
   Precisão: 5
   Scale: 0
-----------------------
Campo: DENOM_CIA
-----------------------
   Descrição: Nome empresarial da companhia
   Domínio: Alfanumérico
   Tipo dados: varchar
   Precisão: 100
   Scale: 0
-----------------------
Campo: CD_CVM
-----------------------
   Descrição: Código CVM
   Domínio: Numérico
   Tipo dados: numeric
   Precisão: 7
   Scale: 0
-----------------------
Campo: GRUPO_DFP
-----------------------
   Descrição: Nome e nível de agregação da demonstração
   Domínio: Alfanumérico
   Tipo dados: varchar
   Precisão: 206
   Scale: 0
-----------------------
Campo: MOEDA
-----------------------
   Descrição: Moeda
   Domínio: Alfanumérico
   Tipo dados: varchar
   Precisão: 4
   Scale: 0
-----------------------
Campo: ESCALA_MOEDA
-----------------------
   Descrição: Escala monetária
   Domínio: Alfanumérico
   Tipo dados: varchar
   Precisão: 7
   Scale: 0
-----------------------
Campo: ORDEM_EXERC
-----------------------
   Descrição: Ordem do exercício social
   Domínio: Alfanumérico
   Tipo dados: varchar
   Precisão: 9
   Scale: 0
-----------------------
Campo: DT_FIM_EXERC
-----------------------
   Descrição: Data fim do exercício social
   Domínio: AAAA-MM-DD
   Tipo dados: date
   Precisão: 10
   Scale: 0
-----------------------
Campo: CD_CONTA
-----------------------
   Descrição: Código da conta
   Domínio: Numérico
   Tipo dados: varchar
   Precisão: 18
   Scale: 0
-----------------------
Campo: DS_CONTA
-----------------------
   Descrição: Descrição da conta
   Domínio: Alfanumérico
   Tipo dados: varchar
   Precisão: 100
   Scale: 0
-----------------------
Campo: VL_CONTA
-----------------------
   Descrição: Valor da conta
   Domínio: Numérico
   Tipo dados: numeric
   Precisão: 29
   Scale: 2


================================================
FILE: parsers/meta/meta_bpp_cia_aberta.txt
================================================
-----------------------
Campo: CNPJ_CIA
-----------------------
   Descrição: CNPJ da companhia
   Domínio: Alfanumérico
   Tipo dados: varchar
   Precisão: 20
   Scale: 0
-----------------------
Campo: DT_REFER
-----------------------
   Descrição: Data de referência do documento
   Domínio: AAAA-MM-DD
   Tipo dados: date
   Precisão: 10
   Scale: 0
-----------------------
Campo: VERSAO
-----------------------
   Descrição: Versão do documento
   Domínio: Numérico
   Tipo dados: smallint
   Precisão: 5
   Scale: 0
-----------------------
Campo: DENOM_CIA
-----------------------
   Descrição: Nome empresarial da companhia
   Domínio: Alfanumérico
   Tipo dados: varchar
   Precisão: 100
   Scale: 0
-----------------------
Campo: CD_CVM
-----------------------
   Descrição: Código CVM
   Domínio: Numérico
   Tipo dados: numeric
   Precisão: 7
   Scale: 0
-----------------------
Campo: GRUPO_DFP
-----------------------
   Descrição: Nome e nível de agregação da demonstração
   Domínio: Alfanumérico
   Tipo dados: varchar
   Precisão: 206
   Scale: 0
-----------------------
Campo: MOEDA
-----------------------
   Descrição: Moeda
   Domínio: Alfanumérico
   Tipo dados: varchar
   Precisão: 4
   Scale: 0
-----------------------
Campo: ESCALA_MOEDA
-----------------------
   Descrição: Escala monetária
   Domínio: Alfanumérico
   Tipo dados: varchar
   Precisão: 7
   Scale: 0
-----------------------
Campo: ORDEM_EXERC
-----------------------
   Descrição: Ordem do exercício social
   Domínio: Alfanumérico
   Tipo dados: varchar
   Precisão: 9
   Scale: 0
-----------------------
Campo: DT_FIM_EXERC
-----------------------
   Descrição: Data fim do exercício social
   Domínio: AAAA-MM-DD
   Tipo dados: date
   Precisão: 10
   Scale: 0
-----------------------
Campo: CD_CONTA
-----------------------
   Descrição: Código da conta
   Domínio: Numérico
   Tipo dados: varchar
   Precisão: 18
   Scale: 0
-----------------------
Campo: DS_CONTA
-----------------------
   Descrição: Descrição da conta
   Domínio: Alfanumérico
   Tipo dados: varchar
   Precisão: 100
   Scale: 0
-----------------------
Campo: VL_CONTA
-----------------------
   Descrição: Valor da conta
   Domínio: Numérico
   Tipo dados: numeric
   Precisão: 29
   Scale: 2


================================================
FILE: parsers/meta/meta_dfc_md_cia_aberta.txt
================================================
-----------------------
Campo: CNPJ_CIA
-----------------------
   Descrição: CNPJ da companhia
   Domínio: Alfanumérico
   Tipo dados: varchar
   Precisão: 20
   Scale: 0
-----------------------
Campo: DT_REFER
-----------------------
   Descrição: Data de referência do documento
   Domínio: AAAA-MM-DD
   Tipo dados: date
   Precisão: 10
   Scale: 0
-----------------------
Campo: VERSAO
-----------------------
   Descrição: Versão do documento
   Domínio: Numérico
   Tipo dados: smallint
   Precisão: 5
   Scale: 0
-----------------------
Campo: DENOM_CIA
-----------------------
   Descrição: Nome empresarial da companhia
   Domínio: Alfanumérico
   Tipo dados: varchar
   Precisão: 100
   Scale: 0
-----------------------
Campo: CD_CVM
-----------------------
   Descrição: Código CVM
   Domínio: Numérico
   Tipo dados: numeric
   Precisão: 7
   Scale: 0
-----------------------
Campo: GRUPO_DFP
-----------------------
   Descrição: Nome e nível de agregação da demonstração
   Domínio: Alfanumérico
   Tipo dados: varchar
   Precisão: 206
   Scale: 0
-----------------------
Campo: MOEDA
-----------------------
   Descrição: Moeda
   Domínio: Alfanumérico
   Tipo dados: varchar
   Precisão: 4
   Scale: 0
-----------------------
Campo: ESCALA_MOEDA
-----------------------
   Descrição: Escala monetária
   Domínio: Alfanumérico
   Tipo dados: varchar
   Precisão: 7
   Scale: 0
-----------------------
Campo: ORDEM_EXERC
-----------------------
   Descrição: Ordem do exercício social
   Domínio: Alfanumérico
   Tipo dados: varchar
   Precisão: 9
   Scale: 0
-----------------------
Campo: DT_INI_EXERC
-----------------------
   Descrição: Data início do exercício social
   Domínio: AAAA-MM-DD
   Tipo dados: date
   Precisão: 10
   Scale: 0
-----------------------
Campo: DT_FIM_EXERC
-----------------------
   Descrição: Data fim do exercício social
   Domínio: AAAA-MM-DD
   Tipo dados: date
   Precisão: 10
   Scale: 0
-----------------------
Campo: CD_CONTA
-----------------------
   Descrição: Código da conta
   Domínio: Numérico
   Tipo dados: varchar
   Precisão: 18
   Scale: 0
-----------------------
Campo: DS_CONTA
-----------------------
   Descrição: Descrição da conta
   Domínio: Alfanumérico
   Tipo dados: varchar
   Precisão: 100
   Scale: 0
-----------------------
Campo: VL_CONTA
-----------------------
   Descrição: Valor da conta
   Domínio: Numérico
   Tipo dados: numeric
   Precisão: 29
   Scale: 2


================================================
FILE: parsers/meta/meta_dfc_mi_cia_aberta.txt
================================================
-----------------------
Campo: CNPJ_CIA
-----------------------
   Descrição: CNPJ da companhia
   Domínio: Alfanumérico
   Tipo dados: varchar
   Precisão: 20
   Scale: 0
-----------------------
Campo: DT_REFER
-----------------------
   Descrição: Data de referência do documento
   Domínio: AAAA-MM-DD
   Tipo dados: date
   Precisão: 10
   Scale: 0
-----------------------
Campo: VERSAO
-----------------------
   Descrição: Versão do documento
   Domínio: Numérico
   Tipo dados: smallint
   Precisão: 5
   Scale: 0
-----------------------
Campo: DENOM_CIA
-----------------------
   Descrição: Nome empresarial da companhia
   Domínio: Alfanumérico
   Tipo dados: varchar
   Precisão: 100
   Scale: 0
-----------------------
Campo: CD_CVM
-----------------------
   Descrição: Código CVM
   Domínio: Numérico
   Tipo dados: numeric
   Precisão: 7
   Scale: 0
-----------------------
Campo: GRUPO_DFP
-----------------------
   Descrição: Nome e nível de agregação da demonstração
   Domínio: Alfanumérico
   Tipo dados: varchar
   Precisão: 206
   Scale: 0
-----------------------
Campo: MOEDA
-----------------------
   Descrição: Moeda
   Domínio: Alfanumérico
   Tipo dados: varchar
   Precisão: 4
   Scale: 0
-----------------------
Campo: ESCALA_MOEDA
-----------------------
   Descrição: Escala monetária
   Domínio: Alfanumérico
   Tipo dados: varchar
   Precisão: 7
   Scale: 0
-----------------------
Campo: ORDEM_EXERC
-----------------------
   Descrição: Ordem do exercício social
   Domínio: Alfanumérico
   Tipo dados: varchar
   Precisão: 9
   Scale: 0
-----------------------
Campo: DT_INI_EXERC
-----------------------
   Descrição: Data início do exercício social
   Domínio: AAAA-MM-DD
   Tipo dados: date
   Precisão: 10
   Scale: 0
-----------------------
Campo: DT_FIM_EXERC
-----------------------
   Descrição: Data fim do exercício social
   Domínio: AAAA-MM-DD
   Tipo dados: date
   Precisão: 10
   Scale: 0
-----------------------
Campo: CD_CONTA
-----------------------
   Descrição: Código da conta
   Domínio: Numérico
   Tipo dados: varchar
   Precisão: 18
   Scale: 0
-----------------------
Campo: DS_CONTA
-----------------------
   Descrição: Descrição da conta
   Domínio: Alfanumérico
   Tipo dados: varchar
   Precisão: 100
   Scale: 0
-----------------------
Campo: VL_CONTA
-----------------------
   Descrição: Valor da conta
   Domínio: Numérico
   Tipo dados: numeric
   Precisão: 29
   Scale: 2


================================================
FILE: parsers/meta/meta_dre_cia_aberta.txt
================================================
-----------------------
Campo: CNPJ_CIA
-----------------------
   Descrição: CNPJ da companhia
   Domínio: Alfanumérico
   Tipo dados: varchar
   Precisão: 20
   Scale: 0
-----------------------
Campo: DT_REFER
-----------------------
   Descrição: Data de referência do documento
   Domínio: AAAA-MM-DD
   Tipo dados: date
   Precisão: 10
   Scale: 0
-----------------------
Campo: VERSAO
-----------------------
   Descrição: Versão do documento
   Domínio: Numérico
   Tipo dados: smallint
   Precisão: 5
   Scale: 0
-----------------------
Campo: DENOM_CIA
-----------------------
   Descrição: Nome empresarial da companhia
   Domínio: Alfanumérico
   Tipo dados: varchar
   Precisão: 100
   Scale: 0
-----------------------
Campo: CD_CVM
-----------------------
   Descrição: Código CVM
   Domínio: Numérico
   Tipo dados: numeric
   Precisão: 7
   Scale: 0
-----------------------
Campo: GRUPO_DFP
-----------------------
   Descrição: Nome e nível de agregação da demonstração
   Domínio: Alfanumérico
   Tipo dados: varchar
   Precisão: 206
   Scale: 0
-----------------------
Campo: ESCALA_DRE
-----------------------
   Descrição: Escala monetária
   Domínio: Alfanumérico
   Tipo dados: varchar
   Precisão: 7
   Scale: 0
-----------------------
Campo: ORDEM_EXERC
-----------------------
   Descrição: Ordem do exercício social
   Domínio: Alfanumérico
   Tipo dados: varchar
   Precisão: 9
   Scale: 0
-----------------------
Campo: DT_INI_EXERC
-----------------------
   Descrição: Data início do exercício social
   Domínio: AAAA-MM-DD
   Tipo dados: date
   Precisão: 10
   Scale: 0
-----------------------
Campo: DT_FIM_EXERC
-----------------------
   Descrição: Data fim do exercício social
   Domínio: AAAA-MM-DD
   Tipo dados: date
   Precisão: 10
   Scale: 0
-----------------------
Campo: CD_CONTA
-----------------------
   Descrição: Código da conta
   Domínio: Numérico
   Tipo dados: varchar
   Precisão: 18
   Scale: 0
-----------------------
Campo: DS_CONTA
-----------------------
   Descrição: Descrição da conta
   Domínio: Alfanumérico
   Tipo dados: varchar
   Precisão: 100
   Scale: 0
-----------------------
Campo: VL_CONTA
-----------------------
   Descrição: Valor da conta
   Domínio: Numérico
   Tipo dados: numeric
   Precisão: 29
   Scale: 2


================================================
FILE: parsers/sectors.go
================================================
package parsers

import (
	"bufio"
	"fmt"
	"os"
	"regexp"
	"strings"

	"github.com/PuerkitoBio/goquery"
	"github.com/dude333/rapina"
	"github.com/gocolly/colly/v2"
	"github.com/pkg/errors"
	yaml "gopkg.in/yaml.v2"
)

// SectorsToYaml grab data from B3 website and prints out to a yaml file
// with all companies grouped by sector, subsector, segment
func SectorsToYaml(yamlFile string) (err error) {
	progress := []string{"/", "-", "\\", "|", "-", "\\"}
	var p int32

	if !overwritePrompt(yamlFile) {
		return rapina.ErrFileNotUpdated
	}
	f, err := os.Create(yamlFile)
	if err != nil {
		return errors.Wrapf(err, "falha ao criar arquivo %s", yamlFile)
	}
	defer f.Close()

	w := bufio.NewWriter(f)

	c := colly.NewCollector(
		// Restrict crawling to specific domains
		// colly.AllowedDomains("bvmf.bmfbovespa.com.br"),
		colly.AllowURLRevisit(),
		colly.Async(false),
		colly.CacheDir(".data/cache"),
	)

	c.OnHTML("tr", func(e *colly.HTMLElement) {
		var sector string
		var subsectors []string
		c := 0
		e.ForEach("td", func(_ int, elem *colly.HTMLElement) {
			elem.DOM.Each(func(_ int, s *goquery.Selection) {
				h, _ := s.Html()
				if c == 0 {
					sector = h
					fmt.Fprintln(w, "  - Setor:", sector)
					fmt.Fprintln(w, "    Subsetores:")
				} else if c == 1 {
					subsectors = strings.Split(h, "<br/>")
					last := subsectors[0]
					for i := range subsectors {
						if subsectors[i] == "" {
							subsectors[i] = last
						}
						last = subsectors[i]
					}
				}
				c++
			})

			lastSub := ""
			elem.ForEach("a[href]", func(i int, elem *colly.HTMLElement) {
				if strings.Contains(elem.Attr("href"), "BuscaEmpresaListada.aspx") {
					// fmt.Printf("\n=> %s > %s > %s:\n", sector, subsectors[i], elem.Text) //, elem.Attr("href"))
					if subsectors[i] != lastSub {
						fmt.Fprintln(w, "      - Subsetor:", subsectors[i])
						fmt.Fprintln(w, "        Segmentos:")
					}
					lastSub = subsectors[i]
					fmt.Fprintln(w, "          - Segmento:", removeYamlInvalidChar(elem.Text))
					fmt.Fprintln(w, "            Empresas:")
					_ = companies(w, "http://bvmf.bmfbovespa.com.br/cias-listadas/empresas-listadas/"+elem.Attr("href"))
				}

				fmt.Printf("\r[%s]", progress[p%6])
				p++
			})
		})
	})

	fmt.Print("[ ] Lendo informações do site da B3")
	fmt.Fprintln(w, "Setores:")

	err = c.Visit("http://bvmf.bmfbovespa.com.br/cias-listadas/empresas-listadas/BuscaEmpresaListada.aspx?opcao=1&indiceAba=1&Idioma=pt-br")

	fmt.Println()
	w.Flush()

	return
}

// companies lists all companies in the same sector/subsector/segment
func companies(w *bufio.Writer, url string) error {
	c := colly.NewCollector(
		// Restrict crawling to specific domains
		// colly.AllowedDomains("bvmf.bmfbovespa.com.br"),
		colly.AllowURLRevisit(),
		colly.Async(false),
		colly.CacheDir(".data/cache"),
	)

	// Find and visit all links
	c.OnHTML("tr", func(e *colly.HTMLElement) {
		// if e.Attr("class") != "GridRow_SiteBmfBovespa GridBovespaItemStyle" {
		// 	return
		// }

		e.ForEachWithBreak("a", func(_ int, elem *colly.HTMLElement) bool {
			if strings.Contains(elem.Attr("href"), "ResumoEmpresaPrincipal.aspx") {
				fmt.Fprintln(w, "              -", removeYamlInvalidChar(elem.Text))
			}
			return false // get only the 1st elem
		})
	})

	return c.Visit(url)
}

// overwritePrompt prompts to overwrite file if it exists
func overwritePrompt(filename string) bool {
	if _, err := os.Stat(filename); err == nil { // check if file exists
		fmt.Printf("\n[?] Deseja sobrescrever o arquivo \"%s\"? (s/N) ", filename)
		reader := bufio.NewReader(os.Stdin)
		prompt, _ := reader.ReadString('\n')

		if !strings.EqualFold(prompt, "s\n") && !strings.EqualFold(prompt, "sim\n") &&
			!strings.EqualFold(prompt, "s\r\n") && !strings.EqualFold(prompt, "sim\r\n") {
			return false
		}
	}

	return true
}

// S contains the sectors
type S struct {
	Sectors []Sector `yaml:"Setores"`
}

// Sector is divided into subsectors
type Sector struct {
	Name       string      `yaml:"Setor"`
	Subsectors []Subsector `yaml:"Subsetores"`
}

// Subsector is divided into segments
type Subsector struct {
	Name     string    `yaml:"Subsetor"`
	Segments []Segment `yaml:"Segmentos"`
}

// Segment contains companies from the same sector/subsector/segment
type Segment struct {
	Name      string   `yaml:"Segmento"`
	Companies []string `yaml:"Empresas"`
}

// FromSector returns all companies from the same sector as the 'company'
func FromSector(company, yamlFile string) (companies []string, sectorName string, err error) {
	y, err := os.ReadFile(yamlFile)
	if err != nil {
		err = errors.Wrapf(err, "ReadFile: %v", err)
		return
	}

	s := S{}
	if err := yaml.Unmarshal(y, &s); err != nil {
		return nil, "", err
	}

	for _, sector := range s.Sectors {
		for _, subsector := range sector.Subsectors {
			for _, segment := range subsector.Segments {
				if FuzzyMatch(company, segment.Companies, 2) {
					companies = segment.Companies
					sectorName = strings.Join([]string{sector.Name, subsector.Name, segment.Name}, " > ")
					return
				}
			}
		}
	}

	return
}

// removeYamlInvalidChar removes yaml invalid characters
func removeYamlInvalidChar(text string) string {
	yaml_invalid_chars := regexp.MustCompile(`[^/\s.A-zÀ-ú0-9&():-]`)
	return yaml_invalid_chars.ReplaceAllString(text, "")
}


================================================
FILE: parsers/sectors_test.go
================================================
package parsers

import (
	"os"
	"testing"
)

func TestFromSector(t *testing.T) {
	tempDir, _ := os.MkdirTemp("", "rapina-test")
	filename := tempDir + "/test_sectors.yml"

	createYaml(filename)
	s, _, _ := FromSector("GRENDENE S.A.", filename)
	expected := [...]string{"ALPARGATAS S.A.", "CAMBUCI S.A.", "GRENDENE S.A.", "VULCABRAS/AZALEIA S.A."}
	if len(s) != 4 {
		t.Errorf("\n- Expected:  %v\n- Got:       %v", expected, s)
	}

	var arr [4]string
	copy(arr[:], s)
	if arr != expected {
		t.Errorf("\n- Expected:  %v\n- Got:       %v", expected, s)
	}

	os.Remove(filename)
}

func createYaml(filename string) {
	yaml := []byte(
		`Setores:
- Setor: Bens Industriais
  Subsetores:
    - Subsetor: Comércio
      Segmentos:
        - Segmento: Material de Transporte
          Empresas:
            - MINASMAQUINAS S.A.
            - WLM PART. E COMÉRCIO DE MÁQUINAS E VEÍCULOS S.A.
- Setor: Consumo Cíclico
  Subsetores:
    - Subsetor: Tecidos. Vestuário e Calçados
      Segmentos:
        - Segmento: Acessórios
          Empresas:
          - MUNDIAL S.A. - PRODUTOS DE CONSUMO
          - TECHNOS S.A.
        - Segmento: Calçados
          Empresas:
            - ALPARGATAS S.A.
            - CAMBUCI S.A.
            - GRENDENE S.A.
            - VULCABRAS/AZALEIA S.A.`)

	_ = os.WriteFile(filename, yaml, 0644)
}


================================================
FILE: parsers/stock.go
================================================
package parsers

/*
	TODO:
	https://query1.finance.yahoo.com/v7/finance/download/RBVA11.SA?period1=1588395063&period2=1619931063&interval=1d&events=history&includeAdjustedClose=true
	https://query1.finance.yahoo.com/v7/finance/download/BBPO11.SA?period1=1619654400&period2=1619740800&interval=1d&events=history&includeAdjustedClose=true
*/

import (
	"bufio"
	"database/sql"
	"fmt"
	"io"
	"os"
	"strconv"
	"strings"
	"sync"

	"github.com/dude333/rapina"
	"github.com/dude333/rapina/progress"
	"github.com/pkg/errors"
	"golang.org/x/text/encoding/charmap"
	"golang.org/x/text/transform"
)

type stockQuote struct {
	Stock  string
	Date   string
	Open   float64
	High   float64
	Low    float64
	Close  float64
	Volume float64
}

type stockCode struct {
	TckrSymb      string // Code
	SgmtNm        string // value: CASH
	SctyCtgyNm    string // values: SHARES, UNIT, FUNDS
	CrpnNm        string // Company name
	SpcfctnCd     string // values: ON, ON NM, PN N2, etc.
	CorpGovnLvlNm string // values: NOVO MERCADO, NIVEL 2, etc.
}

type StockParser struct {
	db  *sql.DB
	log rapina.Logger
}

//
// NewStock creates the required tables, if necessary, and returns a StockParser instance.
//
func NewStock(db *sql.DB, log rapina.Logger) (*StockParser, error) {
	for _, t := range []string{"status", "stock_quotes", "stock_codes"} {
		if err := createTable(db, t); err != nil {
			return nil, err
		}
	}

	s := &StockParser{db: db, log: log}
	return s, nil
}

//
// Quote returns the quote from DB.
//
func (s *StockParser) Quote(code, date string) (float64, error) {
	query := `SELECT close FROM stock_quotes WHERE stock=$1 AND date=$2;`
	var close float64
	err := s.db.QueryRow(query, code, date).Scan(&close)
	if err == sql.ErrNoRows {
		return 0, errors.New("não encontrado no bd")
	}
	if err != nil {
		return 0, errors.Wrapf(err, "lendo cotação de %s do bd", code)
	}

	return close, nil
}

//
// Quote returns the company ON stock code, where stockType is:
// ON, PN, UNT, CI [CI = FII]
//
func (s *StockParser) Code(companyName, stockType string) (string, error) {
	query := `SELECT trading_code FROM stock_codes WHERE company_name LIKE ? AND SpcfctnCd LIKE ?;`
	st := strings.ToUpper(stockType + "%")
	var code string
	err := s.db.QueryRow(query, "%"+companyName+"%", st).Scan(&code)
	if err == sql.ErrNoRows {
		return "", errors.New("não encontrado no bd")
	}
	if err != nil {
		return "", errors.Wrapf(err, "lendo código de %s do bd", companyName)
	}

	return code, nil
}

func (s *StockParser) SaveB3Quotes(filename string) error {
	isNew, err := isNewFile(s.db, filename)
	if !isNew && err == nil { // if error, process file
		progress.Warning("%s já processado anteriormente", filename)
		return errors.New("este arquivo de cotações já foi importado anteriormente")
	}

	if err := s.populateStockQuotes(filename); err != nil {
		return err
	}

	storeFile(s.db, filename)

	return nil
}

func (s *StockParser) populateStockQuotes(filename string) error {
	fh, err := os.Open(filename)
	if err != nil {
		return errors.Wrapf(err, "abrindo arquivo %s", filename)
	}
	defer fh.Close()

	// BEGIN TRANSACTION
	tx, err := s.db.Begin()
	if err != nil {
		return errors.Wrap(err, "Failed to begin transaction")
	}

	dec := transform.NewReader(fh, charmap.ISO8859_1.NewDecoder())
	scanner := bufio.NewScanner(dec)
	for scanner.Scan() {
		line := scanner.Text()
		if len(line) == 0 {
			continue
		}
		q, err := parseB3Quote(line)
		if err != nil {
			continue // ignore line
		}
		fmt.Printf("%+v\n", q)
	}

	// END TRANSACTION
	if err := tx.Commit(); err != nil {
		return errors.Wrap(err, "Failed to commit transaction")
	}

	if err := scanner.Err(); err != nil {
		return errors.Wrapf(err, "lendo arquivo %s", filename)
	}

	return nil
}

//
// Save parses the 'stream', get the 'code' stock quotes and
// store it on 'db'. Returns the number of registers saved.
//
func (s *StockParser) Save(stream io.Reader, code string) (int, error) {
	if s.db == nil {
		return 0, errors.New("bd inválido")
	}
	if stream == nil {
		return 0, errors.New("sem dados")
	}

	scanner := bufio.NewScanner(stream)

	// Read 1st line
	scanner.Scan()
	prov := provider(scanner.Text())

	var r rec
	if err := r.open(s.db, prov); err != nil {
		return 0, err
	}
	defer r.close()

	// Read stream, line by line
	var count int
	for scanner.Scan() {
		line := scanner.Text()

		var q *stockQuote
		var c *stockCode
		var err error
		switch prov {
		case b3Quotes:
			q, err = parseB3Quote(line)
		case yahoo:
			q, err = parseYahoo(line, code)
		case alphaVantage:
			q, err = parseAlphaVantage(line, code)
		case b3Codes:
			c, err = parseB3Code(line)
		}
		if err != nil {
			continue // ignore lines with error
		}

		if q != nil {
			err = r.storeQuote(q)
			if err == nil {
				count++
			}
		}
		if c != nil {
			err = r.storeCode(c)
			if err == nil {
				count++
			}
		}
	}

	if err := scanner.Err(); err != nil {
		return count, err
	}

	return count, nil
}

// open prepares the insert statement.
func (s *rec) open(db *sql.DB, provider int) error {
	var err error
	insert := `INSERT OR IGNORE INTO stock_quotes 
	(stock, date, open, high, low, close, volume) VALUES (?,?,?,?,?,?,?);`

	if provider == b3Codes {
		insert = `INSERT OR IGNORE INTO stock_codes 
	(trading_code, company_name, SpcfctnCd, CorpGovnLvlNm) VALUES (?,?,?,?);`
	}

	s.stmt, err = db.Prepare(insert)
	if err != nil || s.stmt == nil {
		return errors.Wrap(err, "insert on db")
	}

	return nil
}

// storeQuote stores the data using the insert statement.
func (s *rec) storeQuote(q *stockQuote) error {
	if s.stmt == nil {
		return errors.New("sql statement not initalized")
	}

	s.mu.Lock()

	res, err := s.stmt.Exec(
		q.Stock,
		q.Date,
		q.Open,
		q.High,
		q.Low,
		q.Close,
		q.Volume,
	)

	s.mu.Unlock()

	if err != nil {
		return errors.Wrap(err, "salvando cotação")
	}

	n, err := res.RowsAffected()
	if n == 0 || err != nil {
		return errors.New("registro não salvo (duplicado)")
	}

	return nil
}

// storeQuote stores the data using the insert statement.
func (s *rec) storeCode(c *stockCode) error {
	if s.stmt == nil {
		return errors.New("sql statement not initalized")
	}

	s.mu.Lock()

	res, err := s.stmt.Exec(
		c.TckrSymb, // trading_code
		c.CrpnNm,   // company_name
		c.SpcfctnCd,
		c.CorpGovnLvlNm,
	)

	s.mu.Unlock()

	if err != nil {
		return errors.Wrap(err, "salvando códigos")
	}

	n, err := res.RowsAffected()
	if n == 0 || err != nil {
		return errors.New("registro não salvo (duplicado)")
	}

	return nil
}

// close closes the insert statement.
func (s *rec) close() error {
	var err error
	if s.stmt != nil {
		err = s.stmt.Close()
	}
	return err
}

// API providers.
const (
	none int = iota
	alphaVantage
	yahoo
	b3Quotes
	b3Codes
)

// provider returns stream type based on header
func provider(header string) int {
	if header == "timestamp,open,high,low,close,volume" {
		return alphaVantage
	}
	if header == "Date,Open,High,Low,Close,Adj Close,Volume" {
		return yahoo
	}
	if strings.HasPrefix(header, "00COTAHIST.") {
		return b3Quotes
	}
	if strings.HasPrefix(header, "RptDt;TckrSymb;Asst;AsstDesc;SgmtNm;MktNm;SctyCtgyNm;XprtnDt;") {
		return b3Codes
	}
	return none
}

// parseAlphaVantage parses lines downloaded from Alpha Vantage API server
// and returns *stockQuote for 'code'.
func parseAlphaVantage(line, code string) (*stockQuote, error) {
	fields := strings.Split(line, ",")
	if len(fields) != 6 {
		return nil, errors.New("linha inválida") // ignore lines with error
	}

	// Columns: timestamp,open,high,low,close,volume
	var err error
	var floats [5]float64
	for i := 1; i <= 5; i++ {
		floats[i-1], err = strconv.ParseFloat(fields[i], 64)
		if err != nil {
			return nil, errors.Wrap(err, "campo inválido")
		}
	}

	return &stockQuote{
		Stock:  code,
		Date:   fields[0],
		Open:   floats[0],
		High:   floats[1],
		Low:    floats[2],
		Close:  floats[3],
		Volume: floats[4],
	}, nil
}

// parseYahoo parses lines downloaded from Yahoo Finance API server
// and returns *stockQuote for 'code'.
func parseYahoo(line, code string) (*stockQuote, error) {
	fields := strings.Split(line, ",")
	if len(fields) != 7 {
		return nil, errors.New("linha inválida") // ignore lines with error
	}

	// Columns: Date,Open,High,Low,Close,Adj Close,Volume
	var err error
	var floats [6]float64
	for i := 1; i <= 6; i++ {
		floats[i-1], err = strconv.ParseFloat(fields[i], 64)
		if err != nil {
			return nil, errors.Wrap(err, "campo inválido")
		}
	}

	return &stockQuote{
		Stock:  code,
		Date:   fields[0],
		Open:   floats[0],
		High:   floats[1],
		Low:    floats[2],
		Close:  floats[3],
		Volume: floats[5],
	}, nil
}

// parseB3Quote parses the line based on this layout:
// http://www.b3.com.br/data/files/33/67/B9/50/D84057102C784E47AC094EA8/SeriesHistoricas_Layout.pdf
//
//   CAMPO/CONTEÚDO  TIPO E TAMANHO  POS. INIC.	 POS. FINAL
//   TIPREG “01”     N(02)           01          02
//   DATA “AAAAMMDD” N(08)           03          10
//   CODBDI          X(02)           11          12
//   CODNEG          X(12)           13          24
//   TPMERC          N(03)           25          27
//   PREABE          (11)V99         57          69
//   PREMAX          (11)V99         70          82
//   PREMIN          (11)V99         83          95
//   PREULT          (11)V99         109         121
//   QUATOT          N18             153         170
//   VOLTOT          (16)V99         171         188
//
// CODBDI:
//   02 LOTE PADRÃO
//   12 FUNDO IMOBILIÁRIO
//
// TPMERC:
//   010 VISTA
//   020 FRACIONÁRIO
func parseB3Quote(line string) (*stockQuote, error) {
	if len(line) != 245 {
		return nil, errors.New("linha deve conter 245 bytes")
	}

	recType := line[0:2]
	if recType != "01" {
		return nil, fmt.Errorf("registro %s ignorado", recType)
	}

	codBDI := line[10:12]
	if codBDI != "02" && codBDI != "12" && codBDI != "13" && codBDI != "14" {
		return nil, fmt.Errorf("BDI %s ignorado", codBDI)
	}

	tpMerc := line[24:27]
	if tpMerc != "010" && tpMerc != "020" {
		return nil, fmt.Errorf("tipo de mercado %s ignorado", tpMerc)
	}

	date := line[2:6] + "-" + line[6:8] + "-" + line[8:10]
	code := strings.TrimSpace(line[12:24])

	numRanges := [5]struct {
		i, f int
	}{
		{56, 69},   // PREABE = open
		{69, 82},   // PREMAX = high
		{82, 95},   // PREMIN = low
		{108, 121}, // PREULT = close
		{170, 188}, // VOLTOT = volume
	}
	var vals [5]int
	for i, r := range numRanges {
		num, err := strconv.Atoi(line[r.i:r.f])
		if err != nil {
			return nil, err
		}
		vals[i] = num
	}

	return &stockQuote{
		Stock:  code,
		Date:   date,
		Open:   float64(vals[0]) / 100,
		High:   float64(vals[1]) / 100,
		Low:    float64(vals[2]) / 100,
		Close:  float64(vals[3]) / 100,
		Volume: float64(vals[4]) / 100,
	}, nil
}

type rec struct {
	stmt *sql.Stmt
	mu   sync.Mutex // ensures atomic writes to db
}

// parseB3Code parses lines downloaded from B3 server
// and returns *stockCode.
//
func parseB3Code(line string) (*stockCode, error) {
	fields := strings.Split(line, ";")

	// Columns:
	// RptDt;TckrSymb(2);Asst;AsstDesc;SgmtNm(5);MktNm;SctyCtgyNm(7);XprtnDt;XprtnCd;
	// TradgStartDt;TradgEndDt;BaseCd;ConvsCritNm;MtrtyDtTrgtPt;ReqrdConvsInd;
	// ISIN;CFICd;DlvryNtceStartDt;DlvryNtceEndDt;OptnTp;CtrctMltplr;AsstQtnQty;
	// AllcnRndLot;TradgCcy;DlvryTpNm;WdrwlDays;WrkgDays;ClnrDays;RlvrBasePricNm;
	// OpngFutrPosDay;SdTpCd1;UndrlygTckrSymb1;SdTpCd2;UndrlygTckrSymb2;
	// PureGoldWght;ExrcPric;OptnStyle;ValTpNm;PrmUpfrntInd;OpngPosLmtDt;
	// DstrbtnId;PricFctr;DaysToSttlm;SrsTpNm;PrtcnFlg;AutomtcExrcInd;SpcfctnCd(47);
	// CrpnNm(48);CorpActnStartDt;CtdyTrtmntTpNm;MktCptlstn;CorpGovnLvlNm(52)
	if len(fields) != 52 {
		return nil, fmt.Errorf("linha inválida %d", len(fields)) // ignore lines with error
	}

	s := stockCode{
		TckrSymb:      fields[1],
		SgmtNm:        fields[4],
		SctyCtgyNm:    fields[6],
		CrpnNm:        fields[47],
		SpcfctnCd:     fields[46],
		CorpGovnLvlNm: fields[51],
	}

	if s.SgmtNm != "CASH" ||
		(s.SctyCtgyNm != "SHARES" &&
			s.SctyCtgyNm != "FUNDS" &&
			s.SctyCtgyNm != "UNIT") {
		return nil, errors.New("linha ignorada")
	}

	return &s, nil
}


================================================
FILE: parsers/stock_test.go
================================================
package parsers

import (
	"reflect"
	"strings"
	"testing"
)

func Test_parseB3(t *testing.T) {

	const file = `012021010412NSLU11      010FII LOURDES CI  ER       R$  000000002840000000000284000000000027700000000002809000000000281900000000028029000000002819000168000000000000001381000000000038793560000000000000009999123100000010000000000000BRNSLUCTF008272
012021010412NVHO11      010FII NOVOHORICI  ER       R$  00000000015400000000001590000000000153500000000015370000000001540000000000153600000000015400009200000000000000620000000000000953349000000000000000999912310
Download .txt
gitextract_5rc_9ybs/

├── .githooks/
│   └── pre-commit
├── .github/
│   └── workflows/
│       └── test-lint-release.yml
├── .gitignore
├── LICENSE
├── Makefile
├── NOTICE
├── README.md
├── README_en.md
├── cmd/
│   └── rapina/
│       ├── cmdutils.go
│       ├── cmdutils_test.go
│       ├── fii.go
│       ├── fii_dividends.go
│       ├── fii_monthly.go
│       ├── flags.go
│       ├── list.go
│       ├── main.go
│       ├── report.go
│       ├── server.go
│       └── update.go
├── common.go
├── common_test.go
├── errors.go
├── fetch/
│   ├── fetch.go
│   ├── fetch_fii.go
│   ├── fetch_fii_test.go
│   ├── fetch_http.go
│   ├── fetch_http_test.go
│   ├── fetch_stock.go
│   ├── fetch_test.go
│   └── unzip.go
├── fii.go
├── go.mod
├── go.sum
├── logger.go
├── parsers/
│   ├── codeaccounts.go
│   ├── companies.go
│   ├── fii.go
│   ├── fiidb.go
│   ├── financial.go
│   ├── financial_test.go
│   ├── fre.go
│   ├── fuzzy.go
│   ├── fuzzy_test.go
│   ├── md5.go
│   ├── md5_test.go
│   ├── meta/
│   │   ├── meta_bpa_cia_aberta.txt
│   │   ├── meta_bpp_cia_aberta.txt
│   │   ├── meta_dfc_md_cia_aberta.txt
│   │   ├── meta_dfc_mi_cia_aberta.txt
│   │   └── meta_dre_cia_aberta.txt
│   ├── sectors.go
│   ├── sectors_test.go
│   ├── stock.go
│   ├── stock_test.go
│   ├── tables.go
│   └── transform.go
├── progress/
│   ├── cmd/
│   │   └── main.go
│   └── progress.go
├── reports/
│   ├── db.go
│   ├── db_test.go
│   ├── excel.go
│   ├── format.go
│   ├── format_test.go
│   ├── list.go
│   ├── logger.go
│   ├── logger_test.go
│   ├── reports.go
│   ├── reports_fii.go
│   ├── reports_html.go
│   └── reports_test.go
├── server/
│   ├── fs_dev.go
│   ├── fs_prod.go
│   ├── payload.go
│   ├── server.go
│   └── templates/
│       ├── fii.html
│       ├── financials.html
│       ├── index.html
│       └── layout.html
└── stock.go
Download .txt
SYMBOL INDEX (407 symbols across 57 files)

FILE: cmd/rapina/cmdutils.go
  constant dataDir (line 15) | dataDir = ".data"
  constant yamlFile (line 16) | yamlFile = "./setores.yml"
  type Parms (line 19) | type Parms struct
  function openDatabase (line 37) | func openDatabase() (db *sql.DB, err error) {
  function promptUser (line 54) | func promptUser(list []string, label string) (result string) {
  function filename (line 82) | func filename(path, name string) (fpath string, err error) {

FILE: cmd/rapina/cmdutils_test.go
  function TestFilename (line 9) | func TestFilename(t *testing.T) {

FILE: cmd/rapina/fii.go
  type fiiFlags (line 15) | type fiiFlags struct
  function init (line 34) | func init() {

FILE: cmd/rapina/fii_dividends.go
  type fiiDividendsFlags (line 19) | type fiiDividendsFlags struct
  function init (line 55) | func init() {
  function FIIDividends (line 63) | func FIIDividends(parms map[string]string, codes []string, n int) error {

FILE: cmd/rapina/fii_monthly.go
  type fiiMonthlyFlags (line 16) | type fiiMonthlyFlags struct
  function init (line 47) | func init() {
  function FIIMonthly (line 55) | func FIIMonthly(parms map[string]string, codes []string, n int) error {

FILE: cmd/rapina/flags.go
  constant Fverbose (line 6) | Fverbose = "verbose"
  constant Fnum (line 9) | Fnum = "num"
  constant Fformat (line 12) | Fformat = "format"

FILE: cmd/rapina/list.go
  function init (line 37) | func init() {
  function ListCompanies (line 75) | func ListCompanies() (err error) {
  function ListSector (line 95) | func ListSector(company, yamlFile string) (err error) {
  function ListCompaniesProfits (line 112) | func ListCompaniesProfits(rate float32) (err error) {

FILE: cmd/rapina/main.go
  function Execute (line 58) | func Execute() int {
  function init (line 66) | func init() {
  function initConfig (line 106) | func initConfig() {
  function main (line 137) | func main() {

FILE: cmd/rapina/report.go
  function init (line 55) | func init() {
  function report (line 68) | func report(company string) {
  function SelectCompany (line 113) | func SelectCompany(company string, scriptMode bool) string {
  function Report (line 171) | func Report(p Parms) (err error) {

FILE: cmd/rapina/server.go
  type serverFlags (line 15) | type serverFlags struct
  function init (line 37) | func init() {
  function serve (line 43) | func serve(parms map[string]string) error {

FILE: cmd/rapina/update.go
  function init (line 83) | func init() {

FILE: common.go
  function IsDate (line 13) | func IsDate(date string) bool {
  function IsURL (line 40) | func IsURL(str string) bool {
  function JoinURL (line 46) | func JoinURL(base string, paths ...string) string {
  function MonthsFromToday (line 55) | func MonthsFromToday(n int) []string {
  function LastBusinessDayOfYear (line 78) | func LastBusinessDayOfYear(year int) string {
  function LastBusinessDay (line 98) | func LastBusinessDay(n int) string {

FILE: common_test.go
  function TestIsDate (line 9) | func TestIsDate(t *testing.T) {
  function TestIsUrl (line 53) | func TestIsUrl(t *testing.T) {
  function TestMonthsFromToday (line 82) | func TestMonthsFromToday(t *testing.T) {
  function TestLastBusinessDayOfYear (line 122) | func TestLastBusinessDayOfYear(t *testing.T) {

FILE: fetch/fetch.go
  function CVM (line 51) | func CVM(db *sql.DB, dataDir string) error {
  type fn (line 60) | type fn
  function try (line 63) | func try(f fn, db *sql.DB, dataDir, errMsg string, now, limit, n int) {
  function processAnnualReport (line 85) | func processAnnualReport(db *sql.DB, dataDir string, year int) error {
  function processQuarterlyReport (line 121) | func processQuarterlyReport(db *sql.DB, dataDir string, year int) error {
  function processFREReport (line 159) | func processFREReport(db *sql.DB, dataDir string, year int) error {
  function fetchFiles (line 194) | func fetchFiles(url, dataDir string, zipfile string) ([]string, error) {
  function fetchFilesVerbosity (line 201) | func fetchFilesVerbosity(url, dataDir string, zipfile string, verbose bo...
  type WriteCounter (line 224) | type WriteCounter struct
    method Write (line 229) | func (wc *WriteCounter) Write(p []byte) (int, error) {
    method printProgress (line 236) | func (wc WriteCounter) printProgress() {
  function downloadFile (line 243) | func downloadFile(url, filepath string, verbose bool) (err error) {
  function Sectors (line 295) | func Sectors(yamlFile string) (err error) {
  function filesCleanup (line 304) | func filesCleanup(files []string) {
  function findFile (line 316) | func findFile(list []string, pattern string) (string, error) {

FILE: fetch/fetch_fii.go
  constant MAX_N (line 33) | MAX_N = 100
  type FII (line 36) | type FII struct
    method Dividends (line 69) | func (fii FII) Dividends(code string, n int) (*[]rapina.Dividend, erro...
    method dividendsFromDB (line 93) | func (fii FII) dividendsFromDB(code string, n int) (*[]rapina.Dividend...
    method dividendsFromServer (line 119) | func (fii *FII) dividendsFromServer(code string, n int) (*[]rapina.Div...
    method dividendReport (line 141) | func (fii *FII) dividendReport(code string, ids []id) (*[]rapina.Divid...
    method MonthlyReportIDs (line 275) | func (fii *FII) MonthlyReportIDs(code string, n int) ([]id, error) {
    method monthlyReport (line 289) | func (fii *FII) monthlyReport(code string, ids []id) (*[]rapina.Monthl...
    method Details (line 350) | func (fii *FII) Details(fiiCode string) (*rapina.FIIDetails, error) {
    method reportIDs (line 408) | func (fii *FII) reportIDs(rt repType, code string, n int) ([]id, error) {
  function NewFII (line 41) | func NewFII(db *sql.DB, log rapina.Logger) (*FII, error) {
  type id (line 53) | type id
  type Report (line 57) | type Report struct
  type docID (line 60) | type docID struct
  function parseData (line 215) | func parseData(data []string) (rapina.Dividend, bool) {
  function comma2dot (line 245) | func comma2dot(val string) float64 {
  function fixDate (line 253) | func fixDate(date string) string {
  function getTextContent (line 261) | func getTextContent(n *html.Node) string {
  type repType (line 401) | type repType
  constant repMonthly (line 404) | repMonthly repType = iota + 1
  constant repDividends (line 405) | repDividends
  function minmax (line 470) | func minmax(n, min, max int) int {

FILE: fetch/fetch_fii_test.go
  function Test_comma2dot (line 7) | func Test_comma2dot(t *testing.T) {
  function Test_FixDate (line 36) | func Test_FixDate(t *testing.T) {

FILE: fetch/fetch_http.go
  constant _http_timeout (line 13) | _http_timeout = 30 * time.Second
  type HTTPFetch (line 16) | type HTTPFetch struct
    method JSON (line 27) | func (h HTTPFetch) JSON(url string, target interface{}) error {
  function NewHTTP (line 21) | func NewHTTP() *HTTPFetch {
  function getJSON (line 41) | func getJSON(url string, target interface{}) error {

FILE: fetch/fetch_http_test.go
  function init (line 13) | func init() {
  function jsonsMock (line 20) | func jsonsMock(w http.ResponseWriter, r *http.Request) {
  type jsonData (line 24) | type jsonData struct
  function TestHTTPFetch_JSON (line 28) | func TestHTTPFetch_JSON(t *testing.T) {

FILE: fetch/fetch_stock.go
  constant APInone (line 23) | APInone = iota
  constant APIalphavantage (line 24) | APIalphavantage
  constant APIyahoo (line 25) | APIyahoo
  type Stock (line 29) | type Stock struct
    method Quote (line 57) | func (s *Stock) Quote(code, date string) (float64, error) {
    method stockQuoteFromB3 (line 109) | func (s *Stock) stockQuoteFromB3(date string) error {
    method stockQuoteFromAPIServer (line 149) | func (s *Stock) stockQuoteFromAPIServer(code, date string, apiProvider...
    method Code (line 198) | func (s *Stock) Code(companyName, stockType string) (string, error) {
    method UpdateStockCodes (line 223) | func (s *Stock) UpdateStockCodes() error {
  function NewStock (line 40) | func NewStock(db *sql.DB, log rapina.Logger, apiKey, dataDir string) (*S...
  type b3CodesFile (line 210) | type b3CodesFile struct
  function apiURL (line 269) | func apiURL(provider int, apiKey, code, date string) string {
  function map2str (line 299) | func map2str(data map[string]interface{}) string {
  function ifNot (line 308) | func ifNot(err error) bool {

FILE: fetch/fetch_test.go
  function Test_findFile (line 9) | func Test_findFile(t *testing.T) {

FILE: fetch/unzip.go
  function Unzip (line 17) | func Unzip(src string, dest string, verbose bool) ([]string, error) {
  function valid (line 90) | func valid(filename string) bool {

FILE: fii.go
  type Dividend (line 4) | type Dividend struct
  type Monthly (line 12) | type Monthly struct
  type FIIDetails (line 16) | type FIIDetails struct
  type FIIStorage (line 55) | type FIIStorage interface

FILE: logger.go
  type Logger (line 6) | type Logger interface

FILE: parsers/codeaccounts.go
  constant UNDEF (line 11) | UNDEF uint32 = iota
  constant SPACE (line 12) | SPACE
  constant Caixa (line 15) | Caixa
  constant AplicFinanceiras (line 16) | AplicFinanceiras
  constant Estoque (line 17) | Estoque
  constant Equity (line 18) | Equity
  constant ContasARecebCirc (line 19) | ContasARecebCirc
  constant ContasARecebNCirc (line 20) | ContasARecebNCirc
  constant AtivoCirc (line 21) | AtivoCirc
  constant AtivoNCirc (line 22) | AtivoNCirc
  constant AtivoTotal (line 23) | AtivoTotal
  constant PassivoCirc (line 24) | PassivoCirc
  constant PassivoNCirc (line 25) | PassivoNCirc
  constant PassivoTotal (line 26) | PassivoTotal
  constant DividaCirc (line 27) | DividaCirc
  constant DividaNCirc (line 28) | DividaNCirc
  constant DividendosJCP (line 29) | DividendosJCP
  constant DividendosMin (line 30) | DividendosMin
  constant Vendas (line 33) | Vendas
  constant CustoVendas (line 34) | CustoVendas
  constant DespesasOp (line 35) | DespesasOp
  constant EBIT (line 36) | EBIT
  constant ResulFinanc (line 37) | ResulFinanc
  constant ResulOpDescont (line 38) | ResulOpDescont
  constant LucLiq (line 39) | LucLiq
  constant FCO (line 42) | FCO
  constant FCI (line 43) | FCI
  constant FCF (line 44) | FCF
  constant Deprec (line 47) | Deprec
  constant JurosCapProp (line 48) | JurosCapProp
  constant Dividendos (line 49) | Dividendos
  constant Shares (line 52) | Shares
  constant FreeFloat (line 53) | FreeFloat
  constant EstoqueMedio (line 56) | EstoqueMedio
  constant EquityAvg (line 57) | EquityAvg
  constant Escala (line 60) | Escala
  constant Quote (line 63) | Quote
  type account (line 67) | type account struct
  function acctCode (line 123) | func acctCode(cdAccount, dsAccount string) uint32 {

FILE: parsers/companies.go
  type company (line 9) | type company struct
  function loadCompanies (line 14) | func loadCompanies(db *sql.DB) (map[string]company, error) {
  function saveCompanies (line 38) | func saveCompanies(db *sql.DB, companies map[string]company) error {
  function updateCompanies (line 59) | func updateCompanies(companies map[string]company, cnpj, name string) {

FILE: parsers/fiidb.go
  type FIIParser (line 22) | type FIIParser struct
    method SaveDetails (line 39) | func (fii *FIIParser) SaveDetails(stream []byte) error {
    method Details (line 72) | func (fii *FIIParser) Details(code string) (*rapina.FIIDetails, error) {
    method Dividends (line 104) | func (fii *FIIParser) Dividends(code, monthYear string) (*[]rapina.Div...
    method SaveDividend (line 150) | func (fii *FIIParser) SaveDividend(dividend rapina.Dividend) error {
    method SelectFIIDetails (line 165) | func (fii *FIIParser) SelectFIIDetails(code string) (*rapina.FIIDetail...
  function NewFII (line 29) | func NewFII(db *sql.DB, log rapina.Logger) (*FIIParser, error) {
  function trimFIIDetails (line 196) | func trimFIIDetails(f *rapina.FIIDetails) {

FILE: parsers/financial.go
  function ImportCsv (line 28) | func ImportCsv(db *sql.DB, dataType string, file string) (err error) {
  function populateTable (line 84) | func populateTable(db *sql.DB, dataType, file string) (int, error) {
  function prepareFields (line 226) | func prepareFields(dataType string, header map[string]int, fields []stri...

FILE: parsers/financial_test.go
  function tempFilename (line 11) | func tempFilename(t *testing.T) string {
  function samples (line 20) | func samples(filename string) error {
  function TestImportCsv (line 39) | func TestImportCsv(t *testing.T) {
  function TestGetHash (line 73) | func TestGetHash(t *testing.T) {
  function TestRemoveDiacritics (line 91) | func TestRemoveDiacritics(t *testing.T) {
  function Test_prepareFields (line 109) | func Test_prepareFields(t *testing.T) {
  function BenchmarkPrepareFields (line 200) | func BenchmarkPrepareFields(b *testing.B) {

FILE: parsers/fre.go
  function populateFRE (line 21) | func populateFRE(db *sql.DB, file string) (int, error) {
  function prepareFREFields (line 133) | func prepareFREFields(header map[string]int, fields []string, companies ...

FILE: parsers/fuzzy.go
  function FuzzyMatch (line 15) | func FuzzyMatch(src string, list []string, distance int) bool {
  function FuzzyFind (line 23) | func FuzzyFind(source string, targets []string, maxDistance int) (found ...
  function fix (line 53) | func fix(txt string) string {

FILE: parsers/fuzzy_test.go
  function TestFuzzyFind (line 5) | func TestFuzzyFind(t *testing.T) {

FILE: parsers/md5.go
  function isNewFile (line 15) | func isNewFile(db *sql.DB, filename string) (isNew bool, err error) {
  function storeFile (line 36) | func storeFile(db *sql.DB, filename string) (md5 string) {
  function md5FromFile (line 52) | func md5FromFile(filename string) (string, error) {

FILE: parsers/md5_test.go
  function TestIsNewFile (line 12) | func TestIsNewFile(t *testing.T) {
  function openDatabase (line 39) | func openDatabase() (db *sql.DB, err error) {

FILE: parsers/sectors.go
  function SectorsToYaml (line 19) | func SectorsToYaml(yamlFile string) (err error) {
  function companies (line 98) | func companies(w *bufio.Writer, url string) error {
  function overwritePrompt (line 125) | func overwritePrompt(filename string) bool {
  type S (line 141) | type S struct
  type Sector (line 146) | type Sector struct
  type Subsector (line 152) | type Subsector struct
  type Segment (line 158) | type Segment struct
  function FromSector (line 164) | func FromSector(company, yamlFile string) (companies []string, sectorNam...
  function removeYamlInvalidChar (line 192) | func removeYamlInvalidChar(text string) string {

FILE: parsers/sectors_test.go
  function TestFromSector (line 8) | func TestFromSector(t *testing.T) {
  function createYaml (line 28) | func createYaml(filename string) {

FILE: parsers/stock.go
  type stockQuote (line 26) | type stockQuote struct
  type stockCode (line 36) | type stockCode struct
  type StockParser (line 45) | type StockParser struct
    method Quote (line 67) | func (s *StockParser) Quote(code, date string) (float64, error) {
    method Code (line 85) | func (s *StockParser) Code(companyName, stockType string) (string, err...
    method SaveB3Quotes (line 100) | func (s *StockParser) SaveB3Quotes(filename string) error {
    method populateStockQuotes (line 116) | func (s *StockParser) populateStockQuotes(filename string) error {
    method Save (line 159) | func (s *StockParser) Save(stream io.Reader, code string) (int, error) {
  function NewStock (line 53) | func NewStock(db *sql.DB, log rapina.Logger) (*StockParser, error) {
  constant none (line 313) | none int = iota
  constant alphaVantage (line 314) | alphaVantage
  constant yahoo (line 315) | yahoo
  constant b3Quotes (line 316) | b3Quotes
  constant b3Codes (line 317) | b3Codes
  function provider (line 321) | func provider(header string) int {
  function parseAlphaVantage (line 339) | func parseAlphaVantage(line, code string) (*stockQuote, error) {
  function parseYahoo (line 368) | func parseYahoo(line, code string) (*stockQuote, error) {
  function parseB3Quote (line 418) | func parseB3Quote(line string) (*stockQuote, error) {
  type rec (line 470) | type rec struct
    method open (line 223) | func (s *rec) open(db *sql.DB, provider int) error {
    method storeQuote (line 242) | func (s *rec) storeQuote(q *stockQuote) error {
    method storeCode (line 274) | func (s *rec) storeCode(c *stockCode) error {
    method close (line 303) | func (s *rec) close() error {
  function parseB3Code (line 478) | func parseB3Code(line string) (*stockCode, error) {

FILE: parsers/stock_test.go
  function Test_parseB3 (line 9) | func Test_parseB3(t *testing.T) {
  function Test_parseB3Code (line 36) | func Test_parseB3Code(t *testing.T) {

FILE: parsers/tables.go
  constant currentDbVersion (line 11) | currentDbVersion = 210514
  constant currentFIIDbVersion (line 12) | currentFIIDbVersion = 210426
  constant currentStockCodesVersion (line 13) | currentStockCodesVersion = 210518
  constant currentStockQuotesVersion (line 14) | currentStockQuotesVersion = 210305
  function allTables (line 122) | func allTables() []string {
  function whatTable (line 135) | func whatTable(dataType string) (table string, err error) {
  function createTable (line 169) | func createTable(db *sql.DB, dataType string) (err error) {
  function createAllTables (line 210) | func createAllTables(db *sql.DB) (err error) {
  function dbVersion (line 228) | func dbVersion(db *sql.DB, dataType string) (v int, table string) {
  function wipeDB (line 246) | func wipeDB(db *sql.DB, dataType string) (err error) {
  function createIndexes (line 263) | func createIndexes(db *sql.DB, table string) error {
  function hasTable (line 300) | func hasTable(db *sql.DB, tableName string) bool {

FILE: parsers/transform.go
  function Hash (line 18) | func Hash(s string) uint32 {
  function RemoveDiacritics (line 28) | func RemoveDiacritics(original string) (result string) {

FILE: progress/cmd/main.go
  function main (line 10) | func main() {
  function f1 (line 42) | func f1() {

FILE: progress/progress.go
  type event (line 13) | type event struct
  constant spinners (line 27) | spinners = `/-\|`
  constant colorReset (line 30) | colorReset = "\033[0m"
  constant colorRed (line 32) | colorRed = "\033[31m"
  constant colorYellow (line 34) | colorYellow = "\033[33m"
  constant colorBlue (line 35) | colorBlue   = "\033[34m"
  constant colorCyan (line 37) | colorCyan = "\033[36m"
  type Progress (line 41) | type Progress struct
  function init (line 50) | func init() {
  function SetDebug (line 54) | func SetDebug(on bool) {
  function Cursor (line 58) | func Cursor(show bool) {
  function Status (line 69) | func Status(format string, a ...interface{}) {
  function Error (line 83) | func Error(err error) {
  function ErrorMsg (line 98) | func ErrorMsg(format string, a ...interface{}) {
  function Warning (line 113) | func Warning(format string, a ...interface{}) {
  function Debug (line 128) | func Debug(format string, a ...interface{}) {
  function Running (line 145) | func Running(msg string) {
  function Spinner (line 150) | func Spinner() {
  function RunOK (line 155) | func RunOK() {
  function RunFail (line 160) | func RunFail() {
  function Download (line 174) | func Download(a string) {
  function clearLine (line 201) | func clearLine() {
  function output (line 211) | func output(buf []byte) {
  function outputln (line 215) | func outputln(s string) {

FILE: reports/db.go
  type accItems (line 14) | type accItems struct
  type AccountValue (line 20) | type AccountValue struct
  method accountsItems (line 30) | func (r Report) accountsItems(cid int) (items []accItems, err error) {
  method accountsValues (line 65) | func (r Report) accountsValues(year int) (map[uint32]float32, error) {
  function avg (line 120) | func avg(nums ...float32) float32 {
  method lastYear (line 140) | func (r Report) lastYear(cid int) (int, bool, error) {
  method LastYearRange (line 176) | func (r Report) LastYearRange(cid int) (int, int, error) {
  method dfp (line 215) | func (r Report) dfp(cid, year int, _values map[uint32]float32) error {
  method RawAccounts (line 245) | func (r Report) RawAccounts(cid, year int) ([]AccountValue, error) {
  method lastDate (line 292) | func (r Report) lastDate(cid int) (int, string, error) {
  method lastBalance (line 318) | func (r Report) lastBalance(cid int) (map[uint32]float32, error) {
  method ttm (line 366) | func (r Report) ttm(cid int, _values map[uint32]float32) error {
  method lastDFPYear (line 437) | func (r Report) lastDFPYear(cid int) (int, error) {
  method shares (line 454) | func (r Report) shares(cid int, year int, values map[uint32]float32) err...
  method sharesAvg (line 483) | func (r Report) sharesAvg(cids []string, year int, values map[uint32]flo...
  method value (line 511) | func (r Report) value(cid, year int, code uint32) (float32, error) {
  method accountsAverage (line 538) | func (r Report) accountsAverage(company string, year int) (map[uint32]fl...
  method movingAvg (line 603) | func (r Report) movingAvg(cids []string, year int, code uint32) (float32...
  method fromSector (line 629) | func (r Report) fromSector(company string) (companies []string, sectorNa...
  type CompanyInfo (line 656) | type CompanyInfo struct
  function companies (line 664) | func companies(db *sql.DB) ([]CompanyInfo, error) {
  type TickerInfo (line 691) | type TickerInfo struct
  function tickers (line 699) | func tickers(db *sql.DB, companyName string) ([]TickerInfo, error) {
  method setCompanyAndTicker (line 736) | func (r *Report) setCompanyAndTicker(company string, spcfctnCd string) e...
  method getCid (line 769) | func (r *Report) getCid(companyName string) (int, error) {
  method scale (line 780) | func (r Report) scale(cid, year int, table string) float32 {
  function timeRange (line 806) | func timeRange(db *sql.DB) (int, int, error) {
  function removeDuplicates (line 844) | func removeDuplicates(elements []string) []string { // change string to ...
  type profit (line 863) | type profit struct
  function companyProfits (line 868) | func companyProfits(db *sql.DB, companyID int) ([]profit, error) {

FILE: reports/db_test.go
  function Test_avg (line 5) | func Test_avg(t *testing.T) {

FILE: reports/excel.go
  type Excel (line 12) | type Excel struct
    method saveAndCloseExcel (line 28) | func (e *Excel) saveAndCloseExcel(filename string) (err error) {
    method newSheet (line 45) | func (e *Excel) newSheet(name string) (s *Sheet, err error) {
  function newExcel (line 19) | func newExcel() (e *Excel) {
  type Sheet (line 40) | type Sheet struct
    method printCell (line 61) | func (s Sheet) printCell(row, col int, value interface{}, styleID int) {
    method printTitle (line 73) | func (s *Sheet) printTitle(cell string, title string) (err error) {
    method print (line 89) | func (s *Sheet) print(startingCel string, slice *[]string, format int,...
    method printValue (line 115) | func (s *Sheet) printValue(cell string, value float32, format int, bol...
    method printFormula (line 134) | func (s *Sheet) printFormula(cell string, formula string, format int, ...
    method mergeCell (line 176) | func (s *Sheet) mergeCell(a, b string) {
    method autoWidth (line 183) | func (s *Sheet) autoWidth() {
    method setColWidth (line 207) | func (s *Sheet) setColWidth(col int, width float64) {
  function jsonStyle (line 153) | func jsonStyle(size, format int, bold bool) ([]byte, error) {
  function axis (line 215) | func axis(col, row int) string {
  function cell2axis (line 222) | func cell2axis(cell string) (col, row int) {
  function colLetter (line 232) | func colLetter(col int) string {

FILE: reports/format.go
  constant DEFAULT (line 15) | DEFAULT = iota + 1
  constant GENERAL (line 18) | GENERAL
  constant NUMBER (line 19) | NUMBER
  constant INDEX (line 20) | INDEX
  constant PERCENT (line 21) | PERCENT
  constant EMPTY (line 23) | EMPTY
  constant LEFT (line 26) | LEFT
  constant RIGHT (line 27) | RIGHT
  constant CENTER (line 28) | CENTER
  type formatFont (line 32) | type formatFont struct
  type formatAlignment (line 41) | type formatAlignment struct
  type formatBorder (line 53) | type formatBorder struct
  type formatFill (line 59) | type formatFill struct
  type formatStyle (line 67) | type formatStyle struct
    method size (line 117) | func (f *formatStyle) size(s int) {
    method newStyle (line 121) | func (f formatStyle) newStyle(e *excelize.File) (style int) {
  function newFormat (line 86) | func newFormat(format int, position int, bold bool) (f *formatStyle) {

FILE: reports/format_test.go
  function TestFormat (line 9) | func TestFormat(t *testing.T) {

FILE: reports/list.go
  function ListCompanies (line 19) | func ListCompanies(db *sql.DB) (names []string, err error) {
  function ListTickers (line 48) | func ListTickers(db *sql.DB, companyName string) (names []string, err er...
  function GetSpcfctnCd (line 77) | func GetSpcfctnCd(db *sql.DB, companyName string, ticker string) string {
  function ListSector (line 104) | func ListSector(db *sql.DB, company, yamlFile string) (err error) {
  function ListCompaniesProfits (line 139) | func ListCompaniesProfits(db *sql.DB, rate float32) error {

FILE: reports/logger.go
  type Logger (line 9) | type Logger struct
    method SetOut (line 19) | func (l *Logger) SetOut(out io.Writer) {
    method Run (line 24) | func (l *Logger) Run(format string, v ...interface{}) {
    method Ok (line 33) | func (l *Logger) Ok() {
    method Nok (line 38) | func (l *Logger) Nok() {
    method Printf (line 43) | func (l *Logger) Printf(format string, v ...interface{}) {
    method Trace (line 48) | func (l *Logger) Trace(format string, v ...interface{}) {
    method Debug (line 53) | func (l *Logger) Debug(format string, v ...interface{}) {
    method Info (line 58) | func (l *Logger) Info(format string, v ...interface{}) {
    method Warn (line 63) | func (l *Logger) Warn(format string, v ...interface{}) {
    method Error (line 68) | func (l *Logger) Error(format string, v ...interface{}) {
    method output (line 75) | func (l *Logger) output(s string) {
    method outputln (line 84) | func (l *Logger) outputln(s string) {
  function NewLogger (line 15) | func NewLogger(out io.Writer) *Logger {

FILE: reports/logger_test.go
  function TestLogger (line 10) | func TestLogger(t *testing.T) {
  function TestLogger_Run (line 67) | func TestLogger_Run(t *testing.T) {

FILE: reports/reports.go
  constant sectorAverage (line 21) | sectorAverage = "MÉDIA DO SETOR"
  constant grpAccts (line 24) | grpAccts int = iota + 100
  constant grpShares (line 25) | grpShares
  constant grpExtra (line 26) | grpExtra
  constant grpFleuriet (line 27) | grpFleuriet
  type metric (line 31) | type metric struct
  type Report (line 39) | type Report struct
    method sectorReport (line 376) | func (r Report) sectorReport(sheet *Sheet, company string) (err error) {
    method companySummary (line 436) | func (r *Report) companySummary(sheet *Sheet, row, col *int, _company,...
    method Summary (line 597) | func (r *Report) Summary(company string) (map[string]string, error) {
    method printCodesAndDescriptions (line 753) | func (r Report) printCodesAndDescriptions(sheet *Sheet, accounts []acc...
  function New (line 69) | func New(parms map[string]interface{}) (*Report, error) {
  function ReportToXlsx (line 123) | func ReportToXlsx(parms map[string]interface{}) error {
  function ReportToStdout (line 312) | func ReportToStdout(parms map[string]interface{}) error {
  function buildStdAccountReport (line 351) | func buildStdAccountReport(data []AccountValue) (*strings.Builder, error) {
  function metricsList (line 621) | func metricsList(v map[uint32]float32) (metrics []metric) {
  function zeroIfNeg (line 702) | func zeroIfNeg(n float32) float32 {
  function safeDiv (line 709) | func safeDiv(n, d float32) float32 {
  function ident (line 724) | func ident(str string) (spaces string, baseItem bool) {
  function sum (line 782) | func sum(values map[uint32]float32) float32 {

FILE: reports/reports_fii.go
  constant Rtable (line 21) | Rtable = iota + 1
  constant Rcsv (line 22) | Rcsv
  constant Rcsvrend (line 23) | Rcsvrend
  type FIITerminal (line 27) | type FIITerminal struct
    method SetParms (line 59) | func (t *FIITerminal) SetParms(parms map[string]string) {
    method Dividends (line 76) | func (t FIITerminal) Dividends(codes []string, n int) error {
    method printDividends (line 149) | func (t FIITerminal) printDividends(code string, dividends *[]rapina.D...
    method csvDividends (line 177) | func (t FIITerminal) csvDividends(code string, dividends *[]rapina.Div...
    method csvDividendsOnly (line 199) | func (t FIITerminal) csvDividendsOnly(code string, n int, dividends *[...
    method Monthly (line 234) | func (t FIITerminal) Monthly(codes []string, n int) error {
  type FIITerminalOptions (line 33) | type FIITerminalOptions struct
  function NewFIITerminal (line 38) | func NewFIITerminal(db *sql.DB, opts FIITerminalOptions) (*FIITerminal, ...
  function revMonthsFromToday (line 223) | func revMonthsFromToday(n int) []string {

FILE: reports/reports_test.go
  function AssertEqual (line 11) | func AssertEqual(t *testing.T, msg string, a interface{}, b interface{}) {
  function TestIdent (line 19) | func TestIdent(t *testing.T) {
  function TestZeroIfNeg (line 47) | func TestZeroIfNeg(t *testing.T) {
  function TestSafeDiv (line 56) | func TestSafeDiv(t *testing.T) {
  function TestMetricsList (line 65) | func TestMetricsList(t *testing.T) {
  function TestStdBuildReport (line 91) | func TestStdBuildReport(t *testing.T) {

FILE: server/fs_dev.go
  function init (line 14) | func init() {

FILE: server/fs_prod.go
  function init (line 15) | func init() {

FILE: server/payload.go
  function fiiDividendsPayload (line 12) | func fiiDividendsPayload(srv *Server, fiiCodes []string, months int) int...
  function fiiDividends (line 26) | func fiiDividends(srv *Server, codes []string, n int) interface{} {

FILE: server/server.go
  type Server (line 21) | type Server struct
  type ServerOption (line 31) | type ServerOption
  function WithDB (line 33) | func WithDB(db *sql.DB) ServerOption {
  function WithAPIKey (line 38) | func WithAPIKey(apiKey string) ServerOption {
  function WithDataDir (line 43) | func WithDataDir(dataDir string) ServerOption {
  function Verbose (line 48) | func Verbose(on bool) ServerOption {
  function initServer (line 54) | func initServer(opts ...ServerOption) (*Server, error) {
  function HTML (line 88) | func HTML(opts ...ServerOption) {
  function renderTemplate (line 105) | func renderTemplate(srv *Server) http.HandlerFunc {
  function ptFmtFloat (line 142) | func ptFmtFloat(f float64) string {
  function parseCodes (line 147) | func parseCodes(text string) []string {
  function split (line 159) | func split(r rune) bool {
  function parseNumeric (line 164) | func parseNumeric(numeric string, alt int) int {

FILE: stock.go
  type StockStorage (line 7) | type StockStorage interface
Condensed preview — 79 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (324K chars).
[
  {
    "path": ".githooks/pre-commit",
    "chars": 463,
    "preview": "#!/bin/sh\n\n# git config core.hooksPath .githooks\n\necho \"Running pre-commit checks at `pwd`\"\n\n{\n  echo \"golangci-lint run"
  },
  {
    "path": ".github/workflows/test-lint-release.yml",
    "chars": 3165,
    "preview": "name: Test, Lint & Release\n\non: [ push, pull_request ]\n\njobs:\n  go-test:\n    strategy:\n      fail-fast: false\n      matr"
  },
  {
    "path": ".gitignore",
    "chars": 448,
    "preview": "# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\ndebug\n*.db-journal\n.vscode\nbin/**\n\n# Temporary and d"
  },
  {
    "path": "LICENSE",
    "chars": 1073,
    "preview": "The MIT License (MIT)\n\nCopyright © 2018 Adriano P\n\nPermission is hereby granted, free of charge, to any person obtaining"
  },
  {
    "path": "Makefile",
    "chars": 1272,
    "preview": "BUILDDIR=./cmd/...\nSOURCEDIR=.\nSOURCES := $(shell find $(SOURCEDIR) -name '*.go')\n\nBINARYDIR=./bin/\nBINARY=bin/rapina\nWI"
  },
  {
    "path": "NOTICE",
    "chars": 1403,
    "preview": "================================================================================\n| Open Database License (ODbL) |\n\nConta"
  },
  {
    "path": "README.md",
    "chars": 9861,
    "preview": "# 𝚛𝚊𝚙𝚒𝚗𝚊\n\nDownload e processamento de dados<sup>[1](#disclaimer)</sup> financeiros de empresas brasileiras diretamente d"
  },
  {
    "path": "README_en.md",
    "chars": 5047,
    "preview": "# 𝚛𝚊𝚙𝚒𝚗𝚊\n\nDownload and process Brazilian companies' financial data directly from [CVM](http://dados.cvm.gov.br/dados/CIA"
  },
  {
    "path": "cmd/rapina/cmdutils.go",
    "chars": 2922,
    "preview": "package main\n\nimport (\n\t\"database/sql\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/manifoldco/promptui\"\n\t\"git"
  },
  {
    "path": "cmd/rapina/cmdutils_test.go",
    "chars": 705,
    "preview": "package main\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n)\n\nfunc TestFilename(t *testing.T) {\n\ttempDir, _ := os.MkdirTem"
  },
  {
    "path": "cmd/rapina/fii.go",
    "chars": 854,
    "preview": "/*\nCopyright © 2021 Adriano P <dev@dude333.com>\nDistributed under the MIT License.\n*/\npackage main\n\nimport (\n\t\"fmt\"\n\t\"os"
  },
  {
    "path": "cmd/rapina/fii_dividends.go",
    "chars": 1936,
    "preview": "/*\nCopyright © 2021 Adriano P <dev@dude333.com>\nDistributed under the MIT License.\n*/\npackage main\n\nimport (\n\t\"fmt\"\n\t\"lo"
  },
  {
    "path": "cmd/rapina/fii_monthly.go",
    "chars": 1725,
    "preview": "/*\nCopyright © 2021 Adriano P <dev@dude333.com>\nDistributed under the MIT License.\n*/\npackage main\n\nimport (\n\t\"log\"\n\t\"st"
  },
  {
    "path": "cmd/rapina/flags.go",
    "chars": 163,
    "preview": "package main\n\n// Flags constants\nconst (\n\t// Root persistent\n\tFverbose = \"verbose\"\n\n\t// fiiCmd persistent\n\tFnum = \"num\"\n"
  },
  {
    "path": "cmd/rapina/list.go",
    "chars": 3306,
    "preview": "// Copyright © 2018 Adriano P\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of t"
  },
  {
    "path": "cmd/rapina/main.go",
    "chars": 5078,
    "preview": "// Copyright © 2018 Adriano P\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of t"
  },
  {
    "path": "cmd/rapina/report.go",
    "chars": 5510,
    "preview": "/// Copyright © 2018 Adriano P\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of "
  },
  {
    "path": "cmd/rapina/server.go",
    "chars": 1120,
    "preview": "/*\nCopyright © 2021 Adriano P <dev@dude333.com>\nDistributed under the MIT License.\n*/\npackage main\n\nimport (\n\t\"log\"\n\n\t\"g"
  },
  {
    "path": "cmd/rapina/update.go",
    "chars": 2605,
    "preview": "// Copyright © 2018 Adriano P\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of t"
  },
  {
    "path": "common.go",
    "chars": 2480,
    "preview": "package rapina\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"path\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\n// IsDate checks if date is in forma"
  },
  {
    "path": "common_test.go",
    "chars": 2890,
    "preview": "package rapina\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestIsDate(t *testing.T) {\n\ttype args struct {\n\t\tdate str"
  },
  {
    "path": "errors.go",
    "chars": 402,
    "preview": "package rapina\n\nimport \"errors\"\n\n// Error codes\nvar (\n\tErrRecordExists   = errors.New(\"insert ignored, register already "
  },
  {
    "path": "fetch/fetch.go",
    "chars": 8585,
    "preview": "// Copyright © 2018 Adriano P\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of t"
  },
  {
    "path": "fetch/fetch_fii.go",
    "chars": 12262,
    "preview": "package fetch\n\n/*\n\tURL List:\n\n\tFundos.NET: where the report IDs are obtained.\n\t=> https://fnet.bmfbovespa.com.br/fnet/pu"
  },
  {
    "path": "fetch/fetch_fii_test.go",
    "chars": 1060,
    "preview": "package fetch\n\nimport (\n\t\"testing\"\n)\n\nfunc Test_comma2dot(t *testing.T) {\n\ttype args struct {\n\t\tval string\n\t}\n\ttests := "
  },
  {
    "path": "fetch/fetch_http.go",
    "chars": 1336,
    "preview": "package fetch\n\nimport (\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/dude333/rapina/progress\""
  },
  {
    "path": "fetch/fetch_http_test.go",
    "chars": 625,
    "preview": "package fetch\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nvar ts *ht"
  },
  {
    "path": "fetch/fetch_stock.go",
    "chars": 7860,
    "preview": "package fetch\n\nimport (\n\t\"crypto/tls\"\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"time\"\n\n\t\"gi"
  },
  {
    "path": "fetch/fetch_test.go",
    "chars": 908,
    "preview": "package fetch\n\nimport (\n\t\"testing\"\n\n\t_ \"github.com/mattn/go-sqlite3\"\n)\n\nfunc Test_findFile(t *testing.T) {\n\ttype args st"
  },
  {
    "path": "fetch/unzip.go",
    "chars": 2097,
    "preview": "package fetch\n\nimport (\n\t\"archive/zip\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n)\n\n//\n// UnzipVerbosity will decom"
  },
  {
    "path": "fii.go",
    "chars": 2571,
    "preview": "package rapina\n\n// Dividend contains the stock 'Code', and the 'Date' for the stock dividend 'Val'.\ntype Dividend struct"
  },
  {
    "path": "go.mod",
    "chars": 1125,
    "preview": "module github.com/dude333/rapina\n\nrequire (\n\tgithub.com/360EntSecGroup-Skylar/excelize v1.4.1\n\tgithub.com/PuerkitoBio/go"
  },
  {
    "path": "go.sum",
    "chars": 35851,
    "preview": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ngithub.com/360EntSecGroup-Skylar/exce"
  },
  {
    "path": "logger.go",
    "chars": 449,
    "preview": "package rapina\n\nimport \"io\"\n\n// Logger interface contains the methods needed to poperly display log messages.\ntype Logge"
  },
  {
    "path": "parsers/codeaccounts.go",
    "chars": 3529,
    "preview": "package parsers\n\nimport (\n\t\"strings\"\n)\n\n// Bookkeeping account codes\n// If you add new const values, run 'go generate'\n/"
  },
  {
    "path": "parsers/companies.go",
    "chars": 1377,
    "preview": "package parsers\n\nimport (\n\t\"database/sql\"\n\n\t\"github.com/pkg/errors\"\n)\n\ntype company struct {\n\tid   int\n\tname string\n}\n\nf"
  },
  {
    "path": "parsers/fii.go",
    "chars": 1352,
    "preview": "package parsers\n\n/*\n//\n// FetchFIIs downloads the list of FIIs to get their code (e.g. 'HGLG'),\n// then it uses this cod"
  },
  {
    "path": "parsers/fiidb.go",
    "chars": 4927,
    "preview": "package parsers\n\nimport (\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/dude333/rapina\"\n\t\"git"
  },
  {
    "path": "parsers/financial.go",
    "chars": 7288,
    "preview": "// financial.go\n// Parses data from csv files containing financial statements\n\npackage parsers\n\nimport (\n\t\"bufio\"\n\t\"data"
  },
  {
    "path": "parsers/financial_test.go",
    "chars": 6208,
    "preview": "package parsers\n\nimport (\n\t\"database/sql\"\n\t\"os\"\n\t\"testing\"\n\n\t_ \"github.com/mattn/go-sqlite3\"\n)\n\nfunc tempFilename(t *tes"
  },
  {
    "path": "parsers/fre.go",
    "chars": 4393,
    "preview": "package parsers\n\nimport (\n\t\"bufio\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\t\"golang"
  },
  {
    "path": "parsers/fuzzy.go",
    "chars": 1319,
    "preview": "package parsers\n\nimport (\n\t\"strings\"\n\n\t\"github.com/lithammer/fuzzysearch/fuzzy\"\n)\n\n//\n// FuzzyMatch measures the Levensh"
  },
  {
    "path": "parsers/fuzzy_test.go",
    "chars": 597,
    "preview": "package parsers\n\nimport \"testing\"\n\nfunc TestFuzzyFind(t *testing.T) {\n\tlist := []struct {\n\t\tsrc      string\n\t\ttrg      ["
  },
  {
    "path": "parsers/md5.go",
    "chars": 1091,
    "preview": "package parsers\n\nimport (\n\t\"crypto/md5\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n)\n\n//\n// isNewFile checks the database to see"
  },
  {
    "path": "parsers/md5_test.go",
    "chars": 955,
    "preview": "package parsers\n\nimport (\n\t\"database/sql\"\n\t\"os\"\n\t\"testing\"\n\n\t_ \"github.com/mattn/go-sqlite3\"\n\t\"github.com/pkg/errors\"\n)\n"
  },
  {
    "path": "parsers/meta/meta_bpa_cia_aberta.txt",
    "chars": 2262,
    "preview": "-----------------------\nCampo: CNPJ_CIA\n-----------------------\n   Descrição: CNPJ da companhia\n   Domínio: Alfanumérico"
  },
  {
    "path": "parsers/meta/meta_bpp_cia_aberta.txt",
    "chars": 2262,
    "preview": "-----------------------\nCampo: CNPJ_CIA\n-----------------------\n   Descrição: CNPJ da companhia\n   Domínio: Alfanumérico"
  },
  {
    "path": "parsers/meta/meta_dfc_md_cia_aberta.txt",
    "chars": 2447,
    "preview": "-----------------------\nCampo: CNPJ_CIA\n-----------------------\n   Descrição: CNPJ da companhia\n   Domínio: Alfanumérico"
  },
  {
    "path": "parsers/meta/meta_dfc_mi_cia_aberta.txt",
    "chars": 2447,
    "preview": "-----------------------\nCampo: CNPJ_CIA\n-----------------------\n   Descrição: CNPJ da companhia\n   Domínio: Alfanumérico"
  },
  {
    "path": "parsers/meta/meta_dre_cia_aberta.txt",
    "chars": 2289,
    "preview": "-----------------------\nCampo: CNPJ_CIA\n-----------------------\n   Descrição: CNPJ da companhia\n   Domínio: Alfanumérico"
  },
  {
    "path": "parsers/sectors.go",
    "chars": 5298,
    "preview": "package parsers\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"os\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/PuerkitoBio/goquery\"\n\t\"github.com/dud"
  },
  {
    "path": "parsers/sectors_test.go",
    "chars": 1326,
    "preview": "package parsers\n\nimport (\n\t\"os\"\n\t\"testing\"\n)\n\nfunc TestFromSector(t *testing.T) {\n\ttempDir, _ := os.MkdirTemp(\"\", \"rapin"
  },
  {
    "path": "parsers/stock.go",
    "chars": 12111,
    "preview": "package parsers\n\n/*\n\tTODO:\n\thttps://query1.finance.yahoo.com/v7/finance/download/RBVA11.SA?period1=1588395063&period2=16"
  },
  {
    "path": "parsers/stock_test.go",
    "chars": 3969,
    "preview": "package parsers\n\nimport (\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc Test_parseB3(t *testing.T) {\n\n\tconst file = `01202101"
  },
  {
    "path": "parsers/tables.go",
    "chars": 6441,
    "preview": "package parsers\n\nimport (\n\t\"database/sql\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n)\n\nconst currentDbVersion = 210514"
  },
  {
    "path": "parsers/transform.go",
    "chars": 644,
    "preview": "package parsers\n\nimport (\n\t\"hash/fnv\"\n\t\"unicode\"\n\n\t\"golang.org/x/text/runes\"\n\t\"golang.org/x/text/transform\"\n\t\"golang.org"
  },
  {
    "path": "progress/cmd/main.go",
    "chars": 831,
    "preview": "package main\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/dude333/rapina/progress\"\n)\n\nfunc main() {\n\n\tprogress.Cursor(false"
  },
  {
    "path": "progress/progress.go",
    "chars": 3782,
    "preview": "// progress prints the program progress on screen. It's similar to a logger, but with\n// better formatting.\npackage prog"
  },
  {
    "path": "reports/db.go",
    "chars": 19797,
    "preview": "package reports\n\nimport (\n\t\"database/sql\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/dude333/rapina\"\n\t\"github.com/dude33"
  },
  {
    "path": "reports/db_test.go",
    "chars": 570,
    "preview": "package reports\n\nimport \"testing\"\n\nfunc Test_avg(t *testing.T) {\n\ttype args struct {\n\t\tnums []float32\n\t}\n\ttests := []str"
  },
  {
    "path": "reports/excel.go",
    "chars": 5043,
    "preview": "package reports\n\nimport (\n\t\"encoding/json\"\n\t\"strconv\"\n\n\t\"github.com/360EntSecGroup-Skylar/excelize\"\n\t\"github.com/pkg/err"
  },
  {
    "path": "reports/format.go",
    "chars": 3107,
    "preview": "package reports\n\nimport (\n\t\"encoding/json\"\n\n\t\"github.com/360EntSecGroup-Skylar/excelize\"\n\t\"github.com/dude333/rapina/par"
  },
  {
    "path": "reports/format_test.go",
    "chars": 765,
    "preview": "package reports\n\nimport (\n\t\"testing\"\n\n\t\"github.com/360EntSecGroup-Skylar/excelize\"\n)\n\nfunc TestFormat(t *testing.T) {\n\tv"
  },
  {
    "path": "reports/list.go",
    "chars": 4850,
    "preview": "package reports\n\nimport (\n\t\"database/sql\"\n\t\"fmt\"\n\t\"math\"\n\t\"strings\"\n\n\t\"github.com/dude333/rapina/parsers\"\n\t\"github.com/p"
  },
  {
    "path": "reports/logger.go",
    "chars": 2023,
    "preview": "package reports\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n)\n\ntype Logger struct {\n\tout io.Writer // destination for output\n\tbuf []byt"
  },
  {
    "path": "reports/logger_test.go",
    "chars": 1514,
    "preview": "package reports\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestLogger(t *testing.T) {\n"
  },
  {
    "path": "reports/reports.go",
    "chars": 20982,
    "preview": "package reports\n\nimport (\n\t\"database/sql\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"path\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/"
  },
  {
    "path": "reports/reports_fii.go",
    "chars": 5495,
    "preview": "package reports\n\nimport (\n\t\"database/sql\"\n\t\"fmt\"\n\t\"math\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/dude333/rapina\"\n\t\"github.com/d"
  },
  {
    "path": "reports/reports_html.go",
    "chars": 16,
    "preview": "package reports\n"
  },
  {
    "path": "reports/reports_test.go",
    "chars": 2531,
    "preview": "package reports\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\tp \"github.com/dude333/rapina/parsers\"\n)\n\n// AssertEqual checks if valu"
  },
  {
    "path": "server/fs_dev.go",
    "chars": 223,
    "preview": "// +build dev\n\npackage server\n\nimport (\n\t\"io/fs\"\n\t\"log\"\n\t\"os\"\n)\n\nvar _fs = os.DirFS(\".\")\nvar _contentFS fs.FS\n\nfunc init"
  },
  {
    "path": "server/fs_prod.go",
    "chars": 241,
    "preview": "// +build !dev\n\npackage server\n\nimport (\n\t\"embed\"\n\t\"io/fs\"\n\t\"log\"\n)\n\n//go:embed templates\nvar _fs embed.FS\nvar _contentF"
  },
  {
    "path": "server/payload.go",
    "chars": 2125,
    "preview": "package server\n\nimport (\n\t\"math\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/dude333/rapina/progress\"\n)\n\n// fiiDividendsPayload "
  },
  {
    "path": "server/server.go",
    "chars": 3641,
    "preview": "package server\n\nimport (\n\t\"database/sql\"\n\t\"errors\"\n\t\"html/template\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\""
  },
  {
    "path": "server/templates/fii.html",
    "chars": 5485,
    "preview": "{{define \"body\"}}\n\n<script type=\"text/javascript\">\n  const pt = new Intl.NumberFormat(\"pt-BR\", {\n    minimumFractionDigi"
  },
  {
    "path": "server/templates/financials.html",
    "chars": 43,
    "preview": "{{define \"body\"}}\n<h2>Finanças</h2>\n{{end}}"
  },
  {
    "path": "server/templates/index.html",
    "chars": 152,
    "preview": "{{define \"body\"}}\n\n<h2>Relatórios</h2>\n<a href=\"fii.html\" class=\"\">Rendimentos dos FII</a>\n<br>\n<a href=\"financials.html"
  },
  {
    "path": "server/templates/layout.html",
    "chars": 6371,
    "preview": "{{define \"layout\"}}\n<!doctype html>\n<html lang=\"pt\">\n\n<head>\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"w"
  },
  {
    "path": "stock.go",
    "chars": 334,
    "preview": "package rapina\n\nimport \"io\"\n\n// StockStorage is the interface that contains the methods needed to parse, save and\n// ret"
  }
]

About this extraction

This page contains the full source code of the dude333/rapina GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 79 files (286.7 KB), approximately 99.8k tokens, and a symbol index with 407 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!