|
2 | 2 |
|
3 | 3 | """Download flags of countries (with error handling). |
4 | 4 |
|
5 | | -asyncio async/await version using run_in_executor for save_flag. |
| 5 | +asyncio async/await version |
6 | 6 |
|
7 | 7 | """ |
8 | | - |
| 8 | +# tag::FLAGS2_ASYNCIO_TOP[] |
9 | 9 | import asyncio |
10 | 10 | from collections import Counter |
| 11 | +from http import HTTPStatus |
| 12 | +from pathlib import Path |
11 | 13 |
|
12 | | -import aiohttp |
| 14 | +import httpx |
13 | 15 | import tqdm # type: ignore |
14 | 16 |
|
15 | | -from flags2_common import main, HTTPStatus, Result, save_flag |
| 17 | +from flags2_common import main, DownloadStatus, save_flag |
16 | 18 |
|
17 | 19 | # default set low to avoid errors from remote site, such as |
18 | 20 | # 503 - Service Temporarily Unavailable |
19 | 21 | DEFAULT_CONCUR_REQ = 5 |
20 | 22 | MAX_CONCUR_REQ = 1000 |
21 | 23 |
|
22 | 24 |
|
23 | | -class FetchError(Exception): |
24 | | - def __init__(self, country_code: str): |
25 | | - self.country_code = country_code |
26 | | - |
27 | | - |
28 | | -async def get_flag(session: aiohttp.ClientSession, |
| 25 | +async def get_flag(session: httpx.AsyncClient, # <2> |
29 | 26 | base_url: str, |
30 | 27 | cc: str) -> bytes: |
31 | 28 | url = f'{base_url}/{cc}/{cc}.gif'.lower() |
32 | | - async with session.get(url) as resp: |
33 | | - if resp.status == 200: |
34 | | - return await resp.read() |
35 | | - else: |
36 | | - resp.raise_for_status() |
37 | | - return bytes() |
| 29 | + resp = await session.get(url, timeout=3.1, follow_redirects=True) # <3> |
| 30 | + resp.raise_for_status() |
| 31 | + return resp.content |
38 | 32 |
|
39 | | -# tag::FLAGS2_ASYNCIO_EXECUTOR[] |
40 | | -async def download_one(session: aiohttp.ClientSession, |
| 33 | + |
| 34 | +async def download_one(session: httpx.AsyncClient, |
41 | 35 | cc: str, |
42 | 36 | base_url: str, |
43 | 37 | semaphore: asyncio.Semaphore, |
44 | | - verbose: bool) -> Result: |
| 38 | + verbose: bool) -> DownloadStatus: |
45 | 39 | try: |
46 | 40 | async with semaphore: |
47 | 41 | image = await get_flag(session, base_url, cc) |
48 | | - except aiohttp.ClientResponseError as exc: |
49 | | - if exc.status == 404: |
50 | | - status = HTTPStatus.not_found |
51 | | - msg = 'not found' |
| 42 | + except httpx.HTTPStatusError as exc: |
| 43 | + res = exc.response |
| 44 | + if res.status_code == HTTPStatus.NOT_FOUND: |
| 45 | + status = DownloadStatus.NOT_FOUND |
| 46 | + msg = f'not found: {res.url}' |
52 | 47 | else: |
53 | | - raise FetchError(cc) from exc |
| 48 | + raise |
54 | 49 | else: |
55 | | - loop = asyncio.get_running_loop() # <1> |
56 | | - loop.run_in_executor(None, # <2> |
57 | | - save_flag, image, f'{cc}.gif') # <3> |
58 | | - status = HTTPStatus.ok |
| 50 | +# tag::FLAGS2_ASYNCIO_EXECUTOR[] |
| 51 | + loop = asyncio.get_running_loop() # <1> |
| 52 | + loop.run_in_executor(None, save_flag, # <2> |
| 53 | + image, f'{cc}.gif') # <3> |
| 54 | +# end::FLAGS2_ASYNCIO_EXECUTOR[] |
| 55 | + status = DownloadStatus.OK |
59 | 56 | msg = 'OK' |
60 | 57 | if verbose and msg: |
61 | 58 | print(cc, msg) |
62 | | - return Result(status, cc) |
63 | | -# end::FLAGS2_ASYNCIO_EXECUTOR[] |
| 59 | + return status |
64 | 60 |
|
65 | 61 | async def supervisor(cc_list: list[str], |
66 | 62 | base_url: str, |
67 | 63 | verbose: bool, |
68 | | - concur_req: int) -> Counter[HTTPStatus]: |
69 | | - counter: Counter[HTTPStatus] = Counter() |
70 | | - semaphore = asyncio.Semaphore(concur_req) |
71 | | - async with aiohttp.ClientSession() as session: |
| 64 | + concur_req: int) -> Counter[DownloadStatus]: # <1> |
| 65 | + counter: Counter[DownloadStatus] = Counter() |
| 66 | + semaphore = asyncio.Semaphore(concur_req) # <2> |
| 67 | + async with httpx.AsyncClient() as session: |
72 | 68 | to_do = [download_one(session, cc, base_url, semaphore, verbose) |
73 | | - for cc in sorted(cc_list)] |
74 | | - |
75 | | - to_do_iter = asyncio.as_completed(to_do) |
| 69 | + for cc in sorted(cc_list)] # <3> |
| 70 | + to_do_iter = asyncio.as_completed(to_do) # <4> |
76 | 71 | if not verbose: |
77 | | - to_do_iter = tqdm.tqdm(to_do_iter, total=len(cc_list)) |
78 | | - for coro in to_do_iter: |
| 72 | + to_do_iter = tqdm.tqdm(to_do_iter, total=len(cc_list)) # <5> |
| 73 | + for coro in to_do_iter: # <6> |
79 | 74 | try: |
80 | | - res = await coro |
81 | | - except FetchError as exc: |
82 | | - country_code = exc.country_code |
83 | | - try: |
84 | | - error_msg = exc.__cause__.message # type: ignore |
85 | | - except AttributeError: |
86 | | - error_msg = 'Unknown cause' |
87 | | - if verbose and error_msg: |
88 | | - print(f'*** Error for {country_code}: {error_msg}') |
89 | | - status = HTTPStatus.error |
90 | | - else: |
91 | | - status = res.status |
92 | | - |
93 | | - counter[status] += 1 |
94 | | - |
95 | | - return counter |
96 | | - |
| 75 | + status = await coro # <7> |
| 76 | + except httpx.HTTPStatusError as exc: # <13> |
| 77 | + error_msg = 'HTTP error {resp.status_code} - {resp.reason_phrase}' |
| 78 | + error_msg = error_msg.format(resp=exc.response) |
| 79 | + error = exc |
| 80 | + except httpx.RequestError as exc: # <15> |
| 81 | + error_msg = f'{exc} {type(exc)}'.strip() |
| 82 | + error = exc |
| 83 | + except KeyboardInterrupt: # <7> |
| 84 | + break |
| 85 | + else: # <8> |
| 86 | + error = None |
| 87 | + |
| 88 | + if error: |
| 89 | + status = DownloadStatus.ERROR # <9> |
| 90 | + if verbose: # <11> |
| 91 | + cc = Path(str(error.request.url)).stem.upper() |
| 92 | + print(f'{cc} error: {error_msg}') |
| 93 | + counter[status] += 1 # <10> |
| 94 | + |
| 95 | + return counter # <12> |
97 | 96 |
|
98 | 97 | def download_many(cc_list: list[str], |
99 | 98 | base_url: str, |
100 | 99 | verbose: bool, |
101 | | - concur_req: int) -> Counter[HTTPStatus]: |
| 100 | + concur_req: int) -> Counter[DownloadStatus]: |
102 | 101 | coro = supervisor(cc_list, base_url, verbose, concur_req) |
103 | 102 | counts = asyncio.run(coro) # <14> |
104 | 103 |
|
105 | 104 | return counts |
106 | 105 |
|
107 | | - |
108 | 106 | if __name__ == '__main__': |
109 | 107 | main(download_many, DEFAULT_CONCUR_REQ, MAX_CONCUR_REQ) |
| 108 | +# end::FLAGS2_ASYNCIO_START[] |
0 commit comments