Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lab-01-fundamentos/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ lab-01-fundamentos/
│ └── tui_dashboard.py # Dashboard interactivo con Textual
├── tests/
│ └── test_scripts.py # Tests con pytest y fixtures
├── docs/
│ └── architecture.md # Arquitectura, flujo de datos y decisiones de diseño
└── README.md
```

Expand Down
118 changes: 118 additions & 0 deletions lab-01-fundamentos/docs/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Lab 01 — Arquitectura y Funcionamiento

Este documento explica la estructura del lab, el flujo de datos entre scripts y las decisiones de diseño.

---

## Visión General

```
┌─────────────────────────────────────────────────────────────┐
│ Docker Network │
│ │
│ ┌─────────────────┐ psycopg2 ┌─────────────────┐ │
│ │ crud_postgres.py│ ──────────────▶ │ PostgreSQL │ │
│ │ │ ◀────────────── │ (lab01_db) │ │
│ └─────────────────┘ SQL results └─────────────────┘ │
│ │
│ ┌─────────────────┐ │
│ │ io_formats.py │ CSV → Polars → Parquet │
│ │ │ (sin DB, solo archivos locales) │
│ └─────────────────┘ │
│ │
│ ┌─────────────────┐ │
│ │ async_demo.py │ asyncio + aiohttp (I/O concurrente) │
│ └─────────────────┘ │
│ │
│ ┌─────────────────┐ │
│ │tui_dashboard.py │ Textual TUI → consulta PostgreSQL │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```

---

## Scripts

### `crud_postgres.py`

Demuestra conexión directa a PostgreSQL con `psycopg2` sin ORM. Realiza:
- `CREATE TABLE` con schema de paradas de bus
- `INSERT` masivo con `execute_values` (bulk insert eficiente)
- `SELECT` con filtros y ordenamiento
- `UPDATE` y `DELETE` con confirmación de filas afectadas

La conexión se gestiona con context managers (`with psycopg2.connect(...) as conn`) para garantizar el cierre y rollback automático ante excepciones.

### `io_formats.py`

Pipeline de transformación de datos sin base de datos:

```
data/stops_raw.csv
▼ leer con Polars (lazy frame)
▼ transformar: filtrar, renombrar columnas, cast de tipos
▼ escribir Parquet (columnar, comprimido)
data/stops_clean.parquet
```

**Por qué Polars sobre Pandas:** Polars usa Apache Arrow internamente, lo que permite lazy evaluation (el plan de query se optimiza antes de ejecutar) y operaciones vectorizadas en Rust. Para ETL de archivos es 5-20× más rápido que Pandas.

**Por qué Parquet sobre CSV:** Parquet es columnar y comprimido. Una query que solo necesita 2 de 10 columnas solo lee esas 2 columnas del disco. CSV siempre lee todo el archivo.

### `async_demo.py`

Demuestra concurrencia I/O con `asyncio`. Lanza múltiples peticiones HTTP en paralelo con `aiohttp` usando `asyncio.gather()`. Compara tiempos vs. requests síncronos para evidenciar la diferencia en workloads I/O-bound.

### `tui_dashboard.py`

Dashboard interactivo en terminal construido con **Textual**. Consulta las paradas de PostgreSQL y las muestra en una tabla navegable con teclado. Demuestra que las herramientas de inspección de datos no requieren un navegador web.

---

## Flujo de Datos

```
stops_raw.csv ──▶ io_formats.py ──▶ stops_clean.parquet
▼ (datos de ejemplo)
crud_postgres.py ──▶ PostgreSQL
tui_dashboard.py
```

---

## Testing

Los tests en `tests/test_scripts.py` usan pytest con fixtures que:
1. Crean una conexión a PostgreSQL de test
2. Aplican el schema
3. Insertan datos de prueba
4. Verifican los resultados de cada operación CRUD
5. Hacen rollback al finalizar (aislamiento entre tests)

Se usa `pytest.fixture(scope="function")` para que cada test tenga un estado limpio.

---

## Gestión de Dependencias — uv

`uv` reemplaza a `pip` + `venv`. Es un resolvedor de dependencias escrito en Rust, ~10-100× más rápido que pip. El archivo `pyproject.toml` declara las dependencias; `uv pip install --system` las instala en el entorno del contenedor.

---

## Decisiones de Diseño

| Decisión | Alternativa | Razón |
|---|---|---|
| psycopg2 directo (sin ORM) | SQLAlchemy / Django ORM | Demostrar el nivel más bajo: SQL puro, cursores, transacciones manuales |
| Polars | Pandas | Velocidad, lazy evaluation, API más ergonómica para ETL |
| Parquet | CSV / JSON | Formato columnar, compresión, eficiencia en lectura parcial |
| Textual | Rich / curses | API declarativa para TUI, componentes reutilizables, soporte de eventos |
| asyncio + aiohttp | threading / requests | Modelo de concurrencia cooperativo sin overhead de threads para I/O-bound |
| uv | pip + venv | Resolución de dependencias ~100× más rápida, reproducible |
2 changes: 2 additions & 0 deletions lab-02-django-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ lab-02-django-api/
│ ├── factories.py # Factories con factory-boy para datos de test
│ ├── test_api.py # Tests de endpoints con APIClient
│ └── test_models.py # Tests de modelos y consultas espaciales
├── docs/
│ └── architecture.md # Arquitectura, flujo de datos y decisiones de diseño
└── README.md
```

