Skip to content

Roadmap de 10 sprints: saltar bloqueos + calidad de conexión (v1.10.0) - #6

Merged
PabloAlaniz merged 11 commits into
mainfrom
claude/10-sprint-roadmap-5u7vp1
Oct 6, 2026
Merged

PabloAlaniz merged 11 commits into
mainfrom
claude/10-sprint-roadmap-5u7vp1

Conversation

@PabloAlaniz

Copy link
Copy Markdown
Owner

Resumen

Implementa un roadmap de 10 sprints sobre tor-session-manager, enfocado en dos ejes: 🛡️ saltar cualquier bloqueo (llegar a Tor bajo censura + esquivar bloqueos del destino) y ⚡ mejor calidad de conexión (medir/elegir circuitos rápidos, robustez, concurrencia).

Partimos de una librería de ~230 líneas con un solo TorClient (rotar NEWNYM, SOCKS5, IP contra un único endpoint) y quedó en 13 módulos, 144 tests, 94% coverage. Cada sprint es un commit autocontenido con tests. Cada bump de versión es aditivo/retrocompatible (semver minor).

Sprints

Sprint Entrega Versión
1 · Robustez ⚡🛡️ IP checkers con fallback, wait_for_new_ip(), _retry_with_backoff(), AllIPCheckersFailedError 1.1.0
2 · Introspección ⚡🛡️ CircuitInfo/RelayInfo, list_circuits(), get_circuit_info(), get_exit_country() 1.2.0
3 · Medición ⚡ CircuitHealth, measure_latency()/measure_throughput(), benchmark() 1.3.0
4 · Selección de exit 🛡️⚡ set_exit_country(), set_exit_nodes(), new_circuit(), pinned_exit() 1.4.0
5 · Pool best-circuit ⚡ CircuitPool (aislamiento por credencial SOCKS), pin_fastest(), drop()/prune() 1.5.0
6 · Detección de bloqueo 🛡️ detect_block(), request_with_retry() (auto-rotación), is_exit_blocklisted(), BlockedResponseError 1.6.0
7 · Async ⚡ TorClientAsync (aiohttp/aiohttp_socks), gather_requests() concurrente; extra [async] 1.7.0
8 · Censura 🛡️ bridges + pluggable transports (obfs4/snowflake/meek), launch_bridged_tor(), BridgeConfigError 1.8.0
9 · Anti-fingerprint 🛡️⚡ HeaderRotator (perfiles coherentes) + RateLimiter (pacing por host) 1.9.0
10 · CLI + release ⚡🛡️ CLI tor-session, as_dict() serializable, fixture pytest, py.typed, workflow de PyPI, badges 1.10.0

Cobertura de los dos ejes

  • 🛡️ Saltar bloqueos — destino: selección de país de salida (S4), detección de Cloudflare/CAPTCHA/403-429 + auto-rotación (S6), rotación de headers + pacing (S9). Censura para llegar a Tor: bridges + pluggable transports (S8).
  • ⚡ Calidad — robustez/reintentos (S1), introspección (S2), medición de latencia/throughput (S3), elegir el circuito más rápido (S5), concurrencia async (S7).

Además reconcilia los archivos de roadmap/FODA duplicados y contradictorios previos (/ROADMAP.md vs /docs/ROADMAP.md, /FODA.md vs /docs/FODA.md), marcándolos como supersedidos por el nuevo docs/SPRINTS.md.

Notas de diseño (honestas)

  • Tests: unitarios y mockeados (sin daemon Tor real), consistente con la suite original. La validación contra un Tor corriendo queda como paso manual documentado. El CI ahora instala .[dev,async] para correr también los tests async.
  • JA3/TLS: requests usa el ssl de la stdlib, así que el fingerprint del handshake es fijo; la rotación de headers es la palanca a nivel aplicación. Spoofear JA3 (curl_cffi) queda fuera de alcance y está documentado.
  • Scrapy: se difirió el middleware — Scrapy no soporta proxies SOCKS de forma nativa, así que shippear uno sería frágil. Se documenta como futuro; el fixture de pytest sí se incluye porque funciona.
  • Release: .github/workflows/publish.yml publica a PyPI vía trusted publishing solo en un GitHub release (no se dispara en este PR).

