Skip to content

Commit 6bb0893

Browse files
committed
Add authentication for private repos
Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com>
1 parent bb9ee83 commit 6bb0893

7 files changed

Lines changed: 125 additions & 1 deletion

File tree

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ install_requires =
6666
requests >= 2.7.0
6767
resolvelib
6868
saneyaml >= 0.5.2
69+
tinynetrc
6970
toml >= 0.10.0
7071

7172
[options.packages.find]

src/python_inspector/resolve_cli.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414

1515
import click
1616
from packaging.requirements import Requirement
17+
from tinynetrc import Netrc
1718

1819
from python_inspector import dependencies
20+
from python_inspector import utils
1921
from python_inspector import utils_pypi
2022
from python_inspector.cli_utils import FileOptionType
2123
from python_inspector.resolution import get_resolved_dependencies
@@ -40,6 +42,15 @@
4042
help="Path to pip requirements file listing thirdparty packages. "
4143
"This option can be used multiple times.",
4244
)
45+
@click.option(
46+
"-n",
47+
"--netrc",
48+
"netrc_file",
49+
type=click.Path(exists=True, readable=True, path_type=str, dir_okay=False),
50+
metavar="NETRC-FILE",
51+
required=False,
52+
help="Netrc file to use for authentication. ",
53+
)
4354
@click.option(
4455
"--spec",
4556
"--specifier",
@@ -111,6 +122,7 @@
111122
@click.help_option("-h", "--help")
112123
def resolve_dependencies(
113124
requirement_files,
125+
netrc_file,
114126
specifiers,
115127
python_version,
116128
operating_system,
@@ -141,6 +153,9 @@ def resolve_dependencies(
141153

142154
click.secho(f"Resolving dependencies...")
143155

156+
netrc = None
157+
if netrc_file:
158+
netrc = Netrc(file=netrc_file)
144159
# TODO: deduplicate me
145160
direct_dependencies = []
146161

@@ -179,9 +194,16 @@ def resolve_dependencies(
179194
existing.use_cached_index = use_cached_index
180195
repos.append(existing)
181196
else:
197+
credentials = None
198+
if netrc:
199+
login, password = utils.get_netrc_auth(index_url, netrc)
200+
credentials = (
201+
dict(login=login, password=password) if login and password else None
202+
)
182203
repo = utils_pypi.PypiSimpleRepository(
183204
index_url=index_url,
184205
use_cached_index=use_cached_index,
206+
credentials=credentials,
185207
)
186208
repos.append(repo)
187209

src/python_inspector/utils.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
#
4+
# Copyright (c) nexB Inc. and others. All rights reserved.
5+
# ScanCode is a trademark of nexB Inc.
6+
# SPDX-License-Identifier: Apache-2.0
7+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
8+
# See https://github.com/nexB/python-inspector for support or download.
9+
# See https://aboutcode.org for more information about nexB OSS projects.
10+
#
11+
def get_netrc_auth(url, netrc):
12+
"""
13+
Return login and password if url is in netrc
14+
else return login and password as None
15+
"""
16+
if netrc.get(url):
17+
return (netrc[url].get("login"), netrc[url].get("password"))
18+
return (None, None)

src/python_inspector/utils_pypi.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,11 @@ class Distribution(NameVer):
494494
metadata=dict(help="Extra data"),
495495
)
496496

497+
credentials = attr.ib(
498+
type=dict,
499+
default=None,
500+
)
501+
497502
@property
498503
def package_url(self):
499504
"""
@@ -565,6 +570,7 @@ def download(
565570
fetch_and_save(
566571
path_or_url=self.path_or_url,
567572
dest_dir=dest_dir,
573+
credentials=self.credentials,
568574
filename=self.filename,
569575
as_text=False,
570576
verbose=verbose,
@@ -1387,6 +1393,8 @@ class PypiSimpleRepository:
13871393
repr=False,
13881394
)
13891395

1396+
credentials = attr.ib(type=dict, default=None)
1397+
13901398
def _get_package_versions_map(
13911399
self,
13921400
name,
@@ -1483,6 +1491,7 @@ def fetch_links(
14831491
package_url = f"{self.index_url}/{normalized_name}"
14841492
text = CACHE.get(
14851493
path_or_url=package_url,
1494+
credentials=self.credentials,
14861495
as_text=True,
14871496
force=not self.use_cached_index,
14881497
verbose=verbose,
@@ -1520,6 +1529,7 @@ def __attrs_post_init__(self):
15201529

15211530
def get(
15221531
self,
1532+
credentials,
15231533
path_or_url,
15241534
as_text=True,
15251535
force=False,
@@ -1540,6 +1550,7 @@ def get(
15401550
print(f" FILE CACHE MISS: {path_or_url}")
15411551
content = get_file_content(
15421552
path_or_url=path_or_url,
1553+
credentials=credentials,
15431554
as_text=as_text,
15441555
verbose=verbose,
15451556
echo_func=echo_func,
@@ -1559,6 +1570,7 @@ def get(
15591570

15601571
def get_file_content(
15611572
path_or_url,
1573+
credentials,
15621574
as_text=True,
15631575
verbose=False,
15641576
echo_func=None,
@@ -1572,6 +1584,7 @@ def get_file_content(
15721584
print(f"Fetching: {path_or_url}")
15731585
_headers, content = get_remote_file_content(
15741586
url=path_or_url,
1587+
credentials=credentials,
15751588
as_text=as_text,
15761589
verbose=verbose,
15771590
echo_func=echo_func,
@@ -1606,6 +1619,7 @@ class RemoteNotFetchedException(Exception):
16061619

16071620
def get_remote_file_content(
16081621
url,
1622+
credentials,
16091623
as_text=True,
16101624
headers_only=False,
16111625
headers=None,
@@ -1631,7 +1645,20 @@ def get_remote_file_content(
16311645
echo_func = print
16321646
if verbose:
16331647
echo_func(f"DOWNLOADING: {url}")
1634-
with requests.get(url, allow_redirects=True, stream=True, headers=headers) as response:
1648+
1649+
auth = None
1650+
if credentials:
1651+
auth = (credentials.get("login"), credentials.get("password"))
1652+
1653+
stream = requests.get(
1654+
url,
1655+
allow_redirects=True,
1656+
stream=True,
1657+
headers=headers,
1658+
auth=auth,
1659+
)
1660+
1661+
with stream as response:
16351662
status = response.status_code
16361663
if status != requests.codes.ok: # NOQA
16371664
if status == 429 and _delay < 20:
@@ -1640,6 +1667,7 @@ def get_remote_file_content(
16401667

16411668
return get_remote_file_content(
16421669
url,
1670+
credentials=credentials,
16431671
as_text=as_text,
16441672
headers_only=headers_only,
16451673
_delay=increased_delay,
@@ -1658,6 +1686,7 @@ def fetch_and_save(
16581686
path_or_url,
16591687
dest_dir,
16601688
filename,
1689+
credentials,
16611690
as_text=True,
16621691
verbose=False,
16631692
echo_func=None,
@@ -1670,6 +1699,7 @@ def fetch_and_save(
16701699
"""
16711700
content = CACHE.get(
16721701
path_or_url=path_or_url,
1702+
credentials=credentials,
16731703
as_text=as_text,
16741704
verbose=verbose,
16751705
echo_func=echo_func,

tests/data/test.netrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
machine https://pyp1.org/simple login test password test123

tests/test_cli.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,27 @@ def test_cli_with_multiple_index_url_and_tilde_req():
7474
)
7575

7676

77+
@pytest.mark.online
78+
def test_cli_with_multiple_index_url_and_tilde_req_and_netrc_file_without_matching_url():
79+
expected_file = test_env.get_test_loc("tilde_req-expected.json", must_exist=False)
80+
netrc_file = test_env.get_test_loc("test.netrc", must_exist=False)
81+
specifier = "zipp~=3.8.0"
82+
extra_options = [
83+
"--index-url",
84+
"https://pypi.org/simple",
85+
"--index-url",
86+
"https://thirdparty.aboutcode.org/pypi/simple/",
87+
"--netrc",
88+
netrc_file,
89+
]
90+
check_specs_resolution(
91+
specifier=specifier,
92+
expected_file=expected_file,
93+
extra_options=extra_options,
94+
regen=REGEN_TEST_FIXTURES,
95+
)
96+
97+
7798
@pytest.mark.online
7899
def test_cli_with_pinned_requirements_file():
79100
requirements_file = test_env.get_test_loc("pinned-requirements.txt")

tests/test_utils.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
#
4+
# Copyright (c) nexB Inc. and others. All rights reserved.
5+
# ScanCode is a trademark of nexB Inc.
6+
# SPDX-License-Identifier: Apache-2.0
7+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
8+
# See https://github.com/nexB/python-inspector for support or download.
9+
# See https://aboutcode.org for more information about nexB OSS projects.
10+
#
11+
import os
12+
13+
from commoncode.testcase import FileDrivenTesting
14+
from tinynetrc import Netrc
15+
16+
from python_inspector.utils import get_netrc_auth
17+
18+
test_env = FileDrivenTesting()
19+
test_env.test_data_dir = os.path.join(os.path.dirname(__file__), "data")
20+
21+
22+
def test_get_netrc_auth():
23+
netrc_file = test_env.get_test_loc("test.netrc")
24+
netrc = Netrc(netrc_file)
25+
assert get_netrc_auth(url="https://pyp1.org/simple", netrc=netrc) == ("test", "test123")
26+
27+
28+
def test_get_netrc_auth_with_no_matching_url():
29+
netrc_file = test_env.get_test_loc("test.netrc")
30+
netrc = Netrc(netrc_file)
31+
assert get_netrc_auth(url="https://pypi2.org/simple", netrc=netrc) == (None, None)

0 commit comments

Comments
 (0)