Expand Down
138 changes: 138 additions & 0 deletions lab-02-django-api/docs/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# Lab 02 — Arquitectura y Funcionamiento

Este documento explica la arquitectura de la API REST, el modelo de datos geoespacial y las decisiones de diseño.

---

## Visión General

```
┌────────────────────────────────────────────────────────────────┐
│ Docker Network │
│ │
│ Cliente HTTP │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Django (DRF) │ │
│ │ │ │
│ │ JWT Auth ──▶ Permissions ──▶ ViewSets ──▶ Serializers │ │
│ │ │ │ │
│ │ Querysets │ │
│ └──────────────────────────────────┬───────────────────────┘ │
│ │ psycopg2 │
│ ▼ │
│ ┌─────────────────┐ │
│ │ PostgreSQL │ │
│ │ + PostGIS │ │
│ │ (geometrías) │ │
│ └─────────────────┘ │
└────────────────────────────────────────────────────────────────┘
```

---

## Modelo de Datos

```
Stop (parada)
├── code: CharField (unique)
├── name: CharField
├── location: PointField (PostGIS) ← coordenada GPS
├── zone: CharField (choices)
├── total_routes: IntegerField
└── is_active: BooleanField

Route (ruta)
├── code: CharField (unique)
├── name: CharField
├── origin: CharField
├── destination: CharField
├── path: LineStringField (PostGIS) ← trazado geográfico (opcional)
├── stops: ManyToManyField → Stop
└── is_active: BooleanField

Schedule (horario)
├── route: ForeignKey → Route
├── stop: ForeignKey → Stop
├── departure_time: TimeField
└── stop_order: IntegerField
```

---

## Capas de la Aplicación

### Autenticación — JWT

`djangorestframework-simplejwt` emite tokens firmados con HMAC-SHA256. El flujo es:

```
POST /api/auth/token/ → { access, refresh }
POST /api/auth/token/refresh/ → { access } (renueva con refresh)
```

Cada request a endpoints protegidos incluye `Authorization: Bearer <access_token>`. DRF valida la firma y extrae el usuario del payload sin consultar la base de datos.

### Permisos — `permissions.py`

Se implementa `IsOwnerOrReadOnly`: un permiso custom de DRF que permite lectura a cualquier usuario autenticado, pero escritura (`PUT`, `PATCH`, `DELETE`) solo al creador del objeto. Hereda de `BasePermission` y sobreescribe `has_object_permission()`.

### ViewSets

Se usa `ModelViewSet` de DRF que genera automáticamente los 5 endpoints REST:

