1
1
import os
2
2
import time
3
3
from functools import wraps
4
- import timeout_decorator
5
- from timeout_decorator import TimeoutError
4
+
5
+ import requests
6
6
7
7
from office365 .runtime .auth .user_credential import UserCredential
8
8
from office365 .runtime .client_request_exception import ClientRequestException
11
11
from office365 .sharepoint .folders .folder import Folder
12
12
13
13
14
- def handle_sharepoint_errors (max_retries : int = 5 , delay_seconds : int = 3 , timeout_seconds : int = 120 ):
14
+ def handle_sharepoint_errors (max_retries : int = 5 , delay_seconds : int = 3 ):
15
15
"""
16
16
Decorador para tratar exceções de requisições do SharePoint, com lógica
17
- de nova autenticação, novas tentativas e controle de timeout .
17
+ de nova autenticação e novas tentativas para erros comuns .
18
18
19
19
Args:
20
20
max_retries (int): Número máximo de tentativas para erros recuperáveis.
21
21
delay_seconds (int): Atraso entre as tentativas.
22
- timeout_seconds (int): Tempo máximo de espera para a execução da operação em segundos.
23
22
"""
24
23
25
24
def decorator (func ):
26
25
@wraps (func )
27
26
def wrapper (self , * args , ** kwargs ):
28
27
last_exception = None
29
28
30
- # Envolve a lógica de retentativas em uma função para aplicar o timeout a cada tentativa.
31
- @timeout_decorator .timeout (timeout_seconds , use_signals = False )
32
- def run_operation ():
33
- return func (self , * args , ** kwargs )
34
-
35
29
for attempt in range (max_retries ):
36
30
try :
37
- self .ctx .clear ()
38
- # A chamada com timeout é feita aqui
39
- return run_operation ()
31
+ # A chamada da função original acontece aqui.
32
+ return func (self , * args , ** kwargs )
40
33
41
- except TimeoutError :
42
- # Se a operação exceder o tempo limite, capturamos o erro aqui .
34
+ except requests . exceptions . Timeout as e :
35
+ # Captura a exceção de timeout da biblioteca 'requests' .
43
36
print (
44
- f"Erro: A operação '{ func .__name__ } ' excedeu o tempo limite de { timeout_seconds } segundos. Tentativa { attempt + 1 } /{ max_retries } ." )
45
- last_exception = TimeoutError (
46
- f"A operação '{ func .__name__ } ' excedeu o tempo limite de { timeout_seconds } s." )
47
- # Continua para a próxima tentativa após um delay
37
+ f"Erro: A operação '{ func .__name__ } ' excedeu o tempo limite de { self .timeout } segundos. "
38
+ f"Tentativa { attempt + 1 } /{ max_retries } ..." )
39
+ last_exception = e
48
40
time .sleep (delay_seconds )
49
41
continue
50
42
51
43
except ClientRequestException as e :
52
44
last_exception = e
53
- if e .response .status_code == 429 :
54
- wait_time = 60 * (attempt + 1 )
55
- print (f"Erro 429 (Muitas solicitações) detectado. Aguardando { wait_time } segundos..." )
56
- time .sleep (wait_time )
45
+ if e .response .status_code == 429 : # Too Many Requests
46
+ # O SharePoint pode retornar um header 'Retry-After'
47
+ retry_after = int (e .response .headers .get ("Retry-After" , 60 * (attempt + 1 )))
48
+ print (f"Erro 429 (Muitas solicitações) detectado. Aguardando { retry_after } segundos..." )
49
+ time .sleep (retry_after )
57
50
continue
58
- elif e .response .status_code == 403 :
51
+ elif e .response .status_code == 403 : # Forbidden
59
52
print ("Erro 403 (Proibido) detectado. Tentando relogar..." )
60
53
if not (self .username and self .password ):
61
54
print ("Credenciais não disponíveis para relogin. Abortando." )
62
55
raise e
63
56
64
57
if self .login (self .username , self .password ):
65
58
print ("Relogin bem-sucedido. Tentando a operação novamente." )
66
- # Tenta novamente a operação dentro do mesmo loop
67
59
continue
68
60
else :
69
61
print ("Falha ao relogar. Abortando." )
70
62
raise e
71
- elif e .response .status_code == 503 or "503" in str ( e ):
63
+ elif e .response .status_code in [ 503 , 504 ]: # Service Unavailable / Gateway Timeout
72
64
print (
73
- f"Erro 503 (Serviço Indisponível). Tentativa { attempt + 1 } /{ max_retries } em { delay_seconds } s..." )
65
+ f"Erro { e .response .status_code } (Servidor indisponível/sobrecarregado). "
66
+ f"Tentativa { attempt + 1 } /{ max_retries } em { delay_seconds } s..." )
74
67
time .sleep (delay_seconds )
75
68
continue
76
69
else :
77
- print (f"Erro não recuperável encontrado: { e } " )
70
+ print (f"Erro de cliente não recuperável encontrado: { e } " )
78
71
raise e
79
72
80
73
except Exception as e :
@@ -91,13 +84,30 @@ def run_operation():
91
84
92
85
93
86
class SharepointService :
94
- def __init__ (self , site_url : str ):
87
+ def __init__ (self , site_url : str , timeout_seconds : int = 90 ):
88
+ """
89
+ Inicializa o serviço do SharePoint.
90
+
91
+ Args:
92
+ site_url (str): A URL do site do SharePoint.
93
+ timeout_seconds (int): O tempo em segundos para o timeout de cada requisição.
94
+ """
95
95
self .site_url = site_url
96
96
self .ctx = ClientContext (self .site_url )
97
97
self .username = None
98
98
self .password = None
99
+ self .timeout = timeout_seconds
100
+
101
+ # **SOLUÇÃO**: Adiciona um manipulador de eventos para definir o timeout ANTES de cada requisição.
102
+ self .ctx .pending_request ().beforeExecute += self ._set_request_timeout
103
+
104
+ def _set_request_timeout (self , request_options ):
105
+ """
106
+ Este método é chamado antes de cada requisição para injetar o parâmetro de timeout.
107
+ """
108
+ request_options .timeout = self .timeout
99
109
100
- def login (self , username , password ) :
110
+ def login (self , username : str , password : str ) -> bool :
101
111
"""Autentica no site do SharePoint usando as credenciais fornecidas."""
102
112
print (f"Fazendo login no SharePoint com o usuário { username } ..." )
103
113
self .username = username
@@ -108,7 +118,7 @@ def login(self, username, password):
108
118
self .ctx .execute_query ()
109
119
print ("Login realizado com sucesso." )
110
120
return True
111
- except ClientRequestException as e :
121
+ except ( ClientRequestException , requests . exceptions . Timeout ) as e :
112
122
print (f"Erro ao fazer login: { e } " )
113
123
return False
114
124
@@ -163,19 +173,7 @@ def listar_pastas(self, pasta_pai: Folder | str):
163
173
164
174
@handle_sharepoint_errors ()
165
175
def criar_pasta (self , pasta_pai : Folder | str , nome_pasta : str ):
166
- """
167
- Cria uma nova pasta no Sharepoint, suportando criação de subpastas com "/"
168
-
169
- Args:
170
- pasta_pai: Caminho ou objeto Folder onde a nova pasta será criada
171
- nome_pasta: Nome da nova pasta a ser criada, pode incluir "/" para criar subpastas
172
-
173
- Returns:
174
- O objeto da pasta criada
175
-
176
- Raises:
177
- Exception: Se a pasta pai não for encontrada
178
- """
176
+ """Cria uma nova pasta no Sharepoint, suportando criação de subpastas com "/" """
179
177
if isinstance (pasta_pai , str ):
180
178
pasta = self .obter_pasta (pasta_pai )
181
179
if pasta is None :
@@ -185,33 +183,35 @@ def criar_pasta(self, pasta_pai: Folder | str, nome_pasta: str):
185
183
if "/" in nome_pasta :
186
184
path_parts = nome_pasta .split ("/" )
187
185
current_folder = pasta_pai
188
-
189
186
for part in path_parts :
190
187
if part :
191
188
current_folder = self .criar_pasta (current_folder , part )
192
-
193
189
return current_folder
194
190
else :
195
- subpastas = self .listar_pastas (pasta_pai )
196
- pasta = next ((subpasta for subpasta in subpastas if nome_pasta in str ( subpasta .name ) ), None )
191
+ subpastas = list ( self .listar_pastas (pasta_pai ) )
192
+ pasta = next ((subpasta for subpasta in subpastas if nome_pasta == subpasta .name ), None )
197
193
if pasta is not None :
198
194
return pasta
199
- print ("Criando pasta {0}..." .format (nome_pasta ))
200
- pasta = pasta_pai .folders .add (nome_pasta )
201
- pasta .execute_query ()
195
+ print (f"Criando pasta { nome_pasta } ..." )
196
+ pasta = pasta_pai .folders .add (nome_pasta ).execute_query ()
202
197
return pasta
203
198
204
199
@handle_sharepoint_errors ()
205
200
def baixar_arquivo (self , arquivo_sp : File | str , caminho_download : str ):
206
201
"""Baixa um arquivo do SharePoint para um caminho local."""
202
+ nome_arquivo = ""
207
203
if isinstance (arquivo_sp , str ):
208
- file_to_download = self .ctx .web .get_file_by_server_relative_url (arquivo_sp )
204
+ file_to_download = self .obter_arquivo (arquivo_sp )
205
+ if file_to_download is None :
206
+ raise FileNotFoundError (f"Arquivo '{ arquivo_sp } ' não encontrado no SharePoint." )
207
+ nome_arquivo = file_to_download .name
209
208
else :
210
209
file_to_download = arquivo_sp
210
+ nome_arquivo = arquivo_sp .name
211
211
212
212
with open (caminho_download , "wb" ) as local_file :
213
- file_to_download .download_session (local_file ).execute_query ()
214
- print (f"Arquivo '{ os . path . basename ( str ( arquivo_sp )) } ' baixado para '{ caminho_download } '." )
213
+ file_to_download .download (local_file ).execute_query ()
214
+ print (f"Arquivo '{ nome_arquivo } ' baixado para '{ caminho_download } '." )
215
215
216
216
@handle_sharepoint_errors ()
217
217
def enviar_arquivo (self , pasta_destino : Folder | str , arquivo_local : str , nome_arquivo_sp : str = None ):
@@ -222,14 +222,12 @@ def enviar_arquivo(self, pasta_destino: Folder | str, arquivo_local: str, nome_a
222
222
raise FileNotFoundError (f"A pasta de destino '{ pasta_destino } ' não foi encontrada." )
223
223
pasta_destino = pasta
224
224
225
- if not nome_arquivo_sp :
226
- nome_arquivo_sp = os .path .basename (arquivo_local )
225
+ nome_arquivo_sp = nome_arquivo_sp or os .path .basename (arquivo_local )
227
226
228
227
with open (arquivo_local , 'rb' ) as file_content :
229
- fbytes = file_content .read ()
228
+ print (f"Enviando arquivo '{ nome_arquivo_sp } '..." )
229
+ arquivo = pasta_destino .files .upload (nome_arquivo_sp , file_content ).execute_query ()
230
230
231
- print (f"Enviando arquivo '{ nome_arquivo_sp } '..." )
232
- arquivo = pasta_destino .upload_file (nome_arquivo_sp , fbytes ).execute_query ()
233
231
print (f"Arquivo '{ nome_arquivo_sp } ' enviado com sucesso!" )
234
232
return arquivo
235
233
@@ -242,48 +240,46 @@ def mover_arquivo(self, arquivo_origem: File, pasta_destino: Folder | str):
242
240
raise FileNotFoundError (f"A pasta de destino '{ pasta_destino } ' não foi encontrada." )
243
241
pasta_destino = pasta
244
242
245
- print (f"Movendo '{ arquivo_origem .name } ' para '{ pasta_destino .name } '..." )
246
-
247
- novo_arquivo = arquivo_origem .moveto (pasta_destino , flag = 1 )
248
- novo_arquivo .execute_query ()
249
-
250
- return novo_arquivo
243
+ print (f"Movendo '{ arquivo_origem .name } ' para '{ pasta_destino .serverRelativeUrl } '..." )
244
+ novo_caminho = f"{ pasta_destino .serverRelativeUrl } /{ arquivo_origem .name } "
245
+ arquivo_origem .moveto (novo_caminho , 1 ).execute_query ()
246
+ print ("Arquivo movido com sucesso." )
251
247
252
248
@handle_sharepoint_errors ()
253
- def copiar_arquivo (self , arquivo_origem : File , pasta_destino : Folder | str ):
249
+ def copiar_arquivo (self , arquivo_origem : File , pasta_destino : Folder | str , novo_nome : str = None ):
254
250
"""Copia um arquivo para outra pasta."""
255
251
if isinstance (pasta_destino , str ):
256
252
pasta = self .obter_pasta (pasta_destino )
257
253
if pasta is None :
258
254
raise FileNotFoundError (f"A pasta de destino '{ pasta_destino } ' não foi encontrada." )
259
255
pasta_destino = pasta
260
256
261
- print (f"Copiando '{ arquivo_origem .name } ' para '{ pasta_destino .name } '..." )
262
- novo_arquivo = arquivo_origem .copyto (pasta_destino , True ).execute_query ()
257
+ nome_final = novo_nome or arquivo_origem .name
258
+ print (f"Copiando '{ arquivo_origem .name } ' para '{ pasta_destino .serverRelativeUrl } /{ nome_final } '..." )
259
+ arquivo_origem .copyto (f"{ pasta_destino .serverRelativeUrl } /{ nome_final } " , True ).execute_query ()
263
260
print ("Arquivo copiado com sucesso." )
264
- return novo_arquivo
265
261
266
262
@handle_sharepoint_errors ()
267
- def renomear_arquivo (self , arquivo : File , novo_nome : str ) -> File :
263
+ def renomear_arquivo (self , arquivo : File , novo_nome : str ):
268
264
"""Renomeia um arquivo no SharePoint."""
269
265
print (f"Renomeando '{ arquivo .name } ' para '{ novo_nome } '..." )
270
- novo_arquivo = arquivo .rename (novo_nome )
271
- novo_arquivo .execute_query ()
266
+ arquivo .rename (novo_nome ).execute_query ()
272
267
print ("Arquivo renomeado com sucesso." )
273
- return novo_arquivo
274
268
275
269
@handle_sharepoint_errors ()
276
- def compartilhar_item (self , item : File | Folder , tipo : int ):
277
- resultado = item .share_link ( tipo )
278
- resultado .execute_query ()
270
+ def compartilhar_item (self , item : File | Folder , tipo : int = 0 ):
271
+ """Cria um link de compartilhamento para um item. tipo 0: View, 1: Edit"""
272
+ resultado = item . share_link ( tipo ) .execute_query ()
279
273
return resultado .value .sharingLinkInfo .Url
280
274
281
- def obter_pasta_por_nome (self , pasta_raiz : Folder , nome ):
282
- pastas = list (self .listar_pastas (pasta_raiz ))
283
- pasta_encontrada = next ((pasta for pasta in pastas if nome in pasta .name ), None )
275
+ def obter_pasta_por_nome (self , pasta_raiz : Folder , nome : str ) -> Folder | None :
276
+ """Busca uma subpasta pelo nome exato dentro de uma pasta raiz."""
277
+ pastas = self .listar_pastas (pasta_raiz )
278
+ pasta_encontrada = next ((pasta for pasta in pastas if pasta .name == nome ), None )
284
279
return pasta_encontrada
285
280
286
- def obter_arquivo_por_nome (self , pasta : Folder , nome ):
287
- arquivos = list (self .listar_arquivos (pasta ))
288
- arquivo_encontrado = next ((arquivo for arquivo in arquivos if nome in arquivo .name ), None )
281
+ def obter_arquivo_por_nome (self , pasta : Folder , nome : str ) -> File | None :
282
+ """Busca um arquivo pelo nome exato dentro de uma pasta."""
283
+ arquivos = self .listar_arquivos (pasta )
284
+ arquivo_encontrado = next ((arquivo for arquivo in arquivos if arquivo .name == nome ), None )
289
285
return arquivo_encontrado
0 commit comments