luxorasap 0.1.2__tar.gz → 0.1.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- luxorasap-0.1.4/PKG-INFO +128 -0
- luxorasap-0.1.4/README.md +81 -0
- {luxorasap-0.1.2 → luxorasap-0.1.4}/pyproject.toml +7 -3
- {luxorasap-0.1.2 → luxorasap-0.1.4}/src/luxorasap/__init__.py +1 -1
- {luxorasap-0.1.2 → luxorasap-0.1.4}/src/luxorasap/btgapi/__init__.py +3 -2
- {luxorasap-0.1.2 → luxorasap-0.1.4}/src/luxorasap/btgapi/reports.py +52 -2
- {luxorasap-0.1.2 → luxorasap-0.1.4}/src/luxorasap/ingest/cloud/__init__.py +7 -0
- luxorasap-0.1.4/src/luxorasap.egg-info/PKG-INFO +128 -0
- luxorasap-0.1.4/tests/test_btgapi_reports.py +139 -0
- luxorasap-0.1.2/PKG-INFO +0 -241
- luxorasap-0.1.2/README.md +0 -196
- luxorasap-0.1.2/src/luxorasap.egg-info/PKG-INFO +0 -241
- luxorasap-0.1.2/tests/test_btgapi_reports.py +0 -62
- {luxorasap-0.1.2 → luxorasap-0.1.4}/setup.cfg +0 -0
- {luxorasap-0.1.2 → luxorasap-0.1.4}/src/luxorasap/btgapi/auth.py +0 -0
- {luxorasap-0.1.2 → luxorasap-0.1.4}/src/luxorasap/btgapi/trades.py +0 -0
- {luxorasap-0.1.2 → luxorasap-0.1.4}/src/luxorasap/datareader/__init__.py +0 -0
- {luxorasap-0.1.2 → luxorasap-0.1.4}/src/luxorasap/datareader/core.py +0 -0
- {luxorasap-0.1.2 → luxorasap-0.1.4}/src/luxorasap/ingest/__init__.py +0 -0
- {luxorasap-0.1.2 → luxorasap-0.1.4}/src/luxorasap/ingest/legacy_local/dataloader.py +0 -0
- {luxorasap-0.1.2 → luxorasap-0.1.4}/src/luxorasap/utils/__init__.py +0 -0
- {luxorasap-0.1.2 → luxorasap-0.1.4}/src/luxorasap/utils/dataframe/__init__.py +0 -0
- {luxorasap-0.1.2 → luxorasap-0.1.4}/src/luxorasap/utils/dataframe/reader.py +0 -0
- {luxorasap-0.1.2 → luxorasap-0.1.4}/src/luxorasap/utils/dataframe/transforms.py +0 -0
- {luxorasap-0.1.2 → luxorasap-0.1.4}/src/luxorasap/utils/storage/__init__.py +0 -0
- {luxorasap-0.1.2 → luxorasap-0.1.4}/src/luxorasap/utils/storage/blob.py +0 -0
- {luxorasap-0.1.2 → luxorasap-0.1.4}/src/luxorasap.egg-info/SOURCES.txt +0 -0
- {luxorasap-0.1.2 → luxorasap-0.1.4}/src/luxorasap.egg-info/dependency_links.txt +0 -0
- {luxorasap-0.1.2 → luxorasap-0.1.4}/src/luxorasap.egg-info/entry_points.txt +0 -0
- {luxorasap-0.1.2 → luxorasap-0.1.4}/src/luxorasap.egg-info/requires.txt +0 -0
- {luxorasap-0.1.2 → luxorasap-0.1.4}/src/luxorasap.egg-info/top_level.txt +0 -0
- {luxorasap-0.1.2 → luxorasap-0.1.4}/tests/test_btgapi_auth.py +0 -0
- {luxorasap-0.1.2 → luxorasap-0.1.4}/tests/test_btgapi_trades.py +0 -0
- {luxorasap-0.1.2 → luxorasap-0.1.4}/tests/test_datareader.py +0 -0
- {luxorasap-0.1.2 → luxorasap-0.1.4}/tests/test_ingest_cloud.py +0 -0
- {luxorasap-0.1.2 → luxorasap-0.1.4}/tests/test_ingest_legacy_local.py +0 -0
- {luxorasap-0.1.2 → luxorasap-0.1.4}/tests/test_utils_dataframe.py +0 -0
- {luxorasap-0.1.2 → luxorasap-0.1.4}/tests/test_utils_storage.py +0 -0
luxorasap-0.1.4/PKG-INFO
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: luxorasap
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Summary: Toolbox da Luxor para ingestão, análise e automação de dados financeiros.
|
|
5
|
+
Author-email: Luxor Group <backoffice@luxor.com.br>
|
|
6
|
+
License: Proprietary – All rights reserved
|
|
7
|
+
Project-URL: Homepage, https://github.com/luxor-group/luxor-asap
|
|
8
|
+
Project-URL: Documentation, https://luxor-group.github.io/luxorasap-docs/
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: Other/Proprietary License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: pandas>=2.2
|
|
15
|
+
Requires-Dist: numpy>=1.25
|
|
16
|
+
Requires-Dist: loguru>=0.7
|
|
17
|
+
Requires-Dist: python-dotenv>=1.0
|
|
18
|
+
Requires-Dist: azure-storage-blob>=12.19
|
|
19
|
+
Requires-Dist: pyarrow>=15.0
|
|
20
|
+
Requires-Dist: requests>=2.32
|
|
21
|
+
Requires-Dist: pydantic>=2.7
|
|
22
|
+
Requires-Dist: scipy>=1.13
|
|
23
|
+
Requires-Dist: openpyxl
|
|
24
|
+
Provides-Extra: storage
|
|
25
|
+
Requires-Dist: azure-storage-blob>=12.19; extra == "storage"
|
|
26
|
+
Requires-Dist: pyarrow>=15.0; extra == "storage"
|
|
27
|
+
Provides-Extra: dataframe
|
|
28
|
+
Requires-Dist: pandas>=2.2; extra == "dataframe"
|
|
29
|
+
Provides-Extra: datareader
|
|
30
|
+
Requires-Dist: luxorasap[dataframe,storage]; extra == "datareader"
|
|
31
|
+
Requires-Dist: numpy>=1.25; extra == "datareader"
|
|
32
|
+
Requires-Dist: scipy>=1.13; extra == "datareader"
|
|
33
|
+
Provides-Extra: ingest
|
|
34
|
+
Requires-Dist: luxorasap[dataframe,storage]; extra == "ingest"
|
|
35
|
+
Requires-Dist: pandas>=2.2; extra == "ingest"
|
|
36
|
+
Provides-Extra: btgapi
|
|
37
|
+
Requires-Dist: requests>=2.32; extra == "btgapi"
|
|
38
|
+
Requires-Dist: pydantic>=2.7; extra == "btgapi"
|
|
39
|
+
Provides-Extra: dev
|
|
40
|
+
Requires-Dist: pytest>=8.2; extra == "dev"
|
|
41
|
+
Requires-Dist: requests-mock>=1.11; extra == "dev"
|
|
42
|
+
Requires-Dist: black>=24.4.0; extra == "dev"
|
|
43
|
+
Requires-Dist: isort>=5.13; extra == "dev"
|
|
44
|
+
Requires-Dist: bumpver>=2024.3; extra == "dev"
|
|
45
|
+
Requires-Dist: pre-commit>=3.7; extra == "dev"
|
|
46
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
47
|
+
|
|
48
|
+
# 🧠 LuxorASAP
|
|
49
|
+
|
|
50
|
+
**Luxor Automatic System for Assets and Portfolios** é o toolbox oficial da Luxor para automação de pipelines de dados, integração com APIs financeiras e gerenciamento eficiente de dados patrimoniais no Azure.
|
|
51
|
+
|
|
52
|
+
Projetado para ser rápido, reutilizável e seguro, este pacote unifica a ingestão, leitura e transformação de dados utilizados nas análises e marcações do time de investimentos.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 🚀 Funcionalidades
|
|
57
|
+
|
|
58
|
+
- 📡 Integração com a API de relatórios e boletas do BTG Pactual
|
|
59
|
+
- 🗂️ Carregamento padronizado de arquivos (Excel, Parquet, Blob)
|
|
60
|
+
- 💾 Escrita incremental e segura no ADLS (Azure Blob Storage)
|
|
61
|
+
- 📊 Análises de preço, retorno e risco com API de consulta (`LuxorQuery`)
|
|
62
|
+
- 🔗 Modularidade entre `btgapi`, `datareader`, `ingest`, `utils`
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 🧩 Estrutura do Projeto
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
luxor-asap/
|
|
70
|
+
├── src/luxorasap/
|
|
71
|
+
│ ├── btgapi/ # Integração com BTG Pactual
|
|
72
|
+
│ ├── datareader/ # Interface de leitura e análise de dados
|
|
73
|
+
│ ├── ingest/ # Carga de dados no ADLS
|
|
74
|
+
│ └── utils/ # Funções auxiliares (parquet, dataframe)
|
|
75
|
+
└── tests/ # Testes automatizados com Pytest
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 📚 Documentação
|
|
81
|
+
|
|
82
|
+
A documentação externa completa está disponível em:
|
|
83
|
+
|
|
84
|
+
[](https://luxor-group.github.io/luxorasap-docs/)
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 🔧 Requisitos
|
|
89
|
+
|
|
90
|
+
- Python 3.9+
|
|
91
|
+
- Azure Blob Storage configurado
|
|
92
|
+
- Variáveis de ambiente via `.env` (ou passadas manualmente):
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
AZURE_STORAGE_CONNECTION_STRING=...
|
|
96
|
+
BTG_CLIENT_ID=...
|
|
97
|
+
BTG_CLIENT_SECRET=...
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 📦 Instalação
|
|
103
|
+
|
|
104
|
+
Para instalar localmente:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
pip install -e .
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Ou via PyPI (futuramente):
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
pip install luxor-asap
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## 🧪 Testes
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
pytest -v
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## 📄 Licença
|
|
127
|
+
|
|
128
|
+
Projeto de uso interno do Luxor Group. Todos os direitos reservados.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# 🧠 LuxorASAP
|
|
2
|
+
|
|
3
|
+
**Luxor Automatic System for Assets and Portfolios** é o toolbox oficial da Luxor para automação de pipelines de dados, integração com APIs financeiras e gerenciamento eficiente de dados patrimoniais no Azure.
|
|
4
|
+
|
|
5
|
+
Projetado para ser rápido, reutilizável e seguro, este pacote unifica a ingestão, leitura e transformação de dados utilizados nas análises e marcações do time de investimentos.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 🚀 Funcionalidades
|
|
10
|
+
|
|
11
|
+
- 📡 Integração com a API de relatórios e boletas do BTG Pactual
|
|
12
|
+
- 🗂️ Carregamento padronizado de arquivos (Excel, Parquet, Blob)
|
|
13
|
+
- 💾 Escrita incremental e segura no ADLS (Azure Blob Storage)
|
|
14
|
+
- 📊 Análises de preço, retorno e risco com API de consulta (`LuxorQuery`)
|
|
15
|
+
- 🔗 Modularidade entre `btgapi`, `datareader`, `ingest`, `utils`
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 🧩 Estrutura do Projeto
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
luxor-asap/
|
|
23
|
+
├── src/luxorasap/
|
|
24
|
+
│ ├── btgapi/ # Integração com BTG Pactual
|
|
25
|
+
│ ├── datareader/ # Interface de leitura e análise de dados
|
|
26
|
+
│ ├── ingest/ # Carga de dados no ADLS
|
|
27
|
+
│ └── utils/ # Funções auxiliares (parquet, dataframe)
|
|
28
|
+
└── tests/ # Testes automatizados com Pytest
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 📚 Documentação
|
|
34
|
+
|
|
35
|
+
A documentação externa completa está disponível em:
|
|
36
|
+
|
|
37
|
+
[](https://luxor-group.github.io/luxorasap-docs/)
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 🔧 Requisitos
|
|
42
|
+
|
|
43
|
+
- Python 3.9+
|
|
44
|
+
- Azure Blob Storage configurado
|
|
45
|
+
- Variáveis de ambiente via `.env` (ou passadas manualmente):
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
AZURE_STORAGE_CONNECTION_STRING=...
|
|
49
|
+
BTG_CLIENT_ID=...
|
|
50
|
+
BTG_CLIENT_SECRET=...
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 📦 Instalação
|
|
56
|
+
|
|
57
|
+
Para instalar localmente:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install -e .
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Ou via PyPI (futuramente):
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install luxor-asap
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 🧪 Testes
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pytest -v
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 📄 Licença
|
|
80
|
+
|
|
81
|
+
Projeto de uso interno do Luxor Group. Todos os direitos reservados.
|
|
@@ -10,8 +10,8 @@ build-backend = "setuptools.build_meta"
|
|
|
10
10
|
#############################
|
|
11
11
|
[project]
|
|
12
12
|
name = "luxorasap"
|
|
13
|
-
version = "0.1.
|
|
14
|
-
description = "Luxor
|
|
13
|
+
version = "0.1.4"
|
|
14
|
+
description = "Toolbox da Luxor para ingestão, análise e automação de dados financeiros."
|
|
15
15
|
readme = "README.md"
|
|
16
16
|
requires-python = ">=3.9"
|
|
17
17
|
authors = [{ name = "Luxor Group", email = "backoffice@luxor.com.br" }]
|
|
@@ -56,6 +56,10 @@ dev = [
|
|
|
56
56
|
"build>=1.2"
|
|
57
57
|
]
|
|
58
58
|
|
|
59
|
+
[project.urls]
|
|
60
|
+
Homepage = "https://github.com/luxor-group/luxor-asap"
|
|
61
|
+
Documentation = "https://luxor-group.github.io/luxorasap-docs/"
|
|
62
|
+
|
|
59
63
|
#############################
|
|
60
64
|
# Console scripts (opcional)
|
|
61
65
|
#############################
|
|
@@ -74,7 +78,7 @@ exclude = ["tests*"]
|
|
|
74
78
|
# bumpver (sem-ver)
|
|
75
79
|
#############################
|
|
76
80
|
[tool.bumpver]
|
|
77
|
-
current_version = "0.1.
|
|
81
|
+
current_version = "0.1.4"
|
|
78
82
|
version_pattern = "MAJOR.MINOR.PATCH"
|
|
79
83
|
|
|
80
84
|
# regex explícito – obrigatório no bumpver 2024+
|
|
@@ -13,7 +13,7 @@ from types import ModuleType
|
|
|
13
13
|
try:
|
|
14
14
|
__version__: str = metadata.version(__name__)
|
|
15
15
|
except metadata.PackageNotFoundError: # editable install
|
|
16
|
-
__version__ = "0.1.
|
|
16
|
+
__version__ = "0.1.4"
|
|
17
17
|
|
|
18
18
|
# ─── Lazy loader ─────────────────────────────────────────────────
|
|
19
19
|
def __getattr__(name: str) -> ModuleType:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Wrapper para as APIs do BTG Pactual."""
|
|
2
2
|
|
|
3
3
|
from .auth import get_access_token, BTGApiError
|
|
4
|
-
from .reports import request_portfolio, await_report_ticket_result, process_zip_to_dfs, request_investors_transactions_report
|
|
4
|
+
from .reports import request_portfolio, await_report_ticket_result, process_zip_to_dfs, request_investors_transactions_report, request_fundflow_report
|
|
5
5
|
from .trades import submit_offshore_equity_trades, await_transaction_ticket_result
|
|
6
6
|
|
|
7
7
|
__all__ = [
|
|
@@ -12,5 +12,6 @@ __all__ = [
|
|
|
12
12
|
"submit_offshore_equity_trades",
|
|
13
13
|
"await_transaction_ticket_result",
|
|
14
14
|
"process_zip_to_dfs",
|
|
15
|
-
"request_investors_transactions_report"
|
|
15
|
+
"request_investors_transactions_report",
|
|
16
|
+
"request_fundflow_report",
|
|
16
17
|
]
|
|
@@ -16,7 +16,8 @@ __all__ = [
|
|
|
16
16
|
"check_report_ticket",
|
|
17
17
|
"await_report_ticket_result",
|
|
18
18
|
"process_zip_to_dfs",
|
|
19
|
-
"request_investors_transactions_report"
|
|
19
|
+
"request_investors_transactions_report",
|
|
20
|
+
"request_fundflow_report"
|
|
20
21
|
]
|
|
21
22
|
|
|
22
23
|
_REPORT_ENDPOINT = "https://funds.btgpactual.com/reports/Portfolio"
|
|
@@ -24,6 +25,7 @@ _TICKET_ENDPOINT = "https://funds.btgpactual.com/reports/Ticket"
|
|
|
24
25
|
_INVESTOR_TX_ENDPOINT = (
|
|
25
26
|
"https://funds.btgpactual.com/reports/RTA/InvestorTransactionsFileReport"
|
|
26
27
|
)
|
|
28
|
+
_FUNDFLOW_ENDPOINT = "https://funds.btgpactual.com/reports/RTA/FundFlow"
|
|
27
29
|
_REPORT_TYPES = {"excel": 10, "xml5": 81, "pdf": 2}
|
|
28
30
|
|
|
29
31
|
|
|
@@ -105,6 +107,15 @@ def check_report_ticket(token: str, ticket: str, *, page: Optional[int] = None)
|
|
|
105
107
|
return _download_url(url)
|
|
106
108
|
except Exception as exc:
|
|
107
109
|
raise BTGApiError(f"Falha ao interpretar resultado: {exc}") from exc
|
|
110
|
+
|
|
111
|
+
# 4. result pode ser uma lista de dados
|
|
112
|
+
if isinstance(result, list):
|
|
113
|
+
# Vamos tentar transformar num dataframe e retornar
|
|
114
|
+
try:
|
|
115
|
+
df = pd.DataFrame(result)
|
|
116
|
+
return df
|
|
117
|
+
except Exception as exc:
|
|
118
|
+
raise BTGApiError(f"Falha ao converter resultado em DataFrame: {exc}") from exc
|
|
108
119
|
|
|
109
120
|
raise BTGApiError("Formato de resposta desconhecido")
|
|
110
121
|
|
|
@@ -186,4 +197,43 @@ def request_investors_transactions_report( token: str, query_date: dt.date, *,
|
|
|
186
197
|
return r.json()["ticket"]
|
|
187
198
|
raise BTGApiError(
|
|
188
199
|
f"Erro InvestorTransactionsFileReport: {r.status_code} – {r.text}"
|
|
189
|
-
)
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def request_fundflow_report( token: str, start_date: dt.date,
|
|
204
|
+
end_date: dt.date, *, fund_name: str = "", date_type: str = "LIQUIDACAO", page_size: int = 100) -> str:
|
|
205
|
+
"""Dispara geração do **Fund Flow** (RTA) e devolve *ticket*.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
token: JWT obtido via :pyfunc:`luxorasap.btgapi.get_access_token`.
|
|
209
|
+
start_date,end_date: Datas do intervalo desejado.
|
|
210
|
+
fund_name: Nome do fundo conforme BTG. String vazia retorna as movimentacoes para todos os fundos.
|
|
211
|
+
date_type: Enum da API (`LIQUIDACAO`, `MOVIMENTO`, etc.).
|
|
212
|
+
page_size: Página retornada por chamada (default 100).
|
|
213
|
+
|
|
214
|
+
Returns
|
|
215
|
+
-------
|
|
216
|
+
str
|
|
217
|
+
ID do ticket a ser acompanhado em :pyfunc:`await_fundflow_ticket_result`.
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
body = {
|
|
221
|
+
"contract": {
|
|
222
|
+
"startDate": f"{start_date}T00:00:00Z",
|
|
223
|
+
"endDate": f"{end_date}T00:00:00Z",
|
|
224
|
+
"dateType": date_type,
|
|
225
|
+
"fundName": fund_name,
|
|
226
|
+
},
|
|
227
|
+
"pageSize": page_size,
|
|
228
|
+
"webhookEndpoint": "string",
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
r = requests.post(
|
|
232
|
+
_FUNDFLOW_ENDPOINT,
|
|
233
|
+
headers={"X-SecureConnect-Token": token, "Content-Type": "application/json"},
|
|
234
|
+
json=body,
|
|
235
|
+
timeout=30,
|
|
236
|
+
)
|
|
237
|
+
if r.ok:
|
|
238
|
+
return r.json()["ticket"]
|
|
239
|
+
raise BTGApiError(f"Erro FundFlow: {r.status_code} - {r.text}")
|
|
@@ -20,8 +20,15 @@ def save_table(
|
|
|
20
20
|
index_name: str = "index",
|
|
21
21
|
normalize_columns: bool = True,
|
|
22
22
|
directory: str = "enriched/parquet",
|
|
23
|
+
override=False
|
|
23
24
|
):
|
|
24
25
|
"""Salva DataFrame como Parquet em ADLS (sobrescrevendo)."""
|
|
26
|
+
|
|
27
|
+
if override == False:
|
|
28
|
+
lq = LuxorQuery()
|
|
29
|
+
if lq.table_exists(table_name):
|
|
30
|
+
return
|
|
31
|
+
|
|
25
32
|
df = prep_for_save(df, index=index, index_name=index_name, normalize=normalize_columns)
|
|
26
33
|
_client.write_df(df.astype(str), f"{directory}/{table_name}.parquet")
|
|
27
34
|
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: luxorasap
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Summary: Toolbox da Luxor para ingestão, análise e automação de dados financeiros.
|
|
5
|
+
Author-email: Luxor Group <backoffice@luxor.com.br>
|
|
6
|
+
License: Proprietary – All rights reserved
|
|
7
|
+
Project-URL: Homepage, https://github.com/luxor-group/luxor-asap
|
|
8
|
+
Project-URL: Documentation, https://luxor-group.github.io/luxorasap-docs/
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: Other/Proprietary License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: pandas>=2.2
|
|
15
|
+
Requires-Dist: numpy>=1.25
|
|
16
|
+
Requires-Dist: loguru>=0.7
|
|
17
|
+
Requires-Dist: python-dotenv>=1.0
|
|
18
|
+
Requires-Dist: azure-storage-blob>=12.19
|
|
19
|
+
Requires-Dist: pyarrow>=15.0
|
|
20
|
+
Requires-Dist: requests>=2.32
|
|
21
|
+
Requires-Dist: pydantic>=2.7
|
|
22
|
+
Requires-Dist: scipy>=1.13
|
|
23
|
+
Requires-Dist: openpyxl
|
|
24
|
+
Provides-Extra: storage
|
|
25
|
+
Requires-Dist: azure-storage-blob>=12.19; extra == "storage"
|
|
26
|
+
Requires-Dist: pyarrow>=15.0; extra == "storage"
|
|
27
|
+
Provides-Extra: dataframe
|
|
28
|
+
Requires-Dist: pandas>=2.2; extra == "dataframe"
|
|
29
|
+
Provides-Extra: datareader
|
|
30
|
+
Requires-Dist: luxorasap[dataframe,storage]; extra == "datareader"
|
|
31
|
+
Requires-Dist: numpy>=1.25; extra == "datareader"
|
|
32
|
+
Requires-Dist: scipy>=1.13; extra == "datareader"
|
|
33
|
+
Provides-Extra: ingest
|
|
34
|
+
Requires-Dist: luxorasap[dataframe,storage]; extra == "ingest"
|
|
35
|
+
Requires-Dist: pandas>=2.2; extra == "ingest"
|
|
36
|
+
Provides-Extra: btgapi
|
|
37
|
+
Requires-Dist: requests>=2.32; extra == "btgapi"
|
|
38
|
+
Requires-Dist: pydantic>=2.7; extra == "btgapi"
|
|
39
|
+
Provides-Extra: dev
|
|
40
|
+
Requires-Dist: pytest>=8.2; extra == "dev"
|
|
41
|
+
Requires-Dist: requests-mock>=1.11; extra == "dev"
|
|
42
|
+
Requires-Dist: black>=24.4.0; extra == "dev"
|
|
43
|
+
Requires-Dist: isort>=5.13; extra == "dev"
|
|
44
|
+
Requires-Dist: bumpver>=2024.3; extra == "dev"
|
|
45
|
+
Requires-Dist: pre-commit>=3.7; extra == "dev"
|
|
46
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
47
|
+
|
|
48
|
+
# 🧠 LuxorASAP
|
|
49
|
+
|
|
50
|
+
**Luxor Automatic System for Assets and Portfolios** é o toolbox oficial da Luxor para automação de pipelines de dados, integração com APIs financeiras e gerenciamento eficiente de dados patrimoniais no Azure.
|
|
51
|
+
|
|
52
|
+
Projetado para ser rápido, reutilizável e seguro, este pacote unifica a ingestão, leitura e transformação de dados utilizados nas análises e marcações do time de investimentos.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 🚀 Funcionalidades
|
|
57
|
+
|
|
58
|
+
- 📡 Integração com a API de relatórios e boletas do BTG Pactual
|
|
59
|
+
- 🗂️ Carregamento padronizado de arquivos (Excel, Parquet, Blob)
|
|
60
|
+
- 💾 Escrita incremental e segura no ADLS (Azure Blob Storage)
|
|
61
|
+
- 📊 Análises de preço, retorno e risco com API de consulta (`LuxorQuery`)
|
|
62
|
+
- 🔗 Modularidade entre `btgapi`, `datareader`, `ingest`, `utils`
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 🧩 Estrutura do Projeto
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
luxor-asap/
|
|
70
|
+
├── src/luxorasap/
|
|
71
|
+
│ ├── btgapi/ # Integração com BTG Pactual
|
|
72
|
+
│ ├── datareader/ # Interface de leitura e análise de dados
|
|
73
|
+
│ ├── ingest/ # Carga de dados no ADLS
|
|
74
|
+
│ └── utils/ # Funções auxiliares (parquet, dataframe)
|
|
75
|
+
└── tests/ # Testes automatizados com Pytest
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 📚 Documentação
|
|
81
|
+
|
|
82
|
+
A documentação externa completa está disponível em:
|
|
83
|
+
|
|
84
|
+
[](https://luxor-group.github.io/luxorasap-docs/)
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 🔧 Requisitos
|
|
89
|
+
|
|
90
|
+
- Python 3.9+
|
|
91
|
+
- Azure Blob Storage configurado
|
|
92
|
+
- Variáveis de ambiente via `.env` (ou passadas manualmente):
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
AZURE_STORAGE_CONNECTION_STRING=...
|
|
96
|
+
BTG_CLIENT_ID=...
|
|
97
|
+
BTG_CLIENT_SECRET=...
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 📦 Instalação
|
|
103
|
+
|
|
104
|
+
Para instalar localmente:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
pip install -e .
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Ou via PyPI (futuramente):
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
pip install luxor-asap
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## 🧪 Testes
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
pytest -v
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## 📄 Licença
|
|
127
|
+
|
|
128
|
+
Projeto de uso interno do Luxor Group. Todos os direitos reservados.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import datetime as dt, io, json, zipfile
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from luxorasap.btgapi.reports import (
|
|
6
|
+
request_portfolio,
|
|
7
|
+
await_report_ticket_result,
|
|
8
|
+
process_zip_to_dfs,
|
|
9
|
+
check_report_ticket,
|
|
10
|
+
request_fundflow_report,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
_TICKET_URL = "https://funds.btgpactual.com/reports/Ticket"
|
|
14
|
+
_POST_URL = "https://funds.btgpactual.com/reports/Portfolio"
|
|
15
|
+
_FUNDFLOW_URL = "https://funds.btgpactual.com/reports/RTA/FundFlow"
|
|
16
|
+
|
|
17
|
+
_TOKEN = "dummy-token"
|
|
18
|
+
|
|
19
|
+
def test_request_portfolio_returns_ticket(requests_mock):
|
|
20
|
+
requests_mock.post(_POST_URL, json={"ticket": "ABC"})
|
|
21
|
+
tk = request_portfolio("tok", "FUND", dt.date(2025,1,1), dt.date(2025,1,31))
|
|
22
|
+
assert tk == "ABC"
|
|
23
|
+
|
|
24
|
+
def test_await_ticket_inline_zip(requests_mock, monkeypatch):
|
|
25
|
+
# 1ª chamada: ainda processando 2ª: devolve ZIP binário
|
|
26
|
+
# Explicando o que requests_mock faz:
|
|
27
|
+
#
|
|
28
|
+
requests_mock.get(_TICKET_URL, [
|
|
29
|
+
{"json": {"result": "Processando"}},
|
|
30
|
+
{"content": b"ZIP!"},
|
|
31
|
+
])
|
|
32
|
+
monkeypatch.setattr("time.sleep", lambda *_: None)
|
|
33
|
+
out = await_report_ticket_result("tok", "ABC", attempts=2, interval=0)
|
|
34
|
+
assert out == b"ZIP!"
|
|
35
|
+
|
|
36
|
+
def test_await_ticket_via_urldownload(requests_mock, monkeypatch):
|
|
37
|
+
dl_url = "https://download/file.zip"
|
|
38
|
+
# 1ª chamada ao ticket devolve JSON com UrlDownload
|
|
39
|
+
requests_mock.get(_TICKET_URL, json={"result": json.dumps({"UrlDownload": dl_url})})
|
|
40
|
+
requests_mock.get(dl_url, content=b"ZIP2", headers={"Content-Type": "application/zip"})
|
|
41
|
+
monkeypatch.setattr("time.sleep", lambda *_: None)
|
|
42
|
+
out = await_report_ticket_result("tok", "XYZ", attempts=1)
|
|
43
|
+
assert out == b"ZIP2"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_process_zip_to_dfs():
|
|
47
|
+
buf = io.BytesIO()
|
|
48
|
+
with zipfile.ZipFile(buf, "w") as zf:
|
|
49
|
+
df = pd.DataFrame({"x": [1]})
|
|
50
|
+
zf.writestr("data.csv", df.to_csv(index=False))
|
|
51
|
+
dfs = process_zip_to_dfs(buf.getvalue())
|
|
52
|
+
assert dfs["data.csv"].iloc[0, 0] == 1
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_process_zip_latin1_csv():
|
|
56
|
+
import io, zipfile, pandas as pd
|
|
57
|
+
from luxorasap.btgapi.reports import process_zip_to_dfs
|
|
58
|
+
|
|
59
|
+
df_src = pd.DataFrame({"nome": ["ação", "æøå"]})
|
|
60
|
+
csv_latin1 = df_src.to_csv(index=False, encoding="latin1")
|
|
61
|
+
|
|
62
|
+
buf = io.BytesIO()
|
|
63
|
+
with zipfile.ZipFile(buf, "w") as zf:
|
|
64
|
+
zf.writestr("dados.csv", csv_latin1)
|
|
65
|
+
|
|
66
|
+
dfs = process_zip_to_dfs(buf.getvalue())
|
|
67
|
+
assert dfs["dados.csv"].equals(df_src)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_request_fundflow_report_success(requests_mock):
|
|
71
|
+
"""POST devolvendo ticket e corpo montado corretamente."""
|
|
72
|
+
start, end = dt.date(2025, 5, 4), dt.date(2025, 6, 4)
|
|
73
|
+
fund_name = "luxor lipizzaner fia"
|
|
74
|
+
expected_id = "TCK-123"
|
|
75
|
+
|
|
76
|
+
# valida corpo enviado
|
|
77
|
+
def _match(request):
|
|
78
|
+
payload = request.json()
|
|
79
|
+
c = payload["contract"]
|
|
80
|
+
assert c["startDate"] == f"{start}T00:00:00Z"
|
|
81
|
+
assert c["endDate"] == f"{end}T00:00:00Z"
|
|
82
|
+
assert c["fundName"] == fund_name
|
|
83
|
+
assert c["dateType"] == "LIQUIDACAO"
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
requests_mock.post(
|
|
87
|
+
_FUNDFLOW_URL,
|
|
88
|
+
additional_matcher=_match,
|
|
89
|
+
json={"ticket": expected_id},
|
|
90
|
+
status_code=200,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
ticket = request_fundflow_report(
|
|
94
|
+
_TOKEN, start, end, fund_name=fund_name
|
|
95
|
+
)
|
|
96
|
+
assert ticket == expected_id
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_check_report_ticket_returns_dataframe(requests_mock):
|
|
100
|
+
"""GET devolvendo JSON-list deve virar DataFrame."""
|
|
101
|
+
ticket_id = "TCK-LIST"
|
|
102
|
+
sample_rows = [
|
|
103
|
+
{"customerName": "A", "valueTotal": 1_000},
|
|
104
|
+
{"customerName": "B", "valueTotal": 2_000},
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
# qualquer query param é aceito; podemos validar se quiser via matcher
|
|
108
|
+
requests_mock.get(
|
|
109
|
+
_TICKET_URL,
|
|
110
|
+
json={"result": sample_rows},
|
|
111
|
+
status_code=200,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
df = check_report_ticket(_TOKEN, ticket_id)
|
|
115
|
+
assert isinstance(df, pd.DataFrame)
|
|
116
|
+
assert len(df) == 2
|
|
117
|
+
assert set(df.columns) == {"customerName", "valueTotal"}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_await_report_ticket_polls_until_ready(requests_mock, monkeypatch):
|
|
121
|
+
"""Primeira chamada ‘Processando’ → segunda retorna lista."""
|
|
122
|
+
ticket_id = "TCK-POLL"
|
|
123
|
+
ready_rows = [{"x": 1}]
|
|
124
|
+
|
|
125
|
+
# sequencia de respostas: processamento → pronto
|
|
126
|
+
requests_mock.get(
|
|
127
|
+
_TICKET_URL,
|
|
128
|
+
[
|
|
129
|
+
{"json": {"result": "Processando"}, "status_code": 200},
|
|
130
|
+
{"json": {"result": ready_rows}, "status_code": 200},
|
|
131
|
+
],
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# evita atraso real de sleep
|
|
135
|
+
monkeypatch.setattr("luxorasap.btgapi.reports.time.sleep", lambda *_: None)
|
|
136
|
+
|
|
137
|
+
df = await_report_ticket_result(_TOKEN, ticket_id, attempts=2, interval=0)
|
|
138
|
+
assert isinstance(df, pd.DataFrame)
|
|
139
|
+
assert df.iloc[0, 0] == 1
|