| Método | URL | Acción |
|---|---|---|
| GET | `/api/routes/` | list |
| POST | `/api/routes/` | create |
| GET | `/api/routes/{id}/` | retrieve |
| PUT/PATCH | `/api/routes/{id}/` | update |
| DELETE | `/api/routes/{id}/` | destroy |

Además hay acciones custom (`@action`) para consultas geoespaciales.

### Consultas Geoespaciales — PostGIS

`geospatial.py` centraliza las queries geoespaciales:

```python
# Paradas dentro de un radio dado
Stop.objects.filter(
location__distance_lte=(point, D(m=radius_m))
).order_by('location')
```

PostGIS convierte coordenadas GPS en objetos geométricos indexados con GiST. Las queries de distancia usan ese índice — sin PostGIS habría que calcular distancias en Python para cada fila de la tabla.

---

## Testing — pytest-django + factory_boy

### Factories

`factory_boy` genera objetos de modelo con datos realistas sin definirlos manualmente en cada test:

```python
class StopFactory(DjangoModelFactory):
name = factory.Faker("street_name")
location = factory.LazyFunction(lambda: Point(-84.08, 9.93))
...
```

### Fixtures de pytest-django

`@pytest.fixture` con `django_db` habilita acceso a la base de datos. Cada test corre en una transacción que se revierte al finalizar — aislamiento total sin necesidad de limpiar datos manualmente.

---

## Decisiones de Diseño

