From e30ef3e676541a99e6fbddd63e46a745a0443ec6 Mon Sep 17 00:00:00 2001 From: Jimisola Laursen Date: Thu, 25 May 2023 01:05:18 +0200 Subject: [PATCH 1/2] feat: Change to argparse --- pyproject.toml | 2 +- src/maven_artifact/__init__.py | 4 - src/maven_artifact/artifact.py | 18 ----- src/maven_artifact/downloader.py | 80 ++---------------- src/maven_artifact/main.py | 134 +++++++++++++++++++++++++++++++ src/maven_artifact/requestor.py | 9 ++- src/maven_artifact/resolver.py | 2 +- src/maven_artifact/utils.py | 39 +++++++++ 8 files changed, 188 insertions(+), 100 deletions(-) create mode 100755 src/maven_artifact/main.py create mode 100644 src/maven_artifact/utils.py diff --git a/pyproject.toml b/pyproject.toml index 9b172cd..f366888 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/src/maven_artifact/__init__.py b/src/maven_artifact/__init__.py index d8f3eac..e69de29 100644 --- a/src/maven_artifact/__init__.py +++ b/src/maven_artifact/__init__.py @@ -1,4 +0,0 @@ -from maven_artifact.requestor import Requestor, RequestException # noqa: F401 -from maven_artifact.artifact import Artifact # noqa: F401 -from maven_artifact.resolver import Resolver # noqa: F401 -from maven_artifact.downloader import Downloader # noqa: F401 diff --git a/src/maven_artifact/artifact.py b/src/maven_artifact/artifact.py index f8e5589..b01aa8e 100644 --- a/src/maven_artifact/artifact.py +++ b/src/maven_artifact/artifact.py @@ -53,21 +53,3 @@ def __str__(self): return "%s:%s:%s:%s" % (self.group_id, self.artifact_id, self.extension, self.version) else: return "%s:%s:%s" % (self.group_id, self.artifact_id, self.version) - - @staticmethod - def parse(input): - parts = input.split(":") - if len(parts) >= 3: - g = parts[0] - a = parts[1] - v = parts[len(parts) - 1] - t = None - c = None - if len(parts) == 4: - t = parts[2] - if len(parts) == 5: - t = parts[2] - c = parts[3] - return Artifact(g, a, v, c, t) - else: - return None diff --git a/src/maven_artifact/downloader.py b/src/maven_artifact/downloader.py index e60733a..ae62f53 100644 --- a/src/maven_artifact/downloader.py +++ b/src/maven_artifact/downloader.py @@ -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 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"): @@ -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 Maven-Coordinate filename - Options: - -m --maven-repo= - -u --username= - -p --password= - -ht --hash-type= - - 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 . - 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() diff --git a/src/maven_artifact/main.py b/src/maven_artifact/main.py new file mode 100755 index 0000000..fdc7d91 --- /dev/null +++ b/src/maven_artifact/main.py @@ -0,0 +1,134 @@ +#!/bin/env python + +import argparse +import os +import sys +import textwrap + +try: + from maven_artifact.utils import Utils +except ImportError: + sys.path.append(os.path.dirname(__file__)) + from maven_artifact.utils import Utils + +from maven_artifact import __version__ +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 + + +__epilog__ = """ + Example: + %(prog)s "org.apache.solr:solr:war:3.5.0"\n + """ + + +def main(): + 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 .. + 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() + + base = args.base + username = args.username + password = args.password + token = args.token + hash_type = args.hash_type + + 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'.") + + dl = Downloader(base=base, username=username, password=password, token=token) + + artifact = Utils.parse(args.maven_coordinate) + + filename = args.filename + + try: + if dl.download(artifact, filename, hash_type): + sys.exit(0) + else: + parser.print_usage() + sys.exit(1) + except RequestException as e: + print(e.msg) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/src/maven_artifact/requestor.py b/src/maven_artifact/requestor.py index ab59cc2..6a51d3d 100644 --- a/src/maven_artifact/requestor.py +++ b/src/maven_artifact/requestor.py @@ -1,6 +1,7 @@ import base64 import requests +from maven_artifact.utils import Utils class RequestException(Exception): def __init__(self, msg): @@ -8,16 +9,22 @@ def __init__(self, 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() + elif Utils.is_base64(self.password): + headers["Authorization"] = "Basic " + base64.decode(self.password) + elif self.token: + headers["Authorization"] = "Bearer " + self.token try: response = getattr(requests, method)(url, headers=headers, **kwargs) diff --git a/src/maven_artifact/resolver.py b/src/maven_artifact/resolver.py index 698eec7..6519282 100644 --- a/src/maven_artifact/resolver.py +++ b/src/maven_artifact/resolver.py @@ -1,6 +1,6 @@ from lxml import etree -from .requestor import RequestException +from maven_artifact.requestor import RequestException class Resolver(object): diff --git a/src/maven_artifact/utils.py b/src/maven_artifact/utils.py new file mode 100644 index 0000000..0adc9df --- /dev/null +++ b/src/maven_artifact/utils.py @@ -0,0 +1,39 @@ +import base64 + +from maven_artifact.artifact import Artifact + + +class Utils: + + @staticmethod + def parse(maven_coordinate): + parts = maven_coordinate.split(":") + if len(parts) >= 3: + g = parts[0] + a = parts[1] + v = parts[len(parts) - 1] + t = None + c = None + if len(parts) == 4: + t = parts[2] + if len(parts) == 5: + t = parts[2] + c = parts[3] + return Artifact(group_id=g, artifact_id=a, version=v, classifier=c, extension=t) + else: + return None + + @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 + From 5bbea04a3bebba2f7cc85f938f752ec1ea026d6e Mon Sep 17 00:00:00 2001 From: Jimisola Laursen Date: Thu, 25 May 2023 10:40:00 +0200 Subject: [PATCH 2/2] feat: Add support for base64 and OAuth tokens --- src/maven_artifact/__init__.py | 5 + src/maven_artifact/artifact.py | 18 +++ src/maven_artifact/downloader.py | 2 +- src/maven_artifact/main.py | 128 ++++++++++-------- src/maven_artifact/requestor.py | 7 +- src/maven_artifact/utils.py | 22 --- .../maven_artifact/test_downloader.py | 3 +- 7 files changed, 98 insertions(+), 87 deletions(-) diff --git a/src/maven_artifact/__init__.py b/src/maven_artifact/__init__.py index e69de29..3fba994 100644 --- a/src/maven_artifact/__init__.py +++ b/src/maven_artifact/__init__.py @@ -0,0 +1,5 @@ +from maven_artifact.requestor import Requestor, RequestException # noqa: F401 +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 diff --git a/src/maven_artifact/artifact.py b/src/maven_artifact/artifact.py index b01aa8e..784f368 100644 --- a/src/maven_artifact/artifact.py +++ b/src/maven_artifact/artifact.py @@ -53,3 +53,21 @@ def __str__(self): return "%s:%s:%s:%s" % (self.group_id, self.artifact_id, self.extension, self.version) else: return "%s:%s:%s" % (self.group_id, self.artifact_id, self.version) + + @staticmethod + def parse(maven_coordinate): + parts = maven_coordinate.split(":") + if len(parts) >= 3: + g = parts[0] + a = parts[1] + v = parts[len(parts) - 1] + t = None + c = None + if len(parts) == 4: + t = parts[2] + if len(parts) == 5: + t = parts[2] + c = parts[3] + return Artifact(group_id=g, artifact_id=a, version=v, classifier=c, extension=t) + else: + return None diff --git a/src/maven_artifact/downloader.py b/src/maven_artifact/downloader.py index ae62f53..bdd70aa 100644 --- a/src/maven_artifact/downloader.py +++ b/src/maven_artifact/downloader.py @@ -1,7 +1,7 @@ import hashlib import os -from maven_artifact.requestor import Requestor +from maven_artifact.requestor import RequestException, Requestor from maven_artifact.resolver import Resolver diff --git a/src/maven_artifact/main.py b/src/maven_artifact/main.py index fdc7d91..fedc3e7 100755 --- a/src/maven_artifact/main.py +++ b/src/maven_artifact/main.py @@ -4,14 +4,14 @@ 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.dirname(__file__)) + sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) from maven_artifact.utils import Utils -from maven_artifact import __version__ from maven_artifact.requestor import RequestException from maven_artifact.downloader import Downloader @@ -55,6 +55,62 @@ def _split_lines(self, text, width): 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 .. + 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 @@ -62,68 +118,20 @@ def _split_lines(self, text, width): def main(): - 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 .. - 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() - - base = args.base - username = args.username - password = args.password - token = args.token - hash_type = args.hash_type - - 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'.") - - dl = Downloader(base=base, username=username, password=password, token=token) - - artifact = Utils.parse(args.maven_coordinate) - - filename = args.filename + mc = MainCommand() + args = mc._get_arguments() try: - if dl.download(artifact, filename, hash_type): + 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: - parser.print_usage() + print("Download failed.") sys.exit(1) except RequestException as e: print(e.msg) diff --git a/src/maven_artifact/requestor.py b/src/maven_artifact/requestor.py index 6a51d3d..22c559c 100644 --- a/src/maven_artifact/requestor.py +++ b/src/maven_artifact/requestor.py @@ -3,6 +3,7 @@ from maven_artifact.utils import Utils + class RequestException(Exception): def __init__(self, msg): self.msg = msg @@ -20,11 +21,11 @@ def request(self, url, onFail, onSuccess=None, method: str = "get", **kwargs): 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"] = "Basic " + base64.decode(self.password) + headers["Authorization"] = f"Basic {self.password}" elif self.token: - headers["Authorization"] = "Bearer " + self.token + headers["Authorization"] = f"Bearer {base64.b64encode(self.token.encode()).decode()}" try: response = getattr(requests, method)(url, headers=headers, **kwargs) diff --git a/src/maven_artifact/utils.py b/src/maven_artifact/utils.py index 0adc9df..e33742d 100644 --- a/src/maven_artifact/utils.py +++ b/src/maven_artifact/utils.py @@ -1,28 +1,7 @@ import base64 -from maven_artifact.artifact import Artifact - class Utils: - - @staticmethod - def parse(maven_coordinate): - parts = maven_coordinate.split(":") - if len(parts) >= 3: - g = parts[0] - a = parts[1] - v = parts[len(parts) - 1] - t = None - c = None - if len(parts) == 4: - t = parts[2] - if len(parts) == 5: - t = parts[2] - c = parts[3] - return Artifact(group_id=g, artifact_id=a, version=v, classifier=c, extension=t) - else: - return None - @staticmethod def is_base64(sb): try: @@ -36,4 +15,3 @@ def is_base64(sb): return base64.b64encode(base64.b64decode(sb_bytes)) == sb_bytes except Exception: return False - diff --git a/tests/integration/maven_artifact/test_downloader.py b/tests/integration/maven_artifact/test_downloader.py index d320fac..38635a2 100644 --- a/tests/integration/maven_artifact/test_downloader.py +++ b/tests/integration/maven_artifact/test_downloader.py @@ -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():