Skip to content

Commit

Permalink
Merge pull request #24 from jimisola/main
Browse files Browse the repository at this point in the history
Adds support for base64 and OAuth bearer tokens
  • Loading branch information
hamnis authored May 25, 2023
2 parents e16db78 + 5bbea04 commit 7b2a88f
Show file tree
Hide file tree
Showing 9 changed files with 182 additions and 83 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ requires-python = ">=3.8"
dependencies = ["lxml", "requests"]

[project.scripts]
maven-artifact = "maven_artifact.downloader:main"
maven-artifact = "maven_artifact.main:main"

[tool.hatch.version]
source = "vcs"
Expand Down
1 change: 1 addition & 0 deletions src/maven_artifact/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
from maven_artifact.artifact import Artifact # noqa: F401
from maven_artifact.resolver import Resolver # noqa: F401
from maven_artifact.downloader import Downloader # noqa: F401
from maven_artifact.utils import Utils # noqa: F401
6 changes: 3 additions & 3 deletions src/maven_artifact/artifact.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ def __str__(self):
return "%s:%s:%s" % (self.group_id, self.artifact_id, self.version)

@staticmethod
def parse(input):
parts = input.split(":")
def parse(maven_coordinate):
parts = maven_coordinate.split(":")
if len(parts) >= 3:
g = parts[0]
a = parts[1]
Expand All @@ -68,6 +68,6 @@ def parse(input):
if len(parts) == 5:
t = parts[2]
c = parts[3]
return Artifact(g, a, v, c, t)
return Artifact(group_id=g, artifact_id=a, version=v, classifier=c, extension=t)
else:
return None
80 changes: 5 additions & 75 deletions src/maven_artifact/downloader.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import hashlib
import os
from .requestor import Requestor, RequestException
from .resolver import Resolver
from .artifact import Artifact
import sys
import getopt

from maven_artifact.requestor import RequestException, Requestor
from maven_artifact.resolver import Resolver


class Downloader(object):
def __init__(self, base="https://repo.maven.apache.org/maven2", username=None, password=None):
self.requestor = Requestor(username, password)
def __init__(self, base="https://repo.maven.apache.org/maven2", username=None, password=None, token=None):
self.requestor = Requestor(username=username, password=password, token=token)
self.resolver = Resolver(base, self.requestor)

def download(self, artifact, filename=None, hash_type="md5"):
Expand Down Expand Up @@ -43,71 +41,3 @@ def onError(uri, err):
remote_hash = self.requestor.request(url, onError, lambda r: r.text)
local_hash = getattr(hashlib, hash_type)(open(file, "rb").read()).hexdigest()
return remote_hash == local_hash


__doc__ = """
Usage:
%(program_name)s <options> Maven-Coordinate filename
Options:
-m <url> --maven-repo=<url>
-u <username> --username=<username>
-p <password> --password=<password>
-ht <hashtype> --hash-type=<hashtype>
Maven-Coordinate are defined by: http://maven.apache.org/pom.html#Maven_Coordinates
The possible options are:
- groupId:artifactId:version
- groupId:artifactId:packaging:version
- groupId:artifactId:packaging:classifier:version
filename is optional. If not supplied the filename will be <artifactId>.<extension>
The filename directory must exist prior to download.
Example:
%(program_name)s "org.apache.solr:solr:war:3.5.0"
"""


def main():
try:
opts, args = getopt.getopt(sys.argv[1:], "m:u:p:ht", ["maven-repo=", "username=", "password=", "hash-type="])
except getopt.GetoptError as err:
# print help information and exit:
print(str(err)) # will print something like "option -a not recognized"
usage()
sys.exit(2)

if not len(args):
print("No maven coordiantes supplied")
usage()
sys.exit(2)
else:
options = dict(opts)

base = options.get("-m") or options.get("--maven-repo", "https://repo1.maven.org/maven2")
username = options.get("-u") or options.get("--username")
password = options.get("-p") or options.get("--password")
hash_type = options.get("-ht") or options.get("--hash-type", "md5")

dl = Downloader(base, username, password)

artifact = Artifact.parse(args[0])

filename = args[1] if len(args) == 2 else None

try:
if dl.download(artifact, filename, hash_type):
sys.exit(0)
else:
usage()
sys.exit(1)
except RequestException as e:
print(e.msg)
sys.exit(1)


def usage():
print(__doc__ % {"program_name": os.path.basename(sys.argv[0])})


if __name__ == "__main__":
main()
142 changes: 142 additions & 0 deletions src/maven_artifact/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
#!/bin/env python

import argparse
import os
import sys
import textwrap
from maven_artifact.artifact import Artifact

try:
from maven_artifact.utils import Utils
except ImportError:
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from maven_artifact.utils import Utils

from maven_artifact.requestor import RequestException
from maven_artifact.downloader import Downloader


class DescriptionWrappedNewlineFormatter(argparse.HelpFormatter):
"""An argparse formatter that:
* preserves newlines (like argparse.RawDescriptionHelpFormatter),
* removes leading indent (great for multiline strings),
* and applies reasonable text wrapping.
Source: https://stackoverflow.com/a/64102901/79125
"""

def _fill_text(self, text, width, indent):
# Strip the indent from the original python definition that plagues most of us.
text = textwrap.dedent(text)
text = textwrap.indent(text, indent) # Apply any requested indent.
text = text.splitlines() # Make a list of lines
text = [textwrap.fill(line, width) for line in text] # Wrap each line
text = "\n".join(text) # Join the lines again
return text


class WrappedNewlineFormatter(DescriptionWrappedNewlineFormatter):
"""An argparse formatter that:
* preserves newlines (like argparse.RawTextHelpFormatter),
* removes leading indent and applies reasonable text wrapping (like DescriptionWrappedNewlineFormatter),
* applies to all help text (description, arguments, epilogue).
"""

def _split_lines(self, text, width):
# Allow multiline strings to have common leading indentation.
text = textwrap.dedent(text)
text = text.splitlines()
lines = []
for line in text:
wrapped_lines = textwrap.fill(line, width).splitlines()
lines.extend(subline for subline in wrapped_lines)
if line:
lines.append("") # Preserve line breaks.
return lines


class MainCommand:
def _get_arguments(self):
parser = argparse.ArgumentParser(formatter_class=WrappedNewlineFormatter, epilog=__epilog__)
parser.add_argument(
"maven_coordinate",
help="""
defined by http://maven.apache.org/pom.html#Maven_Coordinates. The possible options are:
- groupId:artifactId:version
- groupId:artifactId:packaging:version
- groupId:artifactId:packaging:classifier:version""",
)
parser.add_argument(
"filename",
nargs="?",
help="""
If not supplied the filename will be <artifactId>.<extension>.
The filename directory must exist prior to download.""",
)
parser.add_argument(
"-m",
"--maven-repo",
dest="base",
default="https://repo.maven.apache.org/maven2/",
help="Maven repository URL (default: https://repo.maven.apache.org/maven2/)",
)

parser.add_argument("-u", "--username", help="username (must be combined with --password)")
parser.add_argument(
"-p",
"--password",
help="""
password (must be combined with --username) or
base64 encoded username and password (can not not be combined with --username)""",
)
parser.add_argument(
"-t", "--token", help="OAuth bearer token (can not be combined with --username or --password)"
)

parser.add_argument("-ht", "--hash-type", default="md5", help="hash type (default: md5)")

args = parser.parse_args()

username = args.username
password = args.password
token = args.token

if username and not password:
parser.error("The 'username' parameter requires the 'password' parameter.")
elif (username or password) and token:
parser.error("The 'token' parameter cannot be used together with 'username' or 'password'.")
elif (password) and not (username or token) and not Utils.is_base64(password):
parser.error("The 'password' parameter must be base64 if not used together with 'username'.")

return args


__epilog__ = """
Example:
%(prog)s "org.apache.solr:solr:war:3.5.0"\n
"""


def main():
mc = MainCommand()
args = mc._get_arguments()

try:
dl = Downloader(base=args.base, username=args.username, password=args.password, token=args.token)

artifact = Artifact.parse(args.maven_coordinate)

filename = args.filename

if dl.download(artifact, filename, args.hash_type):
sys.exit(0)
else:
print("Download failed.")
sys.exit(1)
except RequestException as e:
print(e.msg)
sys.exit(1)


if __name__ == "__main__":
main()
12 changes: 10 additions & 2 deletions src/maven_artifact/requestor.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
import base64
import requests

from maven_artifact.utils import Utils


class RequestException(Exception):
def __init__(self, msg):
self.msg = msg


class Requestor(object):
def __init__(self, username=None, password=None, user_agent="Maven Artifact Downloader/1.0"):
def __init__(self, username=None, password=None, token=None, user_agent="Maven Artifact Downloader/1.0"):
self.user_agent = user_agent
self.username = username
self.password = password
self.token = token

def request(self, url, onFail, onSuccess=None, method: str = "get", **kwargs):
headers = {"User-Agent": self.user_agent}

if self.username and self.password:
token = self.username + ":" + self.password
headers["Authorization"] = "Basic " + base64.b64encode(token.encode()).decode()
headers["Authorization"] = f"Basic {base64.b64encode(token.encode()).decode()}"
elif Utils.is_base64(self.password):
headers["Authorization"] = f"Basic {self.password}"
elif self.token:
headers["Authorization"] = f"Bearer {base64.b64encode(self.token.encode()).decode()}"

try:
response = getattr(requests, method)(url, headers=headers, **kwargs)
Expand Down
2 changes: 1 addition & 1 deletion src/maven_artifact/resolver.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from lxml import etree

from .requestor import RequestException
from maven_artifact.requestor import RequestException


class Resolver(object):
Expand Down
17 changes: 17 additions & 0 deletions src/maven_artifact/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import base64


class Utils:
@staticmethod
def is_base64(sb):
try:
if isinstance(sb, str):
# If there's any unicode here, an exception will be thrown and the function will return false
sb_bytes = bytes(sb, "ascii")
elif isinstance(sb, bytes):
sb_bytes = sb
else:
raise ValueError("Argument must be string or bytes")
return base64.b64encode(base64.b64decode(sb_bytes)) == sb_bytes
except Exception:
return False
3 changes: 2 additions & 1 deletion tests/integration/maven_artifact/test_downloader.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import os
import tempfile

from maven_artifact import Artifact, Downloader
from maven_artifact import Downloader
from maven_artifact.artifact import Artifact


def test_downloader_of_existing_artifact():
Expand Down

0 comments on commit 7b2a88f

Please sign in to comment.