Verificación

  • pytest --cov=tor_session_manager → 144 tests en verde, 94% coverage.
  • tor-session --version / --help resuelven; python -m build genera sdist+wheel con py.typed y los entry points.
  • black/isort limpios sobre el código nuevo.

🤖 Generated with Claude Code


Generated by Claude Code

claude and others added 11 commits July 4, 2026 00:38
…de 10 sprints

Sprint 1 del roadmap enfocado en saltar bloqueos y calidad de conexión:

- ip_checkers.py: resuelve la IP de salida probando varios servicios en orden
  (ipify, ifconfig.me, icanhazip, httpbin); get_ip() ya no depende de un solo
  endpoint. Nueva excepción AllIPCheckersFailedError.
- TorClient.wait_for_new_ip(): rota hasta que la IP de salida cambie de verdad
  (rotate() puede reutilizar el mismo exit).
- _retry_with_backoff(): helper de reintentos con backoff exponencial reutilizable.
- Tests unitarios mockeados (sin Tor real) para checkers, retry y wait_for_new_ip.
- docs/SPRINTS.md: roadmap de 10 sprints como fuente única de verdad; se marcan
  como supersedidos los ROADMAP/FODA duplicados y contradictorios.
- Bump a v1.1.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsmHxv6jNwyMzbFRi5CveS
Da visibilidad de los circuitos Tor activos, base para la selección de exit
(Sprint 4) y el best-circuit (Sprints 3/5):

- circuits.py: dataclasses RelayInfo y CircuitInfo (con exit_relay/exit_country)
  y parse_circuit(), que mapea un circuito de stem y resuelve dirección/país del
  exit vía get_network_status + ip-to-country de forma best-effort (si un lookup
  falla, deja None sin romper).
- TorClient.list_circuits(): todos los circuitos activos.
- TorClient.get_circuit_info(circuit_id=None): circuito indicado, o el activo
  (BUILT/GENERAL más reciente); TorSessionError si no hay ninguno.
- TorClient.get_exit_country(circuit_id=None): país del exit del circuito.
- Tests mockeados en tests/test_circuits.py (sin Tor real). 23 tests, 90% cov.
- README + docs/SPRINTS.md actualizados. Bump a v1.2.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsmHxv6jNwyMzbFRi5CveS
…/benchmark)

Permite medir qué tan bueno es el circuito actual, insumo para la selección de
exit (Sprint 4) y el best-circuit (Sprint 5):

- quality.py: dataclass CircuitHealth (latency_ms, throughput_kbps, samples,
  measured_at, .ok) y funciones puras measure_latency() (mediana de N muestras
  cronometradas, robusta a outliers) y measure_throughput() (KB/s sobre descarga
  de tamaño conocido, con guarda de división por cero). Testeable sin Tor real.
- TorClient.measure_latency() / measure_throughput() / benchmark(): benchmark
  corre ambas de forma tolerante (si una falla, ese campo queda None) y sella
  measured_at. Endpoints por defecto en httpbin, configurables.
- Tests mockeados en tests/test_quality.py. 32 tests en total, 88% cov global
  (quality.py 100%).
- README + docs/SPRINTS.md actualizados. Bump a v1.3.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsmHxv6jNwyMzbFRi5CveS
Permite elegir por dónde sale el tráfico, para esquivar bloqueos del destino
(forzar país permitido o exit no bloqueado) o apuntar a un exit concreto:

- exits.py: helpers puros normalize_country() y format_exit_nodes() que arman el
  valor de ExitNodes combinando tokens {cc} y $FINGERPRINT.
- TorClient.set_exit_country() / set_exit_nodes() / reset_exit_nodes(): configuran
  ExitNodes + StrictNodes en runtime vía el control port.
