1
1
import base64
2
2
import contextlib
3
+ import dataclasses
3
4
import itertools
4
5
import os .path
5
6
import typing
8
9
import httpx
9
10
10
11
11
- # TODO: make configurable
12
- PYPI = 'https://pypi.org'
13
- STORAGE_DIR = '/home/ckuehl/tmp/pypi-view'
12
+ @dataclasses .dataclass (frozen = True )
13
+ class PyPIConfig :
14
+ cache_path : str
15
+ pypi_url : str
14
16
15
17
16
18
class PackageDoesNotExist (Exception ):
17
19
pass
18
20
19
21
20
- async def package_metadata (client : httpx .AsyncClient , package : str ) -> typing .Dict :
21
- resp = await client .get (f'{ PYPI } /pypi/{ package } /json' )
22
+ async def package_metadata (config : PyPIConfig , client : httpx .AsyncClient , package : str ) -> typing .Dict :
23
+ resp = await client .get (f'{ config . pypi_url } /pypi/{ package } /json' )
22
24
if resp .status_code == 404 :
23
25
raise PackageDoesNotExist (package )
24
26
resp .raise_for_status ()
25
27
return resp .json ()
26
28
27
29
28
- async def files_for_package (package : str ) -> typing .Dict [str , typing .Set [str ]]:
30
+ async def files_for_package (config : PyPIConfig , package : str ) -> typing .Dict [str , typing .Set [str ]]:
29
31
async with httpx .AsyncClient () as client :
30
- metadata = await package_metadata (client , package )
32
+ metadata = await package_metadata (config , client , package )
31
33
32
34
return {
33
35
version : {file_ ['filename' ] for file_ in files }
@@ -40,9 +42,9 @@ class CannotFindFileError(Exception):
40
42
pass
41
43
42
44
43
- def _storage_path (package : str , filename : str ) -> str :
45
+ def _storage_path (config : PyPIConfig , package : str , filename : str ) -> str :
44
46
return os .path .join (
45
- STORAGE_DIR ,
47
+ config . cache_path ,
46
48
# Base64-encoding the names to calculate the storage path just to be
47
49
# extra sure to avoid any path traversal vulnerabilities.
48
50
base64 .urlsafe_b64encode (package .encode ('utf8' )).decode ('ascii' ),
@@ -63,18 +65,18 @@ async def _atomic_file(path: str, mode: str = 'w') -> typing.Any:
63
65
await aiofiles .os .rename (f .name , path )
64
66
65
67
66
- async def downloaded_file_path (package : str , filename : str ) -> str :
68
+ async def downloaded_file_path (config : PyPIConfig , package : str , filename : str ) -> str :
67
69
"""Return path on filesystem to downloaded PyPI file.
68
70
69
71
May be instant if the file is already cached; otherwise it will download
70
72
it and may take a while.
71
73
"""
72
- stored_path = _storage_path (package , filename )
74
+ stored_path = _storage_path (config , package , filename )
73
75
if await aiofiles .os .path .exists (stored_path ):
74
76
return stored_path
75
77
76
78
async with httpx .AsyncClient () as client :
77
- metadata = await package_metadata (client , package )
79
+ metadata = await package_metadata (config , client , package )
78
80
79
81
# Parsing versions from non-wheel Python packages isn't perfectly
80
82
# reliable, so just search through all releases until we find a
0 commit comments