Skip to content

Commit c8f66e0

Browse files
committed
bump version to 0.1.9 and refactor error handling in SharePoint service
1 parent ebbb279 commit c8f66e0

File tree

2 files changed

+79
-84
lines changed

2 files changed

+79
-84
lines changed

pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "office365-service"
3-
version = "0.1.8"
3+
version = "0.1.9"
44
description = "Add your description here"
55
readme = "README.md"
66
authors = [
@@ -10,7 +10,6 @@ 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",
1413
]
1514

1615
[build-system]
Lines changed: 78 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import os
22
import time
33
from functools import wraps
4-
import timeout_decorator
5-
from timeout_decorator import TimeoutError
4+
5+
import requests
66

77
from office365.runtime.auth.user_credential import UserCredential
88
from office365.runtime.client_request_exception import ClientRequestException
@@ -11,70 +11,63 @@
1111
from office365.sharepoint.folders.folder import Folder
1212

1313

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):
1515
"""
1616
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.
1818
1919
Args:
2020
max_retries (int): Número máximo de tentativas para erros recuperáveis.
2121
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.
2322
"""
2423

2524
def decorator(func):
2625
@wraps(func)
2726
def wrapper(self, *args, **kwargs):
2827
last_exception = None
2928

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-
3529
for attempt in range(max_retries):
3630
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)
4033

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'.
4336
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
4840
time.sleep(delay_seconds)
4941
continue
5042

5143
except ClientRequestException as e:
5244
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)
5750
continue
58-
elif e.response.status_code == 403:
51+
elif e.response.status_code == 403: # Forbidden
5952
print("Erro 403 (Proibido) detectado. Tentando relogar...")
6053
if not (self.username and self.password):
6154
print("Credenciais não disponíveis para relogin. Abortando.")
6255
raise e
6356

6457
if self.login(self.username, self.password):
6558
print("Relogin bem-sucedido. Tentando a operação novamente.")
66-
# Tenta novamente a operação dentro do mesmo loop
6759
continue
6860
else:
6961
print("Falha ao relogar. Abortando.")
7062
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
7264
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...")
7467
time.sleep(delay_seconds)
7568
continue
7669
else:
77-
print(f"Erro não recuperável encontrado: {e}")
70+
print(f"Erro de cliente não recuperável encontrado: {e}")
7871
raise e
7972

8073
except Exception as e:
@@ -91,13 +84,30 @@ def run_operation():
9184

9285

9386
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+
"""
9595
self.site_url = site_url
9696
self.ctx = ClientContext(self.site_url)
9797
self.username = None
9898
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
99109

100-
def login(self, username, password):
110+
def login(self, username: str, password: str) -> bool:
101111
"""Autentica no site do SharePoint usando as credenciais fornecidas."""
102112
print(f"Fazendo login no SharePoint com o usuário {username}...")
103113
self.username = username
@@ -108,7 +118,7 @@ def login(self, username, password):
108118
self.ctx.execute_query()
109119
print("Login realizado com sucesso.")
110120
return True
111-
except ClientRequestException as e:
121+
except (ClientRequestException, requests.exceptions.Timeout) as e:
112122
print(f"Erro ao fazer login: {e}")
113123
return False
114124

@@ -163,19 +173,7 @@ def listar_pastas(self, pasta_pai: Folder | str):
163173

164174
@handle_sharepoint_errors()
165175
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 "/" """
179177
if isinstance(pasta_pai, str):
180178
pasta = self.obter_pasta(pasta_pai)
181179
if pasta is None:
@@ -185,33 +183,35 @@ def criar_pasta(self, pasta_pai: Folder | str, nome_pasta: str):
185183
if "/" in nome_pasta:
186184
path_parts = nome_pasta.split("/")
187185
current_folder = pasta_pai
188-
189186
for part in path_parts:
190187
if part:
191188
current_folder = self.criar_pasta(current_folder, part)
192-
193189
return current_folder
194190
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)
197193
if pasta is not None:
198194
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()
202197
return pasta
203198

204199
@handle_sharepoint_errors()
205200
def baixar_arquivo(self, arquivo_sp: File | str, caminho_download: str):
206201
"""Baixa um arquivo do SharePoint para um caminho local."""
202+
nome_arquivo = ""
207203
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
209208
else:
210209
file_to_download = arquivo_sp
210+
nome_arquivo = arquivo_sp.name
211211

212212
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}'.")
215215

216216
@handle_sharepoint_errors()
217217
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
222222
raise FileNotFoundError(f"A pasta de destino '{pasta_destino}' não foi encontrada.")
223223
pasta_destino = pasta
224224

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)
227226

228227
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()
230230

231-
print(f"Enviando arquivo '{nome_arquivo_sp}'...")
232-
arquivo = pasta_destino.upload_file(nome_arquivo_sp, fbytes).execute_query()
233231
print(f"Arquivo '{nome_arquivo_sp}' enviado com sucesso!")
234232
return arquivo
235233

@@ -242,48 +240,46 @@ def mover_arquivo(self, arquivo_origem: File, pasta_destino: Folder | str):
242240
raise FileNotFoundError(f"A pasta de destino '{pasta_destino}' não foi encontrada.")
243241
pasta_destino = pasta
244242

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.")
251247

252248
@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):
254250
"""Copia um arquivo para outra pasta."""
255251
if isinstance(pasta_destino, str):
256252
pasta = self.obter_pasta(pasta_destino)
257253
if pasta is None:
258254
raise FileNotFoundError(f"A pasta de destino '{pasta_destino}' não foi encontrada.")
259255
pasta_destino = pasta
260256

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()
263260
print("Arquivo copiado com sucesso.")
264-
return novo_arquivo
265261

266262
@handle_sharepoint_errors()
267-
def renomear_arquivo(self, arquivo: File, novo_nome: str) -> File:
263+
def renomear_arquivo(self, arquivo: File, novo_nome: str):
268264
"""Renomeia um arquivo no SharePoint."""
269265
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()
272267
print("Arquivo renomeado com sucesso.")
273-
return novo_arquivo
274268

275269
@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()
279273
return resultado.value.sharingLinkInfo.Url
280274

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)
284279
return pasta_encontrada
285280

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)
289285
return arquivo_encontrado

0 commit comments

Comments
 (0)