- TorClient.new_circuit(exit_country=..., verify=True): aplica la restricción,
  rota y confirma un circuito funcional; si el país no tiene exits usables levanta
  TorSessionError claro en vez de colgarse.
- TorClient.pinned_exit(): context manager que fija el exit y siempre lo limpia al
  salir (idiomático como rotated_session).
- Tests mockeados en tests/test_exits.py. 50 tests en total, 92% cov global.
- README + docs/SPRINTS.md actualizados. Bump a v1.4.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsmHxv6jNwyMzbFRi5CveS
Une la medición (Sprint 3) con la selección (Sprint 4): mantiene varios
circuitos en paralelo, los benchmarkea y usa el más rápido.

- pool.py: PooledCircuit (session/health/exit_ip/proxies) y CircuitPool. Cada
  lane es un circuito independiente vía credencial SOCKS distinta
  (socks5h://tsmN:tsmN@...), aprovechando IsolateSOCKSAuth de Tor — sin
  attachment de streams por control port.
- CircuitPool.build()/benchmark()/ranked()/fastest()/pin_fastest() (+ pinned_proxies),
  drop(circuit) y prune(keep) para descartar lentos/bloqueados, close() + context
  manager. Reutiliza measure_latency/measure_throughput/fetch_ip_with_fallback.
- TorClient.circuit_pool(size=3) como factory.
- Tests mockeados en tests/test_pool.py. 63 tests en total, 93% cov global
  (pool.py 97%).
- README + docs/SPRINTS.md actualizados. Bump a v1.5.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsmHxv6jNwyMzbFRi5CveS
El núcleo de "saltar bloqueos": muchos sitios bloquean IPs de exit Tor
(Cloudflare, CAPTCHA, 403/429). Detecta esas respuestas y rota a un exit nuevo
automáticamente hasta pasar.

- blocking.py: detect_block(response) reconoce challenges de Cloudflare/CAPTCHA
  en el body y statuses de bloqueo (403/429/503). Markers/statuses ajustables
  para minimizar falsos positivos. Lógica pura, testeable sin red.
- BlockedResponseError (con .reason/.response) en exceptions.py.
- TorClient.request(): request a través de Tor. request_with_retry(): rota ante
  bloqueo hasta max_rotations; levanta BlockedResponseError si se agotan.
  is_exit_blocklisted(url): motivo de bloqueo del exit actual (o None).
- Tests mockeados en tests/test_blocking.py. 78 tests en total, 93% cov global
  (blocking.py 100%).
- README + docs/SPRINTS.md actualizados. Bump a v1.6.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsmHxv6jNwyMzbFRi5CveS
…ente

La palanca de concurrencia: un cliente async sobre aiohttp para scraping
paralelo a través de Tor.

- aio.py: TorClientAsync. Data-plane (get_ip, request, request_with_retry,
  measure_latency/throughput, benchmark) sobre aiohttp + aiohttp_socks (rdns
  para DNS por Tor). Control-plane (rotate, is_ready, set_exit_*, new_circuit)
  delegado al TorClient síncrono vía asyncio.to_thread. AsyncResponse reutiliza
  tal cual el detect_block del Sprint 6.
- gather_requests(): fetch concurrente acotado por asyncio.Semaphore
  (max_concurrency); fallo por-URL -> None, sin cortar el lote.
- Dependencia opcional [async] (aiohttp, aiohttp_socks); __init__ con import
  guardado y placeholder que da ImportError claro si falta el extra.
- CI instala [dev,async] para correr los tests async (que si no se skipean).
- Tests mockeados en tests/test_aio.py con asyncio.run + importorskip. 96 tests
  en total, 92% cov global.
- README + docs/SPRINTS.md actualizados. Bump a v1.7.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsmHxv6jNwyMzbFRi5CveS
La otra mitad de "saltar bloqueos": llegar a Tor cuando el ISP/país bloquea Tor
mismo, vía bridges y pluggable transports (obfs4, snowflake, meek, webtunnel).

- bridges.py: Bridge dataclass + parse_bridge_line() (vanilla y con transport),
  transport_binary() (resuelve el PT del PATH o path explícito), build_bridge_config()
  (arma UseBridges/Bridge/ClientTransportPlugin, dedup de transports) y
  launch_bridged_tor() (lanza un Tor gestionado vía stem.process.launch_tor_with_config).
- BridgeConfigError (bridge line inválida / binario PT faltante).
- Nota: UseBridges/ClientTransportPlugin son launch-time; el flujo es lanzar un Tor
  propio. Los binarios PT no se bundlean, se resuelven del PATH con error claro si faltan.
- Tests mockeados en tests/test_bridges.py (sin Tor ni binarios PT). 113 tests en total,
  93% cov global (bridges.py 100%).
- README + docs/SPRINTS.md actualizados. Bump a v1.8.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsmHxv6jNwyMzbFRi5CveS
…ativo)

