rcb-requests 0.1.0__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.
- rcb_requests-0.1.0/PKG-INFO +159 -0
- rcb_requests-0.1.0/README.md +137 -0
- rcb_requests-0.1.0/pyproject.toml +53 -0
- rcb_requests-0.1.0/src/rcb_requests/__init__.py +26 -0
- rcb_requests-0.1.0/src/rcb_requests/client/__init__.py +3 -0
- rcb_requests-0.1.0/src/rcb_requests/client/session.py +122 -0
- rcb_requests-0.1.0/src/rcb_requests/page_elements/__init__.py +13 -0
- rcb_requests-0.1.0/src/rcb_requests/page_elements/form_login.py +43 -0
- rcb_requests-0.1.0/src/rcb_requests/page_elements/popup.py +75 -0
- rcb_requests-0.1.0/src/rcb_requests/page_elements/reneg_consulta_detalhe.py +114 -0
- rcb_requests-0.1.0/src/rcb_requests/page_elements/reneg_filtro_pesquisa.py +81 -0
- rcb_requests-0.1.0/src/rcb_requests/page_elements/sidebar.py +141 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/__init__.py +49 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/acordos_financiamento_consulta_por_contrato/__init__.py +6 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/acordos_financiamento_consulta_por_contrato/acordos_financiamento_consulta_por_contrato_page.py +7 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/acordos_financiamento_exclusao/__init__.py +4 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/acordos_financiamento_exclusao/acordos_financiamento_exclusao_page.py +7 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/acordos_financiamento_inclusao/__init__.py +4 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/acordos_financiamento_inclusao/acordos_financiamento_inclusao_page.py +7 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/acordos_leasing_consulta_por_contrato/__init__.py +4 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/acordos_leasing_consulta_por_contrato/acordos_leasing_consulta_por_contrato_page.py +7 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/acordos_leasing_exclusao/__init__.py +4 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/acordos_leasing_exclusao/acordos_leasing_exclusao_page.py +7 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/acordos_leasing_inclusao/__init__.py +4 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/acordos_leasing_inclusao/acordos_leasing_inclusao_page.py +7 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/calculadora_pesquisa/__init__.py +4 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/calculadora_pesquisa/calculadora_pesquisa_page.py +7 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/consulta_gca/__init__.py +4 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/consulta_gca/consulta_gca_page.py +7 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/home_page.py +11 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/lamina_cancelamento/__init__.py +4 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/lamina_cancelamento/lamina_cancelamento_page.py +7 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/lamina_consulta_por_contrato/__init__.py +4 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/lamina_consulta_por_contrato/lamina_consulta_por_contrato_page.py +7 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/lamina_solicitacao_por_contrato/__init__.py +4 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/lamina_solicitacao_por_contrato/lamina_solicitacao_por_contrato_page.py +7 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/login_page.py +82 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/renegociacao_consulta/__init__.py +9 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/renegociacao_consulta/renegociacao_consulta_detalhe_page.py +16 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/renegociacao_consulta/renegociacao_consulta_page.py +9 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/renegociacao_consulta/renegociacao_result_search.py +80 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/renegociacao_exclusao/__init__.py +4 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/renegociacao_exclusao/renegociacao_exclusao_page.py +7 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/renegociacao_inclusao/__init__.py +4 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/renegociacao_inclusao/renegociacao_inclusao_page.py +7 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/renegociacao_reenvio/__init__.py +4 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/renegociacao_reenvio/renegociacao_reenvio_page.py +7 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/sidebar_pages.py +62 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/usuario_secundario_troca_senha/__init__.py +4 -0
- rcb_requests-0.1.0/src/rcb_requests/pages/usuario_secundario_troca_senha/usuario_secundario_troca_senha_page.py +7 -0
- rcb_requests-0.1.0/src/rcb_requests/shared/__init__.py +6 -0
- rcb_requests-0.1.0/src/rcb_requests/shared/page.py +21 -0
- rcb_requests-0.1.0/src/rcb_requests/shared/page_element.py +28 -0
- rcb_requests-0.1.0/src/rcb_requests/types/__init__.py +37 -0
- rcb_requests-0.1.0/src/rcb_requests/types/captcha.py +4 -0
- rcb_requests-0.1.0/src/rcb_requests/types/reneg_consulta_detalhe.py +126 -0
- rcb_requests-0.1.0/src/rcb_requests/types/reneg_filtro_options.py +47 -0
- rcb_requests-0.1.0/src/rcb_requests/types/reneg_lista_row.py +17 -0
- rcb_requests-0.1.0/src/rcb_requests/types/response.py +7 -0
- rcb_requests-0.1.0/src/rcb_requests/types/sidebar_options.py +60 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rcb-requests
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Cliente async (Page Object) para automação do portal RCB Santander.
|
|
5
|
+
License: Proprietary
|
|
6
|
+
Keywords: rpa,rcb,santander,aiohttp,automation
|
|
7
|
+
Author: Emanuel Borges Albano
|
|
8
|
+
Author-email: emanuel.albano@goesnicoladelli.com.br
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Typing :: Typed
|
|
14
|
+
Requires-Dist: aiohttp (>=3.9,<4.0)
|
|
15
|
+
Requires-Dist: beautifulsoup4 (>=4.12,<5.0)
|
|
16
|
+
Requires-Dist: utils-rpa (>=0.1.2,<0.2.0)
|
|
17
|
+
Project-URL: Homepage, https://gitlab.com/goesnicoladelli/rcb-requests
|
|
18
|
+
Project-URL: Issues, https://gitlab.com/goesnicoladelli/rcb-requests/-/issues
|
|
19
|
+
Project-URL: Repository, https://gitlab.com/goesnicoladelli/rcb-requests
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# rcb-requests
|
|
23
|
+
|
|
24
|
+
Cliente Python async para automação do portal **RCB Santander** (`negocios.santander.com.br`), baseado em [aiohttp](https://docs.aiohttp.org/) e Page Object Model.
|
|
25
|
+
|
|
26
|
+
## Requisitos
|
|
27
|
+
|
|
28
|
+
- Python 3.12+
|
|
29
|
+
- Acesso ao GitLab Package Registry da empresa (para instalar esta lib e `utils-rpa`)
|
|
30
|
+
|
|
31
|
+
## Instalação
|
|
32
|
+
|
|
33
|
+
### Poetry (recomendado)
|
|
34
|
+
|
|
35
|
+
Configure o registry do grupo no GitLab (onde `utils-rpa` também está publicado):
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
poetry source add --priority=supplemental gitlab-group \
|
|
39
|
+
"https://gitlab.com/api/v4/groups/SEU_GRUPO_ID/-/packages/pypi/simple"
|
|
40
|
+
poetry config http-basic.gitlab-group SEU_USUARIO SEU_TOKEN
|
|
41
|
+
poetry add rcb-requests
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### pip
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install rcb-requests \
|
|
48
|
+
--index-url https://gitlab.com/api/v4/projects/SEU_PROJECT_ID/packages/pypi/simple \
|
|
49
|
+
--extra-index-url https://pypi.org/simple
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Use um Personal Access Token ou Deploy Token com permissão `read_package_registry`.
|
|
53
|
+
|
|
54
|
+
## Uso rápido
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
import asyncio
|
|
58
|
+
|
|
59
|
+
import aiohttp
|
|
60
|
+
|
|
61
|
+
from rcb_requests import Renegociacao
|
|
62
|
+
from rcb_requests.client import Session
|
|
63
|
+
from rcb_requests.pages import LoginPage
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
async def main():
|
|
67
|
+
async with aiohttp.ClientSession(
|
|
68
|
+
base_url="https://negocios.santander.com.br",
|
|
69
|
+
timeout=aiohttp.ClientTimeout(total=30),
|
|
70
|
+
) as client:
|
|
71
|
+
session = Session(client)
|
|
72
|
+
login_page = LoginPage(session)
|
|
73
|
+
|
|
74
|
+
await login_page.access_page()
|
|
75
|
+
|
|
76
|
+
response, home = await login_page.login(
|
|
77
|
+
cnpj="00.000.000/0001-00",
|
|
78
|
+
username="usuario",
|
|
79
|
+
password="senha",
|
|
80
|
+
resolve_captcha=lambda image: "abcd", # integre seu resolvedor de captcha
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if not response.success:
|
|
84
|
+
raise RuntimeError(response.error_message)
|
|
85
|
+
|
|
86
|
+
reneg_page = await home.sidebar.click_option(Renegociacao.CONSULTA)
|
|
87
|
+
result_page = await reneg_page.filtro_pesquisa.submit_filter(contrato="12345678901")
|
|
88
|
+
rows = result_page.get_rows_filter()
|
|
89
|
+
print(rows)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
asyncio.run(main())
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Arquitetura
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
Session → transporte HTTP e parsing HTML
|
|
99
|
+
└── Page → tela do RCB (login, home, consultas…)
|
|
100
|
+
└── PageElement → formulários, sidebar, popups
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
- **`Session`**: mantém a última página (`BeautifulSoup`) e monta payloads JSF/ICEfaces via `create_payload`.
|
|
104
|
+
- **`Page` / `PageElement`**: localizam elementos no DOM e disparam requisições.
|
|
105
|
+
- **`types`**: enums da sidebar, dataclasses de resposta e linhas de tabela.
|
|
106
|
+
|
|
107
|
+
## Captcha
|
|
108
|
+
|
|
109
|
+
O login exige um callback `resolve_captcha` que recebe a imagem em base64 (`data:image/jpg;base64,...`) e retorna o texto. Exemplo com `utils-rpa`:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from utils_rpa.anti_captcha import image_to_text
|
|
113
|
+
|
|
114
|
+
resolve = lambda image: image_to_text(os.environ["ANTICAPTCHA_TOKEN"], image)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Debug
|
|
118
|
+
|
|
119
|
+
Salve o HTML da última resposta para inspecionar a página:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
session.save_last_page_debug("ultima_pagina")
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Exemplos
|
|
126
|
+
|
|
127
|
+
Scripts completos (com variáveis de ambiente) ficam em `examples/`:
|
|
128
|
+
|
|
129
|
+
- `examples/login_and_consulta.py`
|
|
130
|
+
- `examples/benchmark_steps.py`
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
export RCB_CNPJ="..."
|
|
134
|
+
export RCB_USERNAME="..."
|
|
135
|
+
export RCB_PASSWORD="..."
|
|
136
|
+
export ANTICAPTCHA_TOKEN="..."
|
|
137
|
+
python examples/login_and_consulta.py
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Desenvolvimento
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
poetry install --with dev
|
|
144
|
+
ruff check src tests
|
|
145
|
+
pytest
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Publicação
|
|
149
|
+
|
|
150
|
+
1. Atualize a versão em `pyproject.toml` e registre em `CHANGELOG.md`.
|
|
151
|
+
2. Crie uma tag SemVer: `git tag v0.1.0 && git push origin v0.1.0`.
|
|
152
|
+
3. O pipeline `.gitlab-ci.yml` publica no GitLab Package Registry.
|
|
153
|
+
|
|
154
|
+
Configure a variável `GITLAB_GROUP_ID` no projeto GitLab para o CI resolver dependências internas (`utils-rpa`).
|
|
155
|
+
|
|
156
|
+
## Licença
|
|
157
|
+
|
|
158
|
+
Uso interno — proprietário.
|
|
159
|
+
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# rcb-requests
|
|
2
|
+
|
|
3
|
+
Cliente Python async para automação do portal **RCB Santander** (`negocios.santander.com.br`), baseado em [aiohttp](https://docs.aiohttp.org/) e Page Object Model.
|
|
4
|
+
|
|
5
|
+
## Requisitos
|
|
6
|
+
|
|
7
|
+
- Python 3.12+
|
|
8
|
+
- Acesso ao GitLab Package Registry da empresa (para instalar esta lib e `utils-rpa`)
|
|
9
|
+
|
|
10
|
+
## Instalação
|
|
11
|
+
|
|
12
|
+
### Poetry (recomendado)
|
|
13
|
+
|
|
14
|
+
Configure o registry do grupo no GitLab (onde `utils-rpa` também está publicado):
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
poetry source add --priority=supplemental gitlab-group \
|
|
18
|
+
"https://gitlab.com/api/v4/groups/SEU_GRUPO_ID/-/packages/pypi/simple"
|
|
19
|
+
poetry config http-basic.gitlab-group SEU_USUARIO SEU_TOKEN
|
|
20
|
+
poetry add rcb-requests
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### pip
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install rcb-requests \
|
|
27
|
+
--index-url https://gitlab.com/api/v4/projects/SEU_PROJECT_ID/packages/pypi/simple \
|
|
28
|
+
--extra-index-url https://pypi.org/simple
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Use um Personal Access Token ou Deploy Token com permissão `read_package_registry`.
|
|
32
|
+
|
|
33
|
+
## Uso rápido
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import asyncio
|
|
37
|
+
|
|
38
|
+
import aiohttp
|
|
39
|
+
|
|
40
|
+
from rcb_requests import Renegociacao
|
|
41
|
+
from rcb_requests.client import Session
|
|
42
|
+
from rcb_requests.pages import LoginPage
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def main():
|
|
46
|
+
async with aiohttp.ClientSession(
|
|
47
|
+
base_url="https://negocios.santander.com.br",
|
|
48
|
+
timeout=aiohttp.ClientTimeout(total=30),
|
|
49
|
+
) as client:
|
|
50
|
+
session = Session(client)
|
|
51
|
+
login_page = LoginPage(session)
|
|
52
|
+
|
|
53
|
+
await login_page.access_page()
|
|
54
|
+
|
|
55
|
+
response, home = await login_page.login(
|
|
56
|
+
cnpj="00.000.000/0001-00",
|
|
57
|
+
username="usuario",
|
|
58
|
+
password="senha",
|
|
59
|
+
resolve_captcha=lambda image: "abcd", # integre seu resolvedor de captcha
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if not response.success:
|
|
63
|
+
raise RuntimeError(response.error_message)
|
|
64
|
+
|
|
65
|
+
reneg_page = await home.sidebar.click_option(Renegociacao.CONSULTA)
|
|
66
|
+
result_page = await reneg_page.filtro_pesquisa.submit_filter(contrato="12345678901")
|
|
67
|
+
rows = result_page.get_rows_filter()
|
|
68
|
+
print(rows)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
asyncio.run(main())
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Arquitetura
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
Session → transporte HTTP e parsing HTML
|
|
78
|
+
└── Page → tela do RCB (login, home, consultas…)
|
|
79
|
+
└── PageElement → formulários, sidebar, popups
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
- **`Session`**: mantém a última página (`BeautifulSoup`) e monta payloads JSF/ICEfaces via `create_payload`.
|
|
83
|
+
- **`Page` / `PageElement`**: localizam elementos no DOM e disparam requisições.
|
|
84
|
+
- **`types`**: enums da sidebar, dataclasses de resposta e linhas de tabela.
|
|
85
|
+
|
|
86
|
+
## Captcha
|
|
87
|
+
|
|
88
|
+
O login exige um callback `resolve_captcha` que recebe a imagem em base64 (`data:image/jpg;base64,...`) e retorna o texto. Exemplo com `utils-rpa`:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from utils_rpa.anti_captcha import image_to_text
|
|
92
|
+
|
|
93
|
+
resolve = lambda image: image_to_text(os.environ["ANTICAPTCHA_TOKEN"], image)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Debug
|
|
97
|
+
|
|
98
|
+
Salve o HTML da última resposta para inspecionar a página:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
session.save_last_page_debug("ultima_pagina")
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Exemplos
|
|
105
|
+
|
|
106
|
+
Scripts completos (com variáveis de ambiente) ficam em `examples/`:
|
|
107
|
+
|
|
108
|
+
- `examples/login_and_consulta.py`
|
|
109
|
+
- `examples/benchmark_steps.py`
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
export RCB_CNPJ="..."
|
|
113
|
+
export RCB_USERNAME="..."
|
|
114
|
+
export RCB_PASSWORD="..."
|
|
115
|
+
export ANTICAPTCHA_TOKEN="..."
|
|
116
|
+
python examples/login_and_consulta.py
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Desenvolvimento
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
poetry install --with dev
|
|
123
|
+
ruff check src tests
|
|
124
|
+
pytest
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Publicação
|
|
128
|
+
|
|
129
|
+
1. Atualize a versão em `pyproject.toml` e registre em `CHANGELOG.md`.
|
|
130
|
+
2. Crie uma tag SemVer: `git tag v0.1.0 && git push origin v0.1.0`.
|
|
131
|
+
3. O pipeline `.gitlab-ci.yml` publica no GitLab Package Registry.
|
|
132
|
+
|
|
133
|
+
Configure a variável `GITLAB_GROUP_ID` no projeto GitLab para o CI resolver dependências internas (`utils-rpa`).
|
|
134
|
+
|
|
135
|
+
## Licença
|
|
136
|
+
|
|
137
|
+
Uso interno — proprietário.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "rcb-requests"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Cliente async (Page Object) para automação do portal RCB Santander."
|
|
5
|
+
authors = [
|
|
6
|
+
{ name = "Emanuel Borges Albano", email = "emanuel.albano@goesnicoladelli.com.br" },
|
|
7
|
+
]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
license = { text = "Proprietary" }
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
keywords = ["rpa", "rcb", "santander", "aiohttp", "automation"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"Programming Language :: Python :: 3.12",
|
|
16
|
+
"Typing :: Typed",
|
|
17
|
+
]
|
|
18
|
+
dependencies = [
|
|
19
|
+
"utils-rpa (>=0.1.2,<0.2.0)",
|
|
20
|
+
"beautifulsoup4 (>=4.12,<5.0)",
|
|
21
|
+
"aiohttp (>=3.9,<4.0)",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://gitlab.com/goesnicoladelli/rcb-requests"
|
|
26
|
+
Repository = "https://gitlab.com/goesnicoladelli/rcb-requests"
|
|
27
|
+
Issues = "https://gitlab.com/goesnicoladelli/rcb-requests/-/issues"
|
|
28
|
+
|
|
29
|
+
[tool.poetry]
|
|
30
|
+
packages = [{ include = "rcb_requests", from = "src" }]
|
|
31
|
+
|
|
32
|
+
[tool.poetry.group.dev.dependencies]
|
|
33
|
+
pytest = "^8.3.0"
|
|
34
|
+
pytest-asyncio = "^0.24.0"
|
|
35
|
+
pytest-cov = "^5.0.0"
|
|
36
|
+
ruff = "^0.9.0"
|
|
37
|
+
|
|
38
|
+
[tool.pytest.ini_options]
|
|
39
|
+
testpaths = ["tests"]
|
|
40
|
+
pythonpath = ["src"]
|
|
41
|
+
asyncio_mode = "auto"
|
|
42
|
+
|
|
43
|
+
[tool.ruff]
|
|
44
|
+
line-length = 100
|
|
45
|
+
src = ["src", "tests"]
|
|
46
|
+
|
|
47
|
+
[tool.ruff.lint]
|
|
48
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
49
|
+
ignore = ["E501"]
|
|
50
|
+
|
|
51
|
+
[build-system]
|
|
52
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
53
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Cliente async (Page Object) para automação do portal RCB Santander."""
|
|
2
|
+
|
|
3
|
+
from rcb_requests.types import (
|
|
4
|
+
AcordosFinanciamento,
|
|
5
|
+
AcordosLeasing,
|
|
6
|
+
Calculadora,
|
|
7
|
+
ConsultaGCA,
|
|
8
|
+
Lamina,
|
|
9
|
+
Renegociacao,
|
|
10
|
+
SidebarOption,
|
|
11
|
+
UsuarioSecundario,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__version__ = "0.1.0"
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"__version__",
|
|
18
|
+
"SidebarOption",
|
|
19
|
+
"AcordosLeasing",
|
|
20
|
+
"AcordosFinanciamento",
|
|
21
|
+
"Calculadora",
|
|
22
|
+
"ConsultaGCA",
|
|
23
|
+
"Lamina",
|
|
24
|
+
"Renegociacao",
|
|
25
|
+
"UsuarioSecundario",
|
|
26
|
+
]
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from aiohttp import ClientSession
|
|
4
|
+
from bs4 import BeautifulSoup
|
|
5
|
+
from utils_rpa import extract_inputs
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Session:
|
|
9
|
+
"""Encapsula um ``aiohttp.ClientSession`` e mantém o estado da última página.
|
|
10
|
+
|
|
11
|
+
Responsável apenas pelo transporte HTTP e pelo parsing do HTML. As páginas e
|
|
12
|
+
elementos recebem uma instância desta classe por composição.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
def __init__(self, client: ClientSession):
|
|
18
|
+
self._client = client
|
|
19
|
+
self.last_page : BeautifulSoup | None = None
|
|
20
|
+
|
|
21
|
+
async def get(self, url: str, params: dict | None = None) -> BeautifulSoup:
|
|
22
|
+
self.logger.debug('Fazendo GET request')
|
|
23
|
+
async with self._client.get(
|
|
24
|
+
url,
|
|
25
|
+
params=params,
|
|
26
|
+
allow_redirects=True,
|
|
27
|
+
) as response:
|
|
28
|
+
self.last_page = self.get_soup(await response.text())
|
|
29
|
+
return self.last_page
|
|
30
|
+
|
|
31
|
+
async def post(
|
|
32
|
+
self,
|
|
33
|
+
url: str,
|
|
34
|
+
json: dict | None = None,
|
|
35
|
+
data: dict | None = None,
|
|
36
|
+
) -> BeautifulSoup:
|
|
37
|
+
self.logger.debug('Fazendo POST request')
|
|
38
|
+
async with self._client.post(
|
|
39
|
+
url,
|
|
40
|
+
json=json,
|
|
41
|
+
data=data,
|
|
42
|
+
allow_redirects=True,
|
|
43
|
+
) as response:
|
|
44
|
+
self.last_page = self.get_soup(await response.text())
|
|
45
|
+
return self.last_page
|
|
46
|
+
|
|
47
|
+
async def close(self):
|
|
48
|
+
self.logger.debug('Fechando sessão')
|
|
49
|
+
await self._client.close()
|
|
50
|
+
|
|
51
|
+
def extract_inputs(self, soup: BeautifulSoup) -> dict:
|
|
52
|
+
self.logger.debug('Extraindo inputs')
|
|
53
|
+
return extract_inputs(soup)
|
|
54
|
+
|
|
55
|
+
def create_payload(self, clicked_element: BeautifulSoup) -> tuple[dict[str, str], str]:
|
|
56
|
+
"""Cria o payload ideal para cada requisição no RCB, com base no elemento clicado.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
clicked_element (BeautifulSoup): Elemento em que foi realizado o clique
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
tuple[dict[str, str], str]: payload key:value e url (action do form)
|
|
63
|
+
"""
|
|
64
|
+
self.logger.debug('Criando payload')
|
|
65
|
+
element_name = clicked_element.name
|
|
66
|
+
form = clicked_element.find_parent('form')
|
|
67
|
+
if form is None:
|
|
68
|
+
raise ValueError('Elemento clicado não está dentro de um <form>.')
|
|
69
|
+
|
|
70
|
+
data = self.extract_inputs(form)
|
|
71
|
+
tag_id = clicked_element.get('id', '')
|
|
72
|
+
form_id = form.get('id', '')
|
|
73
|
+
|
|
74
|
+
data.update({
|
|
75
|
+
'ice.event.captured': tag_id,
|
|
76
|
+
'javax.faces.source': tag_id,
|
|
77
|
+
form_id: form_id,
|
|
78
|
+
f'{form_id}_SUBMIT': 1,
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
match element_name:
|
|
82
|
+
case 'div':
|
|
83
|
+
self.logger.info('click na div')
|
|
84
|
+
a_tag = clicked_element.find('a')
|
|
85
|
+
a_id = a_tag.get('id', '') if a_tag else ''
|
|
86
|
+
data.update({
|
|
87
|
+
'ice.focus': a_id,
|
|
88
|
+
'ice.event.target': a_id,
|
|
89
|
+
tag_id: tag_id,
|
|
90
|
+
})
|
|
91
|
+
case 'input':
|
|
92
|
+
self.logger.info('click no input')
|
|
93
|
+
input_value = clicked_element.get('value', '')
|
|
94
|
+
data.update({
|
|
95
|
+
'ice.focus': tag_id,
|
|
96
|
+
'ice.event.target': tag_id,
|
|
97
|
+
tag_id: input_value,
|
|
98
|
+
})
|
|
99
|
+
case 'select':
|
|
100
|
+
self.logger.info('click em select')
|
|
101
|
+
parent_span = clicked_element.find_parent('span', {'id': True})
|
|
102
|
+
data.update({
|
|
103
|
+
'ice.focus': tag_id,
|
|
104
|
+
'ice.event.target': tag_id,
|
|
105
|
+
'ice.event.captured': parent_span.get('id', '') if parent_span else tag_id,
|
|
106
|
+
'javax.faces.source': parent_span.get('id', '') if parent_span else tag_id,
|
|
107
|
+
})
|
|
108
|
+
case _:
|
|
109
|
+
self.logger.info('click no default')
|
|
110
|
+
data.update({
|
|
111
|
+
'ice.focus': tag_id,
|
|
112
|
+
'ice.event.target': tag_id,
|
|
113
|
+
tag_id: tag_id,
|
|
114
|
+
})
|
|
115
|
+
return data, form.attrs.get('action', '')
|
|
116
|
+
|
|
117
|
+
def save_last_page_debug(self, filename: str = 'last_page') -> None:
|
|
118
|
+
with open(f'{filename}.html', 'w', encoding='utf-8') as f:
|
|
119
|
+
f.write(self.last_page.prettify())
|
|
120
|
+
|
|
121
|
+
def get_soup(self, html: str) -> BeautifulSoup:
|
|
122
|
+
return BeautifulSoup(html, 'html.parser')
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .form_login import FormLogin
|
|
2
|
+
from .popup import Popup
|
|
3
|
+
from .reneg_consulta_detalhe import RenegConsultaDetalhe
|
|
4
|
+
from .reneg_filtro_pesquisa import RenegFiltroPesquisa
|
|
5
|
+
from .sidebar import Sidebar
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
'FormLogin',
|
|
9
|
+
'Popup',
|
|
10
|
+
'RenegConsultaDetalhe',
|
|
11
|
+
'RenegFiltroPesquisa',
|
|
12
|
+
'Sidebar',
|
|
13
|
+
]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
|
|
2
|
+
from rcb_requests.shared import PageElement
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class FormLogin(PageElement):
|
|
6
|
+
__locator__ = '#j_id_x'
|
|
7
|
+
|
|
8
|
+
# Locators
|
|
9
|
+
_captcha_locator = '#imgElem1'
|
|
10
|
+
_button_login = '[id="j_id_x:__1l"]'
|
|
11
|
+
|
|
12
|
+
# Nomes dos campos (chaves do payload)
|
|
13
|
+
_cnpj = 'j_id_x:__13_2'
|
|
14
|
+
_username = 'j_id_x:__16'
|
|
15
|
+
_password = 'j_id_x:__18'
|
|
16
|
+
_captcha = 'j_id_x:__1j_input'
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def captcha_image(self) -> str | None:
|
|
20
|
+
img = self.element.select_one(self._captcha_locator)
|
|
21
|
+
return img.get('src').split(",")[1].strip() if img else None
|
|
22
|
+
|
|
23
|
+
async def login(self, cnpj: str, username: str, password: str, captcha: str):
|
|
24
|
+
"""Realiza o login no sistema.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
cnpj: CNPJ do usuário.
|
|
28
|
+
username: Nome de usuário do usuário.
|
|
29
|
+
password: Senha do usuário.
|
|
30
|
+
captcha: Texto da imagem de captcha.
|
|
31
|
+
"""
|
|
32
|
+
button = self.element.select_one(self._button_login)
|
|
33
|
+
if button is None:
|
|
34
|
+
raise ValueError(f'Botão de login não encontrado ({self._button_login}).')
|
|
35
|
+
|
|
36
|
+
payload, url = self.session.create_payload(button)
|
|
37
|
+
payload.update({
|
|
38
|
+
self._cnpj: cnpj,
|
|
39
|
+
self._username: username,
|
|
40
|
+
self._password: password,
|
|
41
|
+
self._captcha: captcha,
|
|
42
|
+
})
|
|
43
|
+
await self.session.post(url, data=payload)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
|
|
2
|
+
from rcb_requests.shared import PageElement
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Popup(PageElement):
|
|
6
|
+
"""Diálogo (ICEfaces ace:dialog) atualmente visível na página.
|
|
7
|
+
|
|
8
|
+
Os ids externos (ex.: ``j_id_84_main``) são gerados dinamicamente e mudam a
|
|
9
|
+
cada view, então não servem de locator. O que identifica um popup *visível*
|
|
10
|
+
é a AUSÊNCIA da classe ``ace-dialog-hidden``. Cada popup traz o próprio
|
|
11
|
+
``<form>`` (com ViewState) e botões, então reaproveitamos
|
|
12
|
+
``session.create_payload`` para interagir.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
# O :not(...) já descarta os diálogos ocultos (classe ``ace-dialog-hidden``),
|
|
16
|
+
# sobrando apenas o popup efetivamente visível.
|
|
17
|
+
__locator__ = 'div[id$="_main"][class*="dialog"]:not([class*="ace-dialog-hidden"])'
|
|
18
|
+
|
|
19
|
+
_title_locator = '.pop-titulo'
|
|
20
|
+
_message_locator = '.pop-msg'
|
|
21
|
+
_button_locator = 'input[type="submit"], .botao'
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def is_open(self) -> bool:
|
|
25
|
+
return self.element is not None
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def title(self) -> str | None:
|
|
29
|
+
el = self.element
|
|
30
|
+
node = el.select_one(self._title_locator) if el else None
|
|
31
|
+
return node.get_text(strip=True) if node else None
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def message(self) -> str | None:
|
|
35
|
+
el = self.element
|
|
36
|
+
node = el.select_one(self._message_locator) if el else None
|
|
37
|
+
return node.get_text(strip=True) if node else None
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def buttons(self) -> list[str]:
|
|
41
|
+
el = self.element
|
|
42
|
+
if el is None:
|
|
43
|
+
return []
|
|
44
|
+
return [b.get('value', '').strip() for b in el.select(self._button_locator)]
|
|
45
|
+
|
|
46
|
+
async def click_button(self, value: str|None=None):
|
|
47
|
+
"""Clica no botão do popup cujo ``value`` corresponda (case-insensitive).
|
|
48
|
+
Se ``value`` for ``None``, clica no ultimo botão.
|
|
49
|
+
"""
|
|
50
|
+
el = self.element
|
|
51
|
+
if el is None:
|
|
52
|
+
raise RuntimeError('Nenhum popup aberto para interagir.')
|
|
53
|
+
buttons = el.select(self._button_locator)
|
|
54
|
+
if value is None:
|
|
55
|
+
button = buttons[-1]
|
|
56
|
+
else:
|
|
57
|
+
button = next(
|
|
58
|
+
(
|
|
59
|
+
b for b in el.select(self._button_locator)
|
|
60
|
+
if b.get('value', '').strip().casefold() == value.casefold()
|
|
61
|
+
),
|
|
62
|
+
None,
|
|
63
|
+
)
|
|
64
|
+
if button is None:
|
|
65
|
+
raise ValueError(
|
|
66
|
+
f'Botão {value!r} não encontrado. Disponíveis: {self.buttons}'
|
|
67
|
+
)
|
|
68
|
+
payload, url = self.session.create_payload(button)
|
|
69
|
+
await self.session.post(url, data=payload)
|
|
70
|
+
|
|
71
|
+
async def confirm(self):
|
|
72
|
+
await self.click_button('Confirmar')
|
|
73
|
+
|
|
74
|
+
async def cancel(self):
|
|
75
|
+
await self.click_button('Voltar')
|