Cuaderno/Proyectos/0 · Hola, reproducible
Proyecto 0 de 10 · Fase 0

Hola, reproducible

Una función, su test, un Dockerfile que corre ese test y un README. Nada más. Lo que estás probando no es la función: es que toda tu cadena de herramientas funciona de punta a punta.

concepto · la cadena de herramientas1 sesión (2 h)uv · pytest · ruff · git · docker · github
ConstruyesRepo hola-alexandra con gc_content()
AprendesEl ciclo escribir → probar → versionar → empaquetar → compartir
CostoNinguno. Todo corre en tu laptop.
Terminas conUn PR revisado por Rodrigo y mergeado

Objetivos

Al terminar este proyecto vas a haber hecho, con tus manos y sin ayuda de la IA:

  • Crear un proyecto Python con estructura estándar (src/, tests/, pyproject.toml) y su entorno aislado con uv.
  • Escribir un test que falla, luego la función que lo hace pasar.
  • Ver el mismo test correr dentro de un contenedor Docker.
  • Publicar el repo en GitHub y abrir un Pull Request que Rodrigo revisa.
  • Poner un breakpoint y mirar una variable en el debugger de VS Code.
Por qué algo tan chico

Este ciclo lo vas a repetir cien veces en los próximos meses. Aquí lo haces con algo trivial (una función de tres líneas) para que la única dificultad sea el ciclo, no el problema. Si mañana algo no funciona en el Proyecto 4, vas a saber que la herramienta está bien y el problema es tuyo, que es lo que quieres.

Temas que vas a usar

Léelos antes o consúltalos cuando aparezcan. Están en El libro de Python (en español, corto y bien explicado). Los de la derecha son de esta guía.

Antes de empezar

Instala las herramientas y verifica cada una. Si un comando de verificación no responde como se indica, arréglalo antes de seguir; nada de lo que viene funciona sin esto.

1. Terminal

En macOS: la app Terminal (o iTerm2). En Windows: instala WSL2 con Ubuntu y trabaja siempre ahí; todo lo que sigue asume una terminal tipo Unix.

2. uv (instala Python y maneja proyectos)

# macOS (con Homebrew) o Linux
brew install uv          # macOS
curl -LsSf https://astral.sh/uv/install.sh | sh    # Linux / WSL

# instala Python 3.12 (uv lo descarga; no toca el Python del sistema)
uv python install 3.12
uv --version
Deberías ver
uv 0.12.5 (o superior)
Por qué uv y no pip. Hace cuatro cosas que antes eran cuatro herramientas: instala Python, crea el entorno virtual, agrega dependencias y fija versiones exactas en uv.lock. Un entorno por proyecto es como un medio de cultivo por cepa: nunca se mezclan.

3. git y GitHub

git --version
git config --global user.name "Alexandra"
git config --global user.email "tu-correo@ejemplo.com"
git config --global init.defaultBranch main

Crea tu cuenta en github.com si no la tienes. Instala la CLI de GitHub e inicia sesión (te guía para crear la llave SSH):

brew install gh        # macOS · en Linux: https://cli.github.com
gh auth login          # elige GitHub.com → SSH → seguir las instrucciones
gh auth status
Deberías ver
✓ Logged in to github.com account alexandra-… (keyring)

4. Docker Desktop

Descarga Docker Desktop, ábrelo y espera a que el ícono diga que está corriendo. Luego:

docker run --rm hello-world
Deberías ver
Hello from Docker!
This message shows that your installation appears to be working correctly.

5. VS Code

Instala VS Code y, dentro, la extensión Python (de Microsoft). Con eso vienen el debugger y el panel de tests. Opcional pero recomendado: la extensión Ruff.

6. Claude Code

Instálalo siguiendo la documentación oficial. En este proyecto lo usas solo para preguntar, no para escribir. Ver Con Claude más abajo.

Checklist antes de seguir

uv --version · git --version · gh auth status · docker run hello-world · VS Code abre una carpeta y reconoce archivos .py. Cinco verdes, empezamos.

