tai-storage 0.1.4
Librería para gestión unificada de archivos en múltiples orígenes de almacenamiento
Requires Python: <4.0,>=3.10

# TAI-Storage

**Librería para gestión unificada de archivos en múltiples orígenes de almacenamiento**

[![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)

## 📋 Descripción

`tai-storage` proporciona una interfaz única y consistente para manejar archivos almacenados en diferentes orígenes:

- 💾 **Sistema de archivos local** ✅
- ☁️ **Azure Blob Storage** (próximamente)
- 🔐 **Servidores SFTP/FTP** (próximamente)
- 📁 **SharePoint Online** ✅ (Microsoft Graph API + Azure AD)
- Y más...

### Características Principales

✨ **Interfaz Única**: Mismo código funciona con cualquier origen
🔄 **Modo Desarrollo**: Caché local automático para optimizar tiempos de desarrollo
🔒 **Gestión Segura de Secretos**: Integración con variables de entorno
📊 **Logging Avanzado**: Integración con [tai-alphi](https://pypi.org/project/tai-alphi/)
🎯 **Type Hints**: Soporte completo para IDEs y validación de tipos
🧪 **Testeable**: Fácil creación de mocks para testing

## 🚀 Instalación

### Con Poetry (recomendado)

```bash
# Instalación básica
poetry add tai-storage

# Con soporte para Azure
poetry add tai-storage[azure]

# Con soporte para SFTP
poetry add tai-storage[sftp]

# Con soporte para SharePoint
poetry add tai-storage[sharepoint]

# Con todos los orígenes
poetry add tai-storage[all]
```

### Con Pip

```bash
# Instalación básica
pip install tai-storage

# Con soporte para Azure
pip install tai-storage[azure]

# Con soporte para SharePoint
pip install tai-storage[sharepoint]

# Con todos los orígenes
pip install tai-storage[all]
```

## 📖 Uso Básico

```python
from tai_storage import LocalOrigin

# Crear origen local
origin = LocalOrigin(root_path='/data')

# Obtener carpeta y archivo  
folder = origin.get_folder('reports')
file = folder.get_file('report.csv')

# Leer archivo
data = file.read()
text = file.read_text()

# Listar archivos
files = folder.list_files(pattern=r'.*\.csv$')
```

> **Nota:** Implementaciones disponibles: Local ✅, SharePoint ✅ y Azure Blob Storage ✅. StorageFactory ✅ disponible.

## 🏗️ Arquitectura

La librería se basa en tres clases abstractas principales:

### 1. **Origin** (Sistema de Almacenamiento)
Representa el origen de datos (Azure, SFTP, local, etc.)

```python
class Origin(ABC):
    def connect()
    def disconnect()
    def get_folder(path) -> Folder
    def create_folder(path) -> Folder
    def folder_exists(path) -> bool
    # ...
```

### 2. **Folder** (Carpeta/Contenedor)
Representa una carpeta dentro de un origen

```python
class Folder(ABC):
    def list_files(pattern, recursive) -> List[str]
    def get_file(filename) -> File
    def upload_file(source, destination) -> File
    def delete_file(filename)
    # ...
```

### 3. **File** (Archivo)
Representa un archivo individual

```python
class File(ABC):
    def read() -> bytes
    def read_text() -> str
    def write(data: bytes)
    def delete()
    def copy_to(destination: File)
    # Properties: name, extension, size, created_time, modified_time
    # ...
```

## 🎯 Ejemplos de Uso

### Usando StorageFactory (Recomendado)

La forma más sencilla de trabajar con tai-storage es usando `StorageFactory`:

```python
from tai_storage import StorageFactory

# Sistema Local
local = StorageFactory.create_local(root_path='/data')

# Azure Blob Storage con parámetros
azure = StorageFactory.create_azure(
    account_name='myaccount',
    account_key='mykey123'
)

# O usando variables de entorno
# export AZURE_STORAGE_ACCOUNT_NAME=myaccount
# export AZURE_STORAGE_ACCOUNT_KEY=mykey123
azure = StorageFactory.create_azure()  # ← Lee desde env vars

# SharePoint Online
sharepoint = StorageFactory.create_sharepoint(
    tenant_id='your-tenant-id',
    client_id='your-client-id',
    client_secret='your-secret',
    site_name='Documents',
    hostname='contoso.sharepoint.com'
)
```

### Trabajar con Archivos

```python
# Usar cualquier origin (local, Azure, SharePoint...)
origin = StorageFactory.create_local(root_path='/data')

# Obtener carpeta y archivo
folder = origin.get_folder('documents')
file = folder.get_file('report.pdf')

# Verificar existencia y leer
if file.exists():
    data = file.read()
    print(f"Tamaño: {file.size} bytes")
    print(f"Modificado: {file.modified_time}")

# Listar archivos con patrón
pdf_files = folder.list_files(pattern=r'.*\.pdf$')
for filename in pdf_files:
    print(f"Encontrado: {filename}")
```

### Azure Blob Storage

```python
from tai_storage import StorageFactory

# Opción 1: Connection string
azure = StorageFactory.create_azure(
    connection_string='DefaultEndpointsProtocol=https;...'
)

# Opción 2: Account name + key
azure = StorageFactory.create_azure(
    account_name='mystorageaccount',
    account_key='mykey123...'
)

# Opción 3: Variables de entorno
# export AZURE_STORAGE_CONNECTION_STRING='...'
azure = StorageFactory.create_azure()

# Trabajar con containers (similar a folders)
container = azure.get_folder('documents')
files = container.list_files()

for filename in files:
    file = container.get_file(filename)
    print(f"{filename}: {file.size} bytes")
```

### SharePoint Online

```python
from tai_storage import StorageFactory

# Crear origin de SharePoint
sp = StorageFactory.create_sharepoint(
    tenant_id='your-tenant-id',
    client_id='your-client-id',
    client_secret='your-secret',
    site_name='Documents',
    hostname='contoso.sharepoint.com'
)

# Trabajar con carpetas y archivos
folder = sp.get_folder('Shared Documents/Reports')
files = folder.list_files(pattern=r'.*\.xlsx$')

for filename in files:
    file = folder.get_file(filename)
    data = file.read()
    print(f"Procesado: {filename}")
```

### Copiar entre Orígenes

```python
from tai_storage import StorageFactory

# Crear orígenes
local = StorageFactory.create_local(root_path='/data')
azure = StorageFactory.create_azure()

# Copiar de local a Azure
source_file = local.get_folder('input').get_file('data.csv')
dest_folder = azure.get_folder('backup')

# Opción 1: Usando upload_file
data = source_file.read()
dest_folder.upload_file(data, 'data.csv')

# Opción 2: Usando copy_to
dest_file = dest_folder.get_file('data.csv')
source_file.copy_to(dest_file)
```

### Context Manager

```python
from tai_storage import StorageFactory

# Auto connect/disconnect con context manager
with StorageFactory.create_local(root_path='/data') as origin:
    folder = origin.get_folder('documents')
    files = folder.list_files()
    print(f"Encontrados {len(files)} archivos")
# Automáticamente desconectado al salir del contexto

# También funciona con SharePoint y Azure
with StorageFactory.create_azure() as azure:
    container = azure.get_folder('backups')
    # ... trabajar con archivos
# Desconectado automáticamente
```

### Filtrar Archivos por Fecha

```python
from datetime import datetime, timedelta
from tai_storage import StorageFactory

origin = StorageFactory.create_local(root_path='/data')
folder = origin.get_folder('logs')

# Archivos modificados en los últimos 7 días
since = datetime.now() - timedelta(days=7)
recent_files = folder.filter_files_by_date(modified_after=since)

for filename in recent_files:
    file = folder.get_file(filename)
    print(f"{filename}: modificado {file.modified_time}")
```

---

### 🔑 Configurar SharePoint con Azure AD

Para usar SharePoint necesitas registrar una aplicación en Azure AD:

**1. Registrar Aplicación en Azure Portal**

1. Ve a [Azure Portal](https://portal.azure.com) → **Azure Active Directory**
2. En el menú lateral: **App registrations** → **New registration**
3. Configura:
   - **Name**: `tai-storage-app` (o el nombre que prefieras)
   - **Supported account types**: `Accounts in this organizational directory only`
   - **Redirect URI**: Déjalo vacío (no es necesario para aplicaciones daemon)
4. Click en **Register**
5. **Copia el Application (client) ID** → Este es tu `client_id`
6. En la página **Overview**, copia el **Directory (tenant) ID** → Este es tu `tenant_id`

**2. Crear Client Secret**

1. En tu aplicación, ve a **Certificates & secrets**
2. Click en **New client secret**
3. Configura:
   - **Description**: `tai-storage-secret`
   - **Expires**: Selecciona duración apropiada (ej: 24 meses)
4. Click en **Add**
5. **⚠️ IMPORTANTE: Copia el Value inmediatamente** → Este es tu `client_secret`
   - ⚠️ **Solo se muestra una vez**, no podrás volver a verlo

**3. Configurar Permisos de Microsoft Graph API**

1. En tu aplicación, ve a **API permissions**
2. Click en **Add a permission** → **Microsoft Graph** → **Application permissions**
3. Busca y añade estos permisos:
   - `Sites.Selected` - Acceso solo a sitios específicos (recomendado)
4. Click en **Add permissions**
5. **⚠️ CRÍTICO**: Click en **Grant admin consent for [Tu Organización]**
   - ⚠️ Necesitas permisos de administrador para este paso
   - Sin esto, la aplicación NO funcionará

**4. Otorgar Acceso al Sitio Específico**

Ahora debes dar permisos explícitos al sitio concreto usando Microsoft Graph API con PowerShell.

> 💡 **Script disponible**: Puedes usar el script completo disponible en [scripts/permisos_app_sharepoint.ps1](scripts/permisos_app_sharepoint.ps1)

```powershell
# 0. Limpiar sesión anterior (opcional pero recomendado)
Disconnect-MgGraph -ErrorAction SilentlyContinue

# 1. Instalar módulo si no lo tienes
Install-Module Microsoft.Graph -Scope CurrentUser

# 2. Conectar con permisos de admin
Connect-MgGraph -Scopes "Sites.FullControl.All" -NoWelcome

# 3. Configuración - REEMPLAZA CON TUS VALORES
$hostName = "<YOUR_TENANT>.sharepoint.com"  # ej: triplealphain.sharepoint.com
$siteName = "<SITE_NAME>"                   # ej: TeamSite, Demeter
$sitePath = "/sites/$siteName" 

$appId = "TU-CLIENT-ID-AQUI"                # Tu Application (client) ID
$appName = "tai-storage-app"                # Nombre de tu aplicación

Write-Host "Buscando sitio en: $hostName$sitePath ..." -ForegroundColor Cyan

try {
    # 4. Obtener el sitio usando la API directa
    $apiUrl = "https://graph.microsoft.com/v1.0/sites/$($hostName):$($sitePath)"
    
    $site = Invoke-MgGraphRequest -Method GET -Uri $apiUrl
    
    Write-Host "¡EXITO! Sitio encontrado." -ForegroundColor Green
    Write-Host "Nombre: $($site.displayName)"
    Write-Host "ID: $($site.id)" -ForegroundColor Gray

    # 5. Asignar permisos write (lectura y escritura)
    $params = @{
        roles = @("write")  # Opciones: read, write, owner
        grantedToIdentities = @(
            @{
                application = @{
                    id = $appId
                    displayName = $appName
                }
            }
        )
    }

    Invoke-MgGraphRequest -Method POST `
        -Uri "https://graph.microsoft.com/v1.0/sites/$($site.id)/permissions" `
        -Body ($params | ConvertTo-Json -Depth 5) `
        -ContentType "application/json"

    Write-Host "Permisos asignados correctamente." -ForegroundColor Green

} catch {
    # Manejo de errores
    Write-Host "ERROR AL BUSCAR EL SITIO:" -ForegroundColor Red
    
    if ($_.Exception.Message -match "Not Found" -or $_.ErrorDetails.Message -match "Not Found") {
        Write-Host "El sitio '$sitePath' NO EXISTE." -ForegroundColor Yellow
        Write-Host "Verifica el nombre del sitio en SharePoint."
    } else {
        Write-Host $_.Exception.Message -ForegroundColor Yellow
        if ($_.Exception.InnerException) {
            Write-Host "Detalle: $($_.Exception.InnerException.Message)" -ForegroundColor DarkYellow
        }
    }
}
```

**5. Obtener Información del Sitio de SharePoint**

1. Ve a tu sitio de SharePoint: `https://<YOUR_TENANT>.sharepoint.com/sites/<SITE_NAME>`
2. De la URL extrae:
   - **hostname**: `<YOUR_TENANT>.sharepoint.com` (ej: `contoso.sharepoint.com`, `triplealphain.sharepoint.com`)
   - **site_name**: `<SITE_NAME>` (la parte después de `/sites/`, ej: `TeamSite`, `ProjectSite`)

**6. Configurar Variables de Entorno**

```bash
# En tu sistema o .env
export SHAREPOINT_TENANT_ID="12345678-1234-1234-1234-123456789abc"
export SHAREPOINT_CLIENT_ID="87654321-4321-4321-4321-cba987654321"
export SHAREPOINT_CLIENT_SECRET="tu~secret~super~secreto"
export SHAREPOINT_SITE_NAME="<SITE_NAME>"  # ej: TeamSite, ProjectSite
export SHAREPOINT_HOSTNAME="<YOUR_TENANT>.sharepoint.com"  # ej: contoso.sharepoint.com
```

**7. Usar en tu Código**

```python
from tai_storage import SharePointOrigin
from tai_storage.secrets import SecretManager

origin = SharePointOrigin(
    tenant_id=SecretManager.get_secret(env_var="SHAREPOINT_TENANT_ID"),
    client_id=SecretManager.get_secret(env_var="SHAREPOINT_CLIENT_ID"),
    client_secret=SecretManager.get_secret(env_var="SHAREPOINT_CLIENT_SECRET"),
    site_name=SecretManager.get_secret(env_var="SHAREPOINT_SITE_NAME"),
    hostname=SecretManager.get_secret(env_var="SHAREPOINT_HOSTNAME")
)

origin.connect()
# ... trabajar con SharePoint
origin.disconnect()
```

**📋 Checklist de Verificación**

- [ ] Aplicación registrada en Azure AD
- [ ] Application (client) ID copiado → `client_id`
- [ ] Directory (tenant) ID copiado → `tenant_id`
- [ ] Client secret creado y copiado → `client_secret`
- [ ] Permiso `Sites.Selected` añadido (NO `Sites.ReadWrite.All`)
- [ ] **Admin consent otorgado** (paso crítico)
- [ ] **Permisos específicos del sitio otorgados** (PowerShell/API - paso crítico)
- [ ] Hostname y site_name del sitio de SharePoint identificados
- [ ] Variables de entorno configuradas
- [ ] `pip install tai-storage[sharepoint]` ejecutado (o `msal` y `requests` instalados)

**🔒 Ventajas de usar Sites.Selected**

- ✅ **Seguridad mejorada**: La app solo accede al sitio específico que necesita
- ✅ **Principio de mínimo privilegio**: No tiene acceso a otros sitios de la organización
- ✅ **Auditoría clara**: Fácil ver qué apps tienen acceso a qué sitios
- ✅ **Revocación simple**: Puedes revocar acceso sin eliminar la app
- ⚠️ **Requiere configuración adicional**: Necesitas otorgar permisos explícitamente con PowerShell/API

**⚠️ Errores Comunes**

- **"Insufficient privileges"**: No se otorgó admin consent o no se otorgaron permisos al sitio específico
- **"Access denied"**: Usaste `Sites.Selected` pero no otorgaste permisos al sitio con PowerShell/API
- **"Application not found"**: Tenant ID o Client ID incorrectos
- **"Invalid client secret"**: El secret expiró o se copió incorrectamente
- **"Site not found"**: Hostname o site_name incorrectos
- **ImportError msal**: Falta `pip install msal requests`

**🔄 Para otorgar acceso a múltiples sitios**

Si necesitas acceso a varios sitios específicos, repite el paso 4 para cada sitio:

```powershell
# Sitio 1
$site1 = Get-MgSite -Search "<SITE_NAME_1>"  # ej: TeamSite
Invoke-MgGraphRequest -Method POST `
    -Uri "https://graph.microsoft.com/v1.0/sites/$($site1.Id)/permissions" `
    -Body $params

# Sitio 2
$site2 = Get-MgSite -Search "<SITE_NAME_2>"  # ej: ProjectSite
Invoke-MgGraphRequest -Method POST `
    -Uri "https://graph.microsoft.com/v1.0/sites/$($site2.Id)/permissions" `
    -Body $params
```

**📖 Referencias**

- [Sites.Selected Permission Documentation](https://learn.microsoft.com/en-us/graph/permissions-reference#sitesselected)
- [Granting site permissions with Graph API](https://learn.microsoft.com/en-us/graph/api/site-post-permissions)

### Filtrar Archivos por Fecha

```python
from datetime import datetime, timedelta

folder = origin.get_folder('logs')

# Archivos de los últimos 7 días
since = datetime.now() - timedelta(days=7)
recent_files = folder.filter_files_by_date(since=since)

for file_meta in recent_files:
    print(f"{file_meta['name']}: {file_meta['modified_time']}")
```

## ⚙️ Configuración

### Logging con tai-alphi

```python
from tai_storage import configure_logger

# Configurar logging
configure_logger(
    log_level='DEBUG',  # DEBUG, INFO, WARN, ERROR, CRIT
    display_info=['asctime', 'levelname', 'funcName'],
    time_format='%H:%M:%S'
)

# Para acceder directamente al logger desde otros módulos:
from tai_alphi import Alphi
logger = Alphi.get_logger_by_name('tai-storage')
logger.info('Mensaje personalizado')
```

### Variables de Entorno

StorageFactory puede obtener credenciales de variables de entorno:

```bash
# Local
export STORAGE_LOCAL_ROOT_PATH="/data/files"

# Azure Blob Storage (Opción 1: Connection String)
export AZURE_STORAGE_CONNECTION_STRING="DefaultEndpointsProtocol=https;AccountName=...;AccountKey=...;EndpointSuffix=core.windows.net"

# Azure Blob Storage (Opción 2: Account + Key)
export AZURE_STORAGE_ACCOUNT_NAME="mystorageaccount"
export AZURE_STORAGE_ACCOUNT_KEY="mykey123..."

# SharePoint Online
export SHAREPOINT_TENANT_ID="12345678-1234-1234-1234-123456789abc"
export SHAREPOINT_CLIENT_ID="87654321-4321-4321-4321-cba987654321"
export SHAREPOINT_CLIENT_SECRET="tu~secret~super~secreto"
export SHAREPOINT_SITE_NAME="TeamSite"  # Nombre del sitio
export SHAREPOINT_HOSTNAME="contoso.sharepoint.com"  # Tu tenant
```

Luego puedes crear origins sin parámetros:

```python
from tai_storage import StorageFactory

# Lee credenciales de variables de entorno
local = StorageFactory.create_local()
azure = StorageFactory.create_azure()
sharepoint = StorageFactory.create_sharepoint()
```

### Parámetros vs Variables de Entorno

StorageFactory prioriza parámetros directos sobre variables de entorno:

```python
# 1. Parámetros directos (prioridad alta)
origin = StorageFactory.create_azure(
    account_name='myaccount',
    account_key='mykey'
)

# 2. Variables de entorno (fallback)
# export AZURE_STORAGE_ACCOUNT_NAME=myaccount
origin = StorageFactory.create_azure()  # Usa env vars

# 3. Mix (parámetros + env vars)
# export AZURE_STORAGE_ACCOUNT_KEY=mykey
origin = StorageFactory.create_azure(
    account_name='myaccount'  # key viene de env var
)
```

## 🧪 Testing

```python
from tai_storage import LocalOrigin
import tempfile

def test_file_operations():
    with tempfile.TemporaryDirectory() as tmpdir:
        origin = LocalOrigin(root_path=tmpdir)
        folder = origin.create_folder('test')
        
        # Test upload
        folder.upload_file(b'test data', 'test.txt')
        
        # Test read
        file = folder.get_file('test.txt')
        assert file.read() == b'test data'
        
        # Test delete
        file.delete()
        assert not file.exists()
```

Ejecutar tests:
```bash
# Tests de fundamentos (Fase 1)
python -m pytest tests/test_fase1.py -v

# Tests de implementación local (Fase 2)
python -m pytest tests/test_fase2_local.py -v

# Tests de SharePoint (requiere credenciales configuradas)
python -m pytest tests/test_sharepoint.py -v

# Todos los tests
python -m pytest tests/ -v

# Ejemplos funcionales
python examples_local.py
python examples_sharepoint.py  # Requiere credenciales
```

## 📦 Estructura del Proyecto

```
tai_storage/
├── base/                      # Clases abstractas
│   ├── origin.py
│   ├── folder.py
│   └── file.py
├── implementations/           # Implementaciones
│   ├── local/                # ✅ Sistema de archivos local
│   │   ├── origin.py
│   │   ├── folder.py
│   │   └── file.py
│   ├── sharepoint/           # ✅ SharePoint Online (Graph API)
│   │   ├── origin.py
│   │   ├── folder.py
│   │   └── file.py
│   ├── azure/                # 🔜 Azure Blob Storage
│   ├── sftp/                 # 🔜 SFTP
│   └── ftp/                  # 🔜 FTP
├── cache/                     # Sistema de caché para modo DEV
│   ├── cached_origin.py
│   ├── cached_folder.py
│   └── cached_file.py
├── factory.py                 # StorageFactory
├── secrets.py                 # SecretManager
├── exceptions.py              # Excepciones personalizadas
├── logger.py                  # Configuración de logging
└── utils.py                   # Utilidades
```

## 🗺️ Roadmap

- [x] **Fase 1**: Fundamentos (clases base, excepciones, secretos) ✅
- [x] **Fase 2**: Implementación Local ✅
- [x] **Fase 2.5**: Implementación SharePoint (Microsoft Graph API + MSAL) ✅
- [ ] **Fase 3**: Sistema de Caché (Modo DEV)
- [ ] **Fase 4**: Factory y Secrets
- [ ] **Fase 5**: Implementación Azure Blob Storage
- [ ] **Fase 6**: Implementaciones SFTP, FTP

## 🤝 Contribución

Las contribuciones son bienvenidas. Por favor:

1. Fork el proyecto
2. Crea una rama para tu feature (`git checkout -b feature/amazing`)
3. Commit tus cambios (`git commit -m 'Add amazing feature'`)
4. Push a la rama (`git push origin feature/amazing`)
5. Abre un Pull Request

## 📄 Licencia

MIT License - ver archivo [LICENSE](LICENSE) para más detalles

## 🆘 Soporte

Para reportar bugs o solicitar features:
- Abre un issue en GitHub
- Contacta al equipo de desarrollo

## 👥 Autores

- **Mateo Saez Mata** - [msaez@triplealpha.in](mailto:msaez@triplealpha.in)