Skip to content

Commit 7b2a88f

Browse files
authored
Merge pull request #24 from jimisola/main
Adds support for base64 and OAuth bearer tokens
2 parents e16db78 + 5bbea04 commit 7b2a88f

File tree

9 files changed

+182
-83
lines changed

9 files changed

+182
-83
lines changed

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ requires-python = ">=3.8"
5252
dependencies = ["lxml", "requests"]
5353

5454
[project.scripts]
55-
maven-artifact = "maven_artifact.downloader:main"
55+
maven-artifact = "maven_artifact.main:main"
5656

5757
[tool.hatch.version]
5858
source = "vcs"

src/maven_artifact/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
from maven_artifact.artifact import Artifact # noqa: F401
33
from maven_artifact.resolver import Resolver # noqa: F401
44
from maven_artifact.downloader import Downloader # noqa: F401
5+
from maven_artifact.utils import Utils # noqa: F401

src/maven_artifact/artifact.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ def __str__(self):
5555
return "%s:%s:%s" % (self.group_id, self.artifact_id, self.version)
5656

5757
@staticmethod
58-
def parse(input):
59-
parts = input.split(":")
58+
def parse(maven_coordinate):
59+
parts = maven_coordinate.split(":")
6060
if len(parts) >= 3:
6161
g = parts[0]
6262
a = parts[1]
@@ -68,6 +68,6 @@ def parse(input):
6868
if len(parts) == 5:
6969
t = parts[2]
7070
c = parts[3]
71-
return Artifact(g, a, v, c, t)
71+
return Artifact(group_id=g, artifact_id=a, version=v, classifier=c, extension=t)
7272
else:
7373
return None

src/maven_artifact/downloader.py

+5-75
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import hashlib
22
import os
3-
from .requestor import Requestor, RequestException
4-
from .resolver import Resolver
5-
from .artifact import Artifact
6-
import sys
7-
import getopt
3+
4+
from maven_artifact.requestor import RequestException, Requestor
5+
from maven_artifact.resolver import Resolver
86

97

108
class Downloader(object):
11-
def __init__(self, base="https://repo.maven.apache.org/maven2", username=None, password=None):
12-
self.requestor = Requestor(username, password)
9+
def __init__(self, base="https://repo.maven.apache.org/maven2", username=None, password=None, token=None):
10+
self.requestor = Requestor(username=username, password=password, token=token)
1311
self.resolver = Resolver(base, self.requestor)
1412

1513
def download(self, artifact, filename=None, hash_type="md5"):
@@ -43,71 +41,3 @@ def onError(uri, err):
4341
remote_hash = self.requestor.request(url, onError, lambda r: r.text)
4442
local_hash = getattr(hashlib, hash_type)(open(file, "rb").read()).hexdigest()
4543
return remote_hash == local_hash
46-
47-
48-
__doc__ = """
49-
Usage:
50-
%(program_name)s <options> Maven-Coordinate filename
51-
Options:
52-
-m <url> --maven-repo=<url>
53-
-u <username> --username=<username>
54-
-p <password> --password=<password>
55-
-ht <hashtype> --hash-type=<hashtype>
56-
57-
Maven-Coordinate are defined by: http://maven.apache.org/pom.html#Maven_Coordinates
58-
The possible options are:
59-
- groupId:artifactId:version
60-
- groupId:artifactId:packaging:version
61-
- groupId:artifactId:packaging:classifier:version
62-
filename is optional. If not supplied the filename will be <artifactId>.<extension>
63-
The filename directory must exist prior to download.
64-
65-
Example:
66-
%(program_name)s "org.apache.solr:solr:war:3.5.0"
67-
"""
68-
69-
70-
def main():
71-
try:
72-
opts, args = getopt.getopt(sys.argv[1:], "m:u:p:ht", ["maven-repo=", "username=", "password=", "hash-type="])
73-
except getopt.GetoptError as err:
74-
# print help information and exit:
75-
print(str(err)) # will print something like "option -a not recognized"
76-
usage()
77-
sys.exit(2)
78-
79-
if not len(args):
80-
print("No maven coordiantes supplied")
81-
usage()
82-
sys.exit(2)
83-
else:
84-
options = dict(opts)
85-
86-
base = options.get("-m") or options.get("--maven-repo", "https://repo1.maven.org/maven2")
87-
username = options.get("-u") or options.get("--username")
88-
password = options.get("-p") or options.get("--password")
89-
hash_type = options.get("-ht") or options.get("--hash-type", "md5")
90-
91-
dl = Downloader(base, username, password)
92-
93-
artifact = Artifact.parse(args[0])
94-
95-
filename = args[1] if len(args) == 2 else None
96-
97-
try:
98-
if dl.download(artifact, filename, hash_type):
99-
sys.exit(0)
100-
else:
101-
usage()
102-
sys.exit(1)
103-
except RequestException as e:
104-
print(e.msg)
105-
sys.exit(1)
106-
107-
108-
def usage():
109-
print(__doc__ % {"program_name": os.path.basename(sys.argv[0])})
110-
111-
112-
if __name__ == "__main__":
113-
main()

