Compare commits

...

18 commits
0.0.1 ... main

Author SHA1 Message Date
77d7092a24 patch: removed old gitlab ci file.
All checks were successful
ci/woodpecker/pr/woodpecker_ci Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker_ci Pipeline was successful
ci/woodpecker/push/woodpecker_ci Pipeline was successful
ci/woodpecker/tag/woodpecker_ci Pipeline was successful
2025-04-11 12:23:11 +02:00
014feb23c2 Adding info about the migration to readme.
All checks were successful
ci/woodpecker/pr/woodpecker_ci Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker_ci Pipeline was successful
ci/woodpecker/push/woodpecker_ci Pipeline was successful
ci/woodpecker/tag/woodpecker_ci Pipeline was successful
2025-04-11 12:16:59 +02:00
dc8b1ca9ed fix: skip twine file
All checks were successful
ci/woodpecker/pr/woodpecker_ci Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker_ci Pipeline was successful
ci/woodpecker/push/woodpecker_ci Pipeline was successful
ci/woodpecker/tag/woodpecker_ci Pipeline was successful
2025-04-11 12:09:32 +02:00
f4ef58672c fix: fixes publish to forgejo package
Some checks failed
ci/woodpecker/pr/woodpecker_ci Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker_ci Pipeline was successful
ci/woodpecker/push/woodpecker_ci Pipeline failed
ci/woodpecker/tag/woodpecker_ci Pipeline was successful
2025-04-11 11:56:12 +02:00
364af1cb6e ci: now also upload to package on forgejo.
Some checks failed
ci/woodpecker/pr/woodpecker_ci Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker_ci Pipeline was successful
ci/woodpecker/push/woodpecker_ci Pipeline failed
ci/woodpecker/tag/woodpecker_ci Pipeline was successful
2025-04-11 11:42:43 +02:00
03cf68b873 fix: use export for vars
All checks were successful
ci/woodpecker/pr/woodpecker_ci Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker_ci Pipeline was successful
ci/woodpecker/push/woodpecker_ci Pipeline was successful
ci/woodpecker/tag/woodpecker_ci Pipeline was successful
2025-04-10 17:05:49 +02:00
2e2bba2aa5 fix: change source to dot
Some checks failed
ci/woodpecker/pr/woodpecker_ci Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker_ci Pipeline was successful
ci/woodpecker/push/woodpecker_ci Pipeline failed
ci/woodpecker/tag/woodpecker_ci Pipeline was successful
2025-04-10 16:59:59 +02:00
bf5d680612 fix: changed order
Some checks failed
ci/woodpecker/pr/woodpecker_ci Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker_ci Pipeline was successful
ci/woodpecker/push/woodpecker_ci Pipeline failed
ci/woodpecker/tag/woodpecker_ci Pipeline was successful
2025-04-10 16:48:17 +02:00
8f91f64d04 CI: woodpecker CI
Some checks failed
ci/woodpecker/pr/woodpecker_ci Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker_ci Pipeline was successful
ci/woodpecker/push/woodpecker_ci Pipeline failed
ci/woodpecker/tag/woodpecker_ci Pipeline was successful
2025-04-10 16:30:40 +02:00
23214d7d32 fix: removed debug leftover 2025-02-10 13:21:38 +01:00
712d80a0aa fix: Fixed that prereleases version were listed 2025-02-10 13:16:21 +01:00
9680e33730 feat: function to install single package. 2025-02-09 17:48:30 +01:00
09a37ae628 patch: Added classifiers for pypi 2025-02-09 13:26:21 +01:00
cb568730d8 feat: Added local update functions 2025-02-03 16:21:01 +01:00
3587ce1317 refactor: consistent return values 2025-02-01 17:43:54 +00:00
47fe83d1f7 feat: using json to store more information local 2025-01-31 16:31:18 +00:00
282a2145ff 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.
2025-01-31 14:59:09 +00:00
5b462cd39f fix: making pipeline work. 2025-01-31 12:25:17 +01:00
12 changed files with 351 additions and 210 deletions

View file

@ -1,69 +0,0 @@
---
include:
- local: .gitlab-ci/versioning/gitversion.yml
- local: .gitlab-ci/git/create_tag.yml
stages:
- build
- release
gitversion:
extends: .versioning:gitversion
stage: .pre
tags:
- gitlab-org-docker
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # Run this job when commits are pushed or merged to the default branch
build:
stage: build
image: python:3.9.21
tags:
- gitlab-org-docker
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # Run this job when commits are pushed or merged to the default branch
needs:
- job: gitversion
artifacts: true
script:
- sed -i "s/^__version__ = .*/__version__ = \"${GitVersion_MajorMinorPatch}\"/" src/PyPiUpdateCheck/__init__.py
- cat src/PyPiUpdateCheck/__init__.py
- python3 -m pip install build
- python3 -m build
artifacts:
paths:
- dist/*
expire_in: 1 day
publish:
stage: release
image: python:3.9.21
tags:
- gitlab-org-docker
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # Run this job when commits are pushed or merged to the default branch
variables:
TWINE_USERNAME: "__token__"
TWINE_PASSWORD: $TWINE_API
needs:
- job: build
artifacts: true
script:
- python3 -m pip install twine
- python3 -m twine upload dist/*
create_tag:
extends: .git:create_tag
stage: release
tags:
- gitlab-org-docker
variables:
VERSION: $GitVersion_SemVer
TOKEN: $GITLAB_TOKEN
needs:
- job: gitversion
artifacts: true
rules:
- if: $CI_COMMIT_TAG
when: never # Do not run this job when a tag is created manually
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # Run this job when commits are pushed or merged to the default branch

View file

@ -1,15 +0,0 @@
---
.git:create_tag:
image: alpine:3.21
variables:
GIT_STRATEGY: clone
GIT_DEPTH: 0
GIT_LFS_SKIP_SMUDGE: 1
VERSION: ''
TOKEN: '' # Token with push privileges
script:
- apk add git
- git remote set-url origin https://oauth2:$TOKEN@$CI_SERVER_HOST/$CI_PROJECT_PATH
- git tag $VERSION
- git push origin tag $VERSION

View file

@ -1,31 +0,0 @@
---
.versioning:gitversion:
image:
name: mcr.microsoft.com/dotnet/sdk:9.0
variables:
GIT_STRATEGY: clone
GIT_DEPTH: 0 # force a deep/non-shallow fetch need by gitversion
GIT_LFS_SKIP_SMUDGE: 1
cache: [] # caches and before / after scripts can mess things up
script:
- |
dotnet tool install --global GitVersion.Tool --version 5.*
export PATH="$PATH:/root/.dotnet/tools"
dotnet-gitversion -output buildserver
# We could just collect the output file gitversion.properties (with artifacts:report:dotenv: gitversion.properties as it is already in DOTENV format,
# however it contains ~33 variables which unnecessarily consumes many of the 50 max DOTENV variables of the free GitLab version.
# Limits are higher for licensed editions, see https://docs.gitlab.com/ee/ci/yaml/artifacts_reports.html#artifactsreportsdotenv
grep 'GitVersion_LegacySemVer=' gitversion.properties >> gitversion.env
grep 'GitVersion_SemVer=' gitversion.properties >> gitversion.env
grep 'GitVersion_FullSemVer=' gitversion.properties >> gitversion.env
grep 'GitVersion_Major=' gitversion.properties >> gitversion.env
grep 'GitVersion_Minor=' gitversion.properties >> gitversion.env
grep 'GitVersion_Patch=' gitversion.properties >> gitversion.env
grep 'GitVersion_MajorMinorPatch=' gitversion.properties >> gitversion.env
grep 'GitVersion_BuildMetaData=' gitversion.properties >> gitversion.env
artifacts:
reports:
# propagates variables into the pipeline level
dotenv: gitversion.env

View file

@ -0,0 +1,92 @@
steps:
- name: gitversion
depends_on: [] # nothing start emititly
when:
event: push
branch: main
image: mcr.microsoft.com/dotnet/sdk:9.0
environment:
CI_TOKEN:
from_secret: CI_TOKEN
commands:
- git remote set-url origin https://CodeByMrFinchum:$CI_TOKEN@code.boxyfoxy.net/$CI_REPO.git
- git fetch --unshallow --tags
- apt-get update && apt-get install -y jq
- dotnet tool install --global GitVersion.Tool --version 5.*
- export PATH="$PATH:/root/.dotnet/tools"
- dotnet-gitversion -output json > version.json
- ls
- cat version.json
- |
echo "GitVersion_SemVer=$(jq -r '.SemVer' version.json)" >> gitversion.env
echo "GitVersion_LegacySemVer=$(jq -r '.LegacySemVer' version.json)" >> gitversion.env
echo "GitVersion_FullSemVer=$(jq -r '.FullSemVer' version.json)" >> gitversion.env
echo "GitVersion_Major=$(jq -r '.Major' version.json)" >> gitversion.env
echo "GitVersion_Minor=$(jq -r '.Minor' version.json)" >> gitversion.env
echo "GitVersion_Patch=$(jq -r '.Patch' version.json)" >> gitversion.env
echo "GitVersion_MajorMinorPatch=$(jq -r '.MajorMinorPatch' version.json)" >> gitversion.env
echo "GitVersion_BuildMetaData=$(jq -r '.BuildMetaData' version.json)" >> gitversion.env
- name: tagging
depends_on: [gitversion]
when:
event: push
branch: main
image: alpine/git
environment:
CI_TOKEN:
from_secret: CI_TOKEN
commands:
- ls
- cat gitversion.env
- git config --global user.email "ci@noreply.boxyfoxy.net"
- git config --global user.name "CI Bot"
- git remote set-url origin https://CodeByMrFinchum:$${CI_TOKEN}@code.boxyfoxy.net/$${CI_REPO}.git
- . gitversion.env
- git tag $GitVersion_SemVer
- git push origin tag $GitVersion_SemVer
- name: build
depends_on: [gitversion, tagging]
when:
event: push
branch: main
image: python:3.9.21
commands:
- ls
- cat gitversion.env
- export $(cat gitversion.env | xargs)
- sed -i "s/^__version__ = .*/__version__ = \"$GitVersion_SemVer\"/" src/PyPiUpdater/__init__.py
- cat src/PyPiUpdater/__init__.py
- python3 -m pip install build
- python3 -m build
- name: publish_pypi
depends_on: [gitversion, tagging, build]
when:
event: push
branch: main
image: python:3.9.21
environment:
TWINE_PASSWORD:
from_secret: TWINE_API
TWINE_USERNAME: "__token__"
commands:
- ls
- python3 -m pip install twine
- python3 -m twine upload dist/*
- name: publish_forgejo
depends_on: [gitversion, tagging, build]
when:
event: push
branch: main
image: python:3.9.21
environment:
TWINE_PASSWORD:
from_secret: PKG_TOKEN
TWINE_USERNAME: "CodeByMrFinchum"
commands:
- ls
- python3 -m pip install twine
- python3 -m twine upload --verbose --repository-url https://code.boxyfoxy.net/api/packages/CodeByMrFinchum/pypi dist/*

View file

@ -1,5 +1,59 @@
# Changelog
## 0.8-0.9: CI woodpecker (25.04.10-11)
- Changes to the pipeline no
## 0.7.x
### 0.7.2: Removed Debugging Leftovers
- Cleaned up code used for debugging.
### 0.7.1: Fixed Prerelease Update Detection
- Prevented prerelease versions from being listed as updates, as they must be installed manually.
### 0.7.0: Added Function to Install Packages
- Introduced the `install_package` function, allowing packages to be installed directly through the app.
- Useful for optional dependencies that need to be installed separately. This enables installation via the UI.
---
## 0.6.x
### 0.6.1: Classifier
- Added Classifier for pypi
### 0.6.0: New Local Update Feature
- Added support for updating from a local folder containing package files.
- Scans a specified folder for available updates.
- Installs updates directly from local package files.
- **Note:** Local version handling depends on how dependencies are managed.
- Example: If a package requires **PyPiUpdater 0.6.0**, but the installed version is **0.0.1** (e.g., from a dev environment), **OptimaLab35 v0.9.1** may not install correctly.
- This is due to **pip's dependency checks**, ensuring all required versions are satisfied before installation.
---
## 0.5.0: Rework (BREAKING CHANGE)
- Improved code consistency: return values are now always **lists** when containing multiple objects.
- **Simplified the package**: Removed the default waiting period for update checks.
- It is now the **developer's responsibility** to decide when to check for updates. A separate independent function may be introduced later.
- The last update check is still saved in the config and returned as a `time.time()` object.
---
## 0.4.0: Rework (BREAKING CHANGE)
- The log file is now a JSON file, allowing it to store multiple package names, versions, and last update timestamps.
- Some return values are now lists.
---
## 0.3.0: Rework (BREAKING CHANGE)
- Changed how program behaves
---
## 0.2.1: CI/CD pipeline
- Added auto tagging and publishing
---
## 0.0.1: Project Initiation
- First working version
- ATM terminal promt to accept or deny update

View file

@ -1,3 +1,21 @@
# PyPiUpdater
Developed on my [forgejo instance](https://code.boxyfoxy.net/CodeByMrFinchum), [GitLab](https://gitlab.com/CodeByMrFinchum) is used as backup.
## More to come
**UNFINISHED** Still early code, functions might change drasticly
**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
Example has been removed, still very active development.

View file

@ -1,3 +1,3 @@
Simple program to update package from PyPi with pip.
For more info see [PyPiUpdater gitlab](https://gitlab.com/CodeByMrFinchum/PyPiUpdater#).
For more info see [PyPiUpdater forgejo](https://code.boxyfoxy.net/CodeByMrFinchum/PyPiUpdater) or backup repo [PyPiUpdater gitlab](https://gitlab.com/CodeByMrFinchum/PyPiUpdater#).

View file

@ -7,17 +7,21 @@ 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 = [
"Development Status :: 3 - Alpha",
"Programming Language :: Python :: 3",
"Topic :: Software Development :: Libraries :: Python Modules",
"License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
"Operating System :: OS Independent",
]
[project.urls]
Source = "https://gitlab.com/CodeByMrFinchum/PyPiUpdater"
Documentation = "https://gitlab.com/CodeByMrFinchum/PyPiUpdater/-/blob/main/README.md"
Changelog = "https://gitlab.com/CodeByMrFinchum/PyPiUpdater/-/blob/main/CHANGELOG.md"
[tool.hatch.build.targets.wheel]
packages = ["src/PyPiUpdater"]

View file

@ -1,4 +1,5 @@
from .pypi_updater import PyPiUpdater
from .single_updater import PyPiUpdater
#from .multi_updater import MultiPackageUpdater
__all__ = ["PyPiUpdater"]

View file

@ -0,0 +1,4 @@
class MultiPackageUpdater:
def __init__(self, log_path):
print("Not ready yet...")

View file

@ -1,91 +0,0 @@
import requests
from packaging import version
import subprocess
import sys
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 <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.
Returns the latest version or None if an error occurs.
"""
rss_url = f"https://pypi.org/rss/project/{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
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()
return latest_version, None
except requests.exceptions.RequestException as e:
return None, f"Network error: {str(e)}"
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.
"""
try:
local_ver = version.parse(local_version)
online_ver = version.parse(online_version)
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 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)
if online_version is None:
return None, error_message # Error fetching or parsing feed
return self._compare_versions(local_version, online_version)

View file

@ -0,0 +1,174 @@
import requests
import subprocess
import sys
import os
import time
import json
import re
from packaging import version
from packaging.version import parse, Version
from xml.etree import ElementTree as ET
class PyPiUpdater:
def __init__(self, package_name, local_version, log_path):
"""
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 (JSON file).
:param update_interval_seconds: Seconds to wait before update is allowed again (default: 20 hours).
"""
self.package_name = package_name
self.local_version = version.parse(local_version)
self.log_path = log_path
self.latest_version = ""
self.last_update_check = 0.1
def _get_latest_version(self):
"""Fetch the latest stable 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, timeout=5)
response.raise_for_status()
root = ET.fromstring(response.content)
# Extract all versions from the feed
versions = []
for item in root.findall(".//item/title"):
version_text = item.text.strip()
parsed_version = parse(version_text)
# Check if the version is stable (not a pre-release)
if isinstance(parsed_version, Version) and not parsed_version.is_prerelease:
versions.append(parsed_version)
# Return the latest stable version
if versions:
latest_version = str(max(versions))
self.latest_version = latest_version
return [latest_version, None]
return [None, "No stable versions found"]
except requests.exceptions.RequestException as e:
return [None, f"Network error: {str(e)}"]
except Exception as e:
return [None, f"Error parsing feed: {str(e)}"]
def check_for_update(self):
"""Check if an update is available."""
latest_version, error = self._get_latest_version()
if latest_version is None:
return [None, error]
is_newer = version.parse(latest_version) > self.local_version
self._record_update_check() # Save check timestamp & latest version
return [is_newer, latest_version]
def update_package(self):
"""Update the package using pip."""
print(f"Updating {self.package_name}...")
try:
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)}"]
def check_update_local(self, folder_path):
"""
Check if a newer version of the package is available in the local folder.
:param folder_path: Path to the folder containing package files.
:return: (bool, latest_version) - True if newer version found, otherwise False.
"""
if not os.path.exists(folder_path):
return [None, "Folder does not exist"]
pattern = re.compile(rf"{re.escape(self.package_name.lower())}-(\d+\.\d+\.\d+[a-zA-Z0-9]*)")
available_versions = []
for file in os.listdir(folder_path):
match = pattern.search(file)
if match:
found_version = match.group(1)
available_versions.append(version.parse(found_version))
if not available_versions:
return [None, "No valid package versions found in the folder"]
latest_version = max(available_versions)
is_newer = latest_version > self.local_version
return [is_newer, str(latest_version)]
def update_from_local(self, folder_path):
"""
Install the latest package version from a local folder.
:param folder_path: Path to the folder containing package files.
:return: (bool, message) - Success status and message.
"""
print(f"Installing {self.package_name} from {folder_path}...")
try:
subprocess.run(
[sys.executable, "-m", "pip", "install", "--no-index", "--find-links", folder_path, self.package_name, "-U"],
check=True
)
return [True, f"{self.package_name} updated successfully from local folder."]
except subprocess.CalledProcessError as e:
return [False, f"Update from local folder failed: {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 get_last_state(self):
"""Retrieve last update info for the package."""
data = self._read_json()
if self.package_name in data:
entry = data[self.package_name]
return [entry["last_checked"], entry["last_online_version"], self.package_name]
return [None, None, self.package_name]
def _record_update_check(self):
"""Save the last update check time and online version in JSON."""
data = self._read_json()
data[self.package_name] = {
"last_checked": time.time(),
"last_online_version": self.latest_version
}
self._write_json(data)
def clear_all_entries(self):
"""Clear all update history."""
self._write_json({})
def _read_json(self):
"""Read JSON log file."""
if not os.path.exists(self.log_path):
return {}
try:
with open(self.log_path, "r") as f:
return json.load(f)
except (json.JSONDecodeError, FileNotFoundError):
return {}
def _write_json(self, data):
"""Write data to JSON log file."""
with open(self.log_path, "w") as f:
json.dump(data, f, indent=4)
@staticmethod
def install_package(package_name):
"""Attempts to install a package via pip."""
try:
subprocess.run([sys.executable, "-m", "pip", "install", package_name], check = True)
print("Successfull")
return [True, f"{package_name} installed successfully!"]
except subprocess.CalledProcessError as e:
print("Failed")
return [False, f"Failed to install {package_name}:\n{e.stderr}"]