Metadata-Version: 2.4
Name: osdental-mb-library
Version: 0.3.2
Summary: Internal mobile library for OSDental
Author-email: aalvia <aalvia@osdental.ai>
License-Expression: MIT
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: starlette
Requires-Dist: strawberry-graphql
Requires-Dist: python-json-logger
Requires-Dist: grpcio
Requires-Dist: grpcio-tools
Requires-Dist: uvicorn
Requires-Dist: azure-monitor-opentelemetry==1.8.1
Requires-Dist: azure-identity>=1.14.0
Requires-Dist: azure-core>=1.26.0

# osdental-mobile-library

Librería interna compartida para los microservicios móviles de OSDental. Provee conexión a base de datos, autenticación vía gRPC, cliente HTTP, decoradores de auditoría y manejo de excepciones estandarizado.

**Versión:** 0.3.1 · **Python:** ≥ 3.10

---

## Instalación

```bash
pip install osdental-mb-library
```

## Variables de entorno requeridas

| Variable | Descripción |
|---|---|
| `APPLICATIONINSIGHTS_CONNECTION_STRING` | Connection string de Azure Monitor |
| `SECURITY_GRPC_ENDPOINT` | Host del servicio de autenticación gRPC |
| `GRPC_SECURE` | `true` para canal TLS, `false` para inseguro |
| `ENVIRONMENT` | Nombre del entorno (`dev`, `staging`, `prod`) |
| `MICROSERVICE_NAME` | Nombre del microservicio que usa la librería |
| `MICROSERVICE_VERSION` | Versión del microservicio |

---

## Módulos

### `Database.Connection`

Singleton asíncrono para conexiones SQL Server via SQLAlchemy. Soporta connection string directa o Azure Managed Identity.

```python
from osdentalMobileLibrary.Database.Connection import Connection

# Con connection string
db = Connection(db_url="mssql+aioodbc://user:pass@server/db?driver=...")

# Con Azure Managed Identity
db = Connection(db_url="mssql+aioodbc://server/db?...", use_managed_identity=True)
```

**Métodos disponibles:**

```python
# Retorna el primer valor escalar
value = await db.execute_query_return_first_value(query, params)

# Retorna lista de dicts (o un dict si fetchone=True)
rows = await db.execute_query_return_data(query, params, fetchone=False)

# Ejecuta y valida STATUS_CODE, retorna STATUS_MESSAGE
msg = await db.execute_query_return_message(query, params, code="SUCCESS")

# Ejecuta sin retorno, o valida STATUS_CODE si se provee code
await db.execute_query(query, params, code=None)

# Ejecuta múltiples queries en una sola transacción
result = await db.execute_transaction_queries(
    data=[{"query": "...", "params": {}, "code": "SUCCESS"}],
    return_data=False
)
```

> El pool está configurado con `pool_size=20`, `max_overflow=40`, `pool_recycle=3600`.

---

### `Decorators`

#### `AuditLogMobile`

Decorador para resolvers GraphQL (Strawberry). Valida el Bearer token vía gRPC, crea un span de OpenTelemetry en Azure Monitor y bloquea requests no autenticados.

```python
from osdentalMobileLibrary.Decorators.AuditLogMobile import AuditLogMobile

@strawberry.type
class Query:
    @strawberry.field
    @AuditLogMobile()
    async def get_patient(self, info: Info) -> Patient:
        ...
```

El decorador espera que `info.context` contenga un objeto `request` con headers `Authorization` (Bearer) y `Agent-Mobile`.

#### `grpc_retry` / `rest_retry`

Reintentos automáticos con backoff exponencial (3 intentos, espera 1–4 s).

```python
from osdentalMobileLibrary.Decorators.Retry import grpc_retry, rest_retry

@grpc_retry   # reintenta en grpc.RpcError
async def call_grpc(...): ...

@rest_retry   # reintenta en httpx.RequestError
async def call_rest(...): ...
```

---

### `ExternalHttp.APIClient`

Cliente HTTP asíncrono (extiende `httpx.AsyncClient`) con retry automático y logging a Service Bus.

```python
from osdentalMobileLibrary.ExternalHttp.Client import APIClient

async with APIClient() as client:
    # REST
    data = await client.rest_request("GET", "https://api.example.com/resource")

    # GraphQL
    data = await client.graphql_request(
        url="https://api.example.com/graphql",
        consult="query { patients { id } }",
        variables={},
        headers={"Authorization": "Bearer ..."}
    )
```

Cada llamada envía automáticamente el request y el response al Service Bus para auditoría.

---

### `Grpc.Client.AuthClient`

Cliente gRPC asíncrono para validar tokens contra el servicio de autenticación.

```python
from osdentalMobileLibrary.Grpc.Client.AuthClient import AuthClient

async with AuthClient() as auth:
    response = await auth.validate_auth_token(
        bearer_token="eyJ...",
        agent_mobile="ios-v2.1"
    )
    if not response.Data:
        # Token inválido o sin acceso
        ...
```

---

### `Exception`

Jerarquía de excepciones tipadas. Todas extienden `OSDException` e incluyen `message`, `error` y `status_code`.

| Excepción | Uso |
|---|---|
| `OSDException` | Base — error genérico de la aplicación |
| `UnauthorizedException` | Token inválido o sin permisos |
| `RequestDataException` | Parámetros de request inválidos |
| `DatabaseException` | Error en ejecución de query |
| `HttpClientException` | Error en llamada HTTP externa |
| `AzureException` | Error en servicios Azure |
| `ValidationDataException` | Error de validación de datos |
| `MissingFieldException` | Campo requerido ausente |
| `InvalidFormatException` | Formato de dato incorrecto |
| `UnexpectedException` | Error inesperado genérico |

```python
from osdentalMobileLibrary.Exception.ControlledException import DatabaseException

raise DatabaseException(message="Query falló", error=str(e))
```

---

## Estructura del proyecto

```
src/osdentalMobileLibrary/
├── Database/          # Conexión async a SQL Server
├── Decorators/        # AuditLogMobile, grpc_retry, rest_retry
├── Exception/         # Excepciones tipadas
├── ExternalHttp/      # APIClient HTTP/GraphQL
├── Grpc/
│   ├── Base/          # GrpcClientBase
│   ├── Client/        # AuthClient
│   └── Generated/     # Protobuf generado
├── Models/            # Response model
├── ServicesBus/       # TaskQueue para Service Bus
└── Shared/            # Config, Logger, Enums, Utils, Azure
```

## Build y distribución

```bash
python -m build
# Genera dist/osdental_mb_library-x.x.x-py3-none-any.whl
```
