From b52da681c9130fa8bd084f5e7e3fd4a2b6207b26 Mon Sep 17 00:00:00 2001 From: Danila Vershinin Date: Mon, 23 Dec 2024 15:07:50 +0800 Subject: [PATCH] Refactor CLI structure and update dependencies Moved CLI logic to a dedicated `cli.py` module to improve maintainability and readability. Updated Python base image to 3.13 in Dockerfile and added the `truststore` package for enhanced SSL certificate handling. Adjusted tests, imports, and entry points to reflect the new CLI structure. --- Dockerfile | 5 +- bandit.yml | 4 +- setup.py | 2 +- src/lastversion/__init__.py | 20 +- src/lastversion/__main__.py | 5 +- src/lastversion/argparse_version.py | 19 +- src/lastversion/cli.py | 412 +++++++++++++++++++++++++ src/lastversion/lastversion.py | 405 +----------------------- src/lastversion/repo_holders/base.py | 2 +- src/lastversion/repo_holders/github.py | 9 + tests/test_cli.py | 3 +- tests/test_github.py | 2 +- 12 files changed, 452 insertions(+), 436 deletions(-) create mode 100644 src/lastversion/cli.py diff --git a/Dockerfile b/Dockerfile index 8d889a20..9cf98643 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Use a lightweight base image with Python and pip installed -FROM python:3.9-alpine +FROM python:3.13-alpine # Using "lastversion" user as provided by some linter was a mistake and causes issues with GitHub actions being ran as "runner" # and lastversion running as a different user and being unable to work with workspace files for extracting to its directory @@ -15,5 +15,8 @@ COPY setup.py README.md ./ # Install the application and its dependencies RUN pip install -e . +# Additionally install truststore package for SSL certificate verification via pip +RUN pip install truststore + # Set the entrypoint to the command that runs your application ENTRYPOINT ["lastversion"] diff --git a/bandit.yml b/bandit.yml index 20a3b36e..37c9e75f 100644 --- a/bandit.yml +++ b/bandit.yml @@ -3,6 +3,6 @@ skips: ['B105', B404', 'B603'] # Do not check paths including `/tests/`: # they use `assert`, leading to B101 false positives. -# also we do not need check security issues in tests +# also we do not need to check security issues in tests exclude_dirs: - - '/tests/' \ No newline at end of file + - '/tests/' diff --git a/setup.py b/setup.py index d984f3a9..a74de416 100644 --- a/setup.py +++ b/setup.py @@ -80,7 +80,7 @@ }, tests_require=tests_requires, include_package_data=True, - entry_points={"console_scripts": ["lastversion = lastversion:main"]}, + entry_points={"console_scripts": ["lastversion = lastversion.cli:main"]}, classifiers=[ "Intended Audience :: Developers", "Intended Audience :: System Administrators", diff --git a/src/lastversion/__init__.py b/src/lastversion/__init__.py index 8c0e654a..d3734e08 100644 --- a/src/lastversion/__init__.py +++ b/src/lastversion/__init__.py @@ -8,30 +8,14 @@ import logging -from .__about__ import ( - __version__, -) - -# Intentionally import for export here, so it is ok to silence DeepSource test -# skipcq: PY-W2000 -from .lastversion import __self__ - -# skipcq: PY-W2000 -from .lastversion import check_version - -# skipcq: PY-W2000 -from .lastversion import has_update +from .lastversion import check_version, has_update, latest -# skipcq: PY-W2000 -from .lastversion import latest +__all__ = ["check_version", "has_update", "latest"] -# skipcq: PY-W2000 -from .lastversion import main # https://realpython.com/python-logging-source-code/#library-vs-application-logging-what-is-nullhandler # When used as a library, we default to opt-in approach, whereas library user # has to enable logging -# from lastversion logging.getLogger(__name__).addHandler(logging.NullHandler()) # patch up https://github.com/ionrock/cachecontrol/issues/230 logging.getLogger("cachecontrol.controller").addHandler(logging.NullHandler()) diff --git a/src/lastversion/__main__.py b/src/lastversion/__main__.py index 2a660a00..9fae24da 100644 --- a/src/lastversion/__main__.py +++ b/src/lastversion/__main__.py @@ -1,4 +1,5 @@ """Used to run the package as a script with `python -m lastversion`.""" -from lastversion import lastversion -lastversion.main() +from lastversion import cli + +cli.main() diff --git a/src/lastversion/argparse_version.py b/src/lastversion/argparse_version.py index cd8616f3..cb10e143 100644 --- a/src/lastversion/argparse_version.py +++ b/src/lastversion/argparse_version.py @@ -1,10 +1,11 @@ -"""Provides a custom argparse action to show program's version and exit.""" +"""Provides a custom argparse action to show the program's version and exit.""" + import logging import sys as _sys from argparse import SUPPRESS, Action import lastversion -from .__about__ import __version__ +from .__about__ import __version__, __self__ from .exceptions import ApiCredentialsError @@ -12,21 +13,21 @@ class VersionAction(Action): - """Custom argparse action to show program's version and exit.""" + """Custom argparse action to show the program's version and exit.""" def __init__(self, **kwargs): # Set default values if not provided in kwargs - kwargs.setdefault('dest', SUPPRESS) - kwargs.setdefault('default', SUPPRESS) - kwargs.setdefault('nargs', 0) - kwargs.setdefault('help', "show program's version number and exit") + kwargs.setdefault("dest", SUPPRESS) + kwargs.setdefault("default", SUPPRESS) + kwargs.setdefault("nargs", 0) + kwargs.setdefault("help", "show program's version number and exit") super().__init__(**kwargs) - self.version = kwargs.get('version2') + self.version = kwargs.get("version2") def __call__(self, parser, namespace, values, option_string=None): version = f"%(prog)s {__version__}" try: - last_version = lastversion.latest(lastversion.__self__) + last_version = lastversion.latest(__self__) if __version__ == str(last_version): version += ", up to date" else: diff --git a/src/lastversion/cli.py b/src/lastversion/cli.py new file mode 100644 index 00000000..89d63021 --- /dev/null +++ b/src/lastversion/cli.py @@ -0,0 +1,412 @@ +"""CLI entry point.""" + +import argparse +import json +import logging +import os +import re +import sys + +# try to use truststore if available +try: + import truststore + + truststore.inject_into_ssl() +except ImportError: + pass + +from lastversion.__about__ import __self__ +from lastversion import check_version, latest +from lastversion.argparse_version import VersionAction +from lastversion.exceptions import ApiCredentialsError, BadProjectError +from lastversion.holder_factory import HolderFactory +from lastversion.lastversion import log, parse_version, update_spec, install_release +from lastversion.repo_holders.base import BaseProjectHolder +from lastversion.repo_holders.github import TOKEN_PRO_TIP +from lastversion.utils import download_file, extract_file +from lastversion.version import Version + + +def main(argv=None): + """ + The entrypoint to CLI app. + + Args: + argv: List of arguments, helps test CLI without resorting to subprocess module. + """ + # ANSI escape code for starting bold text + start_bold = "\033[1m" + # ANSI escape code for ending the formatting (resets to normal text) + end_bold = "\033[0m" + + epilog = "\n---\n" + epilog += f"{start_bold}Sponsored Message: Check out the GetPageSpeed RPM " + epilog += "repository at https://nginx-extras.getpagespeed.com/ for NGINX " + epilog += "modules and performance tools. Enhance your server performance " + epilog += f"today!{end_bold}" + epilog += "\n---\n" + + if "GITHUB_API_TOKEN" not in os.environ and "GITHUB_TOKEN" not in os.environ: + epilog += TOKEN_PRO_TIP + parser = argparse.ArgumentParser( + description="Find the latest software release.", + epilog=epilog, + prog="lastversion", + ) + parser.add_argument( + "action", + nargs="?", + default="get", + help="Action to run. Default: get", + choices=[ + "get", + "download", + "extract", + "unzip", + "test", + "format", + "install", + "update-spec", + ], + ) + parser.add_argument( + "repo", + metavar="", + help="Repository in format owner/name or any URL that belongs to it, or a version string", + ) + # affects what is considered last release + parser.add_argument( + "--pre", + dest="pre", + action="store_true", + help="Include pre-releases in potential versions", + ) + parser.add_argument( + "--formal", + dest="formal", + action="store_true", + help="Include only formally tagged versions", + ) + parser.add_argument( + "--sem", + dest="sem", + choices=["major", "minor", "patch", "any"], + help="Semantic versioning level base to print or compare against", + ) + parser.add_argument( + "-v", + "--verbose", + action="count", + default=0, + help="Will give you an idea of what is happening under the hood, " + "-vv to increase verbosity level", + ) + # no --download = False, --download filename.tar, --download = None + parser.add_argument( + "-d", + "-o", + "--download", + "--output", + dest="download", + nargs="?", + default=False, + const=None, + metavar="FILENAME", + help="Download with custom filename", + ) + # how / which data of last release we want to present + # assets will give download urls for assets if available and sources archive otherwise + # sources will give download urls for sources always + # json always includes "version", "tag_name" etc. + whichever json data was + # used to satisfy lastversion + parser.add_argument( + "--format", + choices=["version", "assets", "source", "json", "tag"], + help="Output format", + ) + parser.add_argument( + "--assets", + dest="assets", + action="store_true", + help="Returns assets download URLs for last release", + ) + parser.add_argument( + "--source", + dest="source", + action="store_true", + help="Returns only source URL for last release", + ) + parser.add_argument( + "-gt", + "--newer-than", + type=check_version, + metavar="VER", + help="Output only if last version is newer than given version", + ) + parser.add_argument( + "-b", + "--major", + "--branch", + metavar="MAJOR", + help="Only consider releases of a specific major version, e.g. 2.1.x", + ) + parser.add_argument( + "--only", + metavar="REGEX", + help="Only consider releases containing this text. " + "Useful for repos with multiple projects inside", + ) + parser.add_argument( + "--exclude", + metavar="REGEX", + help="Only consider releases NOT containing this text. " + "Useful for repos with multiple projects inside", + ) + parser.add_argument( + "--filter", + metavar="REGEX", + help="Filters --assets result by a regular " "expression", + ) + parser.add_argument( + "--having-asset", + metavar="ASSET", + help="Only consider releases with this asset", + nargs="?", + const=True, + ) + parser.add_argument( + "-su", + "--shorter-urls", + dest="shorter_urls", + action="store_true", + help="A tiny bit shorter URLs produced", + ) + parser.add_argument( + "--even", + dest="even", + action="store_true", + help="Only even versions like 1.[2].x, or 3.[6].x are considered as stable", + ) + parser.add_argument( + "--at", + dest="at", + help="If the repo argument is one word, specifies where to look up the " + "project. The default is via internal lookup or GitHub Search", + choices=HolderFactory.HOLDERS.keys(), + ) + parser.add_argument( + "-y", + "--assumeyes", + dest="assumeyes", + action="store_true", + help="Automatically answer yes for all questions", + ) + parser.add_argument( + "--no-cache", + dest="no_cache", + action="store_true", + help="Do not use cache for HTTP requests", + ) + parser.add_argument("--version", action=VersionAction) + parser.set_defaults( + validate=True, + verbose=False, + format="version", + pre=False, + formal=False, + assets=False, + newer_than=False, + filter=False, + shorter_urls=False, + major=None, + assumeyes=False, + at=None, + having_asset=None, + even=False, + ) + args = parser.parse_args(argv) + + BaseProjectHolder.CACHE_DISABLED = args.no_cache + + if args.repo == "self": + args.repo = __self__ + + # "expand" repo:1.2 as repo --branch 1.2 + # noinspection HttpUrlsUsage + if ":" in args.repo and not ( + args.repo.startswith(("https://", "http://")) and args.repo.count(":") == 1 + ): + # right split ':' once only to preserve it in protocol of URLs + # https://github.com/repo/owner:2.1 + repo_args = args.repo.rsplit(":", 1) + args.repo = repo_args[0] + args.major = repo_args[1] + + # instead of using root logger, we use + logger = logging.getLogger("lastversion") + # create console handler and set level to debug + ch = logging.StreamHandler() + # create formatter + fmt = ( + "%(name)s - %(levelname)s - %(message)s" + if args.verbose + else "%(levelname)s: %(message)s" + ) + formatter = logging.Formatter(fmt) + # add formatter to ch + ch.setFormatter(formatter) + # add ch to logger + logger.addHandler(ch) + + if args.verbose: + logger.setLevel(logging.DEBUG) + log.info("Verbose %s level output.", args.verbose) + if args.verbose >= 2: + cachecontrol_logger = logging.getLogger("cachecontrol") + cachecontrol_logger.removeHandler(logging.NullHandler()) + cachecontrol_logger.addHandler(ch) + cachecontrol_logger.setLevel(logging.DEBUG) + + if args.assets: + args.format = "assets" + + if args.source: + args.format = "source" + + if args.filter: + args.filter = re.compile(args.filter) + + if args.action in ["test", "format"]: + v = parse_version(args.repo) + if not v: + log.critical("Failed to parse as a valid version") + sys.exit(1) + else: + # extract the desired print base + v = v.sem_extract_base(args.sem) + if args.action == "test": + print(f"Parsed as: {v}") + print(f"Stable: {not v.is_prerelease}") + else: + print(v) + return sys.exit(0) + + if args.action == "install": + # we can only install assets + args.format = "json" + if args.having_asset is None: + args.having_asset = r"~\.(AppImage|rpm)$" + try: + import apt + + args.having_asset = r"~\.(AppImage|deb)$" + except ImportError: + pass + + if args.repo.endswith(".spec"): + args.action = "update-spec" + args.format = "dict" + + if not args.sem: + if args.action == "update-spec": + args.sem = "minor" + else: + args.sem = "any" + # imply source download, unless --assets specified + # --download is legacy flag to specify download action or name of desired download file + # --download == None indicates download intent where filename is based on upstream + if args.action == "download" and args.download is False: + args.download = None + + if args.download is not False: + args.action = "download" + if args.format != "assets": + args.format = "source" + + if args.action in ["extract", "unzip"] and args.format != "assets": + args.format = "source" + + if args.newer_than: + base_compare = parse_version(args.repo) + if base_compare: + print(max([args.newer_than, base_compare])) + return sys.exit(2 if base_compare <= args.newer_than else 0) + + # other action are either getting release or doing something with release (extend get action) + try: + res = latest( + args.repo, + args.format, + args.pre, + args.filter, + args.shorter_urls, + args.major, + args.only, + args.at, + having_asset=args.having_asset, + exclude=args.exclude, + even=args.even, + formal=args.formal, + ) + except (ApiCredentialsError, BadProjectError) as error: + log.critical(str(error)) + if ( + isinstance(error, ApiCredentialsError) + and "GITHUB_API_TOKEN" not in os.environ + and "GITHUB_TOKEN" not in os.environ + ): + log.critical(TOKEN_PRO_TIP) + sys.exit(4) + + if res: + if args.action == "update-spec": + return update_spec(args.repo, res, sem=args.sem) + if args.action == "download": + # download command + if args.format == "source": + # there is only one source, but we need an array + res = [res] + download_name = None + # save with custom filename if there's one file to download + if len(res) == 1: + download_name = args.download + for url in res: + log.info("Downloading %s ...", url) + download_file(url, download_name) + sys.exit(0) + + if args.action in ["unzip", "extract"]: + # download command + if args.format == "source": + # there is only one source, but we need an array + res = [res] + for url in res: + log.info("Extracting %s ...", url) + extract_file(url) + sys.exit(0) + + if args.action == "install": + return install_release(res, args) + + # display version in various formats: + if args.format == "assets": + print("\n".join(res)) + elif args.format == "json": + json.dump(res, sys.stdout) + else: + # result may be a tag str, not just Version + if isinstance(res, Version): + res = res.sem_extract_base(args.sem) + print(res) + # special exit code "2" is useful for scripting to detect if no newer release exists + if args.newer_than: + # set up same SEM base + args.newer_than = args.newer_than.sem_extract_base(args.sem) + if res <= args.newer_than: + sys.exit(2) + else: + # empty list returned to --assets, emit 3 + if args.format == "assets" and res is not False: + sys.exit(3) + log.critical("No release was found") + sys.exit(1) diff --git a/src/lastversion/lastversion.py b/src/lastversion/lastversion.py index f2587e99..590e4a8a 100644 --- a/src/lastversion/lastversion.py +++ b/src/lastversion/lastversion.py @@ -11,7 +11,6 @@ """ import argparse -import json import logging import os import re @@ -24,21 +23,15 @@ import yaml from packaging.version import InvalidVersion -from lastversion.repo_holders.base import BaseProjectHolder from lastversion.repo_holders.test import TestProjectHolder -from lastversion.repo_holders.github import TOKEN_PRO_TIP from lastversion.holder_factory import HolderFactory from lastversion.version import Version -from lastversion.__about__ import __self__ -from lastversion.argparse_version import VersionAction from lastversion.spdx_id_to_rpmspec import rpmspec_licenses from lastversion.utils import ( download_file, - extract_file, rpm_installed_version, extract_appimage_desktop_file, ) -from lastversion.exceptions import ApiCredentialsError, BadProjectError log = logging.getLogger(__name__) FAILS_SEM_ERR_FMT = ( @@ -102,6 +95,7 @@ def get_repo_data_from_spec(rpmspec_filename): spec_urls.append(line.split("URL:")[1].strip()) elif line.startswith("Source0:"): source0 = line.split("Source0:")[1].strip() + # noinspection HttpUrlsUsage if source0.startswith("https://") or source0.startswith("http://"): spec_urls.append(source0) elif line.startswith("%global upstream_version "): @@ -280,13 +274,13 @@ def latest( version_macro = ( "upstream_version" if "module_of" in repo_data else "version" ) - version_macro = "%{{{}}}".format(version_macro) + version_macro = f"%{{{version_macro}}}" holder_i = {value: key for key, value in HolderFactory.HOLDERS.items()} release["source"] = holder_i[type(project)] release["spec_tag"] = tag.replace(str(version), version_macro) # spec_tag_no_prefix is the helpful macro that will allow us to know where tarball # extracts to (GitHub-specific) - if release["spec_tag"].startswith("v{}".format(version_macro)) or re.match( + if release["spec_tag"].startswith(f"v{version_macro}") or re.match( r"^v\d", release["spec_tag"] ): release["spec_tag_no_prefix"] = release["spec_tag"].lstrip("v") @@ -455,8 +449,8 @@ def update_spec(repo, res, sem="minor"): now = datetime.utcnow() today = now.strftime("%a %b %d %Y") out.append(ln.rstrip()) - out.append("* {} {}".format(today, packager)) - out.append("- upstream release v{}".format(res["version"])) + out.append(f"* {today} {packager}") + out.append(f"- upstream release v{res['version']}") out.append("\n") elif ln.startswith("Release:"): release_tag_regex = r"^Release:(\s+)(\S+)" @@ -570,392 +564,3 @@ def install_release(res, args): log.error("No installable assets found to install") sys.exit(1) - - -def main(argv=None): - """ - The entrypoint to CLI app. - - Args: - argv: List of arguments, helps test CLI without resorting to subprocess module. - """ - # ANSI escape code for starting bold text - start_bold = "\033[1m" - # ANSI escape code for ending the formatting (resets to normal text) - end_bold = "\033[0m" - - epilog = "\n---\n" - epilog += f"{start_bold}Sponsored Message: Check out the GetPageSpeed RPM " - epilog += "repository at https://nginx-extras.getpagespeed.com/ for NGINX " - epilog += "modules and performance tools. Enhance your server performance " - epilog += f"today!{end_bold}" - epilog += "\n---\n" - - if "GITHUB_API_TOKEN" not in os.environ and "GITHUB_TOKEN" not in os.environ: - epilog += TOKEN_PRO_TIP - parser = argparse.ArgumentParser( - description="Find the latest software release.", - epilog=epilog, - prog="lastversion", - ) - parser.add_argument( - "action", - nargs="?", - default="get", - help="Action to run. Default: get", - choices=[ - "get", - "download", - "extract", - "unzip", - "test", - "format", - "install", - "update-spec", - ], - ) - parser.add_argument( - "repo", - metavar="", - help="Repository in format owner/name or any URL that belongs to it, or a version string", - ) - # affects what is considered last release - parser.add_argument( - "--pre", - dest="pre", - action="store_true", - help="Include pre-releases in potential versions", - ) - parser.add_argument( - "--formal", - dest="formal", - action="store_true", - help="Include only formally tagged versions", - ) - parser.add_argument( - "--sem", - dest="sem", - choices=["major", "minor", "patch", "any"], - help="Semantic versioning level base to print or compare against", - ) - parser.add_argument( - "-v", - "--verbose", - action="count", - default=0, - help="Will give you an idea of what is happening under the hood, " - "-vv to increase verbosity level", - ) - # no --download = False, --download filename.tar, --download = None - parser.add_argument( - "-d", - "-o", - "--download", - "--output", - dest="download", - nargs="?", - default=False, - const=None, - metavar="FILENAME", - help="Download with custom filename", - ) - # how / which data of last release we want to present - # assets will give download urls for assets if available and sources archive otherwise - # sources will give download urls for sources always - # json always includes "version", "tag_name" etc. + whichever json data was - # used to satisfy lastversion - parser.add_argument( - "--format", - choices=["version", "assets", "source", "json", "tag"], - help="Output format", - ) - parser.add_argument( - "--assets", - dest="assets", - action="store_true", - help="Returns assets download URLs for last release", - ) - parser.add_argument( - "--source", - dest="source", - action="store_true", - help="Returns only source URL for last release", - ) - parser.add_argument( - "-gt", - "--newer-than", - type=check_version, - metavar="VER", - help="Output only if last version is newer than given version", - ) - parser.add_argument( - "-b", - "--major", - "--branch", - metavar="MAJOR", - help="Only consider releases of a specific major version, e.g. 2.1.x", - ) - parser.add_argument( - "--only", - metavar="REGEX", - help="Only consider releases containing this text. " - "Useful for repos with multiple projects inside", - ) - parser.add_argument( - "--exclude", - metavar="REGEX", - help="Only consider releases NOT containing this text. " - "Useful for repos with multiple projects inside", - ) - parser.add_argument( - "--filter", - metavar="REGEX", - help="Filters --assets result by a regular " "expression", - ) - parser.add_argument( - "--having-asset", - metavar="ASSET", - help="Only consider releases with this asset", - nargs="?", - const=True, - ) - parser.add_argument( - "-su", - "--shorter-urls", - dest="shorter_urls", - action="store_true", - help="A tiny bit shorter URLs produced", - ) - parser.add_argument( - "--even", - dest="even", - action="store_true", - help="Only even versions like 1.[2].x, or 3.[6].x are considered as stable", - ) - parser.add_argument( - "--at", - dest="at", - help="If the repo argument is one word, specifies where to look up the " - "project. The default is via internal lookup or GitHub Search", - choices=HolderFactory.HOLDERS.keys(), - ) - parser.add_argument( - "-y", - "--assumeyes", - dest="assumeyes", - action="store_true", - help="Automatically answer yes for all questions", - ) - parser.add_argument( - "--no-cache", - dest="no_cache", - action="store_true", - help="Do not use cache for HTTP requests", - ) - parser.add_argument("--version", action=VersionAction) - parser.set_defaults( - validate=True, - verbose=False, - format="version", - pre=False, - formal=False, - assets=False, - newer_than=False, - filter=False, - shorter_urls=False, - major=None, - assumeyes=False, - at=None, - having_asset=None, - even=False, - ) - args = parser.parse_args(argv) - - BaseProjectHolder.CACHE_DISABLED = args.no_cache - - if args.repo == "self": - args.repo = __self__ - - # "expand" repo:1.2 as repo --branch 1.2 - # noinspection HttpUrlsUsage - if ":" in args.repo and not ( - args.repo.startswith(("https://", "http://")) and args.repo.count(":") == 1 - ): - # right split ':' once only to preserve it in protocol of URLs - # https://github.com/repo/owner:2.1 - repo_args = args.repo.rsplit(":", 1) - args.repo = repo_args[0] - args.major = repo_args[1] - - # instead of using root logger, we use - logger = logging.getLogger("lastversion") - # create console handler and set level to debug - ch = logging.StreamHandler() - # create formatter - fmt = ( - "%(name)s - %(levelname)s - %(message)s" - if args.verbose - else "%(levelname)s: %(message)s" - ) - formatter = logging.Formatter(fmt) - # add formatter to ch - ch.setFormatter(formatter) - # add ch to logger - logger.addHandler(ch) - - if args.verbose: - logger.setLevel(logging.DEBUG) - log.info("Verbose %s level output.", args.verbose) - if args.verbose >= 2: - cachecontrol_logger = logging.getLogger("cachecontrol") - cachecontrol_logger.removeHandler(logging.NullHandler()) - cachecontrol_logger.addHandler(ch) - cachecontrol_logger.setLevel(logging.DEBUG) - - if args.assets: - args.format = "assets" - - if args.source: - args.format = "source" - - if args.filter: - args.filter = re.compile(args.filter) - - if args.action in ["test", "format"]: - v = parse_version(args.repo) - if not v: - log.critical("Failed to parse as a valid version") - sys.exit(1) - else: - # extract the desired print base - v = v.sem_extract_base(args.sem) - if args.action == "test": - print(f"Parsed as: {v}") - print(f"Stable: {not v.is_prerelease}") - else: - print(v) - return sys.exit(0) - - if args.action == "install": - # we can only install assets - args.format = "json" - if args.having_asset is None: - args.having_asset = r"~\.(AppImage|rpm)$" - try: - import apt - - args.having_asset = r"~\.(AppImage|deb)$" - except ImportError: - pass - - if args.repo.endswith(".spec"): - args.action = "update-spec" - args.format = "dict" - - if not args.sem: - if args.action == "update-spec": - args.sem = "minor" - else: - args.sem = "any" - # imply source download, unless --assets specified - # --download is legacy flag to specify download action or name of desired download file - # --download == None indicates download intent where filename is based on upstream - if args.action == "download" and args.download is False: - args.download = None - - if args.download is not False: - args.action = "download" - if args.format != "assets": - args.format = "source" - - if args.action in ["extract", "unzip"] and args.format != "assets": - args.format = "source" - - if args.newer_than: - base_compare = parse_version(args.repo) - if base_compare: - print(max([args.newer_than, base_compare])) - return sys.exit(2 if base_compare <= args.newer_than else 0) - - # other action are either getting release or doing something with release (extend get action) - try: - res = latest( - args.repo, - args.format, - args.pre, - args.filter, - args.shorter_urls, - args.major, - args.only, - args.at, - having_asset=args.having_asset, - exclude=args.exclude, - even=args.even, - formal=args.formal, - ) - except (ApiCredentialsError, BadProjectError) as error: - log.critical(str(error)) - if ( - isinstance(error, ApiCredentialsError) - and "GITHUB_API_TOKEN" not in os.environ - and "GITHUB_TOKEN" not in os.environ - ): - log.critical(TOKEN_PRO_TIP) - sys.exit(4) - - if res: - if args.action == "update-spec": - return update_spec(args.repo, res, sem=args.sem) - if args.action == "download": - # download command - if args.format == "source": - # there is only one source, but we need an array - res = [res] - download_name = None - # save with custom filename if there's one file to download - if len(res) == 1: - download_name = args.download - for url in res: - log.info("Downloading %s ...", url) - download_file(url, download_name) - sys.exit(0) - - if args.action in ["unzip", "extract"]: - # download command - if args.format == "source": - # there is only one source, but we need an array - res = [res] - for url in res: - log.info("Extracting %s ...", url) - extract_file(url) - sys.exit(0) - - if args.action == "install": - return install_release(res, args) - - # display version in various formats: - if args.format == "assets": - print("\n".join(res)) - elif args.format == "json": - json.dump(res, sys.stdout) - else: - # result may be a tag str, not just Version - if isinstance(res, Version): - res = res.sem_extract_base(args.sem) - print(res) - # special exit code "2" is useful for scripting to detect if no newer release exists - if args.newer_than: - # set up same SEM base - args.newer_than = args.newer_than.sem_extract_base(args.sem) - if res <= args.newer_than: - sys.exit(2) - else: - # empty list returned to --assets, emit 3 - if args.format == "assets" and res is not False: - sys.exit(3) - log.critical("No release was found") - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/src/lastversion/repo_holders/base.py b/src/lastversion/repo_holders/base.py index 7a761bf7..a82fcd0a 100644 --- a/src/lastversion/repo_holders/base.py +++ b/src/lastversion/repo_holders/base.py @@ -29,7 +29,7 @@ def matches_filter(filter_s, positive, version_s): - """Check if version string matches a filter string. + """Check if a version string matches a filter string. Args: filter_s (str): Filter string. diff --git a/src/lastversion/repo_holders/github.py b/src/lastversion/repo_holders/github.py index e32908e9..4be69e96 100644 --- a/src/lastversion/repo_holders/github.py +++ b/src/lastversion/repo_holders/github.py @@ -76,6 +76,15 @@ class GitHubRepoSession(BaseProjectHolder): # get URL from website instead of GitHub because it is "prepared" source "release_url_format": "https://nginx.org/download/{name}-{version}.{ext}", }, + "freenginx": { + "repo": "freenginx/nginx", + "branches": { + "stable": "\\.\\d?[02468]\\.", + "mainline": "\\.\\d?[13579]\\.", + }, + # get URL from website instead of GitHub because it is "prepared" source + "release_url_format": "https://freenginx.org/download/freenginx-{version}.{ext}", + }, } KNOWN_REPO_URLS = { diff --git a/tests/test_cli.py b/tests/test_cli.py index b4068d29..aafa63b0 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,4 +1,5 @@ """Test CLI functions.""" + import os import subprocess import sys @@ -6,7 +7,7 @@ from packaging import version -from lastversion import main +from lastversion.cli import main from .helpers import captured_exit_code diff --git a/tests/test_github.py b/tests/test_github.py index 8cbec390..68c3aa97 100644 --- a/tests/test_github.py +++ b/tests/test_github.py @@ -5,7 +5,7 @@ from packaging import version -from lastversion import main +from lastversion.cli import main from lastversion.lastversion import latest from tests.helpers import captured_exit_code