Rotar la IP no alcanza si el fingerprint del request te delata. Rota perfiles
de headers coherentes y pacea el ritmo por host.

- fingerprint.py: HeaderProfile + BROWSER_PROFILES (Chrome/Firefox/Safari con
  headers coherentes, sin mezclar UA de uno con headers de otro) y HeaderRotator
  (round-robin determinístico, shuffle opcional; next() devuelve copia).
- pacing.py: RateLimiter con pacing por host (acquire/record), backoff
  multiplicativo ante 429/503 (cap max_interval) y decay en respuestas OK.
- TorClient(rotate_headers=..., rate_limit=...) integrado de forma aditiva/opt-in
  en request(); request_with_retry() lo hereda.
- Nota honesta: JA3/TLS es fijo con requests (stdlib ssl); documentado como
  límite conocido, sin agregar curl_cffi.
- Tests puros en tests/test_fingerprint.py y tests/test_pacing.py + integración
  en tests/test_blocking.py. 129 tests en total, 94% cov global.
- README + docs/SPRINTS.md actualizados. Bump a v1.9.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsmHxv6jNwyMzbFRi5CveS
…ap completo)

Cierre del roadmap de 10 sprints: consolidación y paquete listo para publicar.

- cli.py + entry point `tor-session`: subcomandos ip/rotate/circuit/country/
  benchmark/status, con --json para salida machine-readable. main(argv) testeable.
- Observabilidad: CircuitHealth.as_dict() y CircuitInfo.as_dict() serializables
  (reusan dataclasses.asdict); `status --json` arma el snapshot completo.
- pytest_plugin.py: fixture `tor_client` (skipea si Tor no está listo) vía entry
  point pytest11. Se desactiva su autoload en la suite propia (deflactaba coverage)
  y se cubre con test directo.
- Packaging: py.typed (PEP 561) + package-data, [project.scripts] y [pytest11] en
  pyproject, .github/workflows/publish.yml (PyPI trusted publishing en release).
- README: badges (CI/PyPI/Python/License) + secciones de CLI y fixture de pytest.
- Nota honesta: middleware de Scrapy queda como futuro (Scrapy no soporta SOCKS
  nativo); el fixture de pytest sí se incluye porque funciona.
- Verificado: `tor-session` resuelve, `python -m build` genera sdist+wheel con
  py.typed y entry points. 144 tests, 94% cov global. Bump a v1.10.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsmHxv6jNwyMzbFRi5CveS
Disable environment proxy overrides, reject request-level proxy changes, and
verify fail-closed routing with offline regressions across sync, pool and async.
Distinguish controller, authentication, bootstrap, SOCKS and connectivity status;
return nonzero for unavailable Tor and accept CLI options before or after commands.

Fix existing type annotations and Black formatting. Align declared Python minimum
with the existing Python 3.9+ README, CI and asyncio.to_thread usage; retain a
compatible mypy major version.

Validation: 238 tests pass, 95% coverage; Black, isort, mypy and compileall pass.
No live Tor or external endpoint traffic used in tests.
@PabloAlaniz
PabloAlaniz merged commit 9a586aa into main Oct 6, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants