diff --git a/CHANGELOG.md b/CHANGELOG.md index b451e28..291ec3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog -## 0.1.1: CI/CD pipeline +## 0.3.0: Rework BREAKING +- Changed how program behaves + +## 0.2.1: CI/CD pipeline - Added auto tagging and publishing ## 0.0.1: Project Initiation diff --git a/README.md b/README.md index 851e98d..91f7b25 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,52 @@ # PyPiUpdater +**UNFINISHED** Still early code, functions might change drasticly -## More to come +**PyPiUpdater** is a Python library for managing updates of packages installed via `pip`. + +## Features +- Check the latest version of a package on PyPI + - Determine if an update is available +- Upgrade the package using `pip` +- Restart the Python script after updating + +## Installation + +```bash +pip install PyPiUpdater +``` + +## Usage example +**NOTE:** Example only work for client application, GUI apps such as OptimaLab35 with QT need also to close the window and should not use the restart function. + +**Now the example by ChatGPT:** + +Here's a short example code snippet for your `PyPiUpdater` package to include in your README: + +```python +# Example usage of PyPiUpdater + +from PyPiUpdater import PyPiUpdater + +# Initialize the updater with the package name, current version, and log file path +updater = PyPiUpdater(package_name="OptimaLab35", local_version="0.7.0", log_path="update_log.txt") + +# Check if an update is available (optionally forcing the check, otherwise only checked every 20 hours(default set with update_interval_seconds = int seconds)) +is_newer, latest_version = updater.check_for_update(force=False) + +if is_newer: + print(f"Update available! Latest version: {latest_version}") + # Update the package using pip + success, message = updater.update_package() + print(message) + if success: + # Restart the program after update + updater.restart_program() +else: + print("No update available or checked too recently.") +``` + +### Explanation: +- The example shows how to initialize the `PyPiUpdater` class with the package name, current version, and the path to a log file. +- It checks for an update by calling `check_for_update()`. +- If an update is available, it proceeds with updating the package using `update_package()` and restarts the program with `restart_program()`. +- If no update is available, or it was checked too recently, it prints an appropriate message. diff --git a/pyproject.toml b/pyproject.toml index d369a09..0456616 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "PyPiUpdater" dynamic = ["version"] authors = [{ name = "Mr Finchum" }] description = "Simple program to update package from PyPi with pip." -readme = "pip_README.md" +readme = "README.md" requires-python = ">=3.8" dependencies = ["requests", "packaging"] classifiers = [ diff --git a/src/PyPiUpdater/pypi_updater.py b/src/PyPiUpdater/pypi_updater.py index f3c9f91..880df45 100644 --- a/src/PyPiUpdater/pypi_updater.py +++ b/src/PyPiUpdater/pypi_updater.py @@ -1,60 +1,35 @@ import requests -from packaging import version import subprocess import sys +import os +import time +from packaging import version +from xml.etree import ElementTree as ET class PyPiUpdater: - - def _restart_program(self): - """Restarts the current Python script.""" - print("Restarting the application...") - python = sys.executable - subprocess.run([python] + sys.argv) - sys.exit() # Ensure the old process exits - - def update_package(self, package_name): - """Runs pip install -U .""" - print(f"Updating {package_name}...") - subprocess.run([sys.executable, "-m", "pip", "install", "--upgrade", package_name]) - - - def check_and_update(self, package_name, local_version): - """Checks for updates and installs if needed.""" - checker = PyPiUpdateCheck() - is_newer, latest_version = checker.check_for_update(package_name, local_version) - - if is_newer is None: - print(f"Error checking for updates: {latest_version}") - elif is_newer: - print(f"New version available: {latest_version}") - choice = input("Do you want to update (y/n)\n") - if choice == "y": - self.update_package(package_name) - self._restart_program() # Restart after updating - else: - print("packge not updated") - else: - print("You are up to date!") - -class PyPiUpdateCheck: - - def _get_latest_version_from_rss(self, package_name): + def __init__(self, package_name, local_version, log_path, update_interval_seconds=20 * 3600): """ - Fetch the RSS feed from PyPI and extract the latest version number. - Returns the latest version or None if an error occurs. + Initialize PyPiUpdater. + + :param package_name: Name of the package on PyPI. + :param local_version: Currently installed version. + :param log_path: Path to the update log file (txt file). + :param update_interval_seconds: Seconds to wait before update is allowed again (default: 20 hours). """ - rss_url = f"https://pypi.org/rss/project/{package_name.lower()}/releases.xml" + self.package_name = package_name + self.local_version = version.parse(local_version) + self.log_path = log_path + self.update_interval = update_interval_seconds + + def _get_latest_version(self): + """Fetch the latest version from PyPI RSS feed.""" + rss_url = f"https://pypi.org/rss/project/{self.package_name.lower()}/releases.xml" try: - response = requests.get(rss_url) - response.raise_for_status() # Raise HTTPError for bad responses (4xx, 5xx) - - # Extract the latest version from the RSS XML - # The RSS feed is XML, so we just need to parse the version from the first entry - from xml.etree import ElementTree as ET + response = requests.get(rss_url, timeout=5) + response.raise_for_status() root = ET.fromstring(response.content) - # The version is in the tag of the first <item> in the RSS feed latest_version = root.find(".//item/title").text.strip() return latest_version, None except requests.exceptions.RequestException as e: @@ -62,30 +37,62 @@ class PyPiUpdateCheck: except Exception as e: return None, f"Error parsing feed: {str(e)}" - def _compare_versions(self, local_version, online_version): - """ - Compare the local and online version strings. - Returns (True, online_version) if online is newer, (False, online_version) if same or older. - """ + def check_for_update(self, force = False): + """Check if an update is available.""" + + if not force and not self.should_check_for_update(): + return None, "Checked too recently" + + latest_version, error = self._get_latest_version() + if latest_version is None: + return None, error + + is_newer = version.parse(latest_version) > self.local_version + if is_newer: + self.record_update_check() # Save the check timestamp only if successful + return is_newer, latest_version + + def update_package(self): + """Update the package using pip.""" + print(f"Updating {self.package_name}...") try: - local_ver = version.parse(local_version) - online_ver = version.parse(online_version) + subprocess.run([sys.executable, "-m", "pip", "install", "--upgrade", self.package_name], check=True) + return True, f"{self.package_name} updated successfully." + except subprocess.CalledProcessError as e: + return False, f"Update failed: {str(e)}" - if online_ver > local_ver: - return True, online_version # Online version is newer - else: - return False, online_version # Local version is the same or newer - except Exception as e: - return None, f"Error comparing versions: {str(e)}" + def restart_program(self): + """Restart the Python program after an update.""" + print("Restarting the application...") + python = sys.executable + subprocess.run([python] + sys.argv) + sys.exit() - def check_for_update(self, package_name, local_version): - """ - Check if the given package has a newer version on PyPI compared to the local version. - Returns (True, online_version), (False, online_version), or (None, error_message). - """ - online_version, error_message = self._get_latest_version_from_rss(package_name) + def last_update_check(self): + """Retrieve the last update check timestamp from a text file.""" + if not os.path.exists(self.log_path): + return 0 # If no log, assume a long time passed - if online_version is None: - return None, error_message # Error fetching or parsing feed + try: + with open(self.log_path, "r") as f: + last_check = float(f.readline().strip()) # Read first line as timestamp + return last_check + except Exception: + return 0 # Handle read errors gracefully - return self._compare_versions(local_version, online_version) + def last_update_date_string(self): + time_float = self.last_update_check() + local_time = time.localtime(time_float) + time_string = f"{local_time.tm_mday:02d}/{local_time.tm_mon:02d} {local_time.tm_hour:02d}:{local_time.tm_min:02d}" + return time_string + + def record_update_check(self): + """Save the current time as the last update check timestamp.""" + with open(self.log_path, "w") as f: + f.write(f"{time.time()}") + + def should_check_for_update(self): + """Returns True if enough time has passed since the last check.""" + last_check = self.last_update_check() + elapsed_time = time.time() - last_check + return elapsed_time >= self.update_interval