artespy 0.1.0__py3-none-any.whl

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.
artespy/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from artespy.selenium.handler import SeleniumHandler
2
+
3
+ __all__ = ["SeleniumHandler"]
@@ -0,0 +1,22 @@
1
+ class HandlerException(Exception):
2
+ """Classe base para exceptions relacionadas ao handler."""
3
+
4
+
5
+ class UnknownAlertException(HandlerException):
6
+ pass
7
+
8
+
9
+ class ElementNotFoundException(HandlerException):
10
+ pass
11
+
12
+
13
+ class InvalidPageException(HandlerException):
14
+ pass
15
+
16
+
17
+ class SeiException(Exception):
18
+ """Classe base para exceptions relacionadas ao SEI."""
19
+
20
+
21
+ class InvalidCredentialsError(SeiException):
22
+ pass
@@ -0,0 +1,262 @@
1
+ import datetime as dt
2
+ import os
3
+ from typing import Literal
4
+
5
+ import pandas as pd
6
+ from selenium.webdriver import Chrome, ChromeOptions, ChromeService
7
+ from selenium.webdriver.support.ui import WebDriverWait
8
+ from webdriver_manager.chrome import ChromeDriverManager
9
+
10
+ from artespy.errors import HandlerException, InvalidPageException
11
+ from artespy.selenium.pages.home import HomePage
12
+ from artespy.selenium.pages.login import LoginPage
13
+ from artespy.selenium.pages.processo import ProcessoPage
14
+
15
+
16
+ class SeleniumHandler:
17
+ """Handler Selenium para automatizar operações no portal SEI."""
18
+
19
+ def __init__(self, headless: bool = False, timeout: int = 60) -> None:
20
+ self.headless: bool = headless
21
+ self.timeout: int = timeout
22
+
23
+ self.driver = None
24
+ self.wait = None
25
+ self._page = None
26
+
27
+ @property
28
+ def page(self):
29
+ if not self._page:
30
+ raise HandlerException("Página atual não inicializada.")
31
+ return self._page
32
+
33
+ def _inicializar_driver(self) -> None:
34
+ """Instanciar um novo driver e espera explícita."""
35
+ opts = ChromeOptions()
36
+ opts.add_argument("--log-level=3")
37
+ opts.add_argument("--start-maximized")
38
+ if self.headless:
39
+ opts.add_argument("--headless")
40
+ opts.add_experimental_option("excludeSwitches", ["enable-automation"])
41
+ opts.add_experimental_option("useAutomationExtension", False)
42
+ opts.add_experimental_option(
43
+ "prefs",
44
+ {
45
+ "credentials_enable_service": False,
46
+ "profile.password_manager_enabled": False,
47
+ },
48
+ )
49
+
50
+ chrome_install = ChromeDriverManager().install()
51
+ folder = os.path.dirname(chrome_install)
52
+ driver_path = os.path.join(folder, "chromedriver.exe")
53
+ svc = ChromeService(driver_path)
54
+
55
+ self.driver = Chrome(service=svc, options=opts)
56
+ self.wait = WebDriverWait(self.driver, self.timeout)
57
+
58
+ def _encerrar_driver(self) -> None:
59
+ if self.driver:
60
+ self.driver.close()
61
+ self.driver = None
62
+ if self.wait:
63
+ self.wait = None
64
+
65
+ def __enter__(self) -> "SeleniumHandler":
66
+ """Ponto de entrada do gerenciador de contexto."""
67
+ self._inicializar_driver()
68
+ return self
69
+
70
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
71
+ """Ponto de saída do gerenciador de contexto."""
72
+ self._encerrar_driver()
73
+
74
+ def _get_driver_e_espera(
75
+ self,
76
+ ) -> tuple[Chrome, WebDriverWait[Chrome]]:
77
+ if not self.driver or not self.wait:
78
+ raise HandlerException("Driver e/ou espera não inicializados.")
79
+ return self.driver, self.wait
80
+
81
+ def acessar_sei(self, usuario: str, senha: str, orgao: str = "ARTESP"):
82
+ """Acessa e faz login no portal SEI."""
83
+ driver, wait = self._get_driver_e_espera()
84
+
85
+ if not isinstance(usuario, str):
86
+ msg = "Acessando SEI: usuario deve ser uma string"
87
+ raise TypeError(msg) # pyright:ignore[reportUnreachable]
88
+ if not usuario:
89
+ msg = "Acessando SEI: usuário deve ser uma string não vazia"
90
+ raise ValueError(msg)
91
+ if not isinstance(senha, str):
92
+ msg = "Acessando SEI: senha deve ser uma string"
93
+ raise TypeError(msg) # pyright:ignore[reportUnreachable]
94
+ if not senha:
95
+ msg = "Acessando SEI: senha deve ser uma string não vazia"
96
+ raise ValueError(msg)
97
+ if not isinstance(orgao, str):
98
+ msg = "Acessando SEI: orgao deve ser uma string"
99
+ raise TypeError(msg) # pyright:ignore[reportUnreachable]
100
+ if not orgao:
101
+ msg = "Acessando SEI: órgão deve ser uma string não vazia"
102
+ raise ValueError(msg)
103
+
104
+ LoginPage(driver, wait).fazer_login(usuario, senha, orgao)
105
+ self._page = HomePage(driver, wait)
106
+ self._page.fechar_modal_novidades()
107
+
108
+ def trocar_unidade(self, unidade: str):
109
+ driver, wait = self._get_driver_e_espera()
110
+
111
+ if not isinstance(unidade, str):
112
+ msg = "Trocando unidade: unidade deve ser uma string"
113
+ raise TypeError(msg) # pyright:ignore[reportUnreachable]
114
+ if not unidade:
115
+ msg = "Trocando unidade: unidade deve ser uma string não vazia"
116
+ raise ValueError(msg)
117
+
118
+ self.page.navbar.trocar_unidade(unidade)
119
+ self._page = HomePage(driver, wait)
120
+
121
+ def buscar_processo(self, processo: str) -> bool:
122
+ driver, wait = self._get_driver_e_espera()
123
+
124
+ encontrado = self.page.navbar.buscar_processo(processo)
125
+ if encontrado:
126
+ self._page = ProcessoPage(driver, wait)
127
+ return encontrado
128
+
129
+ def ler_processos_recebidos(self) -> pd.DataFrame:
130
+ if not isinstance(self.page, HomePage):
131
+ msg = "página atual não é a página de controle de processos"
132
+ raise InvalidPageException(msg)
133
+
134
+ r = self.page.ler_tabela_recebidos()
135
+ if not r.empty:
136
+ r = (
137
+ r.drop(columns=["Unnamed: 0", "Recebidos", "Recebidos.2"])
138
+ .rename(columns={"Recebidos.1": "Processo"})
139
+ .drop_duplicates(ignore_index=True)
140
+ )
141
+
142
+ return r
143
+
144
+ def ler_processos_gerados(self) -> pd.DataFrame:
145
+ if not isinstance(self.page, HomePage):
146
+ msg = "página atual não é a página de controle de processos"
147
+ raise InvalidPageException(msg)
148
+
149
+ g = self.page.ler_tabela_gerados()
150
+ if not g.empty:
151
+ g = (
152
+ g.drop(columns=["Unnamed: 0", "Gerados", "Gerados.2"])
153
+ .rename(columns={"Gerados.1": "Processo"})
154
+ .drop_duplicates(ignore_index=True)
155
+ )
156
+
157
+ return g
158
+
159
+ def ler_processos_unidade(self) -> pd.DataFrame:
160
+ """Retorna os processos recebidos e gerados em um DF."""
161
+ r = self.ler_processos_recebidos()
162
+ g = self.ler_processos_gerados()
163
+ return pd.concat([r, g], ignore_index=True)
164
+
165
+ def ler_status(self) -> str | list[str]:
166
+ if not isinstance(self.page, ProcessoPage):
167
+ msg = "página atual não é a página de processo"
168
+ raise InvalidPageException(msg)
169
+ return self.page.ler_status()
170
+
171
+ def ler_marcadores(self) -> list[str]:
172
+ if not isinstance(self.page, ProcessoPage):
173
+ msg = "página atual não é a página de processo"
174
+ raise InvalidPageException(msg)
175
+ return self.page.ler_marcadores()
176
+
177
+ def ler_prazo(self) -> dt.date | None:
178
+ if not isinstance(self.page, ProcessoPage):
179
+ msg = "página atual não é a página de processo"
180
+ raise InvalidPageException(msg)
181
+ return self.page.ler_prazo()
182
+
183
+ def ler_andamentos(self) -> pd.DataFrame:
184
+ if not isinstance(self.page, ProcessoPage):
185
+ msg = "página atual não é a página de processo"
186
+ raise InvalidPageException(msg)
187
+ return self.page.ler_andamentos()
188
+
189
+ def mover_documentos(self, nome: str, destino: str, motivo: str) -> None:
190
+ if not isinstance(self.page, ProcessoPage):
191
+ msg = "página atual não é a página de processo"
192
+ raise InvalidPageException(msg)
193
+ if not isinstance(nome, str):
194
+ msg = "nome deve ser uma string"
195
+ raise TypeError(msg) # pyright: ignore[reportUnreachable]
196
+ if not nome:
197
+ msg = "nome deve ser uma string não vazia"
198
+ raise ValueError(msg)
199
+ if not isinstance(destino, str):
200
+ msg = "destino deve ser uma string"
201
+ raise TypeError(msg) # pyright: ignore[reportUnreachable]
202
+ if not destino:
203
+ msg = "destino deve ser uma string não vazia"
204
+ raise ValueError(msg)
205
+ if not isinstance(motivo, str):
206
+ msg = "motivo deve ser uma string"
207
+ raise TypeError(msg) # pyright: ignore[reportUnreachable]
208
+ if not motivo:
209
+ msg = "motivo deve ser uma string não vazia"
210
+ raise ValueError(msg)
211
+
212
+ self.page.mover_documentos(nome, destino, motivo)
213
+
214
+ def concluir_processo(self) -> None:
215
+ if not isinstance(self.page, ProcessoPage):
216
+ msg = "página atual não é a página de processo"
217
+ raise InvalidPageException(msg)
218
+ self.page.concluir_processo()
219
+
220
+ def anexar_processo(self, processo: str) -> None:
221
+ if not isinstance(self.page, ProcessoPage):
222
+ msg = "página atual não é a página de processo"
223
+ raise InvalidPageException(msg)
224
+ self.page.anexar_processo(processo)
225
+
226
+ def conceder_vistas(
227
+ self,
228
+ email: str,
229
+ tipo: Literal["acompanhamento", "disponibilizacao"],
230
+ motivo: str,
231
+ dias: int,
232
+ token: str,
233
+ ) -> None:
234
+ if not isinstance(self.page, ProcessoPage):
235
+ msg = "página atual não é a página de processo"
236
+ raise InvalidPageException(msg)
237
+ if not isinstance(email, str):
238
+ msg = "email deve ser uma string"
239
+ raise TypeError(msg) # pyright: ignore[reportUnreachable]
240
+ if not email:
241
+ msg = "email deve ser uma string não vazia"
242
+ raise ValueError(msg)
243
+ if not isinstance(motivo, str):
244
+ msg = "motivo deve ser uma string"
245
+ raise TypeError(msg) # pyright: ignore[reportUnreachable]
246
+ if not motivo:
247
+ msg = "motivo deve ser uma string não vazia"
248
+ raise ValueError(msg)
249
+ if not isinstance(dias, int):
250
+ msg = "dias deve ser um int"
251
+ raise TypeError(msg) # pyright: ignore[reportUnreachable]
252
+ if not dias:
253
+ msg = "dias deve ser maior que zero"
254
+ raise ValueError(msg)
255
+ if not isinstance(token, str):
256
+ msg = "token deve ser uma string"
257
+ raise TypeError(msg) # pyright: ignore[reportUnreachable]
258
+ if not token:
259
+ msg = "token deve ser uma string não vazia"
260
+ raise ValueError(msg)
261
+
262
+ self.page.conceder_vistas(email, tipo, motivo, str(dias), token)
@@ -0,0 +1,96 @@
1
+ from typing import final
2
+
3
+ from selenium.webdriver.common.by import By
4
+
5
+
6
+ @final
7
+ class Locators:
8
+ # Login
9
+ InputUsuario = By.ID, "txtUsuario"
10
+ InputSenha = By.ID, "pwdSenha"
11
+ SelOrgao = By.ID, "selOrgao"
12
+ BtnLogin = By.ID, "sbmAcessar"
13
+
14
+ # Geral
15
+ FecharModalNovidade = By.XPATH, "//*[@title='Fechar janela (ESC)']"
16
+ CxUnidade = By.CSS_SELECTOR, "#divInfraBarraSistemaPadrao #lnkInfraUnidade"
17
+ LinhaUnidade = By.XPATH, "//td[contains(text(), '{}')]"
18
+ BuscaRapida = By.ID, "txtPesquisaRapida"
19
+ ProcessoNaoEncontrado = By.CSS_SELECTOR, "#conteudo > div"
20
+
21
+ # Controle de processos
22
+ TblRecebidos = By.ID, "tblProcessosRecebidos"
23
+ SelPagRecebidos = By.ID, "selRecebidosPaginacaoSuperior"
24
+ ProxPagRecebidos = By.ID, "lnkRecebidosProximaPaginaSuperior"
25
+ TblGerados = By.ID, "tblProcessosGerados"
26
+ SelPagGerados = By.ID, "selGeradosPaginacaoSuperior"
27
+ ProxPagGerados = By.ID, "lnkGeradosProximaPaginaSuperior"
28
+
29
+ # Processo
30
+ IframeArvore = By.ID, "ifrArvore"
31
+ IframeConteudo = By.ID, "ifrConteudoVisualizacao"
32
+ IframeVisualizacao = By.ID, "ifrVisualizacao"
33
+ DivStatus = By.ID, "divArvoreInformacao"
34
+ UnidadesStatus = By.TAG_NAME, "a"
35
+ Marcadores = By.XPATH, "//*[contains(@title, 'Marcador')]"
36
+ Prazo = By.XPATH, '//*[contains(@title, "Controle de Prazo:")]'
37
+ BtnAndamentos = By.CSS_SELECTOR, "#divConsultarAndamento > a"
38
+ TipoHistorico = By.ID, "ancTipoHistorico"
39
+ TblAndamentos = By.ID, "tblHistorico"
40
+ SelPagAndamentos = By.ID, "selInfraPaginacaoSuperior"
41
+ ProxPagAndamentos = By.ID, "lnkInfraProximaPaginaSuperior"
42
+ DivArvore = By.ID, "divArvore"
43
+ Documentos = By.XPATH, "//*/a[@target='ifrConteudoVisualizacao']"
44
+ Documento = By.LINK_TEXT, "{}"
45
+ DivArvoreInfo = By.ID, "divArvoreInformacao"
46
+ # Ações
47
+ AcaoMover = By.XPATH, "//*[@title='Mover Documento para outro Processo']"
48
+ AcaoConcluir = By.XPATH, "//*[@title='Concluir Processo']"
49
+ AcaoAnexar = By.XPATH, "//*[@title='Anexar Processo']"
50
+ AcaoVistas = (
51
+ By.XPATH,
52
+ "//*[@title='Gerenciar Disponibilizações de Acesso Externo']",
53
+ )
54
+ AcaoMarcadores = By.XPATH, "//*[@title='Gerenciar Marcador']"
55
+ # Mover Documento
56
+ ProcessoDestino = By.ID, "txtProcedimentoDestino"
57
+ BtnPesquisar = By.ID, "btnPesquisar"
58
+ InputMotivoMover = By.ID, "txaMotivo"
59
+ BtnMover = By.ID, "sbmMover"
60
+ # Concluir Processo
61
+ BtnSalvarConclusao = By.ID, "sbmSalvar"
62
+ # Anexar Processo
63
+ InputAnexar = By.ID, "txtProtocolo"
64
+ BtnPesquisar = By.ID, "btnPesquisar"
65
+ BtnAnexar = By.ID, "sbmAnexar"
66
+ # Vistas
67
+ InputEmailUnidade = By.ID, "selEmailUnidade"
68
+ FiltroUsuarioExterno = By.ID, "lblUsuariosExternos"
69
+ InputDestinatario = By.ID, "txtDestinatario"
70
+ AutocompleteDestinatario = By.ID, "divInfraAjaxtxtDestinatario"
71
+ InputEmailDestinatario = By.ID, "txtEmailDestinatario"
72
+ InputMotivoVistas = By.ID, "txaMotivo"
73
+ OptVistasIntegral = By.ID, "lblIntegral"
74
+ OptVistasParcial = By.ID, "lblParcial"
75
+ PermitirInclusaoDocs = By.ID, "lblSinInclusao"
76
+ SelecionarDocs = By.ID, "imgLupaSeries"
77
+ SelecionarProtocolos = By.ID, "imgLupaProtocolos"
78
+ IframeModal = By.NAME, "modal-frame"
79
+ FecharModalDocs = By.XPATH, "//*[@title='Fechar janela (ESC)']"
80
+ SelPagDocs = By.ID, "selInfraPaginacaoSuperior"
81
+ BtnTransportar = (
82
+ By.CSS_SELECTOR,
83
+ "#divInfraBarraComandosSuperior > #btnTransportarSelecao",
84
+ )
85
+ OptDocAnexo = (
86
+ By.CSS_SELECTOR,
87
+ "a#lnkInfraID-1917 ~ div.infraCheckboxDiv > label",
88
+ )
89
+ OptDocManifestacaoTecnica = (
90
+ By.CSS_SELECTOR,
91
+ "a#lnkInfraID-64 ~ div.infraCheckboxDiv > label",
92
+ )
93
+ OptTodosDocs = By.ID, "imgInfraCheck"
94
+ InputDiasVistas = By.ID, "txtDias"
95
+ InputTokenVistas = By.ID, "pwdSenha"
96
+ BtnDisponibilizar = By.ID, "btnDisponibilizar"
@@ -0,0 +1,34 @@
1
+ from selenium.common.exceptions import TimeoutException
2
+ from selenium.webdriver import Chrome
3
+ from selenium.webdriver.support import expected_conditions as EC
4
+ from selenium.webdriver.support.ui import WebDriverWait
5
+
6
+ from artespy.selenium.pages.components.navbar import Navbar
7
+
8
+
9
+ class BasePage:
10
+ def __init__(self, driver: Chrome, wait: WebDriverWait[Chrome]) -> None:
11
+ self.driver: Chrome = driver
12
+ self.wait: WebDriverWait[Chrome] = wait
13
+
14
+ def get_texto_alerta(self, timeout=5) -> str:
15
+ try:
16
+ return (
17
+ WebDriverWait(self.driver, timeout)
18
+ .until(EC.alert_is_present())
19
+ .text
20
+ )
21
+ except TimeoutException:
22
+ return ""
23
+
24
+ def confirmar_alerta(self):
25
+ self.driver.switch_to.alert.accept()
26
+
27
+ def fechar_alerta(self):
28
+ self.driver.switch_to.alert.dismiss()
29
+
30
+
31
+ class BaseLoggedInPage(BasePage):
32
+ def __init__(self, driver: Chrome, wait: WebDriverWait[Chrome]) -> None:
33
+ super().__init__(driver, wait)
34
+ self.navbar: Navbar = Navbar(driver, wait)
@@ -0,0 +1,8 @@
1
+ from selenium.webdriver import Chrome
2
+ from selenium.webdriver.support.ui import WebDriverWait
3
+
4
+
5
+ class BaseComponent:
6
+ def __init__(self, driver: Chrome, wait: WebDriverWait[Chrome]) -> None:
7
+ self.driver: Chrome = driver
8
+ self.wait: WebDriverWait[Chrome] = wait
@@ -0,0 +1,40 @@
1
+ from selenium.common.exceptions import TimeoutException
2
+ from selenium.webdriver import Keys
3
+ from selenium.webdriver.support import expected_conditions as EC
4
+ from selenium.webdriver.support.ui import WebDriverWait
5
+
6
+ from artespy.selenium.locators import Locators as lc
7
+ from artespy.selenium.pages.components.base import BaseComponent
8
+
9
+
10
+ class Navbar(BaseComponent):
11
+ def get_unidade_atual(self) -> str:
12
+ return self.wait.until(EC.element_to_be_clickable(lc.CxUnidade)).text
13
+
14
+ def acessar_pag_unidade(self) -> None:
15
+ self.wait.until(EC.element_to_be_clickable(lc.CxUnidade)).click()
16
+
17
+ def trocar_unidade(self, unidade: str) -> None:
18
+ u = self.get_unidade_atual()
19
+ if u.lower() == unidade.strip().lower():
20
+ return
21
+
22
+ self.acessar_pag_unidade()
23
+
24
+ loc = lc.LinhaUnidade[0], lc.LinhaUnidade[1].format(unidade)
25
+ self.wait.until(EC.element_to_be_clickable(loc)).click()
26
+
27
+ def buscar_processo(self, processo: str) -> bool:
28
+ self.driver.switch_to.default_content()
29
+ busca = self.wait.until(EC.element_to_be_clickable(lc.BuscaRapida))
30
+ busca.send_keys(processo)
31
+ busca.send_keys(Keys.ENTER)
32
+
33
+ try:
34
+ _ = WebDriverWait(self.driver, 5).until(
35
+ EC.visibility_of_element_located(lc.ProcessoNaoEncontrado)
36
+ )
37
+ except TimeoutException: # processo encontrado
38
+ return True
39
+ else:
40
+ return False
@@ -0,0 +1,28 @@
1
+ import pandas as pd
2
+ from selenium.common.exceptions import TimeoutException
3
+ from selenium.webdriver.support import expected_conditions as EC
4
+ from selenium.webdriver.support.ui import WebDriverWait
5
+
6
+ from artespy.selenium.locators import Locators as lc
7
+ from artespy.selenium.pages.base import BaseLoggedInPage
8
+ from artespy.selenium.pages.mixins.table import TableMixin
9
+
10
+
11
+ class HomePage(BaseLoggedInPage, TableMixin):
12
+ def fechar_modal_novidades(self) -> None:
13
+ try:
14
+ WebDriverWait(self.driver, 5).until(
15
+ EC.element_to_be_clickable(lc.FecharModalNovidade)
16
+ ).click()
17
+ except TimeoutException:
18
+ pass # modal ausente
19
+
20
+ def ler_tabela_recebidos(self) -> pd.DataFrame:
21
+ return self.ler_tabela(
22
+ lc.TblRecebidos, lc.SelPagRecebidos, lc.ProxPagRecebidos
23
+ )
24
+
25
+ def ler_tabela_gerados(self) -> pd.DataFrame:
26
+ return self.ler_tabela(
27
+ lc.TblGerados, lc.SelPagGerados, lc.ProxPagGerados
28
+ )
@@ -0,0 +1,47 @@
1
+ from selenium.webdriver.support import expected_conditions as EC
2
+ from selenium.webdriver.support.ui import Select
3
+ from artespy.errors import InvalidCredentialsError, UnknownAlertException
4
+ from artespy.selenium.locators import Locators as lc
5
+ from artespy.selenium.pages.base import BasePage
6
+
7
+
8
+ class LoginPage(BasePage):
9
+ URL: str = "https://sei.sp.gov.br/sip/login.php?sigla_orgao_sistema=GESP&sigla_sistema=SEI&infra_url=L3NlaS8="
10
+
11
+ def acessar(self) -> None:
12
+ self.driver.get(self.URL)
13
+
14
+ def preencher_usuario(self, usuario: str) -> None:
15
+ self.wait.until(EC.element_to_be_clickable(lc.InputUsuario)).send_keys(
16
+ usuario
17
+ )
18
+
19
+ def preencher_senha(self, senha: str) -> None:
20
+ self.wait.until(EC.element_to_be_clickable(lc.InputSenha)).send_keys(
21
+ senha
22
+ )
23
+
24
+ def selecionar_orgao(self, orgao: str) -> None:
25
+ Select(
26
+ self.wait.until(EC.element_to_be_clickable(lc.SelOrgao))
27
+ ).select_by_visible_text(orgao)
28
+
29
+ def clicar_login(self) -> None:
30
+ self.wait.until(EC.element_to_be_clickable(lc.BtnLogin)).click()
31
+
32
+ def fazer_login(self, usuario: str, senha: str, orgao: str) -> None:
33
+ self.acessar()
34
+
35
+ self.preencher_usuario(usuario)
36
+ self.preencher_senha(senha)
37
+ self.selecionar_orgao(orgao)
38
+ self.clicar_login()
39
+
40
+ t = self.get_texto_alerta()
41
+ if t:
42
+ if t.lower() == "usuário ou senha inválida.":
43
+ msg = "usuário, senha ou órgão inválidos"
44
+ raise InvalidCredentialsError(msg)
45
+ else:
46
+ msg = f"alerta com mensagem inesperada: '{t}'"
47
+ raise UnknownAlertException(msg)
@@ -0,0 +1,105 @@
1
+ import re
2
+ from io import StringIO
3
+ from math import ceil
4
+
5
+ import pandas as pd
6
+ from selenium.common.exceptions import NoSuchElementException, TimeoutException
7
+ from selenium.webdriver.common.by import By
8
+ from selenium.webdriver.remote.webelement import WebElement
9
+ from selenium.webdriver.support import expected_conditions as EC
10
+ from selenium.webdriver.support.ui import Select, WebDriverWait
11
+
12
+ from artespy.errors import ElementNotFoundException
13
+ from artespy.selenium.locators import Locators as lc
14
+
15
+
16
+ class TableMixin:
17
+ def get_tabela(self, locator: tuple[str, str]) -> WebElement | None:
18
+ sw = WebDriverWait(self.driver, 5)
19
+ try:
20
+ return sw.until(EC.visibility_of_element_located(locator))
21
+ except TimeoutException:
22
+ # Tabela de processos recebidos/gerados pode não estar presente
23
+ # caso nenhum processo da categoria correspondente esteja na
24
+ # unidade
25
+ if locator == lc.TblRecebidos or locator == lc.TblGerados:
26
+ return None
27
+ msg = f"Tabela '{locator}' não encontrada"
28
+ raise ElementNotFoundException(msg)
29
+
30
+ def calcular_num_pags(self, tbl: WebElement) -> int:
31
+ try:
32
+ caption = tbl.find_element(By.TAG_NAME, "caption")
33
+ except NoSuchElementException:
34
+ return 0
35
+
36
+ txt = caption.text
37
+ if not txt:
38
+ return 0
39
+
40
+ match = re.search(r"\d+", txt)
41
+ qtd_processos = int(match.group(0)) if match else 0
42
+ return ceil(qtd_processos / 100)
43
+
44
+ def ler_tabela(
45
+ self,
46
+ tbl_lc: tuple[str, str],
47
+ sel_lc: tuple[str, str],
48
+ btn_lc: tuple[str, str],
49
+ ) -> pd.DataFrame:
50
+ tbl = self.get_tabela(tbl_lc)
51
+ if tbl is None:
52
+ return pd.DataFrame()
53
+
54
+ primeira_pag = pd.read_html(StringIO(tbl.get_attribute("outerHTML")))
55
+ df = primeira_pag[0]
56
+
57
+ pags = self.calcular_num_pags(tbl)
58
+ sw = WebDriverWait(self.driver, 10)
59
+ if pags >= 3:
60
+ sel = Select(sw.until(EC.visibility_of_element_located(sel_lc)))
61
+ for i in range(1, len(sel.options)):
62
+ # Relocalizar o select pra evitar StaleElementReferenceException
63
+ Select(
64
+ sw.until(EC.element_to_be_clickable(sel_lc))
65
+ ).select_by_index(i)
66
+ pag = self.wait.until(EC.visibility_of_element_located(tbl_lc))
67
+ pag_df = pd.read_html(StringIO(pag.get_attribute("outerHTML")))
68
+ df = pd.concat([df, pag_df[0]], ignore_index=True)
69
+ if pags == 2:
70
+ sw.until(EC.element_to_be_clickable(btn_lc)).click()
71
+
72
+ pag = self.wait.until(EC.visibility_of_element_located(tbl_lc))
73
+ pag_df = pd.read_html(StringIO(pag.get_attribute("outerHTML")))
74
+ df = pd.concat([df, pag_df[0]], ignore_index=True)
75
+ if not pags:
76
+ try:
77
+ sel = Select(sw.until(EC.visibility_of_element_located(sel_lc)))
78
+ except TimeoutException:
79
+ pass # tabela não possui 3+ páginas
80
+ else:
81
+ for i in range(1, len(sel.options)):
82
+ # Relocalizar o select pra evitar StaleElementReferenceException
83
+ Select(
84
+ sw.until(EC.element_to_be_clickable(sel_lc))
85
+ ).select_by_index(i)
86
+ pag = self.wait.until(
87
+ EC.visibility_of_element_located(tbl_lc)
88
+ )
89
+ pag_df = pd.read_html(
90
+ StringIO(pag.get_attribute("outerHTML"))
91
+ )
92
+ df = pd.concat([df, pag_df[0]], ignore_index=True)
93
+
94
+ return df
95
+
96
+ try:
97
+ sw.until(EC.element_to_be_clickable(btn_lc)).click()
98
+ except TimeoutException:
99
+ pass # tabela não possui 2 páginas
100
+ else:
101
+ pag = self.wait.until(EC.visibility_of_element_located(tbl_lc))
102
+ pag_df = pd.read_html(StringIO(pag.get_attribute("outerHTML")))
103
+ df = pd.concat([df, pag_df[0]], ignore_index=True)
104
+
105
+ return df
@@ -0,0 +1,378 @@
1
+ import datetime as dt
2
+ from logging import getLogger
3
+ import re
4
+ from typing import Literal
5
+
6
+ import pandas as pd
7
+ from selenium.common.exceptions import TimeoutException
8
+ from selenium.webdriver import Chrome, Keys
9
+ from selenium.webdriver.remote.webelement import WebElement
10
+ from selenium.webdriver.support import expected_conditions as EC
11
+ from selenium.webdriver.support.ui import Select, WebDriverWait
12
+
13
+ from artespy.errors import (
14
+ ElementNotFoundException,
15
+ HandlerException,
16
+ SeiException,
17
+ UnknownAlertException,
18
+ )
19
+ from artespy.selenium.locators import Locators as lc
20
+ from artespy.selenium.pages.base import BaseLoggedInPage
21
+ from artespy.selenium.pages.mixins.table import TableMixin
22
+
23
+ logger = getLogger(__name__)
24
+
25
+
26
+ class ProcessoPage(BaseLoggedInPage, TableMixin):
27
+ def __init__(self, driver: Chrome, wait: WebDriverWait[Chrome]) -> None:
28
+ super().__init__(driver, wait)
29
+ self.sw: WebDriverWait[Chrome] = WebDriverWait(driver, 5)
30
+
31
+ def acessar_iframe_arvore(self) -> None:
32
+ self.driver.switch_to.default_content()
33
+ _ = self.wait.until(
34
+ EC.frame_to_be_available_and_switch_to_it(lc.IframeArvore)
35
+ )
36
+
37
+ def acessar_iframe_conteudo(self) -> None:
38
+ self.driver.switch_to.default_content()
39
+ _ = self.wait.until(
40
+ EC.frame_to_be_available_and_switch_to_it(lc.IframeConteudo)
41
+ )
42
+
43
+ def acessar_iframe_visualizacao(self) -> None:
44
+ self.acessar_iframe_conteudo()
45
+ _ = self.wait.until(
46
+ EC.frame_to_be_available_and_switch_to_it(lc.IframeVisualizacao)
47
+ )
48
+
49
+ def acessar_iframe_modal(self) -> None:
50
+ self.driver.switch_to.default_content()
51
+ _ = self.wait.until(
52
+ EC.frame_to_be_available_and_switch_to_it(lc.IframeModal)
53
+ )
54
+
55
+ def acessar_acao(self, loc: tuple[str, str]) -> None:
56
+ self.acessar_iframe_conteudo()
57
+ try:
58
+ self.wait.until(EC.element_to_be_clickable(loc)).click()
59
+ except TimeoutException:
60
+ msg = "timeout ao localizar ação"
61
+ raise ElementNotFoundException(msg)
62
+
63
+ def acessar_acao_mover(self) -> None:
64
+ self.acessar_acao(lc.AcaoMover)
65
+
66
+ def acessar_acao_concluir(self) -> None:
67
+ self.acessar_acao(lc.AcaoConcluir)
68
+
69
+ def acessar_acao_anexar(self) -> None:
70
+ self.acessar_acao(lc.AcaoAnexar)
71
+
72
+ def acessar_acao_vistas(self) -> None:
73
+ self.acessar_acao(lc.AcaoVistas)
74
+
75
+ def ler_status(self) -> str | list[str]:
76
+ self.acessar_iframe_visualizacao()
77
+
78
+ div_status = self.wait.until(
79
+ EC.visibility_of_element_located(lc.DivStatus)
80
+ )
81
+ txt = div_status.text
82
+
83
+ if not txt:
84
+ return ""
85
+
86
+ status: str | list[str] = ""
87
+ if txt.startswith("Processo aberto somente"):
88
+ u = div_status.find_element(
89
+ lc.UnidadesStatus[0], lc.UnidadesStatus[1]
90
+ )
91
+ status = u.text
92
+ elif txt.startswith("Processo aberto nas"):
93
+ elementos = div_status.find_elements(
94
+ lc.UnidadesStatus[0], lc.UnidadesStatus[1]
95
+ )
96
+ unidades = []
97
+ for e in elementos:
98
+ t = e.text
99
+ match = re.match(r"^\d*$|^\w*\.\w*$", t)
100
+ if match is not None:
101
+ continue
102
+ unidades.append(t)
103
+ status = unidades
104
+ elif txt.startswith("Processo sobrestado"):
105
+ status = "Sobretado"
106
+ elif txt.startswith("Processo anexado"):
107
+ status = "Incorporado"
108
+ elif txt.startswith("Processo não possui"):
109
+ status = "Concluído"
110
+ else:
111
+ msg = "div status começa com um fragmento de texto não registrado"
112
+ raise HandlerException(msg)
113
+
114
+ return status
115
+
116
+ def ler_marcadores(self) -> list[str]:
117
+ self.acessar_iframe_arvore()
118
+
119
+ ms = []
120
+ try:
121
+ elems = self.sw.until(
122
+ EC.visibility_of_all_elements_located(lc.Marcadores)
123
+ )
124
+ except TimeoutException:
125
+ return ms
126
+ for e in elems:
127
+ titulo = e.get_attribute("title")
128
+ if not titulo:
129
+ continue
130
+ m = titulo.split("\n")[1]
131
+ ms.append(m)
132
+
133
+ return ms
134
+
135
+ def ler_prazo(self) -> dt.date | None:
136
+ self.acessar_iframe_arvore()
137
+
138
+ try:
139
+ e = self.sw.until(EC.visibility_of_element_located(lc.Prazo))
140
+ except TimeoutException:
141
+ return None
142
+ t = e.get_attribute("title")
143
+ if t is None:
144
+ return None
145
+ return dt.datetime.strptime(t.split(" ")[3], "%d/%m/%Y").date()
146
+
147
+ def visualizar_andamentos(self) -> None:
148
+ self.acessar_iframe_arvore()
149
+ self.wait.until(EC.element_to_be_clickable(lc.BtnAndamentos)).click()
150
+ self.acessar_iframe_visualizacao()
151
+
152
+ tipo = self.wait.until(EC.element_to_be_clickable(lc.TipoHistorico))
153
+ if tipo.text == "Ver histórico completo":
154
+ tipo.click()
155
+
156
+ def retornar_andamentos(self) -> pd.DataFrame:
157
+ self.acessar_iframe_visualizacao()
158
+ andamentos = self.ler_tabela(
159
+ lc.TblAndamentos, lc.SelPagAndamentos, lc.ProxPagAndamentos
160
+ )
161
+ andamentos["Data/Hora"] = pd.to_datetime(
162
+ andamentos["Data/Hora"], format="%d/%m/%Y %H:%M"
163
+ )
164
+ df = andamentos.drop_duplicates(ignore_index=True).sort_values(
165
+ by="Data/Hora", ascending=False
166
+ )
167
+ return df
168
+
169
+ def ler_andamentos(self) -> pd.DataFrame:
170
+ self.visualizar_andamentos()
171
+ return self.retornar_andamentos()
172
+
173
+ def get_documentos(self) -> list[WebElement]:
174
+ self.acessar_iframe_arvore()
175
+ return self.wait.until(
176
+ EC.visibility_of_all_elements_located(lc.Documentos)
177
+ )
178
+
179
+ def get_nomes_documentos(self) -> list[str]:
180
+ elems = self.get_documentos()
181
+ docs = []
182
+ for e in elems:
183
+ docs.append(e.text)
184
+ return docs
185
+
186
+ def acessar_documento(self, nome: str) -> None:
187
+ self.acessar_iframe_arvore()
188
+ loc = lc.Documento[0], lc.Documento[1].format(nome)
189
+ self.sw.until(EC.element_to_be_clickable(loc)).click()
190
+
191
+ def verificar_presenca_documento(self) -> bool:
192
+ self.acessar_iframe_visualizacao()
193
+ div_info = self.wait.until(
194
+ EC.visibility_of_element_located(lc.DivArvoreInfo)
195
+ )
196
+ if "Documento movido" in div_info.text:
197
+ return False
198
+ return True
199
+
200
+ def mover_documentos(self, nome: str, destino: str, motivo: str) -> None:
201
+ docs = self.get_nomes_documentos()
202
+ for d in docs:
203
+ if nome not in d:
204
+ continue
205
+ # Recibo não pode ser movido
206
+ if "Recibo Eletrônico" in d:
207
+ continue
208
+ self.acessar_documento(d)
209
+ presente = self.verificar_presenca_documento()
210
+ if not presente:
211
+ continue
212
+
213
+ self.acessar_acao_mover()
214
+ self.acessar_iframe_visualizacao()
215
+
216
+ self.wait.until(
217
+ EC.element_to_be_clickable(lc.ProcessoDestino)
218
+ ).send_keys(destino)
219
+ self.wait.until(EC.element_to_be_clickable(lc.BtnPesquisar)).click()
220
+
221
+ t = self.get_texto_alerta()
222
+ if t:
223
+ if t == "Processo não encontrado.":
224
+ self.confirmar_alerta()
225
+ msg = f"processo destino ({destino}) não encontrado"
226
+ raise SeiException(msg)
227
+ else:
228
+ self.fechar_alerta()
229
+ msg = f"alerta com mensagem inesperada: '{t}'"
230
+ raise UnknownAlertException(msg)
231
+ self.wait.until(
232
+ EC.element_to_be_clickable(lc.InputMotivoMover)
233
+ ).send_keys(motivo)
234
+ self.wait.until(EC.element_to_be_clickable(lc.BtnMover)).click()
235
+ logger.info(f"documento '{nome}' movido para {destino}")
236
+
237
+ def concluir_processo(self) -> None:
238
+ self.acessar_acao_concluir()
239
+ self.acessar_iframe_visualizacao()
240
+ self.wait.until(
241
+ EC.element_to_be_clickable(lc.BtnSalvarConclusao)
242
+ ).click()
243
+
244
+ def anexar_processo(self, processo: str) -> None:
245
+ self.acessar_acao_anexar()
246
+ self.acessar_iframe_visualizacao()
247
+ self.wait.until(EC.element_to_be_clickable(lc.InputAnexar)).send_keys(
248
+ processo
249
+ )
250
+ self.wait.until(EC.element_to_be_clickable(lc.BtnPesquisar)).click()
251
+ t = self.get_texto_alerta()
252
+ if t:
253
+ if t == "Processo não encontrado.":
254
+ self.confirmar_alerta()
255
+ msg = f"processo a ser anexado ({processo}) não encontrado"
256
+ raise SeiException(msg)
257
+ else:
258
+ self.fechar_alerta()
259
+ msg = f"alerta com mensagem inesperada: '{t}'"
260
+ raise UnknownAlertException(msg)
261
+ self.wait.until(EC.element_to_be_clickable(lc.BtnAnexar)).click()
262
+
263
+ def preencher_destinatario(self, email: str) -> None:
264
+ self.wait.until(
265
+ EC.element_to_be_clickable(lc.FiltroUsuarioExterno)
266
+ ).click()
267
+ destinatario = self.wait.until(
268
+ EC.element_to_be_clickable(lc.InputDestinatario)
269
+ )
270
+ destinatario.send_keys(email)
271
+ try:
272
+ _ = self.wait.until(
273
+ EC.visibility_of_element_located(lc.AutocompleteDestinatario)
274
+ )
275
+ except TimeoutException:
276
+ self.wait.until(
277
+ EC.element_to_be_clickable(lc.InputEmailDestinatario)
278
+ ).send_keys(email)
279
+ else:
280
+ destinatario.send_keys(Keys.ARROW_DOWN)
281
+ destinatario.send_keys(Keys.ENTER)
282
+
283
+ def preencher_motivo_vistas(self, motivo: str) -> None:
284
+ self.wait.until(
285
+ EC.element_to_be_clickable(lc.InputMotivoVistas)
286
+ ).send_keys(motivo)
287
+
288
+ def preencher_opcoes_acompanhamento(self) -> None:
289
+ self.wait.until(
290
+ EC.element_to_be_clickable(lc.OptVistasIntegral)
291
+ ).click()
292
+ self.wait.until(
293
+ EC.element_to_be_clickable(lc.PermitirInclusaoDocs)
294
+ ).click()
295
+ self.wait.until(EC.element_to_be_clickable(lc.SelecionarDocs)).click()
296
+
297
+ self.acessar_iframe_modal()
298
+
299
+ self.wait.until(EC.element_to_be_clickable(lc.OptDocAnexo)).click()
300
+ self.wait.until(EC.element_to_be_clickable(lc.BtnTransportar)).click()
301
+
302
+ sel_pag = Select(
303
+ self.wait.until(EC.element_to_be_clickable(lc.SelPagDocs))
304
+ )
305
+ sel_pag.select_by_index(34)
306
+ self.wait.until(
307
+ EC.element_to_be_clickable(lc.OptDocManifestacaoTecnica)
308
+ ).click()
309
+ self.wait.until(EC.element_to_be_clickable(lc.BtnTransportar)).click()
310
+
311
+ sel_pag = Select( # Evitar StaleElementReference
312
+ self.wait.until(EC.element_to_be_clickable(lc.SelPagDocs))
313
+ )
314
+ sel_pag.select_by_index(0)
315
+
316
+ self.driver.switch_to.default_content()
317
+ self.wait.until(EC.element_to_be_clickable(lc.FecharModalDocs)).click()
318
+
319
+ def preencher_opcoes_disponibilizacao(self) -> None:
320
+ self.wait.until(EC.element_to_be_clickable(lc.OptVistasParcial)).click()
321
+ self.wait.until(
322
+ EC.element_to_be_clickable(lc.SelecionarProtocolos)
323
+ ).click()
324
+
325
+ self.acessar_iframe_modal()
326
+
327
+ self.wait.until(EC.element_to_be_clickable(lc.OptTodosDocs)).click()
328
+ self.wait.until(EC.element_to_be_clickable(lc.BtnTransportar)).click()
329
+
330
+ self.driver.switch_to.default_content()
331
+ self.wait.until(EC.element_to_be_clickable(lc.FecharModalDocs)).click()
332
+
333
+ def preencher_opcoes_cedoc(self) -> None:
334
+ self.wait.until(
335
+ EC.element_to_be_clickable(lc.OptVistasIntegral)
336
+ ).click()
337
+
338
+ def conceder_vistas(
339
+ self,
340
+ email: str,
341
+ tipo: Literal["acompanhamento", "disponibilizacao"],
342
+ motivo: str,
343
+ dias: str,
344
+ token: str,
345
+ ) -> None:
346
+ self.acessar_acao_vistas()
347
+ self.acessar_iframe_visualizacao()
348
+
349
+ Select(
350
+ self.wait.until(EC.element_to_be_clickable(lc.InputEmailUnidade))
351
+ ).select_by_index(2)
352
+
353
+ self.preencher_destinatario(email)
354
+ self.preencher_motivo_vistas(motivo)
355
+ if tipo == "acompanhamento":
356
+ if (
357
+ "cedoc" in email
358
+ or "protocolo" in email
359
+ or "regulatorio" in email
360
+ or "adm.suref" in email
361
+ ):
362
+ self.preencher_opcoes_cedoc()
363
+ else:
364
+ self.preencher_opcoes_acompanhamento()
365
+ elif tipo == "disponibilizacao":
366
+ self.preencher_opcoes_disponibilizacao()
367
+
368
+ self.acessar_iframe_visualizacao()
369
+ self.wait.until(
370
+ EC.element_to_be_clickable(lc.InputDiasVistas)
371
+ ).send_keys(dias)
372
+ self.wait.until(
373
+ EC.element_to_be_clickable(lc.InputTokenVistas)
374
+ ).send_keys(token)
375
+ self.wait.until(
376
+ EC.element_to_be_clickable(lc.BtnDisponibilizar)
377
+ ).click()
378
+ logger.info(f"vistas concedidas para '{email}'")
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: artespy
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Author-email: Leonardo Hossokawa de Oliveira <estagiario.seguros04@artesp.sp.gov.br>
6
+ Requires-Python: >=3.13
7
+ Requires-Dist: lxml>=6.0.2
8
+ Requires-Dist: pandas>=2.3.3
9
+ Requires-Dist: selenium>=4.38.0
10
+ Requires-Dist: webdriver-manager>=4.0.2
@@ -0,0 +1,15 @@
1
+ artespy/__init__.py,sha256=gYDuQkdNUX9hQy4rj1JHZZFa7ynX6ypUaR1bMPHR7es,87
2
+ artespy/errors/__init__.py,sha256=EjFOKtG9Z3Qdzo_LRYiJFzxCF-OIoTnEQvwFg0SyGfQ,444
3
+ artespy/selenium/handler.py,sha256=YdIGJq8ZNaPl9R2pD__-0RnTycma-3EPqLJjpQ5hxdw,10369
4
+ artespy/selenium/locators.py,sha256=zH9-qOexTcOfeE9eZtMP43uyduMU6vYidkD7a3Lt_9Q,3942
5
+ artespy/selenium/pages/base.py,sha256=HnFpwm0323O-zAYx2nOB425ERebhGOGmfXevEdhDp9E,1121
6
+ artespy/selenium/pages/home.py,sha256=Fjwsi-bCIbW2WL0V6jwqPm0V29h8Wg_E-IPBMJHKkBA,1027
7
+ artespy/selenium/pages/login.py,sha256=Kej4gxdp6PJhp-U5QNewcncsPDSl65lJLGd9KqEVsP0,1752
8
+ artespy/selenium/pages/processo.py,sha256=uNiNrXQERk0TpPfghoad1Tos6KH14_QMUMUIzZ8iVHc,13577
9
+ artespy/selenium/pages/components/base.py,sha256=pMUR4074h25hrhdveimvrrUo4NkYGHiKTocqy2ZEJ0k,287
10
+ artespy/selenium/pages/components/navbar.py,sha256=J1GL10wqdepZTkx4oVMuoqI1foqXZSVdhOsM45g7Pdw,1492
11
+ artespy/selenium/pages/mixins/table.py,sha256=me60-IRKeAcfzWf4Lw5FljuY5oXZt71UANCCcnCo1wk,4272
12
+ artespy-0.1.0.dist-info/METADATA,sha256=pzXODMRiVG69BRzigTThU711mgKpbRS1ZBOq1WOJ-7A,323
13
+ artespy-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
14
+ artespy-0.1.0.dist-info/entry_points.txt,sha256=7CEe8vB91bPbabrPYtw6P77JFVvwa-RkUKSWMWOlaiM,41
15
+ artespy-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ artespy = artespy:main