chore: rework

This commit is contained in:
Mr Finchum 2025-01-31 15:55:50 +01:00
parent 5b462cd39f
commit bbe4ce5a9c
4 changed files with 130 additions and 71 deletions

View file

@ -1,6 +1,9 @@
# Changelog # 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 - Added auto tagging and publishing
## 0.0.1: Project Initiation ## 0.0.1: Project Initiation

View file

@ -1,3 +1,52 @@
# PyPiUpdater # 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.

View file

@ -7,7 +7,7 @@ name = "PyPiUpdater"
dynamic = ["version"] dynamic = ["version"]
authors = [{ name = "Mr Finchum" }] authors = [{ name = "Mr Finchum" }]
description = "Simple program to update package from PyPi with pip." description = "Simple program to update package from PyPi with pip."
readme = "pip_README.md" readme = "README.md"
requires-python = ">=3.8" requires-python = ">=3.8"
dependencies = ["requests", "packaging"] dependencies = ["requests", "packaging"]
classifiers = [ classifiers = [

View file

@ -1,60 +1,35 @@
import requests import requests
from packaging import version
import subprocess import subprocess
import sys import sys
import os
import time
from packaging import version
from xml.etree import ElementTree as ET
class PyPiUpdater: class PyPiUpdater:
def __init__(self, package_name, local_version, log_path, update_interval_seconds=20 * 3600):
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 <package_name>."""
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):
""" """
Fetch the RSS feed from PyPI and extract the latest version number. Initialize PyPiUpdater.
Returns the latest version or None if an error occurs.
: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: try:
response = requests.get(rss_url) response = requests.get(rss_url, timeout=5)
response.raise_for_status() # Raise HTTPError for bad responses (4xx, 5xx) response.raise_for_status()
# 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
root = ET.fromstring(response.content) root = ET.fromstring(response.content)
# The version is in the <title> tag of the first <item> in the RSS feed
latest_version = root.find(".//item/title").text.strip() latest_version = root.find(".//item/title").text.strip()
return latest_version, None return latest_version, None
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
@ -62,30 +37,62 @@ class PyPiUpdateCheck:
except Exception as e: except Exception as e:
return None, f"Error parsing feed: {str(e)}" return None, f"Error parsing feed: {str(e)}"
def _compare_versions(self, local_version, online_version): def check_for_update(self, force = False):
""" """Check if an update is available."""
Compare the local and online version strings.
Returns (True, online_version) if online is newer, (False, online_version) if same or older. 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: try:
local_ver = version.parse(local_version) subprocess.run([sys.executable, "-m", "pip", "install", "--upgrade", self.package_name], check=True)
online_ver = version.parse(online_version) 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: def restart_program(self):
return True, online_version # Online version is newer """Restart the Python program after an update."""
else: print("Restarting the application...")
return False, online_version # Local version is the same or newer python = sys.executable
except Exception as e: subprocess.run([python] + sys.argv)
return None, f"Error comparing versions: {str(e)}" sys.exit()
def check_for_update(self, package_name, local_version): def last_update_check(self):
""" """Retrieve the last update check timestamp from a text file."""
Check if the given package has a newer version on PyPI compared to the local version. if not os.path.exists(self.log_path):
Returns (True, online_version), (False, online_version), or (None, error_message). return 0 # If no log, assume a long time passed
"""
online_version, error_message = self._get_latest_version_from_rss(package_name)
if online_version is None: try:
return None, error_message # Error fetching or parsing feed 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