feat: reworking strukture
This is a breaking change. A reminder that this is in very early stage (first day) and nothing is set in stone yet.
This commit is contained in:
parent
5b462cd39f
commit
282a2145ff
4 changed files with 130 additions and 71 deletions
|
@ -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
|
||||||
|
|
51
README.md
51
README.md
|
@ -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.
|
||||||
|
|
|
@ -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 = [
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue