luxorasap 0.1.1__tar.gz → 0.1.3__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.3/PKG-INFO +241 -0
- luxorasap-0.1.3/README.md +196 -0
- {luxorasap-0.1.1 → luxorasap-0.1.3}/pyproject.toml +2 -2
- {luxorasap-0.1.1 → luxorasap-0.1.3}/src/luxorasap/__init__.py +1 -1
- {luxorasap-0.1.1 → luxorasap-0.1.3}/src/luxorasap/btgapi/__init__.py +3 -2
- {luxorasap-0.1.1 → luxorasap-0.1.3}/src/luxorasap/btgapi/auth.py +2 -1
- {luxorasap-0.1.1 → luxorasap-0.1.3}/src/luxorasap/btgapi/reports.py +56 -5
- {luxorasap-0.1.1 → luxorasap-0.1.3}/src/luxorasap/ingest/cloud/__init__.py +9 -2
- luxorasap-0.1.3/src/luxorasap.egg-info/PKG-INFO +241 -0
- luxorasap-0.1.3/tests/test_btgapi_reports.py +139 -0
- luxorasap-0.1.1/PKG-INFO +0 -80
- luxorasap-0.1.1/README.md +0 -35
- luxorasap-0.1.1/src/luxorasap.egg-info/PKG-INFO +0 -80
- luxorasap-0.1.1/tests/test_btgapi_reports.py +0 -62
- {luxorasap-0.1.1 → luxorasap-0.1.3}/setup.cfg +0 -0
- {luxorasap-0.1.1 → luxorasap-0.1.3}/src/luxorasap/btgapi/trades.py +0 -0
- {luxorasap-0.1.1 → luxorasap-0.1.3}/src/luxorasap/datareader/__init__.py +0 -0
- {luxorasap-0.1.1 → luxorasap-0.1.3}/src/luxorasap/datareader/core.py +0 -0
- {luxorasap-0.1.1 → luxorasap-0.1.3}/src/luxorasap/ingest/__init__.py +0 -0
- {luxorasap-0.1.1 → luxorasap-0.1.3}/src/luxorasap/ingest/legacy_local/dataloader.py +0 -0
- {luxorasap-0.1.1 → luxorasap-0.1.3}/src/luxorasap/utils/__init__.py +0 -0
- {luxorasap-0.1.1 → luxorasap-0.1.3}/src/luxorasap/utils/dataframe/__init__.py +0 -0
- {luxorasap-0.1.1 → luxorasap-0.1.3}/src/luxorasap/utils/dataframe/reader.py +0 -0
- {luxorasap-0.1.1 → luxorasap-0.1.3}/src/luxorasap/utils/dataframe/transforms.py +0 -0
- {luxorasap-0.1.1 → luxorasap-0.1.3}/src/luxorasap/utils/storage/__init__.py +0 -0
- {luxorasap-0.1.1 → luxorasap-0.1.3}/src/luxorasap/utils/storage/blob.py +0 -0
- {luxorasap-0.1.1 → luxorasap-0.1.3}/src/luxorasap.egg-info/SOURCES.txt +0 -0
- {luxorasap-0.1.1 → luxorasap-0.1.3}/src/luxorasap.egg-info/dependency_links.txt +0 -0
- {luxorasap-0.1.1 → luxorasap-0.1.3}/src/luxorasap.egg-info/entry_points.txt +0 -0
- {luxorasap-0.1.1 → luxorasap-0.1.3}/src/luxorasap.egg-info/requires.txt +0 -0
- {luxorasap-0.1.1 → luxorasap-0.1.3}/src/luxorasap.egg-info/top_level.txt +0 -0
- {luxorasap-0.1.1 → luxorasap-0.1.3}/tests/test_btgapi_auth.py +0 -0
- {luxorasap-0.1.1 → luxorasap-0.1.3}/tests/test_btgapi_trades.py +0 -0
- {luxorasap-0.1.1 → luxorasap-0.1.3}/tests/test_datareader.py +0 -0
- {luxorasap-0.1.1 → luxorasap-0.1.3}/tests/test_ingest_cloud.py +0 -0
- {luxorasap-0.1.1 → luxorasap-0.1.3}/tests/test_ingest_legacy_local.py +0 -0
- {luxorasap-0.1.1 → luxorasap-0.1.3}/tests/test_utils_dataframe.py +0 -0
- {luxorasap-0.1.1 → luxorasap-0.1.3}/tests/test_utils_storage.py +0 -0
luxorasap-0.1.3/PKG-INFO
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: luxorasap
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Luxor’s unified toolbox for data ingestion, querying and analytics.
|
|
5
|
+
Author-email: Luxor Group <backoffice@luxor.com.br>
|
|
6
|
+
License: Proprietary – All rights reserved
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: Other/Proprietary License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: pandas>=2.2
|
|
13
|
+
Requires-Dist: numpy>=1.25
|
|
14
|
+
Requires-Dist: loguru>=0.7
|
|
15
|
+
Requires-Dist: python-dotenv>=1.0
|
|
16
|
+
Requires-Dist: azure-storage-blob>=12.19
|
|
17
|
+
Requires-Dist: pyarrow>=15.0
|
|
18
|
+
Requires-Dist: requests>=2.32
|
|
19
|
+
Requires-Dist: pydantic>=2.7
|
|
20
|
+
Requires-Dist: scipy>=1.13
|
|
21
|
+
Requires-Dist: openpyxl
|
|
22
|
+
Provides-Extra: storage
|
|
23
|
+
Requires-Dist: azure-storage-blob>=12.19; extra == "storage"
|
|
24
|
+
Requires-Dist: pyarrow>=15.0; extra == "storage"
|
|
25
|
+
Provides-Extra: dataframe
|
|
26
|
+
Requires-Dist: pandas>=2.2; extra == "dataframe"
|
|
27
|
+
Provides-Extra: datareader
|
|
28
|
+
Requires-Dist: luxorasap[dataframe,storage]; extra == "datareader"
|
|
29
|
+
Requires-Dist: numpy>=1.25; extra == "datareader"
|
|
30
|
+
Requires-Dist: scipy>=1.13; extra == "datareader"
|
|
31
|
+
Provides-Extra: ingest
|
|
32
|
+
Requires-Dist: luxorasap[dataframe,storage]; extra == "ingest"
|
|
33
|
+
Requires-Dist: pandas>=2.2; extra == "ingest"
|
|
34
|
+
Provides-Extra: btgapi
|
|
35
|
+
Requires-Dist: requests>=2.32; extra == "btgapi"
|
|
36
|
+
Requires-Dist: pydantic>=2.7; extra == "btgapi"
|
|
37
|
+
Provides-Extra: dev
|
|
38
|
+
Requires-Dist: pytest>=8.2; extra == "dev"
|
|
39
|
+
Requires-Dist: requests-mock>=1.11; extra == "dev"
|
|
40
|
+
Requires-Dist: black>=24.4.0; extra == "dev"
|
|
41
|
+
Requires-Dist: isort>=5.13; extra == "dev"
|
|
42
|
+
Requires-Dist: bumpver>=2024.3; extra == "dev"
|
|
43
|
+
Requires-Dist: pre-commit>=3.7; extra == "dev"
|
|
44
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
45
|
+
|
|
46
|
+
# 📚 Documentação LuxorASAP
|
|
47
|
+
|
|
48
|
+
> Guia do desenvolvedor para os subpacotes **datareader**, **ingest**, **btgapi** e **utils**.
|
|
49
|
+
>
|
|
50
|
+
> • Instalação rápida • Visão arquitetural • APIs detalhadas • Exemplos de uso • Extras opcional
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Índice
|
|
55
|
+
|
|
56
|
+
1. [Visão Geral](#visao-geral)
|
|
57
|
+
2. [Instalação](#instalacao)
|
|
58
|
+
3. [utils](#utils)
|
|
59
|
+
4. [datareader](#datareader)
|
|
60
|
+
5. [ingest](#ingest)
|
|
61
|
+
6. [btgapi](#btgapi)
|
|
62
|
+
7. [Roadmap & Contribuições](#roadmap)
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 1. Visão Geral
|
|
67
|
+
|
|
68
|
+
LuxorASAP é o *toolbox* unificado da Luxor para ingestão, consulta e automação de dados financeiros.
|
|
69
|
+
|
|
70
|
+
| Subpacote | Função‑chave | Extras PyPI |
|
|
71
|
+
| -------------- | --------------------------------------------------------------------- | ---------------------- |
|
|
72
|
+
| **utils** | Utilidades puras (I/O ADLS, transformação de DataFrame, decorators) | `storage`, `dataframe` |
|
|
73
|
+
| **datareader** | Consulta de preços/tabelas no data lake via `LuxorQuery` | `datareader` |
|
|
74
|
+
| **ingest** | Carga de dados nova (parquet/zip/excel) em ADLS + loader legado | `ingest` |
|
|
75
|
+
| **btgapi** | Wrapper autenticado para as APIs do BTG Pactual (relatórios & trades) | `btgapi` |
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 2. Instalação
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# core + leitura de dados
|
|
83
|
+
pip install luxorasap[datareader]
|
|
84
|
+
|
|
85
|
+
# tudo incluído (leitura, ingest, btg)
|
|
86
|
+
pip install luxorasap[datareader,ingest,btgapi]
|
|
87
|
+
|
|
88
|
+
# desenvolvimento
|
|
89
|
+
pip install -e ".[dev]"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Extras podem ser combinados à vontade (`luxorasap[storage]`, etc.).
|
|
93
|
+
|
|
94
|
+
Configuração obrigatória do **ADLS**:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
export AZURE_STORAGE_CONNECTION_STRING="DefaultEndpointsProtocol=..."
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 3. utils
|
|
103
|
+
|
|
104
|
+
Camada de utilidades **sem dependências internas**.
|
|
105
|
+
|
|
106
|
+
### 3.1 storage.BlobParquetClient
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from luxorasap.utils.storage import BlobParquetClient
|
|
110
|
+
client = BlobParquetClient(container="luxorasap")
|
|
111
|
+
|
|
112
|
+
# write
|
|
113
|
+
client.write_df(df, "bronze/parquet/mytable.parquet")
|
|
114
|
+
|
|
115
|
+
# read (tuple -> DataFrame, success_flag)
|
|
116
|
+
df, ok = client.read_df("bronze/parquet/mytable.parquet")
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 3.2 dataframe
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from luxorasap.utils.dataframe import prep_for_save, persist_column_formatting, read_bytes
|
|
123
|
+
|
|
124
|
+
df2 = prep_for_save(df, index=True, index_name="ID", normalize=True)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### 3.3 decorators & misc
|
|
128
|
+
|
|
129
|
+
`with_retry`, `chunkify`, `timer`… ficam em *utils.helpers* (caso precise).
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## 4. datareader
|
|
134
|
+
|
|
135
|
+
### 4.1 Visão rápida
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from luxorasap.datareader import LuxorQuery
|
|
139
|
+
lq = LuxorQuery()
|
|
140
|
+
|
|
141
|
+
# DataFrame completo
|
|
142
|
+
df = lq.get_table("assets")
|
|
143
|
+
|
|
144
|
+
# Série de preços diária
|
|
145
|
+
aapl = lq.get_prices("aapl us equity", start="2025-01-01", end="2025-03-31")
|
|
146
|
+
|
|
147
|
+
# Preço pontual
|
|
148
|
+
price = lq.get_price("aapl us equity", on="2025-03-31")
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
*Caching*: `@lru_cache(maxsize=32)` evita hits repetidos ao Blob.
|
|
152
|
+
|
|
153
|
+
### 4.2 Métodos-chave
|
|
154
|
+
|
|
155
|
+
| Método | Descrição |
|
|
156
|
+
| ----------------------------------------------- | ----------------------------- |
|
|
157
|
+
| `table_exists(name)` | checa metadados no ADLS |
|
|
158
|
+
| `get_table(name)` | DataFrame completo (cached) |
|
|
159
|
+
| `get_prices(asset, start, end, column="Price")` | `pd.Series` |
|
|
160
|
+
| `get_price(asset, on)` | preço pontual (float ou None) |
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## 5. ingest
|
|
165
|
+
|
|
166
|
+
### 5.1 ingest.cloud (novo)
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from luxorasap.ingest import save_table, incremental_load
|
|
170
|
+
from luxorasap.datareader import LuxorQuery
|
|
171
|
+
|
|
172
|
+
save_table("trades", df)
|
|
173
|
+
|
|
174
|
+
lq = LuxorQuery()
|
|
175
|
+
incremental_load(lq, "prices_daily", df_new, increment_column="Date")
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### 5.2 ingest.legacy\_local
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
from luxorasap.ingest import DataLoader # Deprecado – ainda funcional
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
*Decoration*: ao importar `DataLoader` você verá `DeprecationWarning`.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## 6. btgapi
|
|
189
|
+
|
|
190
|
+
### 6.1 Autenticação
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
from luxorasap.btgapi import get_access_token
|
|
194
|
+
TOKEN = get_access_token(test_env=True)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### 6.2 Relatórios – Portfolio & Investor Transactions
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
from luxorasap.btgapi.reports import (
|
|
201
|
+
request_portfolio, await_report_ticket_result, process_zip_to_dfs,
|
|
202
|
+
request_investors_transactions_report,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
ticket = request_portfolio(TOKEN, "LUXOR FUND - CLASS A",
|
|
206
|
+
start=dt.date(2025,1,1), end=dt.date(2025,1,31))
|
|
207
|
+
zip_bytes = await_report_ticket_result(TOKEN, ticket)
|
|
208
|
+
carteiras = process_zip_to_dfs(zip_bytes)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### 6.3 Trades offshore
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
from luxorasap.btgapi.trades import (
|
|
215
|
+
submit_offshore_equity_trades,
|
|
216
|
+
await_transaction_ticket_result,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
ticket = submit_offshore_equity_trades(TOKEN, trades=[{...}], test_env=True)
|
|
220
|
+
status_df = await_transaction_ticket_result(TOKEN, ticket, test_env=True)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### 6.4 Extras
|
|
224
|
+
|
|
225
|
+
* `BTGApiError` — exceção customizada para qualquer falha.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## 7. Roadmap & Contribuições
|
|
230
|
+
|
|
231
|
+
* **Remover** `ingest.legacy_local` quando não houver mais dependências.
|
|
232
|
+
* Suporte a partições Parquet (delta‑like) na gravação.
|
|
233
|
+
* Adicionar `pydantic` para validar contratos BTG.
|
|
234
|
+
* Pull requests bem‑vindos! Rode `make lint && pytest -q` antes de enviar.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
### Contatos
|
|
239
|
+
|
|
240
|
+
* Dados / Back‑Office – [backoffice@luxor.com.br](mailto:backoffice@luxor.com.br)
|
|
241
|
+
* Mantenedor principal – Sergio
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# 📚 Documentação LuxorASAP
|
|
2
|
+
|
|
3
|
+
> Guia do desenvolvedor para os subpacotes **datareader**, **ingest**, **btgapi** e **utils**.
|
|
4
|
+
>
|
|
5
|
+
> • Instalação rápida • Visão arquitetural • APIs detalhadas • Exemplos de uso • Extras opcional
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Índice
|
|
10
|
+
|
|
11
|
+
1. [Visão Geral](#visao-geral)
|
|
12
|
+
2. [Instalação](#instalacao)
|
|
13
|
+
3. [utils](#utils)
|
|
14
|
+
4. [datareader](#datareader)
|
|
15
|
+
5. [ingest](#ingest)
|
|
16
|
+
6. [btgapi](#btgapi)
|
|
17
|
+
7. [Roadmap & Contribuições](#roadmap)
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 1. Visão Geral
|
|
22
|
+
|
|
23
|
+
LuxorASAP é o *toolbox* unificado da Luxor para ingestão, consulta e automação de dados financeiros.
|
|
24
|
+
|
|
25
|
+
| Subpacote | Função‑chave | Extras PyPI |
|
|
26
|
+
| -------------- | --------------------------------------------------------------------- | ---------------------- |
|
|
27
|
+
| **utils** | Utilidades puras (I/O ADLS, transformação de DataFrame, decorators) | `storage`, `dataframe` |
|
|
28
|
+
| **datareader** | Consulta de preços/tabelas no data lake via `LuxorQuery` | `datareader` |
|
|
29
|
+
| **ingest** | Carga de dados nova (parquet/zip/excel) em ADLS + loader legado | `ingest` |
|
|
30
|
+
| **btgapi** | Wrapper autenticado para as APIs do BTG Pactual (relatórios & trades) | `btgapi` |
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 2. Instalação
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# core + leitura de dados
|
|
38
|
+
pip install luxorasap[datareader]
|
|
39
|
+
|
|
40
|
+
# tudo incluído (leitura, ingest, btg)
|
|
41
|
+
pip install luxorasap[datareader,ingest,btgapi]
|
|
42
|
+
|
|
43
|
+
# desenvolvimento
|
|
44
|
+
pip install -e ".[dev]"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Extras podem ser combinados à vontade (`luxorasap[storage]`, etc.).
|
|
48
|
+
|
|
49
|
+
Configuração obrigatória do **ADLS**:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
export AZURE_STORAGE_CONNECTION_STRING="DefaultEndpointsProtocol=..."
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 3. utils
|
|
58
|
+
|
|
59
|
+
Camada de utilidades **sem dependências internas**.
|
|
60
|
+
|
|
61
|
+
### 3.1 storage.BlobParquetClient
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from luxorasap.utils.storage import BlobParquetClient
|
|
65
|
+
client = BlobParquetClient(container="luxorasap")
|
|
66
|
+
|
|
67
|
+
# write
|
|
68
|
+
client.write_df(df, "bronze/parquet/mytable.parquet")
|
|
69
|
+
|
|
70
|
+
# read (tuple -> DataFrame, success_flag)
|
|
71
|
+
df, ok = client.read_df("bronze/parquet/mytable.parquet")
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 3.2 dataframe
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from luxorasap.utils.dataframe import prep_for_save, persist_column_formatting, read_bytes
|
|
78
|
+
|
|
79
|
+
df2 = prep_for_save(df, index=True, index_name="ID", normalize=True)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 3.3 decorators & misc
|
|
83
|
+
|
|
84
|
+
`with_retry`, `chunkify`, `timer`… ficam em *utils.helpers* (caso precise).
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 4. datareader
|
|
89
|
+
|
|
90
|
+
### 4.1 Visão rápida
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from luxorasap.datareader import LuxorQuery
|
|
94
|
+
lq = LuxorQuery()
|
|
95
|
+
|
|
96
|
+
# DataFrame completo
|
|
97
|
+
df = lq.get_table("assets")
|
|
98
|
+
|
|
99
|
+
# Série de preços diária
|
|
100
|
+
aapl = lq.get_prices("aapl us equity", start="2025-01-01", end="2025-03-31")
|
|
101
|
+
|
|
102
|
+
# Preço pontual
|
|
103
|
+
price = lq.get_price("aapl us equity", on="2025-03-31")
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
*Caching*: `@lru_cache(maxsize=32)` evita hits repetidos ao Blob.
|
|
107
|
+
|
|
108
|
+
### 4.2 Métodos-chave
|
|
109
|
+
|
|
110
|
+
| Método | Descrição |
|
|
111
|
+
| ----------------------------------------------- | ----------------------------- |
|
|
112
|
+
| `table_exists(name)` | checa metadados no ADLS |
|
|
113
|
+
| `get_table(name)` | DataFrame completo (cached) |
|
|
114
|
+
| `get_prices(asset, start, end, column="Price")` | `pd.Series` |
|
|
115
|
+
| `get_price(asset, on)` | preço pontual (float ou None) |
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 5. ingest
|
|
120
|
+
|
|
121
|
+
### 5.1 ingest.cloud (novo)
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from luxorasap.ingest import save_table, incremental_load
|
|
125
|
+
from luxorasap.datareader import LuxorQuery
|
|
126
|
+
|
|
127
|
+
save_table("trades", df)
|
|
128
|
+
|
|
129
|
+
lq = LuxorQuery()
|
|
130
|
+
incremental_load(lq, "prices_daily", df_new, increment_column="Date")
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 5.2 ingest.legacy\_local
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from luxorasap.ingest import DataLoader # Deprecado – ainda funcional
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
*Decoration*: ao importar `DataLoader` você verá `DeprecationWarning`.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## 6. btgapi
|
|
144
|
+
|
|
145
|
+
### 6.1 Autenticação
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from luxorasap.btgapi import get_access_token
|
|
149
|
+
TOKEN = get_access_token(test_env=True)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### 6.2 Relatórios – Portfolio & Investor Transactions
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from luxorasap.btgapi.reports import (
|
|
156
|
+
request_portfolio, await_report_ticket_result, process_zip_to_dfs,
|
|
157
|
+
request_investors_transactions_report,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
ticket = request_portfolio(TOKEN, "LUXOR FUND - CLASS A",
|
|
161
|
+
start=dt.date(2025,1,1), end=dt.date(2025,1,31))
|
|
162
|
+
zip_bytes = await_report_ticket_result(TOKEN, ticket)
|
|
163
|
+
carteiras = process_zip_to_dfs(zip_bytes)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### 6.3 Trades offshore
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from luxorasap.btgapi.trades import (
|
|
170
|
+
submit_offshore_equity_trades,
|
|
171
|
+
await_transaction_ticket_result,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
ticket = submit_offshore_equity_trades(TOKEN, trades=[{...}], test_env=True)
|
|
175
|
+
status_df = await_transaction_ticket_result(TOKEN, ticket, test_env=True)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### 6.4 Extras
|
|
179
|
+
|
|
180
|
+
* `BTGApiError` — exceção customizada para qualquer falha.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## 7. Roadmap & Contribuições
|
|
185
|
+
|
|
186
|
+
* **Remover** `ingest.legacy_local` quando não houver mais dependências.
|
|
187
|
+
* Suporte a partições Parquet (delta‑like) na gravação.
|
|
188
|
+
* Adicionar `pydantic` para validar contratos BTG.
|
|
189
|
+
* Pull requests bem‑vindos! Rode `make lint && pytest -q` antes de enviar.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
### Contatos
|
|
194
|
+
|
|
195
|
+
* Dados / Back‑Office – [backoffice@luxor.com.br](mailto:backoffice@luxor.com.br)
|
|
196
|
+
* Mantenedor principal – Sergio
|
|
@@ -10,7 +10,7 @@ build-backend = "setuptools.build_meta"
|
|
|
10
10
|
#############################
|
|
11
11
|
[project]
|
|
12
12
|
name = "luxorasap"
|
|
13
|
-
version = "0.1.
|
|
13
|
+
version = "0.1.3"
|
|
14
14
|
description = "Luxor’s unified toolbox for data ingestion, querying and analytics."
|
|
15
15
|
readme = "README.md"
|
|
16
16
|
requires-python = ">=3.9"
|
|
@@ -74,7 +74,7 @@ exclude = ["tests*"]
|
|
|
74
74
|
# bumpver (sem-ver)
|
|
75
75
|
#############################
|
|
76
76
|
[tool.bumpver]
|
|
77
|
-
current_version = "0.1.
|
|
77
|
+
current_version = "0.1.3"
|
|
78
78
|
version_pattern = "MAJOR.MINOR.PATCH"
|
|
79
79
|
|
|
80
80
|
# 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.3"
|
|
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
|
]
|
|
@@ -52,6 +52,7 @@ def get_access_token(*, client_id=None, client_secret=None, test_env: bool = Tru
|
|
|
52
52
|
|
|
53
53
|
if resp.ok:
|
|
54
54
|
token = resp.json().get("access_token")
|
|
55
|
-
|
|
55
|
+
len_token = len(token) if token else None
|
|
56
|
+
logger.debug(f"Token BTG obtido (len={len_token})")
|
|
56
57
|
return token or ""
|
|
57
58
|
raise BTGApiError(f"Falha ao autenticar: HTTP {resp.status_code} – {resp.text}")
|
|
@@ -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
|
|
|
@@ -93,7 +95,8 @@ def check_report_ticket(token: str, ticket: str, *, page: Optional[int] = None)
|
|
|
93
95
|
# 2. Caso contrário tenta decodificar JSON
|
|
94
96
|
|
|
95
97
|
result = payload.get("result")
|
|
96
|
-
|
|
98
|
+
|
|
99
|
+
if result == "Processando" or result == 'Aguardando processamento':
|
|
97
100
|
raise BTGApiError("Processando")
|
|
98
101
|
|
|
99
102
|
# 3. Quando pronto, result é JSON string com UrlDownload
|
|
@@ -103,7 +106,16 @@ def check_report_ticket(token: str, ticket: str, *, page: Optional[int] = None)
|
|
|
103
106
|
url = info["UrlDownload"]
|
|
104
107
|
return _download_url(url)
|
|
105
108
|
except Exception as exc:
|
|
106
|
-
raise BTGApiError(f"Falha ao interpretar
|
|
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
|
|
107
119
|
|
|
108
120
|
raise BTGApiError("Formato de resposta desconhecido")
|
|
109
121
|
|
|
@@ -129,7 +141,7 @@ def await_report_ticket_result(token: str, ticket: str, *, attempts: int = 10,
|
|
|
129
141
|
return check_report_ticket(token, ticket)
|
|
130
142
|
except BTGApiError as err:
|
|
131
143
|
if "Processando" in str(err):
|
|
132
|
-
logger.debug("Ticket
|
|
144
|
+
logger.debug(f"Ticket {ticket} pendente ({i+1}/{attempts})")
|
|
133
145
|
time.sleep(interval)
|
|
134
146
|
continue
|
|
135
147
|
raise
|
|
@@ -185,4 +197,43 @@ def request_investors_transactions_report( token: str, query_date: dt.date, *,
|
|
|
185
197
|
return r.json()["ticket"]
|
|
186
198
|
raise BTGApiError(
|
|
187
199
|
f"Erro InvestorTransactionsFileReport: {r.status_code} – {r.text}"
|
|
188
|
-
)
|
|
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}")
|
|
@@ -18,10 +18,17 @@ def save_table(
|
|
|
18
18
|
*,
|
|
19
19
|
index: bool = False,
|
|
20
20
|
index_name: str = "index",
|
|
21
|
-
normalize_columns: bool =
|
|
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
|
|
|
@@ -34,7 +41,7 @@ def incremental_load(
|
|
|
34
41
|
increment_column: str = "Date",
|
|
35
42
|
index: bool = False,
|
|
36
43
|
index_name: str = "index",
|
|
37
|
-
normalize_columns: bool =
|
|
44
|
+
normalize_columns: bool = True,
|
|
38
45
|
directory: str = "enriched/parquet",
|
|
39
46
|
):
|
|
40
47
|
"""Concatena novos dados aos existentes, cortando duplicados pela data."""
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: luxorasap
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Luxor’s unified toolbox for data ingestion, querying and analytics.
|
|
5
|
+
Author-email: Luxor Group <backoffice@luxor.com.br>
|
|
6
|
+
License: Proprietary – All rights reserved
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: Other/Proprietary License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: pandas>=2.2
|
|
13
|
+
Requires-Dist: numpy>=1.25
|
|
14
|
+
Requires-Dist: loguru>=0.7
|
|
15
|
+
Requires-Dist: python-dotenv>=1.0
|
|
16
|
+
Requires-Dist: azure-storage-blob>=12.19
|
|
17
|
+
Requires-Dist: pyarrow>=15.0
|
|
18
|
+
Requires-Dist: requests>=2.32
|
|
19
|
+
Requires-Dist: pydantic>=2.7
|
|
20
|
+
Requires-Dist: scipy>=1.13
|
|
21
|
+
Requires-Dist: openpyxl
|
|
22
|
+
Provides-Extra: storage
|
|
23
|
+
Requires-Dist: azure-storage-blob>=12.19; extra == "storage"
|
|
24
|
+
Requires-Dist: pyarrow>=15.0; extra == "storage"
|
|
25
|
+
Provides-Extra: dataframe
|
|
26
|
+
Requires-Dist: pandas>=2.2; extra == "dataframe"
|
|
27
|
+
Provides-Extra: datareader
|
|
28
|
+
Requires-Dist: luxorasap[dataframe,storage]; extra == "datareader"
|
|
29
|
+
Requires-Dist: numpy>=1.25; extra == "datareader"
|
|
30
|
+
Requires-Dist: scipy>=1.13; extra == "datareader"
|
|
31
|
+
Provides-Extra: ingest
|
|
32
|
+
Requires-Dist: luxorasap[dataframe,storage]; extra == "ingest"
|
|
33
|
+
Requires-Dist: pandas>=2.2; extra == "ingest"
|
|
34
|
+
Provides-Extra: btgapi
|
|
35
|
+
Requires-Dist: requests>=2.32; extra == "btgapi"
|
|
36
|
+
Requires-Dist: pydantic>=2.7; extra == "btgapi"
|
|
37
|
+
Provides-Extra: dev
|
|
38
|
+
Requires-Dist: pytest>=8.2; extra == "dev"
|
|
39
|
+
Requires-Dist: requests-mock>=1.11; extra == "dev"
|
|
40
|
+
Requires-Dist: black>=24.4.0; extra == "dev"
|
|
41
|
+
Requires-Dist: isort>=5.13; extra == "dev"
|
|
42
|
+
Requires-Dist: bumpver>=2024.3; extra == "dev"
|
|
43
|
+
Requires-Dist: pre-commit>=3.7; extra == "dev"
|
|
44
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
45
|
+
|
|
46
|
+
# 📚 Documentação LuxorASAP
|
|
47
|
+
|
|
48
|
+
> Guia do desenvolvedor para os subpacotes **datareader**, **ingest**, **btgapi** e **utils**.
|
|
49
|
+
>
|
|
50
|
+
> • Instalação rápida • Visão arquitetural • APIs detalhadas • Exemplos de uso • Extras opcional
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Índice
|
|
55
|
+
|
|
56
|
+
1. [Visão Geral](#visao-geral)
|
|
57
|
+
2. [Instalação](#instalacao)
|
|
58
|
+
3. [utils](#utils)
|
|
59
|
+
4. [datareader](#datareader)
|
|
60
|
+
5. [ingest](#ingest)
|
|
61
|
+
6. [btgapi](#btgapi)
|
|
62
|
+
7. [Roadmap & Contribuições](#roadmap)
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 1. Visão Geral
|
|
67
|
+
|
|
68
|
+
LuxorASAP é o *toolbox* unificado da Luxor para ingestão, consulta e automação de dados financeiros.
|
|
69
|
+
|
|
70
|
+
| Subpacote | Função‑chave | Extras PyPI |
|
|
71
|
+
| -------------- | --------------------------------------------------------------------- | ---------------------- |
|
|
72
|
+
| **utils** | Utilidades puras (I/O ADLS, transformação de DataFrame, decorators) | `storage`, `dataframe` |
|
|
73
|
+
| **datareader** | Consulta de preços/tabelas no data lake via `LuxorQuery` | `datareader` |
|
|
74
|
+
| **ingest** | Carga de dados nova (parquet/zip/excel) em ADLS + loader legado | `ingest` |
|
|
75
|
+
| **btgapi** | Wrapper autenticado para as APIs do BTG Pactual (relatórios & trades) | `btgapi` |
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 2. Instalação
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# core + leitura de dados
|
|
83
|
+
pip install luxorasap[datareader]
|
|
84
|
+
|
|
85
|
+
# tudo incluído (leitura, ingest, btg)
|
|
86
|
+
pip install luxorasap[datareader,ingest,btgapi]
|
|
87
|
+
|
|
88
|
+
# desenvolvimento
|
|
89
|
+
pip install -e ".[dev]"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Extras podem ser combinados à vontade (`luxorasap[storage]`, etc.).
|
|
93
|
+
|
|
94
|
+
Configuração obrigatória do **ADLS**:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
export AZURE_STORAGE_CONNECTION_STRING="DefaultEndpointsProtocol=..."
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 3. utils
|
|
103
|
+
|
|
104
|
+
Camada de utilidades **sem dependências internas**.
|
|
105
|
+
|
|
106
|
+
### 3.1 storage.BlobParquetClient
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from luxorasap.utils.storage import BlobParquetClient
|
|
110
|
+
client = BlobParquetClient(container="luxorasap")
|
|
111
|
+
|
|
112
|
+
# write
|
|
113
|
+
client.write_df(df, "bronze/parquet/mytable.parquet")
|
|
114
|
+
|
|
115
|
+
# read (tuple -> DataFrame, success_flag)
|
|
116
|
+
df, ok = client.read_df("bronze/parquet/mytable.parquet")
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 3.2 dataframe
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from luxorasap.utils.dataframe import prep_for_save, persist_column_formatting, read_bytes
|
|
123
|
+
|
|
124
|
+
df2 = prep_for_save(df, index=True, index_name="ID", normalize=True)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### 3.3 decorators & misc
|
|
128
|
+
|
|
129
|
+
`with_retry`, `chunkify`, `timer`… ficam em *utils.helpers* (caso precise).
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## 4. datareader
|
|
134
|
+
|
|
135
|
+
### 4.1 Visão rápida
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from luxorasap.datareader import LuxorQuery
|
|
139
|
+
lq = LuxorQuery()
|
|
140
|
+
|
|
141
|
+
# DataFrame completo
|
|
142
|
+
df = lq.get_table("assets")
|
|
143
|
+
|
|
144
|
+
# Série de preços diária
|
|
145
|
+
aapl = lq.get_prices("aapl us equity", start="2025-01-01", end="2025-03-31")
|
|
146
|
+
|
|
147
|
+
# Preço pontual
|
|
148
|
+
price = lq.get_price("aapl us equity", on="2025-03-31")
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
*Caching*: `@lru_cache(maxsize=32)` evita hits repetidos ao Blob.
|
|
152
|
+
|
|
153
|
+
### 4.2 Métodos-chave
|
|
154
|
+
|
|
155
|
+
| Método | Descrição |
|
|
156
|
+
| ----------------------------------------------- | ----------------------------- |
|
|
157
|
+
| `table_exists(name)` | checa metadados no ADLS |
|
|
158
|
+
| `get_table(name)` | DataFrame completo (cached) |
|
|
159
|
+
| `get_prices(asset, start, end, column="Price")` | `pd.Series` |
|
|
160
|
+
| `get_price(asset, on)` | preço pontual (float ou None) |
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## 5. ingest
|
|
165
|
+
|
|
166
|
+
### 5.1 ingest.cloud (novo)
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from luxorasap.ingest import save_table, incremental_load
|
|
170
|
+
from luxorasap.datareader import LuxorQuery
|
|
171
|
+
|
|
172
|
+
save_table("trades", df)
|
|
173
|
+
|
|
174
|
+
lq = LuxorQuery()
|
|
175
|
+
incremental_load(lq, "prices_daily", df_new, increment_column="Date")
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### 5.2 ingest.legacy\_local
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
from luxorasap.ingest import DataLoader # Deprecado – ainda funcional
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
*Decoration*: ao importar `DataLoader` você verá `DeprecationWarning`.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## 6. btgapi
|
|
189
|
+
|
|
190
|
+
### 6.1 Autenticação
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
from luxorasap.btgapi import get_access_token
|
|
194
|
+
TOKEN = get_access_token(test_env=True)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### 6.2 Relatórios – Portfolio & Investor Transactions
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
from luxorasap.btgapi.reports import (
|
|
201
|
+
request_portfolio, await_report_ticket_result, process_zip_to_dfs,
|
|
202
|
+
request_investors_transactions_report,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
ticket = request_portfolio(TOKEN, "LUXOR FUND - CLASS A",
|
|
206
|
+
start=dt.date(2025,1,1), end=dt.date(2025,1,31))
|
|
207
|
+
zip_bytes = await_report_ticket_result(TOKEN, ticket)
|
|
208
|
+
carteiras = process_zip_to_dfs(zip_bytes)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### 6.3 Trades offshore
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
from luxorasap.btgapi.trades import (
|
|
215
|
+
submit_offshore_equity_trades,
|
|
216
|
+
await_transaction_ticket_result,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
ticket = submit_offshore_equity_trades(TOKEN, trades=[{...}], test_env=True)
|
|
220
|
+
status_df = await_transaction_ticket_result(TOKEN, ticket, test_env=True)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### 6.4 Extras
|
|
224
|
+
|
|
225
|
+
* `BTGApiError` — exceção customizada para qualquer falha.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## 7. Roadmap & Contribuições
|
|
230
|
+
|
|
231
|
+
* **Remover** `ingest.legacy_local` quando não houver mais dependências.
|
|
232
|
+
* Suporte a partições Parquet (delta‑like) na gravação.
|
|
233
|
+
* Adicionar `pydantic` para validar contratos BTG.
|
|
234
|
+
* Pull requests bem‑vindos! Rode `make lint && pytest -q` antes de enviar.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
### Contatos
|
|
239
|
+
|
|
240
|
+
* Dados / Back‑Office – [backoffice@luxor.com.br](mailto:backoffice@luxor.com.br)
|
|
241
|
+
* Mantenedor principal – Sergio
|
|
@@ -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
|
luxorasap-0.1.1/PKG-INFO
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: luxorasap
|
|
3
|
-
Version: 0.1.1
|
|
4
|
-
Summary: Luxor’s unified toolbox for data ingestion, querying and analytics.
|
|
5
|
-
Author-email: Luxor Group <backoffice@luxor.com.br>
|
|
6
|
-
License: Proprietary – All rights reserved
|
|
7
|
-
Classifier: Programming Language :: Python :: 3
|
|
8
|
-
Classifier: License :: Other/Proprietary License
|
|
9
|
-
Classifier: Operating System :: OS Independent
|
|
10
|
-
Requires-Python: >=3.9
|
|
11
|
-
Description-Content-Type: text/markdown
|
|
12
|
-
Requires-Dist: pandas>=2.2
|
|
13
|
-
Requires-Dist: numpy>=1.25
|
|
14
|
-
Requires-Dist: loguru>=0.7
|
|
15
|
-
Requires-Dist: python-dotenv>=1.0
|
|
16
|
-
Requires-Dist: azure-storage-blob>=12.19
|
|
17
|
-
Requires-Dist: pyarrow>=15.0
|
|
18
|
-
Requires-Dist: requests>=2.32
|
|
19
|
-
Requires-Dist: pydantic>=2.7
|
|
20
|
-
Requires-Dist: scipy>=1.13
|
|
21
|
-
Requires-Dist: openpyxl
|
|
22
|
-
Provides-Extra: storage
|
|
23
|
-
Requires-Dist: azure-storage-blob>=12.19; extra == "storage"
|
|
24
|
-
Requires-Dist: pyarrow>=15.0; extra == "storage"
|
|
25
|
-
Provides-Extra: dataframe
|
|
26
|
-
Requires-Dist: pandas>=2.2; extra == "dataframe"
|
|
27
|
-
Provides-Extra: datareader
|
|
28
|
-
Requires-Dist: luxorasap[dataframe,storage]; extra == "datareader"
|
|
29
|
-
Requires-Dist: numpy>=1.25; extra == "datareader"
|
|
30
|
-
Requires-Dist: scipy>=1.13; extra == "datareader"
|
|
31
|
-
Provides-Extra: ingest
|
|
32
|
-
Requires-Dist: luxorasap[dataframe,storage]; extra == "ingest"
|
|
33
|
-
Requires-Dist: pandas>=2.2; extra == "ingest"
|
|
34
|
-
Provides-Extra: btgapi
|
|
35
|
-
Requires-Dist: requests>=2.32; extra == "btgapi"
|
|
36
|
-
Requires-Dist: pydantic>=2.7; extra == "btgapi"
|
|
37
|
-
Provides-Extra: dev
|
|
38
|
-
Requires-Dist: pytest>=8.2; extra == "dev"
|
|
39
|
-
Requires-Dist: requests-mock>=1.11; extra == "dev"
|
|
40
|
-
Requires-Dist: black>=24.4.0; extra == "dev"
|
|
41
|
-
Requires-Dist: isort>=5.13; extra == "dev"
|
|
42
|
-
Requires-Dist: bumpver>=2024.3; extra == "dev"
|
|
43
|
-
Requires-Dist: pre-commit>=3.7; extra == "dev"
|
|
44
|
-
Requires-Dist: build>=1.2; extra == "dev"
|
|
45
|
-
|
|
46
|
-
# LuxorASAP
|
|
47
|
-
|
|
48
|
-
**LuxorASAP** é o pacote-guarda-chuva que concentra as ferramentas internas de dados da Luxor Group:
|
|
49
|
-
consulta estruturada ao data lake, cargas padronizadas para ADLS, wrappers de API, utilitários e muito mais.
|
|
50
|
-
|
|
51
|
-
[](https://pypi.org/project/luxorasap/)
|
|
52
|
-

|
|
53
|
-
|
|
54
|
-
---
|
|
55
|
-
|
|
56
|
-
## Instalação
|
|
57
|
-
|
|
58
|
-
```bash
|
|
59
|
-
# pacote base
|
|
60
|
-
pip install luxorasap
|
|
61
|
-
|
|
62
|
-
# com o submódulo datareader
|
|
63
|
-
pip install "luxorasap[datareader]"
|
|
64
|
-
```
|
|
65
|
-
## Uso rápido
|
|
66
|
-
```python
|
|
67
|
-
from luxorasap.datareader import LuxorQuery
|
|
68
|
-
|
|
69
|
-
lq = LuxorQuery(blob_directory="enriched/parquet")
|
|
70
|
-
prices = lq.get_prices("aapl us equity", "2024-01-01", "2024-12-31")
|
|
71
|
-
print(prices.head())
|
|
72
|
-
```
|
|
73
|
-
## Submódulos
|
|
74
|
-
| Módulo | Descrição rápida | Extras |
|
|
75
|
-
| ---------------------- | ---------------------------------------- | ------------------------------------- |
|
|
76
|
-
| `luxorasap.datareader` | Leitura de tabelas e séries no data lake | `pip install "luxorasap[datareader]"` |
|
|
77
|
-
| `luxorasap.ingest` | Funções de carga padronizada para ADLS | `"luxorasap[ingest]"` |
|
|
78
|
-
| `luxorasap.btgapi` | Wrapper REST para dados BTG | `"luxorasap[btgapi]"` |
|
|
79
|
-
|
|
80
|
-
© Luxor Group – uso interno. Todos os direitos reservados.
|
luxorasap-0.1.1/README.md
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
# LuxorASAP
|
|
2
|
-
|
|
3
|
-
**LuxorASAP** é o pacote-guarda-chuva que concentra as ferramentas internas de dados da Luxor Group:
|
|
4
|
-
consulta estruturada ao data lake, cargas padronizadas para ADLS, wrappers de API, utilitários e muito mais.
|
|
5
|
-
|
|
6
|
-
[](https://pypi.org/project/luxorasap/)
|
|
7
|
-

|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## Instalação
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
# pacote base
|
|
15
|
-
pip install luxorasap
|
|
16
|
-
|
|
17
|
-
# com o submódulo datareader
|
|
18
|
-
pip install "luxorasap[datareader]"
|
|
19
|
-
```
|
|
20
|
-
## Uso rápido
|
|
21
|
-
```python
|
|
22
|
-
from luxorasap.datareader import LuxorQuery
|
|
23
|
-
|
|
24
|
-
lq = LuxorQuery(blob_directory="enriched/parquet")
|
|
25
|
-
prices = lq.get_prices("aapl us equity", "2024-01-01", "2024-12-31")
|
|
26
|
-
print(prices.head())
|
|
27
|
-
```
|
|
28
|
-
## Submódulos
|
|
29
|
-
| Módulo | Descrição rápida | Extras |
|
|
30
|
-
| ---------------------- | ---------------------------------------- | ------------------------------------- |
|
|
31
|
-
| `luxorasap.datareader` | Leitura de tabelas e séries no data lake | `pip install "luxorasap[datareader]"` |
|
|
32
|
-
| `luxorasap.ingest` | Funções de carga padronizada para ADLS | `"luxorasap[ingest]"` |
|
|
33
|
-
| `luxorasap.btgapi` | Wrapper REST para dados BTG | `"luxorasap[btgapi]"` |
|
|
34
|
-
|
|
35
|
-
© Luxor Group – uso interno. Todos os direitos reservados.
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: luxorasap
|
|
3
|
-
Version: 0.1.1
|
|
4
|
-
Summary: Luxor’s unified toolbox for data ingestion, querying and analytics.
|
|
5
|
-
Author-email: Luxor Group <backoffice@luxor.com.br>
|
|
6
|
-
License: Proprietary – All rights reserved
|
|
7
|
-
Classifier: Programming Language :: Python :: 3
|
|
8
|
-
Classifier: License :: Other/Proprietary License
|
|
9
|
-
Classifier: Operating System :: OS Independent
|
|
10
|
-
Requires-Python: >=3.9
|
|
11
|
-
Description-Content-Type: text/markdown
|
|
12
|
-
Requires-Dist: pandas>=2.2
|
|
13
|
-
Requires-Dist: numpy>=1.25
|
|
14
|
-
Requires-Dist: loguru>=0.7
|
|
15
|
-
Requires-Dist: python-dotenv>=1.0
|
|
16
|
-
Requires-Dist: azure-storage-blob>=12.19
|
|
17
|
-
Requires-Dist: pyarrow>=15.0
|
|
18
|
-
Requires-Dist: requests>=2.32
|
|
19
|
-
Requires-Dist: pydantic>=2.7
|
|
20
|
-
Requires-Dist: scipy>=1.13
|
|
21
|
-
Requires-Dist: openpyxl
|
|
22
|
-
Provides-Extra: storage
|
|
23
|
-
Requires-Dist: azure-storage-blob>=12.19; extra == "storage"
|
|
24
|
-
Requires-Dist: pyarrow>=15.0; extra == "storage"
|
|
25
|
-
Provides-Extra: dataframe
|
|
26
|
-
Requires-Dist: pandas>=2.2; extra == "dataframe"
|
|
27
|
-
Provides-Extra: datareader
|
|
28
|
-
Requires-Dist: luxorasap[dataframe,storage]; extra == "datareader"
|
|
29
|
-
Requires-Dist: numpy>=1.25; extra == "datareader"
|
|
30
|
-
Requires-Dist: scipy>=1.13; extra == "datareader"
|
|
31
|
-
Provides-Extra: ingest
|
|
32
|
-
Requires-Dist: luxorasap[dataframe,storage]; extra == "ingest"
|
|
33
|
-
Requires-Dist: pandas>=2.2; extra == "ingest"
|
|
34
|
-
Provides-Extra: btgapi
|
|
35
|
-
Requires-Dist: requests>=2.32; extra == "btgapi"
|
|
36
|
-
Requires-Dist: pydantic>=2.7; extra == "btgapi"
|
|
37
|
-
Provides-Extra: dev
|
|
38
|
-
Requires-Dist: pytest>=8.2; extra == "dev"
|
|
39
|
-
Requires-Dist: requests-mock>=1.11; extra == "dev"
|
|
40
|
-
Requires-Dist: black>=24.4.0; extra == "dev"
|
|
41
|
-
Requires-Dist: isort>=5.13; extra == "dev"
|
|
42
|
-
Requires-Dist: bumpver>=2024.3; extra == "dev"
|
|
43
|
-
Requires-Dist: pre-commit>=3.7; extra == "dev"
|
|
44
|
-
Requires-Dist: build>=1.2; extra == "dev"
|
|
45
|
-
|
|
46
|
-
# LuxorASAP
|
|
47
|
-
|
|
48
|
-
**LuxorASAP** é o pacote-guarda-chuva que concentra as ferramentas internas de dados da Luxor Group:
|
|
49
|
-
consulta estruturada ao data lake, cargas padronizadas para ADLS, wrappers de API, utilitários e muito mais.
|
|
50
|
-
|
|
51
|
-
[](https://pypi.org/project/luxorasap/)
|
|
52
|
-

|
|
53
|
-
|
|
54
|
-
---
|
|
55
|
-
|
|
56
|
-
## Instalação
|
|
57
|
-
|
|
58
|
-
```bash
|
|
59
|
-
# pacote base
|
|
60
|
-
pip install luxorasap
|
|
61
|
-
|
|
62
|
-
# com o submódulo datareader
|
|
63
|
-
pip install "luxorasap[datareader]"
|
|
64
|
-
```
|
|
65
|
-
## Uso rápido
|
|
66
|
-
```python
|
|
67
|
-
from luxorasap.datareader import LuxorQuery
|
|
68
|
-
|
|
69
|
-
lq = LuxorQuery(blob_directory="enriched/parquet")
|
|
70
|
-
prices = lq.get_prices("aapl us equity", "2024-01-01", "2024-12-31")
|
|
71
|
-
print(prices.head())
|
|
72
|
-
```
|
|
73
|
-
## Submódulos
|
|
74
|
-
| Módulo | Descrição rápida | Extras |
|
|
75
|
-
| ---------------------- | ---------------------------------------- | ------------------------------------- |
|
|
76
|
-
| `luxorasap.datareader` | Leitura de tabelas e séries no data lake | `pip install "luxorasap[datareader]"` |
|
|
77
|
-
| `luxorasap.ingest` | Funções de carga padronizada para ADLS | `"luxorasap[ingest]"` |
|
|
78
|
-
| `luxorasap.btgapi` | Wrapper REST para dados BTG | `"luxorasap[btgapi]"` |
|
|
79
|
-
|
|
80
|
-
© Luxor Group – uso interno. Todos os direitos reservados.
|
|
@@ -1,62 +0,0 @@
|
|
|
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
|
-
)
|
|
10
|
-
|
|
11
|
-
_TICKET_URL = "https://funds.btgpactual.com/reports/Ticket"
|
|
12
|
-
_POST_URL = "https://funds.btgpactual.com/reports/Portfolio"
|
|
13
|
-
|
|
14
|
-
def test_request_portfolio_returns_ticket(requests_mock):
|
|
15
|
-
requests_mock.post(_POST_URL, json={"ticket": "ABC"})
|
|
16
|
-
tk = request_portfolio("tok", "FUND", dt.date(2025,1,1), dt.date(2025,1,31))
|
|
17
|
-
assert tk == "ABC"
|
|
18
|
-
|
|
19
|
-
def test_await_ticket_inline_zip(requests_mock, monkeypatch):
|
|
20
|
-
# 1ª chamada: ainda processando 2ª: devolve ZIP binário
|
|
21
|
-
# Explicando o que requests_mock faz:
|
|
22
|
-
#
|
|
23
|
-
requests_mock.get(_TICKET_URL, [
|
|
24
|
-
{"json": {"result": "Processando"}},
|
|
25
|
-
{"content": b"ZIP!"},
|
|
26
|
-
])
|
|
27
|
-
monkeypatch.setattr("time.sleep", lambda *_: None)
|
|
28
|
-
out = await_report_ticket_result("tok", "ABC", attempts=2, interval=0)
|
|
29
|
-
assert out == b"ZIP!"
|
|
30
|
-
|
|
31
|
-
def test_await_ticket_via_urldownload(requests_mock, monkeypatch):
|
|
32
|
-
dl_url = "https://download/file.zip"
|
|
33
|
-
# 1ª chamada ao ticket devolve JSON com UrlDownload
|
|
34
|
-
requests_mock.get(_TICKET_URL, json={"result": json.dumps({"UrlDownload": dl_url})})
|
|
35
|
-
requests_mock.get(dl_url, content=b"ZIP2", headers={"Content-Type": "application/zip"})
|
|
36
|
-
monkeypatch.setattr("time.sleep", lambda *_: None)
|
|
37
|
-
out = await_report_ticket_result("tok", "XYZ", attempts=1)
|
|
38
|
-
assert out == b"ZIP2"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def test_process_zip_to_dfs():
|
|
42
|
-
buf = io.BytesIO()
|
|
43
|
-
with zipfile.ZipFile(buf, "w") as zf:
|
|
44
|
-
df = pd.DataFrame({"x": [1]})
|
|
45
|
-
zf.writestr("data.csv", df.to_csv(index=False))
|
|
46
|
-
dfs = process_zip_to_dfs(buf.getvalue())
|
|
47
|
-
assert dfs["data.csv"].iloc[0, 0] == 1
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def test_process_zip_latin1_csv():
|
|
51
|
-
import io, zipfile, pandas as pd
|
|
52
|
-
from luxorasap.btgapi.reports import process_zip_to_dfs
|
|
53
|
-
|
|
54
|
-
df_src = pd.DataFrame({"nome": ["ação", "æøå"]})
|
|
55
|
-
csv_latin1 = df_src.to_csv(index=False, encoding="latin1")
|
|
56
|
-
|
|
57
|
-
buf = io.BytesIO()
|
|
58
|
-
with zipfile.ZipFile(buf, "w") as zf:
|
|
59
|
-
zf.writestr("dados.csv", csv_latin1)
|
|
60
|
-
|
|
61
|
-
dfs = process_zip_to_dfs(buf.getvalue())
|
|
62
|
-
assert dfs["dados.csv"].equals(df_src)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|