Skip to content

Commit 27db2ef

Browse files
committed
bump version to 0.1.8 and add timeout handling to SharePoint error decorator
1 parent a4777b3 commit 27db2ef

File tree

2 files changed

+50
-30
lines changed

2 files changed

+50
-30
lines changed

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "office365-service"
3-
version = "0.1.7"
3+
version = "0.1.8"
44
description = "Add your description here"
55
readme = "README.md"
66
authors = [
@@ -10,6 +10,7 @@ requires-python = ">=3.10"
1010
dependencies = [
1111
"hatchling>=1.27.0",
1212
"office365-rest-python-client>=2.6.2",
13+
"timeout-decorator>=0.5.0",
1314
]
1415

1516
[build-system]

src/office365_service/sharepoint_service.py

Lines changed: 48 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import os
22
import time
33
from functools import wraps
4+
import timeout_decorator
5+
from timeout_decorator import TimeoutError
46

57
from office365.runtime.auth.user_credential import UserCredential
68
from office365.runtime.client_request_exception import ClientRequestException
@@ -9,25 +11,49 @@
911
from office365.sharepoint.folders.folder import Folder
1012

1113

12-
def handle_sharepoint_errors(max_retries=5, delay_seconds=3):
14+
def handle_sharepoint_errors(max_retries: int = 5, delay_seconds: int = 3, timeout_seconds: int = 120):
1315
"""
1416
Decorador para tratar exceções de requisições do SharePoint, com lógica
15-
de nova autenticação para erros 403 e novas tentativas para erros 503.
17+
de nova autenticação, novas tentativas e controle de timeout.
18+
19+
Args:
20+
max_retries (int): Número máximo de tentativas para erros recuperáveis.
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.
1623
"""
1724

1825
def decorator(func):
1926
@wraps(func)
2027
def wrapper(self, *args, **kwargs):
2128
last_exception = None
29+
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+
2235
for attempt in range(max_retries):
2336
try:
2437
self.ctx.clear()
25-
return func(self, *args, **kwargs)
38+
# A chamada com timeout é feita aqui
39+
return run_operation()
40+
41+
except TimeoutError:
42+
# Se a operação exceder o tempo limite, capturamos o erro aqui.
43+
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
48+
time.sleep(delay_seconds)
49+
continue
50+
2651
except ClientRequestException as e:
2752
last_exception = e
2853
if e.response.status_code == 429:
29-
print(f"Erro 429 (Muitas solicitações) detectado. Aguardando 60 segundos...")
30-
time.sleep(60 * (attempt+1))
54+
wait_time = 60 * (attempt + 1)
55+
print(f"Erro 429 (Muitas solicitações) detectado. Aguardando {wait_time} segundos...")
56+
time.sleep(wait_time)
3157
continue
3258
elif e.response.status_code == 403:
3359
print("Erro 403 (Proibido) detectado. Tentando relogar...")
@@ -37,31 +63,25 @@ def wrapper(self, *args, **kwargs):
3763

3864
if self.login(self.username, self.password):
3965
print("Relogin bem-sucedido. Tentando a operação novamente.")
40-
try:
41-
return func(self, *args, **kwargs)
42-
except ClientRequestException as e2:
43-
print(f"A operação falhou mesmo após o relogin: {e2}")
44-
raise e2
66+
# Tenta novamente a operação dentro do mesmo loop
67+
continue
4568
else:
4669
print("Falha ao relogar. Abortando.")
4770
raise e
48-
# --- Erro de servidor (temporário) ---
4971
elif e.response.status_code == 503:
5072
print(
5173
f"Erro 503 (Serviço Indisponível). Tentativa {attempt + 1}/{max_retries} em {delay_seconds}s...")
5274
time.sleep(delay_seconds)
53-
continue # Próxima iteração do loop de retentativa
54-
# --- Outros erros de cliente/servidor ---
75+
continue
5576
else:
5677
print(f"Erro não recuperável encontrado: {e}")
5778
raise e
79+
5880
except Exception as e:
59-
# Captura outras exceções (ex: problemas de rede)
6081
print(f"Uma exceção inesperada ocorreu: {e}. Tentando novamente em {delay_seconds}s...")
6182
last_exception = e
6283
time.sleep(delay_seconds)
6384

64-
# Se todas as tentativas falharem, lança a última exceção capturada
6585
print(f"A operação '{func.__name__}' falhou após {max_retries} tentativas.")
6686
raise last_exception
6787

@@ -71,7 +91,6 @@ def wrapper(self, *args, **kwargs):
7191

7292

7393
class SharepointService:
74-
7594
def __init__(self, site_url: str):
7695
self.site_url = site_url
7796
self.ctx = ClientContext(self.site_url)
@@ -105,18 +124,6 @@ def obter_pasta(self, caminho_pasta: str) -> Folder | None:
105124
return None
106125
raise e
107126

108-
@handle_sharepoint_errors()
109-
def obter_arquivo(self, caminho_arquivo: str) -> File | None:
110-
"""Obtém um objeto File a partir do seu caminho relativo no servidor."""
111-
try:
112-
file = self.ctx.web.get_file_by_server_relative_url(caminho_arquivo)
113-
file.get().execute_query()
114-
return file
115-
except ClientRequestException as e:
116-
if e.response.status_code == 404:
117-
return None
118-
raise e
119-
120127
@handle_sharepoint_errors()
121128
def listar_arquivos(self, pasta_alvo: Folder | str):
122129
"""Lista todos os arquivos dentro de uma pasta específica."""
@@ -129,6 +136,18 @@ def listar_arquivos(self, pasta_alvo: Folder | str):
129136
files.expand(["ModifiedBy"]).get().execute_query()
130137
return files
131138

139+
@handle_sharepoint_errors()
140+
def obter_arquivo(self, caminho_arquivo: str) -> File | None:
141+
"""Obtém um objeto File a partir do seu caminho relativo no servidor."""
142+
try:
143+
file = self.ctx.web.get_file_by_server_relative_url(caminho_arquivo)
144+
file.get().execute_query()
145+
return file
146+
except ClientRequestException as e:
147+
if e.response.status_code == 404:
148+
return None
149+
raise e
150+
132151
@handle_sharepoint_errors()
133152
def listar_pastas(self, pasta_pai: Folder | str):
134153
"""Lista todas as subpastas dentro de uma pasta pai."""
@@ -267,4 +286,4 @@ def obter_pasta_por_nome(self, pasta_raiz: Folder, nome):
267286
def obter_arquivo_por_nome(self, pasta: Folder, nome):
268287
arquivos = list(self.listar_arquivos(pasta))
269288
arquivo_encontrado = next((arquivo for arquivo in arquivos if nome in arquivo.name), None)
270-
return arquivo_encontrado
289+
return arquivo_encontrado

0 commit comments

Comments
 (0)