Last active
March 24, 2026 17:26
-
-
Save gabanox/62ba3c3792d8714f1b45e74e6f94227c to your computer and use it in GitHub Desktop.
CineTime S3 Storage Classes Demo — AWS re/Start | Práctica manual de clases de almacenamiento sin Lifecycle Policies
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| ╔══════════════════════════════════════════════════════════════════════╗ | |
| ║ CineTime — Demo de Clases de Almacenamiento S3 ║ | |
| ║ AWS re/Start | Práctica Manual de Storage Classes ║ | |
| ╚══════════════════════════════════════════════════════════════════════╝ | |
| IMPORTANTE: Este script simula mover archivos entre clases de S3 | |
| SIN usar Lifecycle Policies automáticas. | |
| Cada operación usa copy_object() para que veas exactamente | |
| qué llamada se hace a la API de AWS. | |
| USO: | |
| python3 cinetime_s3_demo.py setup → Crea bucket y archivos demo | |
| python3 cinetime_s3_demo.py listar → Estado actual de todos los objetos | |
| python3 cinetime_s3_demo.py escenario 1 → Estrenos (>90 días) → Standard-IA | |
| python3 cinetime_s3_demo.py escenario 2 → Archivos RAW → Glacier Flexible | |
| python3 cinetime_s3_demo.py escenario 3 → Thumbnails → Intelligent-Tiering | |
| python3 cinetime_s3_demo.py escenario 4 → Catálogo general → Intelligent-Tiering | |
| python3 cinetime_s3_demo.py escenario 5 → Contenido archivado → Glacier Deep Archive | |
| python3 cinetime_s3_demo.py reset → Regresa todo a S3 Standard (para repetir demo) | |
| python3 cinetime_s3_demo.py demo → Ejecuta los 5 escenarios secuencialmente | |
| """ | |
| import boto3 | |
| import sys | |
| import time | |
| import json | |
| from botocore.exceptions import ClientError | |
| # ───────────────────────────────────────────────────────────── | |
| # CONFIGURACIÓN — pega aquí las credenciales de tu Lab | |
| # ───────────────────────────────────────────────────────────── | |
| AWS_ACCESS_KEY_ID = "" | |
| AWS_SECRET_ACCESS_KEY = "" | |
| AWS_SESSION_TOKEN = "" | |
| AWS_REGION = "us-east-1" | |
| BUCKET = "MI_BUCKET" | |
| # ───────────────────────────────────────────────────────────── | |
| # COLORES ANSI — para output bonito en la terminal | |
| # ───────────────────────────────────────────────────────────── | |
| class C: | |
| RESET = "\033[0m" | |
| BOLD = "\033[1m" | |
| DIM = "\033[2m" | |
| # Colores de texto | |
| BLACK = "\033[30m" | |
| RED = "\033[31m" | |
| GREEN = "\033[32m" | |
| YELLOW = "\033[33m" | |
| BLUE = "\033[34m" | |
| MAGENTA = "\033[35m" | |
| CYAN = "\033[36m" | |
| WHITE = "\033[37m" | |
| # Colores brillantes | |
| BRED = "\033[91m" | |
| BGREEN = "\033[92m" | |
| BYELLOW = "\033[93m" | |
| BBLUE = "\033[94m" | |
| BMAGENTA= "\033[95m" | |
| BCYAN = "\033[96m" | |
| BWHITE = "\033[97m" | |
| # Fondo | |
| BG_BLUE = "\033[44m" | |
| BG_CYAN = "\033[46m" | |
| # ───────────────────────────────────────────────────────────── | |
| # INFORMACIÓN DE CLASES S3 | |
| # ───────────────────────────────────────────────────────────── | |
| STORAGE_CLASSES = { | |
| "STANDARD": { | |
| "nombre": "S3 Standard", | |
| "color": C.BBLUE, | |
| "costo_gb": 0.023, | |
| "acceso": "Milisegundos", | |
| "minimo": "Sin mínimo", | |
| "uso_ideal": "Datos de producción activos, alto acceso", | |
| "emoji": "⚡" | |
| }, | |
| "STANDARD_IA": { | |
| "nombre": "S3 Standard-IA", | |
| "color": C.BCYAN, | |
| "costo_gb": 0.0125, | |
| "acceso": "Milisegundos", | |
| "minimo": "30 días / 128 KB", | |
| "uso_ideal": "Datos poco frecuentes, alta durabilidad", | |
| "emoji": "📦" | |
| }, | |
| "INTELLIGENT_TIERING": { | |
| "nombre": "S3 Intelligent-Tiering", | |
| "color": C.BMAGENTA, | |
| "costo_gb": 0.023, | |
| "acceso": "Milisegundos", | |
| "minimo": "30 días", | |
| "uso_ideal": "Acceso impredecible, optimización automática", | |
| "emoji": "🧠" | |
| }, | |
| "ONEZONE_IA": { | |
| "nombre": "S3 One Zone-IA", | |
| "color": C.BGREEN, | |
| "costo_gb": 0.01, | |
| "acceso": "Milisegundos", | |
| "minimo": "30 días / 128 KB", | |
| "uso_ideal": "Datos reproducibles, una sola zona AZ", | |
| "emoji": "🗂️" | |
| }, | |
| "GLACIER_IR": { | |
| "nombre": "Glacier Instant Retrieval", | |
| "color": C.BYELLOW, | |
| "costo_gb": 0.004, | |
| "acceso": "Milisegundos", | |
| "minimo": "90 días / 128 KB", | |
| "uso_ideal": "Archivos médicos, acceso trimestral", | |
| "emoji": "❄️" | |
| }, | |
| "GLACIER": { | |
| "nombre": "Glacier Flexible Retrieval", | |
| "color": C.YELLOW, | |
| "costo_gb": 0.0036, | |
| "acceso": "1 min – 12 horas", | |
| "minimo": "90 días / 40 KB", | |
| "uso_ideal": "Backups, archivos RAW, recuperación planificada", | |
| "emoji": "🧊" | |
| }, | |
| "DEEP_ARCHIVE": { | |
| "nombre": "Glacier Deep Archive", | |
| "color": C.BRED, | |
| "costo_gb": 0.00099, | |
| "acceso": "12 – 48 horas", | |
| "minimo": "180 días / 40 KB", | |
| "uso_ideal": "Retención legal, archivo permanente", | |
| "emoji": "🏔️" | |
| }, | |
| } | |
| # ───────────────────────────────────────────────────────────── | |
| # ARCHIVOS DEMO — caso CineTime Streaming | |
| # ───────────────────────────────────────────────────────────── | |
| DEMO_FILES = { | |
| # ── Estrenos (< 90 días) — alta demanda, deben estar en Standard | |
| "estrenos/serie-sombra-del-dragon-ep01.mp4.demo": { | |
| "clase_inicial": "STANDARD", | |
| "escenario": 1, | |
| "clase_destino": "STANDARD_IA", | |
| "tamano_sim": "4.2 GB", | |
| "descripcion": "Serie original CineTime - Episodio 1 (ESTRENO)", | |
| "contenido": """[DEMO VIDEO] CineTime Original - La Sombra del Dragón EP01 | |
| Resolución: 4K HDR | Duración: 52 min | Tamaño real: ~4.2 GB | |
| Fecha estreno: 2026-01-05 | Días en catálogo: 78 días | |
| Estado: EN TEMPORADA DE ESTRENO — acceso masivo simultáneo | |
| Clase S3: STANDARD — miles de usuarios acceden concurrentemente | |
| Métrica: 2.3M reproducciones en los primeros 30 días""" | |
| }, | |
| "estrenos/serie-sombra-del-dragon-ep02.mp4.demo": { | |
| "clase_inicial": "STANDARD", | |
| "escenario": 1, | |
| "clase_destino": "STANDARD_IA", | |
| "tamano_sim": "4.5 GB", | |
| "descripcion": "Serie original CineTime - Episodio 2 (ESTRENO)", | |
| "contenido": """[DEMO VIDEO] CineTime Original - La Sombra del Dragón EP02 | |
| Resolución: 4K HDR | Duración: 58 min | Tamaño real: ~4.5 GB | |
| Fecha estreno: 2026-01-12 | Días en catálogo: 71 días | |
| Estado: EN TEMPORADA DE ESTRENO — acceso masivo simultáneo | |
| Clase S3: STANDARD — latencia mínima requerida para streaming""" | |
| }, | |
| "estrenos/cosmos-la-pelicula-4k.mp4.demo": { | |
| "clase_inicial": "STANDARD", | |
| "escenario": 1, | |
| "clase_destino": "STANDARD_IA", | |
| "tamano_sim": "18.7 GB", | |
| "descripcion": "Película original CineTime 4K (ESTRENO)", | |
| "contenido": """[DEMO VIDEO] CineTime Película - COSMOS: El Último Viaje | |
| Resolución: 4K Dolby Vision | Duración: 2h 18min | Tamaño real: ~18.7 GB | |
| Fecha estreno: 2026-02-14 | Días en catálogo: 38 días | |
| Estado: EN TEMPORADA DE ESTRENO — pico de demanda (San Valentín) | |
| Clase S3: STANDARD — rendimiento máximo, alta disponibilidad""" | |
| }, | |
| # ── Archivos RAW — masivos, acceso muy raro | |
| "raw/filming-sombra-dragon-ep01-raw.r3d.demo": { | |
| "clase_inicial": "STANDARD", | |
| "escenario": 2, | |
| "clase_destino": "GLACIER", | |
| "tamano_sim": "380 GB", | |
| "descripcion": "Footage RAW sin editar - EP01", | |
| "contenido": """[DEMO RAW] CineTime RAW Footage - La Sombra del Dragón EP01 | |
| Cámara: RED Komodo 6K | Formato: R3D RAW | Tamaño real: ~380 GB | |
| Fecha filmación: 2025-10-15 | Última vez accedido: 2025-12-01 | |
| Estado: ARCHIVO RAW — solo para re-ediciones o versiones especiales | |
| Clase S3: se moverá a GLACIER FLEXIBLE | |
| Advertencia: recuperación tomará 3-5 horas cuando se necesite""" | |
| }, | |
| "raw/filming-cosmos-broll-master.r3d.demo": { | |
| "clase_inicial": "STANDARD", | |
| "escenario": 2, | |
| "clase_destino": "GLACIER", | |
| "tamano_sim": "820 GB", | |
| "descripcion": "Footage RAW B-roll película Cosmos", | |
| "contenido": """[DEMO RAW] CineTime RAW B-Roll - COSMOS Película | |
| Cámara: ARRI Alexa 35 | Formato: ARRIRAW | Tamaño real: ~820 GB | |
| Fecha filmación: 2025-08-20 | Última vez accedido: 2025-11-15 | |
| Estado: ARCHIVO RAW — material descartado del corte final | |
| Nunca se accede excepto para: auditorías del director, versiones extendidas | |
| Guardar en Glacier Flexible: ahorro del 84% vs Standard""" | |
| }, | |
| "raw/filming-sombra-dragon-ep02-raw.r3d.demo": { | |
| "clase_inicial": "STANDARD", | |
| "escenario": 2, | |
| "clase_destino": "GLACIER", | |
| "tamano_sim": "420 GB", | |
| "descripcion": "Footage RAW sin editar - EP02", | |
| "contenido": """[DEMO RAW] CineTime RAW Footage - La Sombra del Dragón EP02 | |
| Cámara: RED Komodo 6K | Formato: R3D RAW | Tamaño real: ~420 GB | |
| Fecha filmación: 2025-10-22 | Última vez accedido: 2025-12-08 | |
| Caso de uso Glacier Flexible: si hay demanda de 'Director's Cut' en 2027, | |
| el equipo de post-producción solicita la restauración con 24h de anticipación.""" | |
| }, | |
| # ── Thumbnails y Material Promocional — acceso moderado/impredecible | |
| "thumbnails/sombra-dragon-poster-principal.jpg.demo": { | |
| "clase_inicial": "STANDARD", | |
| "escenario": 3, | |
| "clase_destino": "INTELLIGENT_TIERING", | |
| "tamano_sim": "2.4 MB", | |
| "descripcion": "Poster principal serie - material promocional", | |
| "contenido": """[DEMO THUMBNAIL] CineTime Asset - Sombra del Dragón Poster | |
| Dimensiones: 2000x3000px | Formato: JPEG optimizado | Tamaño: ~2.4 MB | |
| Uso: Página principal, apps móviles, smart TVs, redes sociales | |
| Patrón de acceso: VARIABLE — alto cuando la serie está en tendencia, | |
| bajo cuando pasa la temporada. Intelligent-Tiering lo maneja automáticamente. | |
| IT monitorea accesos y mueve entre tiers de forma transparente.""" | |
| }, | |
| "thumbnails/cosmos-thumbnail-16x9.jpg.demo": { | |
| "clase_inicial": "STANDARD", | |
| "escenario": 3, | |
| "clase_destino": "INTELLIGENT_TIERING", | |
| "tamano_sim": "1.8 MB", | |
| "descripcion": "Thumbnail película Cosmos (16:9)", | |
| "contenido": """[DEMO THUMBNAIL] CineTime Asset - COSMOS Thumbnail | |
| Dimensiones: 1920x1080px | Formato: JPEG | Tamaño: ~1.8 MB | |
| Uso: Grid de películas, reproductor embebido, push notifications | |
| Patrón de acceso: IMPREDECIBLE — depende de algoritmo de recomendaciones. | |
| Si la película aparece en 'Trending' → millones de requests. | |
| Si no → casi ninguno. Intelligent-Tiering es la clase ideal.""" | |
| }, | |
| "thumbnails/sombra-dragon-ep01-still.jpg.demo": { | |
| "clase_inicial": "STANDARD", | |
| "escenario": 3, | |
| "clase_destino": "INTELLIGENT_TIERING", | |
| "tamano_sim": "3.1 MB", | |
| "descripcion": "Still frame EP01 para redes sociales", | |
| "contenido": """[DEMO THUMBNAIL] CineTime Asset - Still EP01 | |
| Uso: Instagram, Twitter, previews en apps | |
| Patrón: Viral en estreno, luego acceso esporádico cuando hay menciones.""" | |
| }, | |
| # ── Catálogo General > 1 año — acceso moderado, patrón variable | |
| "catalogo/serie-neon-city-temporada1-completa.mp4.demo": { | |
| "clase_inicial": "STANDARD", | |
| "escenario": 4, | |
| "clase_destino": "INTELLIGENT_TIERING", | |
| "tamano_sim": "28 GB", | |
| "descripcion": "Serie establecida - 1 año en catálogo", | |
| "contenido": """[DEMO VIDEO] CineTime Catálogo - Neon City Temporada 1 | |
| Resolución: 1080p | Episodios: 10 | Tamaño total: ~28 GB | |
| Fecha estreno: 2025-01-15 | Tiempo en catálogo: 14 meses | |
| Acceso: MODERADO E IMPREDECIBLE — la serie puede volverse viral | |
| si aparece en recomendaciones, redes sociales o binge-watching. | |
| Intelligent-Tiering: mueve a Frequent Access tier automáticamente | |
| si el acceso aumenta. Sin intervención manual.""" | |
| }, | |
| "catalogo/pelicula-la-ultima-frontera-2024.mp4.demo": { | |
| "clase_inicial": "STANDARD", | |
| "escenario": 4, | |
| "clase_destino": "INTELLIGENT_TIERING", | |
| "tamano_sim": "12 GB", | |
| "descripcion": "Película de catálogo - 18 meses", | |
| "contenido": """[DEMO VIDEO] CineTime Catálogo - La Última Frontera (2024) | |
| Resolución: 4K | Duración: 1h 54min | Tamaño: ~12 GB | |
| Tiempo en catálogo: 18 meses | Patrón: esporádico con picos | |
| Intelligent-Tiering tiers: | |
| • Frequent Access: $0.023/GB (cuando hay acceso constante) | |
| • Infrequent Access: $0.0125/GB (después de 30 días sin acceso) | |
| • Archive Instant: $0.004/GB (después de 90 días sin acceso) | |
| Ideal para contenido cuyo patrón no puedes predecir.""" | |
| }, | |
| # ── Contenido fuera del catálogo activo — archivo permanente | |
| "archivado/serie-cancelada-pixel-wars-2022.mp4.demo": { | |
| "clase_inicial": "STANDARD", | |
| "escenario": 5, | |
| "clase_destino": "DEEP_ARCHIVE", | |
| "tamano_sim": "45 GB", | |
| "descripcion": "Serie cancelada - fuera del catálogo activo", | |
| "contenido": """[DEMO VIDEO] CineTime Archivado - Pixel Wars Temporada 1 | |
| Resolución: 1080p | Episodios: 8 | Tamaño: ~45 GB | |
| Fecha cancelación: 2023-06-01 | Estado: FUERA DEL CATÁLOGO | |
| Razón del archivo: serie cancelada por bajo rating | |
| ¿Cuándo se necesitará de nuevo? | |
| → Si se vende la licencia a otro servicio (improbable pero posible) | |
| → Si hay una producción de 'revival' en el futuro | |
| → Para auditorías de derechos de autor | |
| Glacier Deep Archive: $0.00099/GB — el más económico de AWS | |
| Recuperación: 12-48 horas (aceptable para estos casos de uso)""" | |
| }, | |
| "archivado/documental-marea-roja-2021.mp4.demo": { | |
| "clase_inicial": "STANDARD", | |
| "escenario": 5, | |
| "clase_destino": "DEEP_ARCHIVE", | |
| "tamano_sim": "8.5 GB", | |
| "descripcion": "Documental - licencia expirada", | |
| "contenido": """[DEMO VIDEO] CineTime Archivado - Marea Roja (Documental 2021) | |
| Resolución: 4K | Duración: 1h 28min | Tamaño: ~8.5 GB | |
| Razón del archivo: licencia de distribución expirada en 2024 | |
| Conservar por: posible renovación de licencia, derechos residuales | |
| Glacier Deep Archive ahorra 95.7% vs S3 Standard en este archivo.""" | |
| }, | |
| # ── Metadatos — SIEMPRE en Standard, nunca se mueven | |
| "metadatos/catalogo-index-v2.json.demo": { | |
| "clase_inicial": "STANDARD", | |
| "escenario": None, | |
| "clase_destino": None, | |
| "tamano_sim": "12 MB", | |
| "descripcion": "Índice del catálogo - hot path del sistema", | |
| "contenido": """[DEMO METADATA] CineTime - Índice de Catálogo | |
| {"total_titulos": 4827, "series": 312, "peliculas": 891, "documentales": 156} | |
| CLASE: S3 STANDARD — NUNCA MOVER A OTRA CLASE | |
| Por qué: este archivo se lee en CADA búsqueda del usuario. | |
| El reproductor lo consulta antes de mostrar cualquier contenido. | |
| Moverlo a IA o Glacier rompería la experiencia de usuario.""" | |
| }, | |
| "metadatos/subtitulos-es-mx-pack.srt.demo": { | |
| "clase_inicial": "STANDARD", | |
| "escenario": None, | |
| "clase_destino": None, | |
| "tamano_sim": "450 MB", | |
| "descripcion": "Pack de subtítulos ES-MX - se sirve en cada reproducción", | |
| "contenido": """[DEMO METADATA] CineTime - Subtítulos Español México | |
| Cobertura: 89% del catálogo | Idiomas: ES-MX, ES-ES, EN-US | |
| Se leen en CADA reproducción de cualquier título con subtítulos. | |
| S3 Standard es obligatorio — latencia de milisegundos requerida.""" | |
| }, | |
| } | |
| # ───────────────────────────────────────────────────────────── | |
| # CLIENTE S3 | |
| # ───────────────────────────────────────────────────────────── | |
| def get_s3_client(): | |
| return boto3.client( | |
| "s3", | |
| region_name=AWS_REGION, | |
| aws_access_key_id=AWS_ACCESS_KEY_ID, | |
| aws_secret_access_key=AWS_SECRET_ACCESS_KEY, | |
| aws_session_token=AWS_SESSION_TOKEN, | |
| ) | |
| # ───────────────────────────────────────────────────────────── | |
| # HELPERS DE IMPRESIÓN | |
| # ───────────────────────────────────────────────────────────── | |
| def sep(char="─", width=70, color=C.DIM): | |
| print(f"{color}{char * width}{C.RESET}") | |
| def print_header(titulo, subtitulo=""): | |
| print() | |
| sep("═", 70, C.BBLUE) | |
| print(f"{C.BOLD}{C.BBLUE} {titulo}{C.RESET}") | |
| if subtitulo: | |
| print(f"{C.DIM} {subtitulo}{C.RESET}") | |
| sep("═", 70, C.BBLUE) | |
| print() | |
| def print_step(num, texto): | |
| print(f"\n{C.BOLD}{C.BYELLOW} [{num}] {texto}{C.RESET}") | |
| def print_api(llamada, params=""): | |
| print(f"\n {C.DIM}📡 LLAMADA A LA API DE AWS:{C.RESET}") | |
| print(f" {C.BGREEN}{llamada}{C.RESET}") | |
| if params: | |
| for linea in params.strip().split("\n"): | |
| print(f" {C.DIM} {linea}{C.RESET}") | |
| def print_ok(mensaje): | |
| print(f" {C.BGREEN}✓ {mensaje}{C.RESET}") | |
| def print_info(mensaje): | |
| print(f" {C.CYAN}ℹ {mensaje}{C.RESET}") | |
| def print_warn(mensaje): | |
| print(f" {C.BYELLOW}⚠ {mensaje}{C.RESET}") | |
| def print_err(mensaje): | |
| print(f" {C.BRED}✗ {mensaje}{C.RESET}") | |
| def print_concepto(titulo, texto): | |
| sep("─", 70, C.DIM) | |
| print(f" {C.BOLD}{C.BMAGENTA}💡 CONCEPTO: {titulo}{C.RESET}") | |
| for linea in texto.strip().split("\n"): | |
| print(f" {C.DIM}{linea}{C.RESET}") | |
| sep("─", 70, C.DIM) | |
| def badge_clase(clase_id): | |
| info = STORAGE_CLASSES.get(clase_id, {"nombre": clase_id, "color": C.WHITE, "emoji": "?"}) | |
| return f"{info['color']}{C.BOLD}{info['emoji']} {info['nombre']}{C.RESET}" | |
| def print_comparacion_costo(clase_origen, clase_destino, tamano_gb=100): | |
| o = STORAGE_CLASSES.get(clase_origen, {}) | |
| d = STORAGE_CLASSES.get(clase_destino, {}) | |
| if not o or not d: | |
| return | |
| costo_o = o["costo_gb"] * tamano_gb | |
| costo_d = d["costo_gb"] * tamano_gb | |
| ahorro = costo_o - costo_d | |
| pct = ((costo_o - costo_d) / costo_o * 100) if costo_o > 0 else 0 | |
| sep("─", 70, C.DIM) | |
| print(f" {C.BOLD}💰 COMPARACIÓN DE COSTO (simulando {tamano_gb} GB/mes):{C.RESET}") | |
| print(f" {badge_clase(clase_origen):<50} ${costo_o:>7.2f}/mes") | |
| print(f" {badge_clase(clase_destino):<50} ${costo_d:>7.2f}/mes") | |
| print(f" {C.BGREEN}{C.BOLD} Ahorro mensual: ${ahorro:.2f} ({pct:.0f}% menos){C.RESET}") | |
| sep("─", 70, C.DIM) | |
| # ───────────────────────────────────────────────────────────── | |
| # OBTENER CLASE ACTUAL DE UN OBJETO | |
| # ───────────────────────────────────────────────────────────── | |
| def get_storage_class(s3, key): | |
| try: | |
| resp = s3.head_object(Bucket=BUCKET, Key=key) | |
| clase = resp.get("StorageClass", "STANDARD") | |
| return clase | |
| except ClientError as e: | |
| if e.response["Error"]["Code"] == "404": | |
| return None | |
| raise | |
| # ───────────────────────────────────────────────────────────── | |
| # MOVER OBJETO A NUEVA CLASE (copy_object en el mismo key) | |
| # ───────────────────────────────────────────────────────────── | |
| def mover_a_clase(s3, key, clase_destino, verbose=True): | |
| clase_actual = get_storage_class(s3, key) | |
| if clase_actual is None: | |
| print_err(f"Objeto no encontrado: {key}") | |
| return False | |
| nombre_archivo = key.split("/")[-1] | |
| info_dest = STORAGE_CLASSES.get(clase_destino, {}) | |
| if verbose: | |
| print() | |
| sep("─", 70, C.DIM) | |
| print(f" {C.BOLD}{C.BWHITE}📄 Archivo: {C.CYAN}{nombre_archivo}{C.RESET}") | |
| print(f" {C.DIM} Ruta completa: s3://{BUCKET}/{key}{C.RESET}") | |
| print() | |
| print(f" {'Clase actual:':20} {badge_clase(clase_actual)}") | |
| print(f" {'Clase destino:':20} {badge_clase(clase_destino)}") | |
| print(f" {'Tiempo de acceso:':20} {C.DIM}{info_dest.get('acceso','?')}{C.RESET}") | |
| print(f" {'Costo por GB:':20} {C.BGREEN}${info_dest.get('costo_gb', 0):.5f}/GB/mes{C.RESET}") | |
| print(f" {'Almacenamiento mínimo:':20} {C.DIM}{info_dest.get('minimo','?')}{C.RESET}") | |
| if clase_actual == clase_destino: | |
| if verbose: | |
| print_warn(f"Ya está en {clase_destino}, no se necesita mover.") | |
| return True | |
| if verbose: | |
| print_api( | |
| "s3.copy_object(", | |
| f'Bucket="{BUCKET}",\n' | |
| f'CopySource={{"Bucket": "{BUCKET}", "Key": "{key}"}},\n' | |
| f'Key="{key}",\n' | |
| f'StorageClass="{clase_destino}",\n' | |
| f'MetadataDirective="COPY"' | |
| f"\n)" | |
| ) | |
| print_info("En S3 no existe un comando 'mover' o 'cambiar clase'.") | |
| print_info("Se COPIA el objeto sobre sí mismo con la nueva clase.") | |
| print_info("El objeto original se reemplaza. No hay costo adicional de almacenamiento.") | |
| print() | |
| try: | |
| s3.copy_object( | |
| Bucket=BUCKET, | |
| CopySource={"Bucket": BUCKET, "Key": key}, | |
| Key=key, | |
| StorageClass=clase_destino, | |
| MetadataDirective="COPY", | |
| ) | |
| if verbose: | |
| print_ok(f"Objeto movido exitosamente → {badge_clase(clase_destino)}") | |
| # Verificar | |
| clase_nueva = get_storage_class(s3, key) | |
| if verbose: | |
| print_api("s3.head_object( ← verificando el cambio", f'Bucket="{BUCKET}", Key="{key}")') | |
| print_ok(f"Verificado: StorageClass = {C.BOLD}{clase_nueva}{C.RESET}") | |
| return True | |
| except ClientError as e: | |
| if verbose: | |
| print_err(f"Error AWS: {e.response['Error']['Code']} - {e.response['Error']['Message']}") | |
| return False | |
| # ───────────────────────────────────────────────────────────── | |
| # COMANDO: setup | |
| # ───────────────────────────────────────────────────────────── | |
| def cmd_setup(): | |
| print_header( | |
| "SETUP — Creando bucket y archivos demo de CineTime", | |
| "Empresa de Medios / Streaming" | |
| ) | |
| s3 = get_s3_client() | |
| # Crear bucket | |
| print_step("1", f"Crear bucket: {C.BCYAN}{BUCKET}{C.RESET}") | |
| print_api("s3.create_bucket(", f'Bucket="{BUCKET}", Region="{AWS_REGION}")') | |
| try: | |
| s3.create_bucket(Bucket=BUCKET) | |
| print_ok(f"Bucket creado: {BUCKET}") | |
| except ClientError as e: | |
| code = e.response["Error"]["Code"] | |
| if code in ("BucketAlreadyOwnedByYou", "BucketAlreadyExists"): | |
| print_warn("El bucket ya existe — continuando con la carga de archivos.") | |
| else: | |
| print_err(f"Error creando bucket: {code}") | |
| return | |
| # Bloquear acceso público | |
| print_step("2", "Bloquear acceso público al bucket") | |
| print_api("s3.put_public_access_block(...)", "BlockPublicAcls=True, RestrictPublicBuckets=True") | |
| s3.put_public_access_block( | |
| Bucket=BUCKET, | |
| PublicAccessBlockConfiguration={ | |
| "BlockPublicAcls": True, | |
| "IgnorePublicAcls": True, | |
| "BlockPublicPolicy": True, | |
| "RestrictPublicBuckets": True, | |
| } | |
| ) | |
| print_ok("Acceso público bloqueado") | |
| # Subir archivos demo | |
| print_step("3", f"Subiendo {len(DEMO_FILES)} archivos demo a S3 Standard") | |
| print_concepto( | |
| "Por qué todos comienzan en S3 Standard", | |
| "Cuando un archivo se CREA en S3, normalmente va a Standard.\n" | |
| "Las transiciones a otras clases se hacen después (manualmente\n" | |
| "o con Lifecycle Policies) según cómo evoluciona el patrón de acceso." | |
| ) | |
| subidos = 0 | |
| for key, info in DEMO_FILES.items(): | |
| prefijo = key.split("/")[0].upper() | |
| color_prefijo = {"ESTRENOS": C.BBLUE, "RAW": C.YELLOW, "THUMBNAILS": C.BMAGENTA, | |
| "CATALOGO": C.BCYAN, "ARCHIVADO": C.BRED, "METADATOS": C.BGREEN}.get(prefijo, C.WHITE) | |
| print(f"\n {color_prefijo}[{prefijo}]{C.RESET} {C.DIM}{key}{C.RESET}") | |
| print(f" {C.DIM} → Tamaño simulado: {info['tamano_sim']} | {info['descripcion']}{C.RESET}") | |
| print_api("s3.put_object(", f'Bucket="{BUCKET}", Key="{key}", StorageClass="STANDARD")') | |
| try: | |
| s3.put_object( | |
| Bucket=BUCKET, | |
| Key=key, | |
| Body=info["contenido"].encode("utf-8"), | |
| ContentType="text/plain", | |
| StorageClass="STANDARD", | |
| Metadata={ | |
| "tamano-simulado": info["tamano_sim"], | |
| "escenario": str(info["escenario"] or "N/A"), | |
| "clase-destino": info["clase_destino"] or "permanente", | |
| } | |
| ) | |
| print_ok(f"{badge_clase('STANDARD')} — subido correctamente") | |
| subidos += 1 | |
| except ClientError as e: | |
| print_err(f"Error: {e.response['Error']['Message']}") | |
| print() | |
| sep("═", 70, C.BGREEN) | |
| print(f" {C.BOLD}{C.BGREEN}✓ Setup completo: {subidos}/{len(DEMO_FILES)} archivos en s3://{BUCKET}/{C.RESET}") | |
| print(f" {C.DIM} Todos en S3 Standard. Ejecuta los escenarios para 'moverlos'.{C.RESET}") | |
| sep("═", 70, C.BGREEN) | |
| print() | |
| # ───────────────────────────────────────────────────────────── | |
| # COMANDO: listar | |
| # ───────────────────────────────────────────────────────────── | |
| def cmd_listar(): | |
| print_header( | |
| "ESTADO ACTUAL — Objetos en S3", | |
| f"Bucket: {BUCKET}" | |
| ) | |
| s3 = get_s3_client() | |
| print_api("s3.list_objects_v2(", f'Bucket="{BUCKET}")') | |
| print_info("head_object() se llama por cada objeto para obtener su StorageClass.") | |
| print() | |
| try: | |
| resp = s3.list_objects_v2(Bucket=BUCKET) | |
| except ClientError as e: | |
| print_err(f"No se puede listar el bucket: {e.response['Error']['Message']}") | |
| print_warn("Ejecuta primero: python3 cinetime_s3_demo.py setup") | |
| return | |
| objetos = resp.get("Contents", []) | |
| if not objetos: | |
| print_warn("El bucket está vacío. Ejecuta: python3 cinetime_s3_demo.py setup") | |
| return | |
| prefijo_actual = "" | |
| total_costo = 0 | |
| for obj in sorted(objetos, key=lambda x: x["Key"]): | |
| key = obj["Key"] | |
| prefijo = key.split("/")[0] | |
| if prefijo != prefijo_actual: | |
| prefijo_actual = prefijo | |
| print() | |
| print(f" {C.BOLD}{C.BWHITE}📁 {prefijo.upper()}/{C.RESET}") | |
| sep("─", 70, C.DIM) | |
| clase = get_storage_class(s3, key) | |
| info_demo = DEMO_FILES.get(key, {}) | |
| nombre = key.split("/")[-1] | |
| tamano = info_demo.get("tamano_sim", "?") | |
| info_clase = STORAGE_CLASSES.get(clase, {"costo_gb": 0, "acceso": "?", "emoji": "?"}) | |
| esc = info_demo.get("escenario") | |
| esc_label = f"{C.DIM}[Esc.{esc}]{C.RESET}" if esc else f"{C.BGREEN}[FIJO]{C.RESET}" | |
| print(f" {esc_label} {C.CYAN}{nombre:<45}{C.RESET} {badge_clase(clase)}") | |
| print(f" {C.DIM}Tamaño: {tamano:>10} | Acceso: {info_clase['acceso']:<20} | " | |
| f"${info_clase['costo_gb']:.5f}/GB{C.RESET}") | |
| print() | |
| sep("═", 70, C.BBLUE) | |
| print(f" {C.BOLD}{len(objetos)} objetos en total.{C.RESET}") | |
| print(f" {C.DIM}[Esc.N] = se moverá en el Escenario N | [FIJO] = siempre en Standard{C.RESET}") | |
| sep("═", 70, C.BBLUE) | |
| print() | |
| # ───────────────────────────────────────────────────────────── | |
| # ESCENARIO 1 — Estrenos → Standard-IA | |
| # ───────────────────────────────────────────────────────────── | |
| def cmd_escenario_1(): | |
| print_header( | |
| "ESCENARIO 1 — Videos en Estreno → S3 Standard-IA", | |
| "Después de 90 días, el volumen de acceso cae drásticamente" | |
| ) | |
| s3 = get_s3_client() | |
| print_concepto( | |
| "¿Por qué mover los estrenos a Standard-IA después de 90 días?", | |
| "Día 0-90: La serie es NUEVA. Miles de usuarios la ven simultáneamente.\n" | |
| " Necesitamos S3 Standard: baja latencia, alto throughput.\n" | |
| "\n" | |
| "Día 90+: El 'pico de estreno' terminó. El acceso es esporádico.\n" | |
| " Los usuarios que NO vieron la serie en el estreno la buscan\n" | |
| " ocasionalmente. Standard-IA cuesta 46% menos que Standard.\n" | |
| "\n" | |
| "Clave de Standard-IA: pagas por GB almacenado (más barato) + por GET\n" | |
| " Si el acceso es frecuente, Standard es más económico.\n" | |
| " Si es infrecuente, Standard-IA gana." | |
| ) | |
| archivos = {k: v for k, v in DEMO_FILES.items() if v["escenario"] == 1} | |
| print_step("1", f"Moviendo {len(archivos)} archivos de estreno a Standard-IA...") | |
| print_info(f"Simulando: 'Han pasado 90 días desde el estreno de La Sombra del Dragón'") | |
| for key, info in archivos.items(): | |
| mover_a_clase(s3, key, "STANDARD_IA") | |
| time.sleep(0.3) | |
| print_comparacion_costo("STANDARD", "STANDARD_IA", tamano_gb=50) | |
| print_concepto( | |
| "¿Qué pasa si el usuario accede a un objeto en Standard-IA?", | |
| "El acceso sigue siendo en MILISEGUNDOS — igual que Standard.\n" | |
| "La diferencia: se cobra un fee adicional por GET (~$0.01 por 1000 requests).\n" | |
| "Por eso Standard-IA sólo conviene cuando los accesos son POCO FRECUENTES.\n" | |
| "Si hay muchos accesos, el fee de GET puede superar el ahorro en storage." | |
| ) | |
| print() | |
| # ───────────────────────────────────────────────────────────── | |
| # ESCENARIO 2 — RAW → Glacier Flexible Retrieval | |
| # ───────────────────────────────────────────────────────────── | |
| def cmd_escenario_2(): | |
| print_header( | |
| "ESCENARIO 2 — Archivos RAW → Glacier Flexible Retrieval", | |
| "~500 TB de footage sin editar que raramente se necesita" | |
| ) | |
| s3 = get_s3_client() | |
| print_concepto( | |
| "¿Por qué Glacier Flexible para los RAW y no Glacier Deep Archive?", | |
| "Los archivos RAW son los más grandes del negocio (~500 TB).\n" | |
| "Acceso: MUY RARO pero no imposible.\n" | |
| "\n" | |
| "Casos en que se necesitan:\n" | |
| " • El director quiere hacer un 'Director's Cut' (puede ocurrir)\n" | |
| " • Se vende el contenido a otro mercado y necesitan re-editar\n" | |
| " • Auditoría de derechos de autor\n" | |
| "\n" | |
| "Glacier Flexible: recuperación en 1-5 min (Expedited) o 3-5h (Standard).\n" | |
| "Glacier Deep Archive: 12-48 horas.\n" | |
| "\n" | |
| "Para producción de contenido, 3-5h es aceptable (se planifica con anticipación).\n" | |
| "12-48h podría retrasar una producción urgente." | |
| ) | |
| archivos = {k: v for k, v in DEMO_FILES.items() if v["escenario"] == 2} | |
| print_step("1", f"Moviendo {len(archivos)} archivos RAW a Glacier Flexible...") | |
| print_warn("⚠ IMPORTANTE: Una vez en Glacier, NO puedes leer el archivo directamente.") | |
| print_warn(" Debes hacer una 'restauración temporal' antes de descargarlo.") | |
| print_warn(" La restauración crea una copia temporal en Standard por N días.") | |
| print() | |
| for key, info in archivos.items(): | |
| mover_a_clase(s3, key, "GLACIER") | |
| time.sleep(0.3) | |
| print_comparacion_costo("STANDARD", "GLACIER", tamano_gb=500) | |
| print_concepto( | |
| "¿Cómo se recupera un archivo de Glacier Flexible?", | |
| "1. s3.restore_object(Bucket=..., Key=..., RestoreRequest={\n" | |
| " 'Days': 7, # días que estará disponible la copia\n" | |
| " 'GlacierJobParameters': {'Tier': 'Standard'} # 3-5h\n" | |
| " })\n" | |
| "2. Esperar 3-5 horas (Standard) o 1-5 min (Expedited, más caro)\n" | |
| "3. Descargar el archivo normalmente con s3.get_object()\n" | |
| "4. Después de 'Days' días, la copia temporal se elimina automáticamente." | |
| ) | |
| print() | |
| # ───────────────────────────────────────────────────────────── | |
| # ESCENARIO 3 — Thumbnails → Intelligent-Tiering | |
| # ───────────────────────────────────────────────────────────── | |
| def cmd_escenario_3(): | |
| print_header( | |
| "ESCENARIO 3 — Thumbnails y Material Promocional → Intelligent-Tiering", | |
| "Acceso impredecible: alto cuando hay tendencia, bajo el resto del tiempo" | |
| ) | |
| s3 = get_s3_client() | |
| print_concepto( | |
| "¿Por qué Intelligent-Tiering para thumbnails?", | |
| "Los thumbnails tienen un patrón de acceso que NO se puede predecir:\n" | |
| "\n" | |
| " Semana normal: 500K requests/día (moderado)\n" | |
| " Si sale en Trending: 15M requests/día (explosivo)\n" | |
| " Después de tendencia: 200K requests/día (cae)\n" | |
| "\n" | |
| "Con una regla fija de Lifecycle: pagarías por accesos infrecuentes\n" | |
| " cuando en realidad el acceso es impredecible.\n" | |
| "\n" | |
| "Intelligent-Tiering MONITOREA el patrón automáticamente:\n" | |
| " • 0-30 días sin acceso → mueve a Infrequent Access ($0.0125/GB)\n" | |
| " • Nuevo acceso → vuelve a Frequent Access ($0.023/GB)\n" | |
| " Sin latencia adicional, sin necesidad de restaurar." | |
| ) | |
| archivos = {k: v for k, v in DEMO_FILES.items() if v["escenario"] == 3} | |
| print_step("1", f"Moviendo {len(archivos)} assets de thumbnails a Intelligent-Tiering...") | |
| for key, info in archivos.items(): | |
| mover_a_clase(s3, key, "INTELLIGENT_TIERING") | |
| time.sleep(0.3) | |
| print_comparacion_costo("STANDARD", "INTELLIGENT_TIERING", tamano_gb=2) | |
| print_concepto( | |
| "Diferencia entre IT y Standard-IA", | |
| "Standard-IA: Tú decides cuándo mover. Pagas GET fee siempre.\n" | |
| "Intelligent-Tiering: AWS decide cuándo mover. Sin GET fee adicional.\n" | |
| " Cobra $0.0025/1000 objetos por monitoreo.\n" | |
| "\n" | |
| "Para objetos pequeños (<128 KB) IT no conviene (fee de monitoreo\n" | |
| "puede superar el ahorro). Para archivos grandes como thumbnails HD: sí." | |
| ) | |
| print() | |
| # ───────────────────────────────────────────────────────────── | |
| # ESCENARIO 4 — Catálogo General → Intelligent-Tiering | |
| # ───────────────────────────────────────────────────────────── | |
| def cmd_escenario_4(): | |
| print_header( | |
| "ESCENARIO 4 — Catálogo General (>1 año) → Intelligent-Tiering", | |
| "Series y películas establecidas con patrón de acceso variable" | |
| ) | |
| s3 = get_s3_client() | |
| print_concepto( | |
| "¿Por qué no Standard-IA para el catálogo antiguo?", | |
| "Standard-IA cobra por cada GET (lectura del archivo).\n" | |
| "Un usuario que ve una película de 2h genera MUCHOS requests GET\n" | |
| " (el reproductor pide chunks del archivo en streaming).\n" | |
| "\n" | |
| "Si hay 1,000 usuarios simultáneos viendo la misma película:\n" | |
| " → Millones de GET requests → el fee de IA puede ser enorme.\n" | |
| "\n" | |
| "Intelligent-Tiering NO cobra fee por GET.\n" | |
| "Si la película se vuelve viral (trending en redes), IT la mueve\n" | |
| " automáticamente a Frequent Access tier.\n" | |
| "Cuando el acceso baja, vuelve a Infrequent Access.\n" | |
| "Todo automático, sin intervención manual." | |
| ) | |
| archivos = {k: v for k, v in DEMO_FILES.items() if v["escenario"] == 4} | |
| print_step("1", f"Moviendo {len(archivos)} títulos del catálogo a Intelligent-Tiering...") | |
| for key, info in archivos.items(): | |
| mover_a_clase(s3, key, "INTELLIGENT_TIERING") | |
| time.sleep(0.3) | |
| print_comparacion_costo("STANDARD", "INTELLIGENT_TIERING", tamano_gb=200) | |
| print_concepto( | |
| "Los 3 tiers de Intelligent-Tiering", | |
| "Tier 1: Frequent Access → $0.023/GB (como Standard)\n" | |
| "Tier 2: Infrequent Access → $0.0125/GB (tras 30 días sin acceso)\n" | |
| "Tier 3: Archive Instant Access → $0.004/GB (tras 90 días sin acceso)\n" | |
| "\n" | |
| "Si el usuario accede al Tier 3, se recupera en MILISEGUNDOS\n" | |
| "y el objeto sube automáticamente al Tier 1. Transparente para la app." | |
| ) | |
| print() | |
| # ───────────────────────────────────────────────────────────── | |
| # ESCENARIO 5 — Archivado → Glacier Deep Archive | |
| # ───────────────────────────────────────────────────────────── | |
| def cmd_escenario_5(): | |
| print_header( | |
| "ESCENARIO 5 — Contenido Fuera del Catálogo → Glacier Deep Archive", | |
| "Series canceladas y licencias expiradas — archivo permanente" | |
| ) | |
| s3 = get_s3_client() | |
| print_concepto( | |
| "¿Cuándo usar Glacier Deep Archive?", | |
| "Casos de uso:\n" | |
| " ✓ Contenido que NO estará disponible para usuarios en el futuro cercano\n" | |
| " ✓ Conservación obligatoria por contrato o ley (derechos de autor)\n" | |
| " ✓ Backups de DR que solo se usan en catástrofes\n" | |
| " ✓ Datos históricos para análisis eventual\n" | |
| "\n" | |
| "Casos donde NO usar Deep Archive:\n" | |
| " ✗ Si hay chance de que el usuario lo pida en las próximas horas\n" | |
| " ✗ Urgencias médicas, legales o de producción\n" | |
| " ✗ Datos que se analizan regularmente\n" | |
| "\n" | |
| "Costo: $0.00099/GB/mes = $0.99 por TB/mes\n" | |
| " S3 Standard cuesta $23 por TB/mes\n" | |
| " Ahorro: 95.7% — dramático para petabytes." | |
| ) | |
| archivos = {k: v for k, v in DEMO_FILES.items() if v["escenario"] == 5} | |
| print_step("1", f"Moviendo {len(archivos)} títulos a Glacier Deep Archive...") | |
| print_warn("⚠ Glacier Deep Archive: recuperación toma 12-48 horas.") | |
| print_warn(" Solo para casos donde el tiempo de espera es aceptable.") | |
| print() | |
| for key, info in archivos.items(): | |
| mover_a_clase(s3, key, "DEEP_ARCHIVE") | |
| time.sleep(0.3) | |
| print_comparacion_costo("STANDARD", "DEEP_ARCHIVE", tamano_gb=1000) | |
| print_concepto( | |
| "¿Cómo recuperar de Deep Archive? (cuando se necesite relicenciar)", | |
| "s3.restore_object(\n" | |
| " Bucket=BUCKET,\n" | |
| " Key='archivado/serie-cancelada-pixel-wars-2022.mp4.demo',\n" | |
| " RestoreRequest={\n" | |
| " 'Days': 30, # disponible 30 días\n" | |
| " 'GlacierJobParameters': {'Tier': 'Standard'} # 12h\n" | |
| " }\n" | |
| ")\n" | |
| "\n" | |
| "Opción 'Bulk' (más barata): hasta 48h, $0.0025/GB\n" | |
| "Opción 'Standard': 12h, $0.02/GB" | |
| ) | |
| print() | |
| # ───────────────────────────────────────────────────────────── | |
| # COMANDO: reset | |
| # ───────────────────────────────────────────────────────────── | |
| def cmd_reset(): | |
| print_header( | |
| "RESET — Regresando todos los objetos a S3 Standard", | |
| "Para poder repetir la demo desde el inicio" | |
| ) | |
| s3 = get_s3_client() | |
| print_warn("Todos los objetos (excepto metadatos) volverán a S3 Standard.") | |
| print_info("Útil para repetir la demo en clase.\n") | |
| movidos = 0 | |
| for key in DEMO_FILES.keys(): | |
| clase_actual = get_storage_class(s3, key) | |
| if clase_actual and clase_actual != "STANDARD": | |
| nombre = key.split("/")[-1] | |
| print(f" {C.DIM}{nombre:<50}{C.RESET} {badge_clase(clase_actual)} → ", end="") | |
| try: | |
| s3.copy_object( | |
| Bucket=BUCKET, | |
| CopySource={"Bucket": BUCKET, "Key": key}, | |
| Key=key, | |
| StorageClass="STANDARD", | |
| MetadataDirective="COPY", | |
| ) | |
| print(f"{badge_clase('STANDARD')} {C.BGREEN}✓{C.RESET}") | |
| movidos += 1 | |
| except ClientError as e: | |
| print(f"{C.BRED}✗ {e.response['Error']['Code']}{C.RESET}") | |
| else: | |
| pass | |
| print() | |
| print_ok(f"Reset completo: {movidos} objetos regresaron a S3 Standard.") | |
| print() | |
| # ───────────────────────────────────────────────────────────── | |
| # COMANDO: demo completo | |
| # ───────────────────────────────────────────────────────────── | |
| def cmd_demo_completo(): | |
| print_header( | |
| "DEMO COMPLETO — Los 5 escenarios de CineTime", | |
| "Ejecutando toda la estrategia de almacenamiento secuencialmente" | |
| ) | |
| print_info("Este demo simula lo que ocurriría automáticamente con Lifecycle Policies.") | |
| print_info("Aquí lo hacemos MANUAL para entender cada operación.") | |
| print() | |
| escenarios = [ | |
| (1, "Estrenos (>90 días) → Standard-IA", cmd_escenario_1), | |
| (2, "Archivos RAW → Glacier Flexible", cmd_escenario_2), | |
| (3, "Thumbnails → Intelligent-Tiering", cmd_escenario_3), | |
| (4, "Catálogo General → Intelligent-Tiering", cmd_escenario_4), | |
| (5, "Contenido Archivado → Deep Archive", cmd_escenario_5), | |
| ] | |
| for num, titulo, fn in escenarios: | |
| print(f"\n {C.BOLD}{C.BYELLOW}━━━ Escenario {num}: {titulo} ━━━{C.RESET}") | |
| fn() | |
| time.sleep(1) | |
| print() | |
| sep("═", 70, C.BGREEN) | |
| print(f" {C.BOLD}{C.BGREEN}✓ DEMO COMPLETO{C.RESET}") | |
| print(f" {C.DIM} Ejecuta 'listar' para ver el estado final de todos los objetos.{C.RESET}") | |
| print(f" {C.DIM} Ejecuta 'reset' para volver al estado inicial y repetir la demo.{C.RESET}") | |
| sep("═", 70, C.BGREEN) | |
| print() | |
| # ───────────────────────────────────────────────────────────── | |
| # MAIN | |
| # ───────────────────────────────────────────────────────────── | |
| def print_uso(): | |
| print(f""" | |
| {C.BOLD}{C.BBLUE}╔══════════════════════════════════════════════════════════════════════╗ | |
| ║ CineTime — Demo de Clases de Almacenamiento S3 ║ | |
| ║ AWS re/Start | Práctica Manual de Storage Classes ║ | |
| ╚══════════════════════════════════════════════════════════════════════╝{C.RESET} | |
| {C.BOLD}Uso:{C.RESET} | |
| {C.CYAN}python3 cinetime_s3_demo.py{C.RESET} {C.BYELLOW}<comando>{C.RESET} {C.DIM}[argumento]{C.RESET} | |
| {C.BOLD}Comandos:{C.RESET} | |
| {C.BYELLOW}setup{C.RESET} Crea el bucket y sube todos los archivos demo en S3 Standard | |
| {C.BYELLOW}listar{C.RESET} Muestra el estado actual (clase S3) de cada objeto | |
| {C.BYELLOW}escenario 1{C.RESET} Estrenos (>90 días) → {badge_clase('STANDARD_IA')} | |
| {C.BYELLOW}escenario 2{C.RESET} Archivos RAW → {badge_clase('GLACIER')} | |
| {C.BYELLOW}escenario 3{C.RESET} Thumbnails → {badge_clase('INTELLIGENT_TIERING')} | |
| {C.BYELLOW}escenario 4{C.RESET} Catálogo general → {badge_clase('INTELLIGENT_TIERING')} | |
| {C.BYELLOW}escenario 5{C.RESET} Contenido archivado → {badge_clase('DEEP_ARCHIVE')} | |
| {C.BYELLOW}reset{C.RESET} Regresa todos los objetos a S3 Standard (para repetir) | |
| {C.BYELLOW}demo{C.RESET} Ejecuta los 5 escenarios secuencialmente | |
| {C.BOLD}Orden recomendado en clase:{C.RESET} | |
| 1. {C.DIM}python3 cinetime_s3_demo.py setup{C.RESET} | |
| 2. {C.DIM}python3 cinetime_s3_demo.py listar{C.RESET} | |
| 3. {C.DIM}python3 cinetime_s3_demo.py escenario 1{C.RESET} ← discutir con los alumnos | |
| 4. {C.DIM}python3 cinetime_s3_demo.py escenario 2{C.RESET} ← etc. | |
| 5. {C.DIM}python3 cinetime_s3_demo.py listar{C.RESET} ← ver el resultado final | |
| """) | |
| def main(): | |
| args = sys.argv[1:] | |
| if not args: | |
| print_uso() | |
| return | |
| cmd = args[0].lower() | |
| if cmd == "setup": | |
| cmd_setup() | |
| elif cmd == "listar": | |
| cmd_listar() | |
| elif cmd == "escenario" and len(args) >= 2: | |
| n = args[1] | |
| if n == "1": cmd_escenario_1() | |
| elif n == "2": cmd_escenario_2() | |
| elif n == "3": cmd_escenario_3() | |
| elif n == "4": cmd_escenario_4() | |
| elif n == "5": cmd_escenario_5() | |
| else: | |
| print_err(f"Escenario '{n}' no existe. Usa del 1 al 5.") | |
| elif cmd == "reset": | |
| cmd_reset() | |
| elif cmd == "demo": | |
| cmd_demo_completo() | |
| else: | |
| print_err(f"Comando '{cmd}' no reconocido.") | |
| print_uso() | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment