Commit 80665f08 authored by Adrian DC's avatar Adrian DC
Browse files

feat(main, upgrades): implement 'Upgrades' from 'gitlabci-local'

Signed-off-by: default avatarAdrian DC <radian.dc@gmail.com>
parent 29cac8b5
Loading
Loading
Loading
Loading
Loading
+11 −7
Changes for README.md: 11 added lines, 7 removed lines.
Original line number Diff line number Diff line
@@ -28,13 +28,13 @@ The following step is required before using the tool:
<!-- readme-help-start -->

```yaml
usage: gitlab-projects-settings [-h] [--version] [--settings] [--set GROUP KEY VAL] [-t TOKEN] [--dry-run]
                                [--exclude-group] [--exclude-subgroups] [--exclude-projects] [--available-features]
                                [--reset-features [KEEP_FEATURES]] [--disable-features FEATURES]
                                [--enable-features FEATURES] [--reset-members] [--set-avatar FILE]
                                [--set-description TEXT] [--update-description] [--run-housekeeping]
                                [--archive-project | --unarchive-project] [--delete-group] [--delete-project]
                                [--protect-branches] [--protect-tags LEVEL] [--]
usage: gitlab-projects-settings [-h] [--version] [--update-check] [--settings] [--set GROUP KEY VAL] [-t TOKEN]
                                [--dry-run] [--exclude-group] [--exclude-subgroups] [--exclude-projects]
                                [--available-features] [--reset-features [KEEP_FEATURES]]
                                [--disable-features FEATURES] [--enable-features FEATURES] [--reset-members]
                                [--set-avatar FILE] [--set-description TEXT] [--update-description]
                                [--run-housekeeping] [--archive-project | --unarchive-project] [--delete-group]
                                [--delete-project] [--protect-branches] [--protect-tags LEVEL] [--]
                                [gitlab] [path]

gitlab-projects-settings: Configure GitLab groups and projects settings automatically
@@ -42,6 +42,7 @@ gitlab-projects-settings: Configure GitLab groups and projects settings automati
internal arguments:
  -h, --help                        # Show this help message
  --version                         # Show the current version
  --update-check                    # Check for newer package updates
  --settings                        # Show the current settings path and contents
  --set GROUP KEY VAL               # Set settings specific 'VAL' value to [GROUP] > KEY
                                    # or unset by using 'UNSET' as 'VAL'
@@ -92,6 +93,8 @@ positional arguments:

`gitlab-projects-settings` creates a `settings.ini` configuration file in a userspace folder.

For example, it allows to disable the automated updates daily check (`[updates] > enabled`)

The `settings.ini` file location and contents can be shown with the following command:

```bash
@@ -105,6 +108,7 @@ gitlab-projects-settings --settings
- [colored](https://pypi.org/project/colored/): Terminal colors and styles
- [python-gitlab](https://pypi.org/project/python-gitlab/): A python wrapper for the GitLab API
- [setuptools](https://pypi.org/project/setuptools/): Build and manage Python packages
- [update-checker](https://pypi.org/project/update-checker/): Check for package updates on PyPI

---

+1 −0
Changes for requirements/runtime.txt: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
colored>=1.4.2
python-gitlab>=4.4.0
setuptools>=45.1.0
update_checker>=0.18.0
+20 −0
Changes for src/cli/main.py: 20 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -10,6 +10,7 @@ from sys import exit as sys_exit
# Components
from ..package.bundle import Bundle
from ..package.settings import Settings
from ..package.updates import Updates
from ..package.version import Version
from ..prints.colors import Colors
from ..system.platform import Platform
@@ -53,6 +54,12 @@ def main() -> None:
        action='store_true',
        help='Show the current version',
    )
    group.add_argument(
        '--update-check',
        dest='update_check',
        action='store_true',
        help='Check for newer package updates',
    )
    group.add_argument(
        '--settings',
        dest='settings',
@@ -262,6 +269,9 @@ def main() -> None:
        settings.show()
        sys_exit(0)

    # Instantiate updates
    updates: Updates = Updates(name=Bundle.NAME, settings=settings)

    # Version informations
    if options.version:
        print(
@@ -270,6 +280,12 @@ def main() -> None:
        Platform.flush()
        sys_exit(0)

    # Check for current updates
    if options.update_check:
        if not updates.check():
            updates.check(older=True)
        sys_exit(0)

    # Arguments validation
    if not options.token or not options.gitlab or not options.path:
        print(' ')
@@ -289,6 +305,10 @@ def main() -> None:
    print(' ')
    Platform.flush()

    # Check for daily updates
    if updates.enabled and updates.daily:
        updates.check()

    # Result
    if result in [Entrypoint.Result.SUCCESS, Entrypoint.Result.FINALIZE]:
        sys_exit(0)
+7 −0
Changes for src/package/bundle.py: 7 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -12,5 +12,12 @@ class Bundle:
    # Sources
    REPOSITORY: str = 'https://gitlab.com/AdrianDC/gitlab-projects-settings'

    # Releases
    RELEASE_FIRST_TIMESTAMP: int = 1579337311

    # Environment
    ENV_DEBUG_UPDATES_DAILY: str = 'DEBUG_UPDATES_DAILY'
    ENV_DEBUG_UPDATES_DISABLE: str = 'DEBUG_UPDATES_DISABLE'
    ENV_DEBUG_UPDATES_FAKE: str = 'DEBUG_UPDATES_FAKE'
    ENV_DEBUG_UPDATES_OFFLINE: str = 'DEBUG_UPDATES_OFFLINE'
    ENV_GITLAB_TOKEN: str = 'GITLAB_TOKEN'

src/package/updates.py

0 → 100644
+216 −0
Changes for src/package/updates.py: 216 added lines, 0 removed lines.
Original line number Diff line number Diff line
#!/usr/bin/env python3

# Standard libraries
from datetime import datetime
from os import access, environ, W_OK
from time import localtime, strftime, time
from typing import Optional

# Components
from ..prints.boxes import Boxes
from ..prints.colors import Colors
from ..system.platform import Platform
from .bundle import Bundle
from .settings import Settings
from .version import Version

# Updates class
class Updates:

    # Members
    __enabled: bool
    __name: str
    __settings: Settings

    # Constructor
    def __init__(self, name: str, settings: Settings) -> None:

        # Initialize members
        self.__name = name
        self.__settings = settings

        # Detect migration
        self.__migration()

        # Prepare enabled
        if not self.__settings.has('updates', 'enabled'):
            self.__settings.set_bool('updates', 'enabled', True)

        # Check enabled
        self.__enabled = bool(self.__settings.get_bool('updates', 'enabled')) and \
            (Bundle.ENV_DEBUG_UPDATES_DISABLE not in environ \
             or not environ[Bundle.ENV_DEBUG_UPDATES_DISABLE])

    # Migration
    def __migration(self) -> None:

        # Acquire versions
        current_version = Version.get()
        package_version = self.__settings.get('package', 'version')
        if not package_version:
            package_version = '0.0.0'

        # Refresh package version
        if not package_version or current_version != package_version:
            self.__settings.set('package', 'version', current_version)

    # Checker
    def check(self, older: bool = False) -> bool:

        # Reference version
        version = '0.0.0' if older else Version.get()

        # Fake test updates
        if Bundle.ENV_DEBUG_UPDATES_FAKE in environ:
            available = environ[Bundle.ENV_DEBUG_UPDATES_FAKE]
            if available >= version:

                # Show updates message
                release_date = datetime.utcfromtimestamp(Bundle.RELEASE_FIRST_TIMESTAMP)
                Updates.message(name=self.__name, older=older, available=available,
                                date=release_date)
                return True

        # Check if not offline
        if Bundle.ENV_DEBUG_UPDATES_OFFLINE not in environ:

            # Modules libraries, pylint: disable=import-outside-toplevel
            from update_checker import UpdateChecker

            # Check for updates
            check = UpdateChecker(bypass_cache=True).check(self.__name, version)
            if check:

                # Show updates message
                Updates.message(name=self.__name, older=older,
                                available=check.available_version,
                                date=check.release_date)
                return True

        # Older offline failure
        if older:

            # Show offline message
            Updates.message(name=self.__name, offline=True)
            return True

        # Result
        return False

    # Daily
    @property
    def daily(self) -> bool:

        # Acquire updates check last timestamp
        last = self.__settings.get('updates', 'last_timestamp')

        # Fake test updates
        if Bundle.ENV_DEBUG_UPDATES_DAILY in environ:
            last = None

        # Handle daily checks
        current = int(time())
        if not last or strftime('%Y-%m-%d', localtime(current)) != strftime(
                '%Y-%m-%d', localtime(int(last))):
            self.__settings.set('updates', 'last_timestamp', current)
            return True

        # Default fallback
        return False

    # Enabled
    @property
    def enabled(self) -> bool:
        return self.__enabled

    # Message, pylint: disable=too-many-arguments
    @staticmethod
    def message(
        name: str,
        offline: bool = False,
        older: bool = False,
        available: Optional[str] = None,
        date: Optional[datetime] = None,
        recommended: Optional[str] = None,
    ) -> None:

        # Modules libraries, pylint: disable=import-outside-toplevel
        from update_checker import pretty_date

        # Create message box
        box = Boxes()

        # Acquire current version
        version = Version.get()

        # Detect package installer
        package_install: str = ''
        if Platform.PATH_SEPARATOR + 'pipx' + Platform.PATH_SEPARATOR in Version.path():
            package_install = 'pipx upgrade' # pragma: no cover
        else:
            package_install = 'pip3 install -U' # pragma: no cover

        # Detect package ownership
        writable = access(__file__, W_OK)
        if Platform.IS_USER_SUDO or not writable:
            package_install = 'sudo ' + package_install # pragma: no cover

        # Prepare package specification
        package_specification: str = ''
        if 'pipx' in package_install:
            package_specification = f'{name}' # pragma: no cover
        else:
            package_specification = f'{name}>={available}' # pragma: no cover

        # Evaluate same version
        same = available and available == version

        # Version message prefix
        version_outdated = not offline and available and not older and not same
        version_prefix = f'{Colors.YELLOW_LIGHT}Version: {Colors.BOLD}{name}' \
            f' {Colors.RED if version_outdated else Colors.GREEN}{version}'

        # Recommended version message
        if recommended:
            available = recommended
            box.add(f'{version_prefix} {Colors.BOLD}older than recommended version '
                    f'{Colors.RED}{recommended}{Colors.BOLD}')

        # Offline version message
        elif offline:
            box.add(f'{version_prefix} {Colors.BOLD}not found, network might be down')

        # Updated version message
        elif same:
            box.add(
                f'{version_prefix} {Colors.BOLD}was released {pretty_date(date)}{Colors.BOLD}!'
            )

        # Older version message
        elif older:
            box.add(
                f'{version_prefix} {Colors.BOLD}newer than {Colors.RED}{available}' \
                    f' {Colors.BOLD}from {pretty_date(date)}{Colors.BOLD}!'
            )

        # Newer version message
        else:
            box.add(
                f'{version_prefix} {Colors.BOLD}updated {pretty_date(date)}' \
                    f' to {Colors.GREEN}{available}{Colors.BOLD}!'
            )

        # Changelog message
        box.add(
            f'{Colors.YELLOW_LIGHT}Changelog: {Colors.CYAN}{Bundle.REPOSITORY}/-/releases'
        )

        # Update message
        if available:
            box.add(
                f'{Colors.YELLOW_LIGHT}Update: {Colors.BOLD}' \
                    f"Run {Colors.GREEN}{package_install} '{package_specification}'"
            )

        # Print message box
        box.print()
Loading