sharexlsx-py 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.
- sharexlsx_py-0.1.0/PKG-INFO +20 -0
- sharexlsx_py-0.1.0/README.md +10 -0
- sharexlsx_py-0.1.0/pyproject.toml +20 -0
- sharexlsx_py-0.1.0/setup.cfg +4 -0
- sharexlsx_py-0.1.0/sharexlsx_py/__init__.py +5 -0
- sharexlsx_py-0.1.0/sharexlsx_py/auth.py +49 -0
- sharexlsx_py-0.1.0/sharexlsx_py/client.py +111 -0
- sharexlsx_py-0.1.0/sharexlsx_py/utils.py +19 -0
- sharexlsx_py-0.1.0/sharexlsx_py.egg-info/PKG-INFO +20 -0
- sharexlsx_py-0.1.0/sharexlsx_py.egg-info/SOURCES.txt +11 -0
- sharexlsx_py-0.1.0/sharexlsx_py.egg-info/dependency_links.txt +1 -0
- sharexlsx_py-0.1.0/sharexlsx_py.egg-info/requires.txt +2 -0
- sharexlsx_py-0.1.0/sharexlsx_py.egg-info/top_level.txt +2 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sharexlsx_py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Librería para consultar y leer archivos Excel de SharePoint usando MS Graph API
|
|
5
|
+
Author-email: Mario Eduardo <mfernandez@sumimsa.com.mx>
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: msal>=1.20.0
|
|
9
|
+
Requires-Dist: httpx>=0.28.1
|
|
10
|
+
|
|
11
|
+
# sharexlsx_py
|
|
12
|
+
|
|
13
|
+
Librería asíncrona de Python para consultar y leer archivos Excel (`.xlsx`) alojados en Microsoft SharePoint utilizando Microsoft Graph API, `httpx` y `python-calamine`.
|
|
14
|
+
|
|
15
|
+
## Características
|
|
16
|
+
|
|
17
|
+
* **Rendimiento Asíncrono:** Basado en `httpx.AsyncClient` con reutilización de conexiones TCP (Connection Pooling).
|
|
18
|
+
* **Autenticación No Bloqueante:** Delegación de MSAL a hilos secundarios vía `asyncio.to_thread`.
|
|
19
|
+
* **Lectura Rápida de Excel:** Uso del motor `python-calamine` (escrito en Rust) para un parseo acelerado a DataFrames de Pandas.
|
|
20
|
+
* **Procesamiento en Memoria:** Sin persistencia en disco de archivos temporales.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# sharexlsx_py
|
|
2
|
+
|
|
3
|
+
Librería asíncrona de Python para consultar y leer archivos Excel (`.xlsx`) alojados en Microsoft SharePoint utilizando Microsoft Graph API, `httpx` y `python-calamine`.
|
|
4
|
+
|
|
5
|
+
## Características
|
|
6
|
+
|
|
7
|
+
* **Rendimiento Asíncrono:** Basado en `httpx.AsyncClient` con reutilización de conexiones TCP (Connection Pooling).
|
|
8
|
+
* **Autenticación No Bloqueante:** Delegación de MSAL a hilos secundarios vía `asyncio.to_thread`.
|
|
9
|
+
* **Lectura Rápida de Excel:** Uso del motor `python-calamine` (escrito en Rust) para un parseo acelerado a DataFrames de Pandas.
|
|
10
|
+
* **Procesamiento en Memoria:** Sin persistencia en disco de archivos temporales.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sharexlsx_py"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Mario Eduardo", email="mfernandez@sumimsa.com.mx" },
|
|
10
|
+
]
|
|
11
|
+
description = "Librería para consultar y leer archivos Excel de SharePoint usando MS Graph API"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.8"
|
|
14
|
+
dependencies = [
|
|
15
|
+
"msal>=1.20.0",
|
|
16
|
+
"httpx>=0.28.1",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[tool.setuptools.packages.find]
|
|
20
|
+
where = ["."]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
import msal
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class GraphAuth:
|
|
8
|
+
def __init__(self, tenant_id: str, client_id: str, client_secret: str) -> None:
|
|
9
|
+
self.authority = f"https://login.microsoftonline.com/{tenant_id}"
|
|
10
|
+
self.scope = ["https://graph.microsoft.com/.default"]
|
|
11
|
+
|
|
12
|
+
self.app = msal.ConfidentialClientApplication(
|
|
13
|
+
client_id,
|
|
14
|
+
authority=self.authority,
|
|
15
|
+
client_credential=client_secret,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
async def get_access_token(self) -> str:
|
|
19
|
+
"""
|
|
20
|
+
Obtiene el token usando caché o solicita uno nuevo en un hilo secundario (non-blocking).
|
|
21
|
+
"""
|
|
22
|
+
result = await asyncio.to_thread(
|
|
23
|
+
self.app.acquire_token_silent, self.scope, account=None
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
if not result:
|
|
27
|
+
result = await asyncio.to_thread(
|
|
28
|
+
self.app.acquire_token_for_client, scopes=self.scope
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
if result and "access_token" in result:
|
|
32
|
+
return result["access_token"]
|
|
33
|
+
|
|
34
|
+
error_msg = result.get("error_description") if result else "Error desconocido"
|
|
35
|
+
raise PermissionError(f"Error autenticando en MS Graph: {error_msg}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class MSGraphAuth(httpx.Auth):
|
|
39
|
+
"""
|
|
40
|
+
Inyecta dinámicamente el token Bearer en peticiones asíncronas de httpx.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, graph_auth: GraphAuth) -> None:
|
|
44
|
+
self.graph_auth = graph_auth
|
|
45
|
+
|
|
46
|
+
async def async_auth_flow(self, request: httpx.Request):
|
|
47
|
+
token = await self.graph_auth.get_access_token()
|
|
48
|
+
request.headers["Authorization"] = f"Bearer {token}"
|
|
49
|
+
yield request
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Self
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from .auth import GraphAuth, MSGraphAuth
|
|
7
|
+
from .utils import filter_xlsx_items
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SharePointExcelClient:
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
tenant_id: str,
|
|
14
|
+
client_id: str,
|
|
15
|
+
client_secret: str,
|
|
16
|
+
site_hostname: str,
|
|
17
|
+
site_path: str,
|
|
18
|
+
timeout: float = 30.0,
|
|
19
|
+
) -> None:
|
|
20
|
+
self.auth = GraphAuth(tenant_id, client_id, client_secret)
|
|
21
|
+
self.site_hostname = site_hostname
|
|
22
|
+
self.site_path = site_path.strip("/")
|
|
23
|
+
self._site_id: str | None = None
|
|
24
|
+
self.timeout = timeout
|
|
25
|
+
self._client: httpx.AsyncClient | None = None
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def client(self) -> httpx.AsyncClient:
|
|
29
|
+
"""
|
|
30
|
+
Inicializa o retorna la sesión de cliente persistente.
|
|
31
|
+
"""
|
|
32
|
+
if self._client is None or self._client.is_closed:
|
|
33
|
+
self._client = httpx.AsyncClient(
|
|
34
|
+
base_url="https://graph.microsoft.com/v1.0",
|
|
35
|
+
auth=MSGraphAuth(self.auth),
|
|
36
|
+
timeout=httpx.Timeout(self.timeout),
|
|
37
|
+
)
|
|
38
|
+
return self._client
|
|
39
|
+
|
|
40
|
+
async def get_site_id(self) -> str:
|
|
41
|
+
"""
|
|
42
|
+
Obtiene y almacena en caché el Site ID de SharePoint.
|
|
43
|
+
"""
|
|
44
|
+
if not self._site_id:
|
|
45
|
+
url = f"/sites/{self.site_hostname}:/{self.site_path}"
|
|
46
|
+
response = await self.client.get(url)
|
|
47
|
+
response.raise_for_status()
|
|
48
|
+
self._site_id = response.json()["id"]
|
|
49
|
+
|
|
50
|
+
assert self._site_id is not None
|
|
51
|
+
return self._site_id
|
|
52
|
+
|
|
53
|
+
async def list_excel_files(self, relative_folder_path: str = "") -> list[dict]:
|
|
54
|
+
"""
|
|
55
|
+
Retorna únicamente la lista de metadatos de los archivos .xlsx
|
|
56
|
+
"""
|
|
57
|
+
site_id = await self.get_site_id()
|
|
58
|
+
folder_path = relative_folder_path.strip("/")
|
|
59
|
+
|
|
60
|
+
endpoint = (
|
|
61
|
+
f"/sites/{site_id}/drive/root:/{folder_path}:/children"
|
|
62
|
+
if folder_path
|
|
63
|
+
else f"/sites/{site_id}/drive/root/children"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
response = await self.client.get(endpoint)
|
|
67
|
+
response.raise_for_status()
|
|
68
|
+
return filter_xlsx_items(response.json().get("value", []))
|
|
69
|
+
|
|
70
|
+
async def get_excel_bytes(self, relative_file_path: str) -> bytes:
|
|
71
|
+
"""
|
|
72
|
+
Descarga y retorna los bytes puros de un archivo .xlsx específico.
|
|
73
|
+
"""
|
|
74
|
+
site_id = await self.get_site_id()
|
|
75
|
+
file_path = relative_file_path.strip("/")
|
|
76
|
+
endpoint = f"/sites/{site_id}/drive/root:/{file_path}:/content"
|
|
77
|
+
|
|
78
|
+
response = await self.client.get(endpoint)
|
|
79
|
+
response.raise_for_status()
|
|
80
|
+
return response.content # Retorna el binario puro (.xlsx en memoria)
|
|
81
|
+
|
|
82
|
+
async def get_folder_excels_bytes(
|
|
83
|
+
self, relative_folder_path: str = ""
|
|
84
|
+
) -> list[dict]:
|
|
85
|
+
"""
|
|
86
|
+
Descarga en paralelo todos los archivos .xlsx del directorio
|
|
87
|
+
y retorna una lista con metadatos + bytes puros de cada uno.
|
|
88
|
+
"""
|
|
89
|
+
metadatas = await self.list_excel_files(relative_folder_path)
|
|
90
|
+
folder_path = relative_folder_path.strip("/")
|
|
91
|
+
|
|
92
|
+
async def _download(meta: dict) -> dict:
|
|
93
|
+
file_rel_path = f"{folder_path}/{meta['name']}" if folder_path else meta["name"]
|
|
94
|
+
content_bytes = await self.get_excel_bytes(file_rel_path)
|
|
95
|
+
return {**meta, "content": content_bytes}
|
|
96
|
+
|
|
97
|
+
return list(await asyncio.gather(*[_download(meta) for meta in metadatas]))
|
|
98
|
+
|
|
99
|
+
async def close(self) -> None:
|
|
100
|
+
"""
|
|
101
|
+
Cierra el pool de conexiones del cliente HTTP y limpia la referencia.
|
|
102
|
+
"""
|
|
103
|
+
if self._client and not self._client.is_closed:
|
|
104
|
+
await self._client.aclose()
|
|
105
|
+
self._client = None
|
|
106
|
+
|
|
107
|
+
async def __aenter__(self) -> Self:
|
|
108
|
+
return self
|
|
109
|
+
|
|
110
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
111
|
+
await self.close()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
def filter_xlsx_items(items: list[dict]) -> list[dict]:
|
|
2
|
+
"""
|
|
3
|
+
Filtra los elementos retornados por Microsoft Graph API para extraer
|
|
4
|
+
únicamente archivos con extensión .xlsx y normalizar sus metadatos.
|
|
5
|
+
"""
|
|
6
|
+
files = []
|
|
7
|
+
for item in items:
|
|
8
|
+
if "file" in item and item["name"].lower().endswith(".xlsx"):
|
|
9
|
+
files.append(
|
|
10
|
+
{
|
|
11
|
+
"id": item.get("id"),
|
|
12
|
+
"name": item.get("name"),
|
|
13
|
+
"size": item.get("size"),
|
|
14
|
+
"web_url": item.get("webUrl"),
|
|
15
|
+
"created_at": item.get("createdDateTime"),
|
|
16
|
+
"last_modified": item.get("lastModifiedDateTime"),
|
|
17
|
+
}
|
|
18
|
+
)
|
|
19
|
+
return files
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sharexlsx_py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Librería para consultar y leer archivos Excel de SharePoint usando MS Graph API
|
|
5
|
+
Author-email: Mario Eduardo <mfernandez@sumimsa.com.mx>
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: msal>=1.20.0
|
|
9
|
+
Requires-Dist: httpx>=0.28.1
|
|
10
|
+
|
|
11
|
+
# sharexlsx_py
|
|
12
|
+
|
|
13
|
+
Librería asíncrona de Python para consultar y leer archivos Excel (`.xlsx`) alojados en Microsoft SharePoint utilizando Microsoft Graph API, `httpx` y `python-calamine`.
|
|
14
|
+
|
|
15
|
+
## Características
|
|
16
|
+
|
|
17
|
+
* **Rendimiento Asíncrono:** Basado en `httpx.AsyncClient` con reutilización de conexiones TCP (Connection Pooling).
|
|
18
|
+
* **Autenticación No Bloqueante:** Delegación de MSAL a hilos secundarios vía `asyncio.to_thread`.
|
|
19
|
+
* **Lectura Rápida de Excel:** Uso del motor `python-calamine` (escrito en Rust) para un parseo acelerado a DataFrames de Pandas.
|
|
20
|
+
* **Procesamiento en Memoria:** Sin persistencia en disco de archivos temporales.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
sharexlsx_py/__init__.py
|
|
4
|
+
sharexlsx_py/auth.py
|
|
5
|
+
sharexlsx_py/client.py
|
|
6
|
+
sharexlsx_py/utils.py
|
|
7
|
+
sharexlsx_py.egg-info/PKG-INFO
|
|
8
|
+
sharexlsx_py.egg-info/SOURCES.txt
|
|
9
|
+
sharexlsx_py.egg-info/dependency_links.txt
|
|
10
|
+
sharexlsx_py.egg-info/requires.txt
|
|
11
|
+
sharexlsx_py.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|