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.
Files changed (60) hide show
  1. rcb_requests-0.1.0/PKG-INFO +159 -0
  2. rcb_requests-0.1.0/README.md +137 -0
  3. rcb_requests-0.1.0/pyproject.toml +53 -0
  4. rcb_requests-0.1.0/src/rcb_requests/__init__.py +26 -0
  5. rcb_requests-0.1.0/src/rcb_requests/client/__init__.py +3 -0
  6. rcb_requests-0.1.0/src/rcb_requests/client/session.py +122 -0
  7. rcb_requests-0.1.0/src/rcb_requests/page_elements/__init__.py +13 -0
  8. rcb_requests-0.1.0/src/rcb_requests/page_elements/form_login.py +43 -0
  9. rcb_requests-0.1.0/src/rcb_requests/page_elements/popup.py +75 -0
  10. rcb_requests-0.1.0/src/rcb_requests/page_elements/reneg_consulta_detalhe.py +114 -0
  11. rcb_requests-0.1.0/src/rcb_requests/page_elements/reneg_filtro_pesquisa.py +81 -0
  12. rcb_requests-0.1.0/src/rcb_requests/page_elements/sidebar.py +141 -0
  13. rcb_requests-0.1.0/src/rcb_requests/pages/__init__.py +49 -0
  14. rcb_requests-0.1.0/src/rcb_requests/pages/acordos_financiamento_consulta_por_contrato/__init__.py +6 -0
  15. rcb_requests-0.1.0/src/rcb_requests/pages/acordos_financiamento_consulta_por_contrato/acordos_financiamento_consulta_por_contrato_page.py +7 -0
  16. rcb_requests-0.1.0/src/rcb_requests/pages/acordos_financiamento_exclusao/__init__.py +4 -0
  17. rcb_requests-0.1.0/src/rcb_requests/pages/acordos_financiamento_exclusao/acordos_financiamento_exclusao_page.py +7 -0
  18. rcb_requests-0.1.0/src/rcb_requests/pages/acordos_financiamento_inclusao/__init__.py +4 -0
  19. rcb_requests-0.1.0/src/rcb_requests/pages/acordos_financiamento_inclusao/acordos_financiamento_inclusao_page.py +7 -0
  20. rcb_requests-0.1.0/src/rcb_requests/pages/acordos_leasing_consulta_por_contrato/__init__.py +4 -0
  21. rcb_requests-0.1.0/src/rcb_requests/pages/acordos_leasing_consulta_por_contrato/acordos_leasing_consulta_por_contrato_page.py +7 -0
  22. rcb_requests-0.1.0/src/rcb_requests/pages/acordos_leasing_exclusao/__init__.py +4 -0
  23. rcb_requests-0.1.0/src/rcb_requests/pages/acordos_leasing_exclusao/acordos_leasing_exclusao_page.py +7 -0
  24. rcb_requests-0.1.0/src/rcb_requests/pages/acordos_leasing_inclusao/__init__.py +4 -0
  25. rcb_requests-0.1.0/src/rcb_requests/pages/acordos_leasing_inclusao/acordos_leasing_inclusao_page.py +7 -0
  26. rcb_requests-0.1.0/src/rcb_requests/pages/calculadora_pesquisa/__init__.py +4 -0
  27. rcb_requests-0.1.0/src/rcb_requests/pages/calculadora_pesquisa/calculadora_pesquisa_page.py +7 -0
  28. rcb_requests-0.1.0/src/rcb_requests/pages/consulta_gca/__init__.py +4 -0
  29. rcb_requests-0.1.0/src/rcb_requests/pages/consulta_gca/consulta_gca_page.py +7 -0
  30. rcb_requests-0.1.0/src/rcb_requests/pages/home_page.py +11 -0
  31. rcb_requests-0.1.0/src/rcb_requests/pages/lamina_cancelamento/__init__.py +4 -0
  32. rcb_requests-0.1.0/src/rcb_requests/pages/lamina_cancelamento/lamina_cancelamento_page.py +7 -0
  33. rcb_requests-0.1.0/src/rcb_requests/pages/lamina_consulta_por_contrato/__init__.py +4 -0
  34. rcb_requests-0.1.0/src/rcb_requests/pages/lamina_consulta_por_contrato/lamina_consulta_por_contrato_page.py +7 -0
  35. rcb_requests-0.1.0/src/rcb_requests/pages/lamina_solicitacao_por_contrato/__init__.py +4 -0
  36. rcb_requests-0.1.0/src/rcb_requests/pages/lamina_solicitacao_por_contrato/lamina_solicitacao_por_contrato_page.py +7 -0
  37. rcb_requests-0.1.0/src/rcb_requests/pages/login_page.py +82 -0
  38. rcb_requests-0.1.0/src/rcb_requests/pages/renegociacao_consulta/__init__.py +9 -0
  39. rcb_requests-0.1.0/src/rcb_requests/pages/renegociacao_consulta/renegociacao_consulta_detalhe_page.py +16 -0
  40. rcb_requests-0.1.0/src/rcb_requests/pages/renegociacao_consulta/renegociacao_consulta_page.py +9 -0
  41. rcb_requests-0.1.0/src/rcb_requests/pages/renegociacao_consulta/renegociacao_result_search.py +80 -0
  42. rcb_requests-0.1.0/src/rcb_requests/pages/renegociacao_exclusao/__init__.py +4 -0
  43. rcb_requests-0.1.0/src/rcb_requests/pages/renegociacao_exclusao/renegociacao_exclusao_page.py +7 -0
  44. rcb_requests-0.1.0/src/rcb_requests/pages/renegociacao_inclusao/__init__.py +4 -0
  45. rcb_requests-0.1.0/src/rcb_requests/pages/renegociacao_inclusao/renegociacao_inclusao_page.py +7 -0
  46. rcb_requests-0.1.0/src/rcb_requests/pages/renegociacao_reenvio/__init__.py +4 -0
  47. rcb_requests-0.1.0/src/rcb_requests/pages/renegociacao_reenvio/renegociacao_reenvio_page.py +7 -0
  48. rcb_requests-0.1.0/src/rcb_requests/pages/sidebar_pages.py +62 -0
  49. rcb_requests-0.1.0/src/rcb_requests/pages/usuario_secundario_troca_senha/__init__.py +4 -0
  50. rcb_requests-0.1.0/src/rcb_requests/pages/usuario_secundario_troca_senha/usuario_secundario_troca_senha_page.py +7 -0
  51. rcb_requests-0.1.0/src/rcb_requests/shared/__init__.py +6 -0
  52. rcb_requests-0.1.0/src/rcb_requests/shared/page.py +21 -0
  53. rcb_requests-0.1.0/src/rcb_requests/shared/page_element.py +28 -0
  54. rcb_requests-0.1.0/src/rcb_requests/types/__init__.py +37 -0
  55. rcb_requests-0.1.0/src/rcb_requests/types/captcha.py +4 -0
  56. rcb_requests-0.1.0/src/rcb_requests/types/reneg_consulta_detalhe.py +126 -0
  57. rcb_requests-0.1.0/src/rcb_requests/types/reneg_filtro_options.py +47 -0
  58. rcb_requests-0.1.0/src/rcb_requests/types/reneg_lista_row.py +17 -0
  59. rcb_requests-0.1.0/src/rcb_requests/types/response.py +7 -0
  60. 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,3 @@
1
+ from .session import Session
2
+
3
+ __all__ = ['Session']
@@ -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')