Construcción paso a paso

Escribe cada archivo tú misma (no copies y pegues bloques enteros: tipear es parte de aprender). Corre cada comando y compara con lo que "deberías ver". Si difiere, detente ahí y entiende por qué antes de avanzar.

Paso 1 Crear el proyecto y su entorno

Crea la carpeta, inicializa git y fija la versión de Python del proyecto:

mkdir hola-alexandra
cd hola-alexandra
git init
uv python pin 3.12
mkdir -p src/hola tests

uv python pin crea un archivo .python-version: cualquiera que abra este proyecto usará 3.12, aunque tenga otro Python instalado.

Ahora el archivo que describe el proyecto. Escríbelo a mano; cada línea se explica en Entiende el código:

pyproject.toml
[project]
name = "hola"
version = "0.1.0"
description = "Proyecto 0: validar la cadena de herramientas"
requires-python = ">=3.12"
dependencies = []

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/hola"]

[tool.pytest.ini_options]
testpaths = ["tests"]

[tool.ruff]
line-length = 100

El paquete necesita un archivo __init__.py (puede estar vacío) para que Python lo reconozca como paquete, y el proyecto necesita ignorar lo que no se versiona:

touch src/hola/__init__.py
.gitignore
.venv/
__pycache__/
.pytest_cache/
.ruff_cache/
*.pyc
.env
dist/

Agrega las herramientas de desarrollo. Este comando crea el entorno virtual (.venv/), instala pytest y ruff, e instala tu propio paquete en modo editable:

