luxorasap 0.1.2__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.
Files changed (35) hide show
  1. {luxorasap-0.1.2 → luxorasap-0.1.3}/PKG-INFO +1 -1
  2. {luxorasap-0.1.2 → luxorasap-0.1.3}/pyproject.toml +2 -2
  3. {luxorasap-0.1.2 → luxorasap-0.1.3}/src/luxorasap/__init__.py +1 -1
  4. {luxorasap-0.1.2 → luxorasap-0.1.3}/src/luxorasap/btgapi/__init__.py +3 -2
  5. {luxorasap-0.1.2 → luxorasap-0.1.3}/src/luxorasap/btgapi/reports.py +52 -2
  6. {luxorasap-0.1.2 → luxorasap-0.1.3}/src/luxorasap/ingest/cloud/__init__.py +7 -0
  7. {luxorasap-0.1.2 → luxorasap-0.1.3}/src/luxorasap.egg-info/PKG-INFO +1 -1
  8. luxorasap-0.1.3/tests/test_btgapi_reports.py +139 -0
  9. luxorasap-0.1.2/tests/test_btgapi_reports.py +0 -62
  10. {luxorasap-0.1.2 → luxorasap-0.1.3}/README.md +0 -0
  11. {luxorasap-0.1.2 → luxorasap-0.1.3}/setup.cfg +0 -0
  12. {luxorasap-0.1.2 → luxorasap-0.1.3}/src/luxorasap/btgapi/auth.py +0 -0
  13. {luxorasap-0.1.2 → luxorasap-0.1.3}/src/luxorasap/btgapi/trades.py +0 -0
  14. {luxorasap-0.1.2 → luxorasap-0.1.3}/src/luxorasap/datareader/__init__.py +0 -0
  15. {luxorasap-0.1.2 → luxorasap-0.1.3}/src/luxorasap/datareader/core.py +0 -0
  16. {luxorasap-0.1.2 → luxorasap-0.1.3}/src/luxorasap/ingest/__init__.py +0 -0
  17. {luxorasap-0.1.2 → luxorasap-0.1.3}/src/luxorasap/ingest/legacy_local/dataloader.py +0 -0
  18. {luxorasap-0.1.2 → luxorasap-0.1.3}/src/luxorasap/utils/__init__.py +0 -0
  19. {luxorasap-0.1.2 → luxorasap-0.1.3}/src/luxorasap/utils/dataframe/__init__.py +0 -0
  20. {luxorasap-0.1.2 → luxorasap-0.1.3}/src/luxorasap/utils/dataframe/reader.py +0 -0
  21. {luxorasap-0.1.2 → luxorasap-0.1.3}/src/luxorasap/utils/dataframe/transforms.py +0 -0
  22. {luxorasap-0.1.2 → luxorasap-0.1.3}/src/luxorasap/utils/storage/__init__.py +0 -0
  23. {luxorasap-0.1.2 → luxorasap-0.1.3}/src/luxorasap/utils/storage/blob.py +0 -0
  24. {luxorasap-0.1.2 → luxorasap-0.1.3}/src/luxorasap.egg-info/SOURCES.txt +0 -0
  25. {luxorasap-0.1.2 → luxorasap-0.1.3}/src/luxorasap.egg-info/dependency_links.txt +0 -0
  26. {luxorasap-0.1.2 → luxorasap-0.1.3}/src/luxorasap.egg-info/entry_points.txt +0 -0
  27. {luxorasap-0.1.2 → luxorasap-0.1.3}/src/luxorasap.egg-info/requires.txt +0 -0
  28. {luxorasap-0.1.2 → luxorasap-0.1.3}/src/luxorasap.egg-info/top_level.txt +0 -0
  29. {luxorasap-0.1.2 → luxorasap-0.1.3}/tests/test_btgapi_auth.py +0 -0
  30. {luxorasap-0.1.2 → luxorasap-0.1.3}/tests/test_btgapi_trades.py +0 -0
  31. {luxorasap-0.1.2 → luxorasap-0.1.3}/tests/test_datareader.py +0 -0
  32. {luxorasap-0.1.2 → luxorasap-0.1.3}/tests/test_ingest_cloud.py +0 -0
  33. {luxorasap-0.1.2 → luxorasap-0.1.3}/tests/test_ingest_legacy_local.py +0 -0
  34. {luxorasap-0.1.2 → luxorasap-0.1.3}/tests/test_utils_dataframe.py +0 -0
  35. {luxorasap-0.1.2 → luxorasap-0.1.3}/tests/test_utils_storage.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: luxorasap
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Luxor’s unified toolbox for data ingestion, querying and analytics.
5
5
  Author-email: Luxor Group <backoffice@luxor.com.br>
6
6
  License: Proprietary – All rights reserved
@@ -10,7 +10,7 @@ build-backend = "setuptools.build_meta"
10
10
  #############################
11
11
  [project]
12
12
  name = "luxorasap"
13
- version = "0.1.2"
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.2"
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.2"
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
  ]
@@ -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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: luxorasap
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Luxor’s unified toolbox for data ingestion, querying and analytics.
5
5
  Author-email: Luxor Group <backoffice@luxor.com.br>
6
6
  License: Proprietary – All rights reserved
@@ -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
@@ -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