src/maven_artifact/main.py

+142
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
#!/bin/env python
2+
3+
import argparse
4+
import os
5+
import sys
6+
import textwrap
7+
from maven_artifact.artifact import Artifact
8+
9+
try:
10+
from maven_artifact.utils import Utils
11+
except ImportError:
12+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
13+
from maven_artifact.utils import Utils
14+
15+
from maven_artifact.requestor import RequestException
16+
from maven_artifact.downloader import Downloader
17+
18+
19+
class DescriptionWrappedNewlineFormatter(argparse.HelpFormatter):
20+
"""An argparse formatter that:
21+
* preserves newlines (like argparse.RawDescriptionHelpFormatter),
22+
* removes leading indent (great for multiline strings),
23+
* and applies reasonable text wrapping.
24+
25+
Source: https://stackoverflow.com/a/64102901/79125
26+
"""
27+
28+
def _fill_text(self, text, width, indent):
29+
# Strip the indent from the original python definition that plagues most of us.
30+
text = textwrap.dedent(text)
31+
text = textwrap.indent(text, indent) # Apply any requested indent.
32+
text = text.splitlines() # Make a list of lines
33+
text = [textwrap.fill(line, width) for line in text] # Wrap each line
34+
text = "\n".join(text) # Join the lines again
35+
return text
36+
37+
38+
class WrappedNewlineFormatter(DescriptionWrappedNewlineFormatter):
39+
"""An argparse formatter that:
40+
* preserves newlines (like argparse.RawTextHelpFormatter),
41+
* removes leading indent and applies reasonable text wrapping (like DescriptionWrappedNewlineFormatter),
42+
* applies to all help text (description, arguments, epilogue).
43+
"""
44+
45+
def _split_lines(self, text, width):
46+
# Allow multiline strings to have common leading indentation.
47+
text = textwrap.dedent(text)
48+
text = text.splitlines()
49+
lines = []
50+
for line in text:
51+
wrapped_lines = textwrap.fill(line, width).splitlines()
52+
lines.extend(subline for subline in wrapped_lines)
53+
if line:
54+
lines.append("") # Preserve line breaks.
55+
return lines
56+
57+
58+
class MainCommand:
59+
def _get_arguments(self):
60+
parser = argparse.ArgumentParser(formatter_class=WrappedNewlineFormatter, epilog=__epilog__)
61+
parser.add_argument(
62+
"maven_coordinate",
63+
help="""
64+
defined by http://maven.apache.org/pom.html#Maven_Coordinates. The possible options are:
65+
- groupId:artifactId:version
66+
- groupId:artifactId:packaging:version
67+
- groupId:artifactId:packaging:classifier:version""",
68+
)
69+
parser.add_argument(
70+
"filename",
71+
nargs="?",
72+
help="""
73+
If not supplied the filename will be <artifactId>.<extension>.
74+
The filename directory must exist prior to download.""",
75+
)
76+
parser.add_argument(
77+
"-m",
78+
"--maven-repo",
79+
dest="base",
80+
default="https://repo.maven.apache.org/maven2/",
81+
help="Maven repository URL (default: https://repo.maven.apache.org/maven2/)",
82+
)
83+
84+
parser.add_argument("-u", "--username", help="username (must be combined with --password)")
85+
parser.add_argument(
86+
"-p",
87+
"--password",
88+
help="""
89+
password (must be combined with --username) or
90+
base64 encoded username and password (can not not be combined with --username)""",
91+
)
92+
parser.add_argument(
93+
"-t", "--token", help="OAuth bearer token (can not be combined with --username or --password)"
94+
)
95+
96+
parser.add_argument("-ht", "--hash-type", default="md5", help="hash type (default: md5)")
97+
98+
args = parser.parse_args()
99+
100+
username = args.username
101+
password = args.password
102+
token = args.token
103+
104+
if username and not password:
105+
parser.error("The 'username' parameter requires the 'password' parameter.")
106+
elif (username or password) and token:
107+
parser.error("The 'token' parameter cannot be used together with 'username' or 'password'.")
108+
elif (password) and not (username or token) and not Utils.is_base64(password):
109+
parser.error("The 'password' parameter must be base64 if not used together with 'username'.")
110+
111+
return args
112+
113+
114+
__epilog__ = """
115+
Example:
116+
%(prog)s "org.apache.solr:solr:war:3.5.0"\n
117+
"""
118+
119+
120+
def main():
121+
mc = MainCommand()
122+
args = mc._get_arguments()
123+
124+
try:
125+
dl = Downloader(base=args.base, username=args.username, password=args.password, token=args.token)
126+
127+
artifact = Artifact.parse(args.maven_coordinate)
128+
129+
filename = args.filename
130+
131+
if dl.download(artifact, filename, args.hash_type):
132+
sys.exit(0)
133+
else:
134+
print("Download failed.")
135+
sys.exit(1)
136+
except RequestException as e:
137+
print(e.msg)
138+
sys.exit(1)
139+
140+
141+
if __name__ == "__main__":
142+
main()

src/maven_artifact/requestor.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,31 @@
11
import base64
22
import requests
33

4+
from maven_artifact.utils import Utils
5+
46

57
class RequestException(Exception):
68
def __init__(self, msg):
79
self.msg = msg
810

911

1012
class Requestor(object):
11-
def __init__(self, username=None, password=None, user_agent="Maven Artifact Downloader/1.0"):
13+
def __init__(self, username=None, password=None, token=None, user_agent="Maven Artifact Downloader/1.0"):
1214
self.user_agent = user_agent
1315
self.username = username
1416
self.password = password
17+
self.token = token
1518

1619
def request(self, url, onFail, onSuccess=None, method: str = "get", **kwargs):
1720
headers = {"User-Agent": self.user_agent}
21+
1822
if self.username and self.password:
1923
token = self.username + ":" + self.password
20-
headers["Authorization"] = "Basic " + base64.b64encode(token.encode()).decode()
24+
headers["Authorization"] = f"Basic {base64.b64encode(token.encode()).decode()}"
25+
elif Utils.is_base64(self.password):
26+
headers["Authorization"] = f"Basic {self.password}"
27+
elif self.token:
28+
headers["Authorization"] = f"Bearer {base64.b64encode(self.token.encode()).decode()}"
2129

2230
try:
2331
response = getattr(requests, method)(url, headers=headers, **kwargs)

src/maven_artifact/resolver.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from lxml import etree
22

3-
from .requestor import RequestException
3+
from maven_artifact.requestor import RequestException
44

55

66
class Resolver(object):

src/maven_artifact/utils.py

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import base64
2+
3+
4+
class Utils:
5+
@staticmethod
6+
def is_base64(sb):
7+
try:
8+
if isinstance(sb, str):
9+
# If there's any unicode here, an exception will be thrown and the function will return false
10+
sb_bytes = bytes(sb, "ascii")
11+
elif isinstance(sb, bytes):
12+
sb_bytes = sb
13+
else:
14+
raise ValueError("Argument must be string or bytes")
15+
return base64.b64encode(base64.b64decode(sb_bytes)) == sb_bytes
16+
except Exception:
17+
return False

tests/integration/maven_artifact/test_downloader.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import os
22
import tempfile
33

4-
from maven_artifact import Artifact, Downloader
4+
from maven_artifact import Downloader
5+
from maven_artifact.artifact import Artifact
56

67

78
def test_downloader_of_existing_artifact():

0 commit comments

Comments
 (0)