Warming up the neural circuits...
By the end of this chapter you will:
Build an installable scraper package that can:
Demo
python -m scraper scrape urls.txt --concurrency 20 --out results.jsonl
scraper scrape urls.txt --concurrency 20 --out results.jsonlscraper/
├── pyproject.toml
├── src/scraper/
│ ├── __init__.py
│ ├── __main__.py
│ ├── cli.py
│ ├── fetch.py
│ ├── parse.py
│ ├── pipeline.py
│ ├── retries.py
│ └── types.py
└── tests/
├── test_parse.py
├── test_retry.py
└── test_pipeline.pyFlow: URL source -> async fetch -> parse -> validate -> sink
import asyncio
import aiohttp
async def fetch_one(session: aiohttp.ClientSession, url: str, sem: asyncio.Semaphore) -> tuple[str, str]:
async with sem:
async with session
import asyncio
async def with_retries(coro_factory, attempts: int = 3, delay: float = 0.5):
last = None
for i in range(attempts):
try:
return await coro_factory()
except
import json
def write_jsonl(path: str, rows):
with open(path, "w", encoding="utf-8") as f:
for row in rows:
f.write(json[project]
name = "async-web-scraper"
version = "0.1.0"
requires-python = ">=3.11"
[project.scripts]
scraper = "scraper.__main__:main"Mandatory package quality:
src/ layoutCore tests expected:
Example:
import pytest
@pytest.mark.asyncio
async def test_retry_succeeds_after_failure():
calls = {"n": 0}
async def flaky():
calls["n"] += 1
if calls["n"] < 2
Before considering P2 complete:
pytest -q passesmypy src passes for project modulespython -m build) and twine check dist/* passesBe ready to explain:
P2 is the bridge from "I know async syntax" to "I can ship an async product": architecture, operational policy, packaging discipline, and reproducible release workflow.
Why use semaphore limits in an async scraper?