uv add --dev pytest ruff
Deberías ver
Resolved 6 packages in …
Installed 5 packages in …
 + hola==0.1.0 (from file:///…/hola-alexandra)
 + pytest==9.x
 + ruff==0.x
 …

Mira lo que apareció:

ls -a
Deberías ver
.  ..  .git  .gitignore  .python-version  .venv  pyproject.toml  src  tests  uv.lock
uv.lock es la lista exacta de versiones que se instalaron. Se versiona en git. .venv/ es el entorno con esos paquetes instalados: no se versiona (está en .gitignore) porque se regenera con uv sync. Fíjate que pyproject.toml ahora tiene una sección [dependency-groups] con dev = ["pytest…", "ruff…"]: la agregó uv add.

Paso 2 Primer commit: la estructura vacía

Antes de escribir una sola línea de código, guarda la estructura. Es una foto del "antes".

git status
git add .
git commit -m "Estructura inicial del proyecto con uv, pytest y ruff"
git log --oneline
Deberías ver
a1b2c3d (HEAD -> main) Estructura inicial del proyecto con uv, pytest y ruff

Comprueba que .venv/ no entró: git ls-files debe listar solo .gitignore, .python-version, pyproject.toml, src/hola/__init__.py y uv.lock. (La carpeta tests/ está vacía y git no versiona carpetas vacías; entra en el siguiente paso.)

Sobre el mensaje. Dice qué se hizo en modo imperativo y por qué se entiende solo. Cuando el "por qué" no sea obvio, va en el mensaje: en tres meses git log es tu bitácora.

Paso 3 El test, antes que el código

Vas a escribir primero cómo debe comportarse la función, y recién después la función. Un test es un control: una entrada de la que ya conoces la respuesta.

tests/test_core.py
import pytest

from hola.core import gc_content


def test_gc_content_mitad():
    assert gc_content("ATGC") == 0.5


def test_gc_content_todo_gc():
    assert gc_content("GGCC") == 1.0


def test_gc_content_ignora_minusculas():
    assert gc_content("atgc") == 0.5


def test_gc_content_secuencia_vacia_lanza_error():
    with pytest.raises(ValueError):
        gc_content("")

Córrelo. Tiene que fallar: la función no existe todavía.

uv run pytest
Deberías ver (rojo, y está bien)
==================================== ERRORS ====================================
_____________________ ERROR collecting tests/test_core.py ______________________
…
tests/test_core.py:3: in <module>
    from hola.core import gc_content
E   ModuleNotFoundError: No module named 'hola.core'
=========================== short test summary info ============================
ERROR tests/test_core.py
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
Lee el error de abajo hacia arriba. La última línea con E es la causa: no existe el módulo hola.core. Todo lo demás es contexto. Aprender a leer un traceback así es la habilidad más rentable de este mes.

Los cuatro tests son, en lenguaje de laboratorio: dos controles positivos con resultado conocido (0.5 y 1.0), un caso de robustez (minúsculas) y un control negativo (entrada inválida debe fallar de una forma específica). Con eso, la función queda definida antes de existir.

Paso 4 La función que hace pasar el test

src/hola/core.py
"""Funciones sobre secuencias de ADN. Solo cálculo: nada de archivos ni print."""


def gc_content(seq: str) -> float:
    """Fracción de bases G y C en la secuencia, entre 0.0 y 1.0.

    Ignora mayúsculas/minúsculas. Lanza ValueError si la secuencia está vacía.
    """
    if not seq:
        raise ValueError("la secuencia está vacía")
    seq = seq.upper()
    gc = seq.count("G") + seq.count("C")
    return gc / len(seq)
uv run pytest -v
Deberías ver
tests/test_core.py::test_gc_content_mitad PASSED                         [ 25%]
tests/test_core.py::test_gc_content_todo_gc PASSED                       [ 50%]
tests/test_core.py::test_gc_content_ignora_minusculas PASSED             [ 75%]
tests/test_core.py::test_gc_content_secuencia_vacia_lanza_error PASSED   [100%]

============================== 4 passed in 0.01s ===============================

Ahora el linter y el formateador. Ruff revisa errores comunes y deja el formato uniforme (dos líneas en blanco entre funciones, comillas dobles, etc.). Se corre siempre antes de commitear:

uv run ruff check .
uv run ruff format .
Deberías ver
All checks passed!
3 files left unchanged   (o "1 file reformatted" si tenías algo distinto: mira el diff con git diff)

Rompe algo a propósito para ver el test fallar por la razón correcta: cambia if not seq: por if False:, corre uv run pytest, mira cómo se ve un test que falla (no un error de import: una aserción), y vuelve a dejarlo bien.

git add .
git commit -m "Agrega gc_content con sus tests"

Paso 5 Empaquetar con Docker

Hasta aquí todo corre en tu laptop, con tu uv y tu Python. Docker congela ese entorno en una imagen que corre igual en cualquier máquina. Primero, qué no entra a la imagen:

.dockerignore
.venv/
.git/
__pycache__/
.pytest_cache/
.ruff_cache/

Y la receta:

Dockerfile
# Imagen base: Python 3.12 mínimo, sobre Debian
FROM python:3.12-slim

# Copiamos el binario de uv desde su imagen oficial
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv

WORKDIR /app

# Primero las dependencias (cambian poco → la capa se cachea)
COPY pyproject.toml uv.lock ./
# Luego el código y los tests
COPY src ./src
COPY tests ./tests

# Instala exactamente lo que dice uv.lock, nada más
RUN uv sync --frozen

# Lo que corre el contenedor por defecto: los tests
CMD ["uv", "run", "pytest", "-v"]

Construye la imagen (la primera vez descarga la base; tarda un minuto) y luego corre un contenedor a partir de ella:

docker build -t hola .
docker run --rm hola
Deberías ver
…
tests/test_core.py::test_gc_content_mitad PASSED                         [ 25%]
tests/test_core.py::test_gc_content_todo_gc PASSED                       [ 50%]
tests/test_core.py::test_gc_content_ignora_minusculas PASSED             [ 75%]
tests/test_core.py::test_gc_content_secuencia_vacia_lanza_error PASSED   [100%]

============================== 4 passed in 0.01s ===============================

Los mismos cuatro tests, pero corriendo dentro de un Linux mínimo con su propio Python, sin usar nada de tu máquina salvo el código. Eso es reproducibilidad.

docker images hola
Deberías ver
REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
hola         latest    ba5ccbd2560c   1 minute ago     ~320MB
Imagen vs contenedor. La imagen es la receta congelada (hola:latest, 320 MB, no cambia). El contenedor es una corrida de esa receta: nace con docker run, ejecuta su comando y muere (--rm lo borra al terminar). Puedes correr diez contenedores de la misma imagen. Si cambias el código, la imagen vieja no se entera: hay que hacer docker build de nuevo.
git add .
git commit -m "Dockerfile que corre los tests en un contenedor"

Paso 6 README y subir a GitHub

El README es el protocolo: alguien que llega sin contexto tiene que poder correr esto. Escríbelo corto y verdadero:

README.md
# hola-alexandra

Proyecto 0 del cuaderno: valida la cadena de herramientas con una sola
función (`gc_content`) y su test.

## Correr los tests

    uv sync
    uv run pytest

## Correr los tests en Docker

    docker build -t hola .
    docker run --rm hola

## Estructura

    src/hola/core.py      la función (solo cálculo, sin I/O)
    tests/test_core.py    sus controles
    Dockerfile            entorno reproducible
    pyproject.toml        descripción del proyecto y dependencias

## Decisiones

- `src/` layout: obliga a instalar el paquete para probarlo, igual que
  lo haría un usuario. Evita que "funcione en mi máquina" por accidente.
- Ver DECISIONES.md.
DECISIONES.md
# Decisiones

## 2026-08-xx · uv en lugar de pip + venv
Una herramienta hace lo que antes eran cuatro. Alternativa: pip + venv +
pip-tools. Cambio si uv deja de mantenerse.

## 2026-08-xx · La imagen Docker corre los tests
En este proyecto no hay "programa" que correr; lo que quiero verificar es
que el entorno se reproduce. Los tests son la prueba de eso.
git add .
git commit -m "README y registro de decisiones"
gh repo create hola-alexandra --private --source . --push
Deberías ver
✓ Created repository alexandra-…/hola-alexandra on GitHub
✓ Added remote git@github.com:alexandra-…/hola-alexandra.git
✓ Pushed commits to git@github.com:alexandra-…/hola-alexandra.git

Abre el repo en el navegador (gh repo view --web) y agrega a Rodrigo como colaborador: Settings → Collaborators → Add people. Lo necesita para revisar tu PR en el paso 8.

Paso 7 El debugger, antes que print

Abre la carpeta en VS Code (code .). Cuando te pregunte qué intérprete usar, elige el de .venv (aparece como Python 3.12 ('.venv')). Configura el panel de tests:

.vscode/settings.json
{
  "python.testing.pytestEnabled": true,
  "python.testing.pytestArgs": ["tests"]
}
  1. Abre src/hola/core.py y haz click a la izquierda del número de la línea seq = seq.upper(). Aparece un punto rojo: un breakpoint.
  2. Ve al panel Testing (ícono del matraz en la barra izquierda). Deberías ver los cuatro tests. En test_gc_content_ignora_minusculas, click derecho → Debug Test.
  3. La ejecución se detiene en tu breakpoint. En el panel Variables ves seq = 'atgc'. Pulsa Step Over (F10) una vez: ahora seq = 'ATGC'. Otra vez: aparece gc = 2.
  4. Pulsa Continue (F5). El test termina en verde.
Por qué esto y no print. Con el debugger ves todas las variables, en cualquier punto, sin ensuciar el código y sin volver a correr nada. Cuando algo falle en el Proyecto 4 dentro de una función que llama a otra que llama a otra, esto es lo que te va a salvar. Agrega .vscode/ al .gitignore o commitéalo; las dos cosas son válidas, decide y anótalo.

Paso 8 Tu primer Pull Request

De aquí en adelante, ningún cambio entra a main directo. Se hace en una rama, se sube, se abre un PR y alguien lo revisa. Aunque el cambio sea de una línea; la práctica es lo que importa. Vas a agregar una segunda función pequeña, en rama:

git switch -c feat/reverse-complement

Test primero (agrégalo al final de tests/test_core.py):

tests/test_core.py (agregar al final)
from hola.core import reverse_complement


def test_reverse_complement_basico():
    assert reverse_complement("ATGC") == "GCAT"


def test_reverse_complement_ignora_minusculas():
    assert reverse_complement("atgc") == "GCAT"


def test_reverse_complement_base_invalida_lanza_error():
    with pytest.raises(ValueError):
        reverse_complement("ATXG")

Muévelo junto al otro import de arriba (los imports van todos al inicio del archivo; ruff te lo va a marcar si no). Corre uv run pytest: rojo. Ahora la función, en src/hola/core.py:

src/hola/core.py (agregar al final)
COMPLEMENTO = {"A": "T", "T": "A", "G": "C", "C": "G"}


def reverse_complement(seq: str) -> str:
    """Complemento reverso de una secuencia de ADN (5'→3')."""
    seq = seq.upper()
    invalidas = set(seq) - set(COMPLEMENTO)
    if invalidas:
        raise ValueError(f"bases inválidas: {sorted(invalidas)}")
    return "".join(COMPLEMENTO[base] for base in reversed(seq))
uv run pytest -q
uv run ruff check . && uv run ruff format .
docker build -t hola . && docker run --rm hola
Deberías ver
7 passed in 0.01s   (y lo mismo dentro de Docker)
git add .
git commit -m "Agrega reverse_complement con validación de bases"
git push -u origin feat/reverse-complement
gh pr create --title "Agrega reverse_complement" --body "Segunda función del P0. Valida bases y devuelve el complemento reverso. Incluye 3 tests." --reviewer rotorrest

Avísale a Rodrigo. Él va a dejar comentarios en el PR (en GitHub, pestaña Files changed). Responde cada uno: o cambias el código y haces push a la misma rama (el PR se actualiza solo), o explicas por qué no. Cuando apruebe, mergea desde GitHub y trae el resultado a tu máquina:

git switch main
git pull
git log --oneline
Este ciclo es el trabajo. Rama → commits → push → PR → revisión → merge → pull. Lo vas a hacer en cada proyecto, varias veces. Aquí lo hiciste con siete líneas de código para que la única cosa nueva sea el ciclo.

Entiende el código

No sigas al Proyecto 1 hasta poder explicar cada una de estas piezas con tus palabras.

Estructura

hola-alexandra/
├── .python-version      versión de Python del proyecto (la lee uv)
├── pyproject.toml       nombre, versión, dependencias, config de pytest y ruff
├── uv.lock              versiones exactas instaladas (se versiona)
├── .venv/               entorno virtual (NO se versiona; se regenera con uv sync)
├── src/hola/
│   ├── __init__.py      marca la carpeta como paquete
│   └── core.py          la lógica: funciones puras
├── tests/test_core.py   los controles
├── Dockerfile           receta de la imagen
├── .dockerignore        lo que no entra a la imagen
├── README.md            el protocolo
└── DECISIONES.md        el porqué de lo no obvio

pyproject.toml, sección por sección

  • [project]: identidad del paquete. requires-python es una promesa: este código asume 3.12 o más.
  • [build-system]: qué herramienta convierte tu carpeta en un paquete instalable (hatchling). Sin esto, uv add no instala tu propio paquete y el import hola.core falla desde tests/.
  • [tool.hatch.build.targets.wheel]: dónde está el código (src/hola). Es lo que hace que from hola.core import … resuelva.
  • [tool.pytest.ini_options]: dónde buscar tests. [tool.ruff]: largo de línea. Cada herramienta lee su sección de este mismo archivo: un solo lugar de configuración.
  • [dependency-groups] dev (lo agregó uv add --dev): herramientas que necesitas para desarrollar, no para que el paquete funcione. Un usuario de hola no necesita pytest.

Por qué src/

Si el código estuviera en la raíz (hola/core.py al lado de tests/), Python lo encontraría "por casualidad" desde la carpeta actual, y los tests pasarían aunque el paquete estuviera mal instalado. Con src/, la única forma de importar hola es tenerlo instalado en el entorno, igual que lo tendría un usuario. Es una restricción que te protege de un "funciona en mi máquina".

Anatomía de un test

tests/test_core.py
def test_gc_content_mitad():          # 1. nombre: empieza con test_, dice qué caso prueba
    assert gc_content("ATGC") == 0.5   # 2. arrange + act + assert en una línea: entrada conocida, salida esperada

def test_gc_content_secuencia_vacia_lanza_error():
    with pytest.raises(ValueError):    # 3. "dentro de este bloque DEBE saltar ValueError; si no salta, el test falla"
        gc_content("")

pytest descubre solo las funciones que empiezan con test_ en archivos que empiezan con test_. Un assert que da False = test fallido, y pytest te muestra los dos lados de la comparación. No hay más magia que esa.

La función

  • seq: str) -> float: anotaciones. No cambian cómo corre, pero documentan el contrato y VS Code las usa para avisarte errores antes de correr.
  • if not seq: raise ValueError(...): validar en el borde y fallar temprano con un mensaje que dice qué pasó. Sin esto, gc / len(seq) daría ZeroDivisionError: también falla, pero con un mensaje que no explica el problema real.
  • La función no lee archivos, no imprime, no pide nada al usuario. Entra un str, sale un float. Por eso testearla es trivial. Esta separación entre lógica y entrada/salida es el concepto central del Proyecto 1.

El Dockerfile, línea por línea

  • FROM python:3.12-slim: parto de una imagen que ya tiene Python 3.12 sobre un Debian mínimo. Nunca instalo Python a mano dentro de Docker.
  • COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv: copio el ejecutable de uv desde su imagen oficial. Un solo archivo, sin pip.
  • WORKDIR /app: de aquí en adelante todo pasa en /app dentro de la imagen.
  • COPY pyproject.toml uv.lock ./ antes que COPY src: Docker cachea cada capa. Si solo cambias código, la capa de dependencias no se reconstruye. Es una optimización que verás en todos los Dockerfiles serios.
  • RUN uv sync --frozen: instala exactamente lo que dice uv.lock. --frozen falla si el lock no coincide con pyproject.toml: mejor fallar en el build que instalar algo distinto en silencio.
  • CMD [...]: qué corre el contenedor si no le dices otra cosa. Se puede sobreescribir: docker run --rm hola uv run python -c "from hola.core import gc_content; print(gc_content('GGGA'))".

Con Claude

Regla 1 de Trabajar con IA: la primera vez, a mano. Este proyecto entero es esa primera vez, así que Claude Code no escribe nada aquí. Sí lo usas para entender. Abre Claude Code en la carpeta del proyecto (claude) y crea primero el protocolo para él:

CLAUDE.md
# hola-alexandra

Proyecto de aprendizaje. Estoy aprendiendo a programar con criterio; el
objetivo es que YO entienda cada línea.

## Reglas
- No escribas código a menos que te lo pida explícitamente. Por defecto,
  explica y pregunta.
- Si te pido código, propón el cambio más pequeño posible y explica cada línea.
- Nunca modifiques los tests para que pasen.
- No agregues dependencias sin preguntar y decir por qué.
- Antes de decir que algo está listo: `uv run pytest` y `uv run ruff check .`
  deben pasar.
- Responde en español.

Pedidos que sí valen la pena en este proyecto

Explícame línea por línea el Dockerfile. Para cada línea: qué hace y qué pasaría si la quito. ¿Por qué el paquete está en src/hola y no en hola/ en la raíz? Dame el argumento a favor y en contra. Este es mi test que falla [pegar salida de pytest]. No me digas la solución: dime qué línea del traceback es la causa y qué significa. Hazme cinco preguntas de criterio sobre lo que hice en este proyecto (git, tests, docker, pyproject) y corrige mis respuestas con dureza. ¿Qué diferencia hay entre uv sync, uv add y uv run? ¿Cuándo uso cada uno?

Cuando termines, pídele que revise el repo como lo haría un colega senior: "revisa este proyecto y dime tres cosas que un profesional haría distinto, y por qué". Anota lo que aprendas en DECISIONES.md.

Si algo falla

Los errores más comunes de este proyecto, cómo se ven y qué significan. Antes de buscar aquí: lee el traceback de abajo hacia arriba y formula una hipótesis.

ModuleNotFoundError: No module named 'hola' (aunque ya escribiste core.py)

El paquete no está instalado en el entorno, o estás corriendo pytest fuera del entorno. Casi siempre es una de estas:

  • Corriste pytest a secas en lugar de uv run pytest. El primero usa el Python del sistema, que no conoce tu paquete.
  • Falta [build-system] o [tool.hatch.build.targets.wheel] en pyproject.toml. Sin eso, uv no instala tu paquete. Agrégalo y corre uv sync.
  • Falta src/hola/__init__.py. Sin él, la carpeta no es un paquete.

Fíjate en la diferencia con No module named 'hola.core': ese dice que el paquete existe pero el módulo core no. Ahí lo que falta es el archivo.

Cannot connect to the Docker daemon at unix:///var/run/docker.sock

Docker Desktop no está corriendo. Ábrelo, espera a que el ícono deje de animarse, y repite el comando. Docker es un programa servidor (el "daemon") más un cliente (docker en tu terminal); el error dice que el cliente no encuentra al servidor.

Permission denied (publickey) al hacer push

GitHub no reconoce tu llave SSH. Corre gh auth status; si no estás autenticada, gh auth login de nuevo eligiendo SSH y dejando que suba la llave. Alternativa rápida: gh auth setup-git para usar HTTPS con el token de gh.

El test de secuencia vacía pasa aunque no escribí el raise

Sin el if not seq, la función llega a gc / len(seq) con len(seq) == 0 y lanza ZeroDivisionError… que no es ValueError, así que pytest.raises(ValueError) debería fallar. Si te pasa en verde, revisa que el test esté dentro del with (indentación) y que hayas guardado el archivo. Y fíjate en la lección: dos errores distintos por la misma causa; el que tú lanzas explica el problema, el otro no.

uv run pytest dice no tests ran o collected 0 items

pytest no encontró tests. Revisa: el archivo se llama test_core.py (empieza con test_), las funciones empiezan con test_, y testpaths = ["tests"] está en pyproject.toml. Corre uv run pytest --collect-only para ver qué está encontrando.

ruff se queja de que from hola.core import reverse_complement no está arriba (E402)

Los imports van todos al inicio del archivo, juntos. Junta los dos en una línea: from hola.core import gc_content, reverse_complement. Ruff tiene razón; es una convención que hace que cualquier archivo Python se lea igual.

Docker construye pero docker run falla con error: Failed to spawn: pytest o similar

El uv sync dentro de la imagen no instaló el grupo dev, o el lock quedó desactualizado. Comprueba que uv.lock esté commiteado y actualizado (uv lock en tu máquina), que el .dockerignore no excluya uv.lock, y reconstruye con docker build --no-cache -t hola ..

Listo cuando

Las mismas casillas que en el índice; marcarlas aquí las marca allá. Sé honesta: "casi" no cuenta.

La pregunta de Rodrigo en la sesión

"Borro uv.lock y corro uv sync en otra máquina. ¿Qué puede salir distinto y por qué importa?" Si puedes contestar eso, entendiste el proyecto.

Siguiente

En el Proyecto 1 · seqtool conviertes estas dos funciones en un programa de línea de comandos que lee archivos FASTA de verdad, y aprendes la separación más importante de todas: la lógica en un lado, la entrada/salida en otro. Ahí también entra la integración continua (GitHub Actions corriendo tus tests en cada PR).

Si te quedaste con ganas: instala tu paquete como herramienta (uv build y luego uv tool install dist/hola-0.1.0-py3-none-any.whl) y entiende qué es un wheel. Es opcional; anótalo si lo haces.