| Decisión | Alternativa | Razón |
|---|---|---|
| DRF ModelViewSet | APIView manual | Reduce boilerplate; el CRUD estándar se genera automáticamente |
| JWT (simplejwt) | Session auth / OAuth2 | Stateless, ideal para APIs consumidas por frontend desacoplado |
| PostGIS | Calcular distancias en Python | Índice GiST en DB hace queries geoespaciales O(log n) en lugar de O(n) |
| factory_boy | Fixtures estáticas / setUp | Factories son composables y generan datos únicos; evita colisiones entre tests |
| pytest-django | unittest.TestCase | Fixtures de pytest son más ergonómicas y componibles que setUp/tearDown |
| Separar `geospatial.py` | Queries inline en views | Centraliza la lógica de dominio geoespacial, testeable de forma independiente |
2 changes: 2 additions & 0 deletions lab-03-async-realtime/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ lab-03-async-realtime/
├── conftest.py # InMemoryChannelLayer + patch de OriginValidator para tests
├── test_consumers.py # 7 tests — VehicleTrackingConsumer y FleetOverviewConsumer
└── test_tasks.py # 8 tests — ingest_vehicle_position, cleanup, fleet summary
docs/
└── architecture.md # Arquitectura, flujo de datos y decisiones de diseño
```

---
Expand Down
108 changes: 108 additions & 0 deletions lab-03-async-realtime/docs/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Lab 03 — Arquitectura y Funcionamiento

Este documento explica el flujo de mensajes desde la ingesta MQTT hasta la entrega por WebSocket al cliente.

---

## Visión General

```
┌──────────────────────────────────────────────────────────────────────┐
│ Docker Network │
│ │
│ Sensor GPS │
│ (publisher) │
│ │ MQTT publish │
│ ▼ │
│ ┌──────────┐ consume ┌──────────────┐ .delay() ┌───────┐ │
│ │ NanoMQ │ ────────────▶ │mqtt_ingester │ ───────────▶ │Celery │ │
│ │ (MQTT) │ │ (thread) │ │Worker │ │
│ └──────────┘ └──────────────┘ └───┬───┘ │
│ │ │
│ persist + │ │
│ group_send│ │
│ ▼ │
│ ┌──────────┐ channel ┌──────────────┐ WS message ┌──────┐ │
│ │ Redis │ ◀──────────▶ │ Channels │ ────────────▶│ │ │
│ │ (layer) │ │ (Daphne) │ │Client│ │
│ └──────────┘ └──────────────┘ └──────┘ │
│ │
│ ┌──────────┐ broker │
│ │ RabbitMQ │ ◀──────────── Celery tasks │
│ └──────────┘ │
│ │
│ ┌──────────┐ persist │
│ │PostgreSQL│ ◀──────────── Celery worker │
│ └──────────┘ │
└──────────────────────────────────────────────────────────────────────┘
```

---

## Flujo de Datos Completo

1. **Sensor GPS** publica en `transit/vehicle/BUS-001/position` (NanoMQ)
2. **`mqtt_ingester`** (hilo del worker) consume el mensaje y llama `ingest_vehicle_position.delay(...)` — despacha la tarea a Celery sin bloquear
3. **Celery Worker** ejecuta la tarea:
- Persiste `VehiclePosition` en PostgreSQL (con geometría PostGIS)
- Llama `channel_layer.group_send("vehicle_BUS-001", event)` dos veces: una al grupo del vehículo, otra al grupo `"fleet"`
4. **Redis** (Channel Layer) enruta el evento a los consumers suscritos
5. **`VehicleTrackingConsumer`** (o `FleetOverviewConsumer`) recibe el evento y hace `self.send(text_data=json.dumps(event))` al WebSocket conectado

---

## Componentes

### NanoMQ — Broker MQTT

Broker MQTT liviano para telemetría IoT. Cada vehículo publica en su propio topic:
```
transit/vehicle/BUS-001/position
transit/vehicle/BUS-002/position
```

### mqtt_ingester — Consumidor MQTT

Hilo dentro del worker de Celery que se mantiene suscrito al wildcard `transit/vehicle/+/position`. Al recibir un mensaje, no lo procesa directamente — lo despacha como tarea Celery para no bloquear el loop de MQTT.

### Celery + RabbitMQ — Procesamiento Asíncrono

**RabbitMQ** es el broker de mensajes: recibe tareas de productores y las entrega a workers. **Celery** es el framework de workers que ejecuta las tareas. La separación permite escalar workers horizontalmente sin cambiar el código.

La tarea `ingest_vehicle_position` tiene:
- `bind=True`: acceso a `self` para reintentos
- `max_retries=3`: reintenta ante fallos transitorios (ej. DB no disponible)
- `default_retry_delay=5`: espera 5s entre reintentos

### Django Channels + Daphne — WebSockets

Django es WSGI (síncrono). **Channels** añade soporte ASGI (asíncrono) para WebSockets. **Daphne** es el servidor ASGI que maneja conexiones WS persistentes.

Hay dos consumers:
- `VehicleTrackingConsumer`: suscrito al grupo `vehicle_{vehicle_id}` — solo recibe eventos de un vehículo
- `FleetOverviewConsumer`: suscrito al grupo `fleet` — recibe eventos de toda la flota

### Redis — Channel Layer

Redis actúa como bus de mensajes entre el worker de Celery (que llama `group_send`) y los consumers de Channels (que reciben `vehicle_position`). Sin Redis, los workers y los consumers no podrían comunicarse porque corren en procesos separados.

---

## WebSocket URLs

```
ws://localhost:8001/ws/tracking/BUS-001/ ← vehículo específico
ws://localhost:8001/ws/fleet/ ← toda la flota
```

---

## Decisiones de Diseño

| Decisión | Alternativa | Razón |
|---|---|---|
| MQTT → Celery (dispatch) | Procesamiento directo en hilo MQTT | Desacopla ingesta de procesamiento; Celery maneja reintentos y backpressure |
| RabbitMQ como broker Celery | Redis como broker | RabbitMQ tiene routing más avanzado (exchanges, routing keys); Redis es solo una lista |
| Redis como Channel Layer | InMemoryChannelLayer | InMemoryChannelLayer no persiste entre procesos; Redis permite múltiples workers |
| Daphne como servidor ASGI | uvicorn | Daphne es el servidor oficial de Django Channels; soporte nativo para el protocolo ASGI de Channels |
| Dos grupos (vehicle + fleet) | Solo un grupo fleet | Permite subscripción selectiva: un cliente puede seguir un vehículo sin recibir ruido de toda la flota |
Loading
Loading