From 282a2145ffa71b8d1841a08298d9881e0bab2d8b Mon Sep 17 00:00:00 2001
From: Mr Finchum <mr.finchum@pm.me>
Date: Fri, 31 Jan 2025 14:59:09 +0000
Subject: [PATCH 01/17] 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.
---
 CHANGELOG.md                    |   5 +-
 README.md                       |  51 +++++++++++-
 pyproject.toml                  |   2 +-
 src/PyPiUpdater/pypi_updater.py | 143 +++++++++++++++++---------------
 4 files changed, 130 insertions(+), 71 deletions(-)

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 <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):
+    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 <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:
@@ -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

From 47fe83d1f77603b0c22997fae83e15211160bd5a Mon Sep 17 00:00:00 2001
From: Mr Finchum <mr.finchum@pm.me>
Date: Fri, 31 Jan 2025 16:31:18 +0000
Subject: [PATCH 02/17] feat: using json to store more information local

---
 CHANGELOG.md                    | 13 ++++-
 README.md                       | 35 +-------------
 src/PyPiUpdater/pypi_updater.py | 84 +++++++++++++++++++++++----------
 3 files changed, 72 insertions(+), 60 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 291ec3b..2e5aa9f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,11 +1,22 @@
 # Changelog
 
-## 0.3.0: Rework BREAKING
+
+## 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
diff --git a/README.md b/README.md
index 91f7b25..c3f04c1 100644
--- a/README.md
+++ b/README.md
@@ -16,37 +16,4 @@ 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.
+Example has been removed, still very active development.
diff --git a/src/PyPiUpdater/pypi_updater.py b/src/PyPiUpdater/pypi_updater.py
index 880df45..c923438 100644
--- a/src/PyPiUpdater/pypi_updater.py
+++ b/src/PyPiUpdater/pypi_updater.py
@@ -3,23 +3,26 @@ import subprocess
 import sys
 import os
 import time
+import json
 from packaging import version
 from xml.etree import ElementTree as ET
 
 class PyPiUpdater:
-    def __init__(self, package_name, local_version, log_path, update_interval_seconds=20 * 3600):
+    def __init__(self, package_name, local_version, log_path, update_interval_seconds = 20 * 3600):
         """
         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 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.update_interval = update_interval_seconds
+        self.latest_version = ""
+        self.last_update_check = 0.1
 
     def _get_latest_version(self):
         """Fetch the latest version from PyPI RSS feed."""
@@ -31,6 +34,7 @@ class PyPiUpdater:
             root = ET.fromstring(response.content)
 
             latest_version = root.find(".//item/title").text.strip()
+            self.latest_version = latest_version
             return latest_version, None
         except requests.exceptions.RequestException as e:
             return None, f"Network error: {str(e)}"
@@ -39,7 +43,6 @@ class PyPiUpdater:
 
     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"
 
@@ -49,7 +52,7 @@ class PyPiUpdater:
 
         is_newer = version.parse(latest_version) > self.local_version
         if is_newer:
-            self.record_update_check()  # Save the check timestamp only if successful
+            self.record_update_check()  # Save check timestamp & latest version
         return is_newer, latest_version
 
     def update_package(self):
@@ -68,31 +71,62 @@ class PyPiUpdater:
         subprocess.run([python] + sys.argv)
         sys.exit()
 
-    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
+    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]
+            last_check = self.last_update_date_string(entry["last_checked"])
+            return [last_check, entry["last_online_version"], self.package_name]
+        return [None, None, self.package_name]
 
-        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
+    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 last_update_date_string(self):
-        time_float = self.last_update_check()
+    def remove_package_entry(self, package_name):
+        """Remove a specific package entry from the log file."""
+        data = self._read_json()
+        if package_name in data:
+            del data[package_name]
+            self._write_json(data)
+            return True
+        return False
+
+    def clear_all_entries(self):
+        """Clear all update history."""
+        self._write_json({})
+
+    def should_check_for_update(self):
+        """Returns True if enough time has passed since the last check."""
+        data = self._read_json()
+        last_check = data.get(self.package_name, {}).get("last_checked", 0)
+        elapsed_time = time.time() - last_check
+        return elapsed_time >= self.update_interval
+
+    def last_update_date_string(self, time_float):
         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
+    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)

From 3587ce13178e2c38f8b7a59b61d5c7e1278917da Mon Sep 17 00:00:00 2001
From: Mr Finchum <mr.finchum@pm.me>
Date: Sat, 1 Feb 2025 17:43:54 +0000
Subject: [PATCH 03/17] refactor: consistent return values

---
 CHANGELOG.md                    |  7 +++++
 src/PyPiUpdater/pypi_updater.py | 55 ++++++++-------------------------
 2 files changed, 20 insertions(+), 42 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2e5aa9f..ee74deb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
 # Changelog
 
+## 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.
diff --git a/src/PyPiUpdater/pypi_updater.py b/src/PyPiUpdater/pypi_updater.py
index c923438..5767a34 100644
--- a/src/PyPiUpdater/pypi_updater.py
+++ b/src/PyPiUpdater/pypi_updater.py
@@ -8,7 +8,7 @@ from packaging import version
 from xml.etree import ElementTree as ET
 
 class PyPiUpdater:
-    def __init__(self, package_name, local_version, log_path, update_interval_seconds = 20 * 3600):
+    def __init__(self, package_name, local_version, log_path):
         """
         Initialize PyPiUpdater.
 
@@ -20,7 +20,6 @@ class PyPiUpdater:
         self.package_name = package_name
         self.local_version = version.parse(local_version)
         self.log_path = log_path
-        self.update_interval = update_interval_seconds
         self.latest_version = ""
         self.last_update_check = 0.1
 
@@ -35,34 +34,30 @@ class PyPiUpdater:
 
             latest_version = root.find(".//item/title").text.strip()
             self.latest_version = latest_version
-            return latest_version, None
+            return [latest_version, None]
         except requests.exceptions.RequestException as e:
-            return None, f"Network error: {str(e)}"
+            return [None, f"Network error: {str(e)}"]
         except Exception as e:
-            return None, f"Error parsing feed: {str(e)}"
+            return [None, f"Error parsing feed: {str(e)}"]
 
-    def check_for_update(self, force = False):
+    def check_for_update(self):
         """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
+            return [None, error]
 
         is_newer = version.parse(latest_version) > self.local_version
-        if is_newer:
-            self.record_update_check()  # Save check timestamp & latest version
-        return is_newer, latest_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."
+            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)}"
+            return [False, f"Update failed: {str(e)}"]
 
     def restart_program(self):
         """Restart the Python program after an update."""
@@ -76,11 +71,10 @@ class PyPiUpdater:
         data = self._read_json()
         if self.package_name in data:
             entry = data[self.package_name]
-            last_check = self.last_update_date_string(entry["last_checked"])
-            return [last_check, entry["last_online_version"], 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):
+    def _record_update_check(self):
         """Save the last update check time and online version in JSON."""
         data = self._read_json()
         data[self.package_name] = {
@@ -89,37 +83,14 @@ class PyPiUpdater:
         }
         self._write_json(data)
 
-    def remove_package_entry(self, package_name):
-        """Remove a specific package entry from the log file."""
-        data = self._read_json()
-        if package_name in data:
-            del data[package_name]
-            self._write_json(data)
-            return True
-        return False
-
     def clear_all_entries(self):
         """Clear all update history."""
         self._write_json({})
 
-    def should_check_for_update(self):
-        """Returns True if enough time has passed since the last check."""
-        data = self._read_json()
-        last_check = data.get(self.package_name, {}).get("last_checked", 0)
-        elapsed_time = time.time() - last_check
-        return elapsed_time >= self.update_interval
-
-    def last_update_date_string(self, time_float):
-        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 _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)

From cb568730d8bed0ff961dbf9b24958246cb02c77c Mon Sep 17 00:00:00 2001
From: CodeByMrFinchum <mr.finchum@pm.me>
Date: Mon, 3 Feb 2025 16:21:01 +0100
Subject: [PATCH 04/17] feat: Added local update functions

---
 CHANGELOG.md                    | 10 ++++++++
 src/PyPiUpdater/pypi_updater.py | 45 +++++++++++++++++++++++++++++++++
 2 files changed, 55 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ee74deb..678af18 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
 # Changelog
 
+## 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.
diff --git a/src/PyPiUpdater/pypi_updater.py b/src/PyPiUpdater/pypi_updater.py
index 5767a34..4c5b835 100644
--- a/src/PyPiUpdater/pypi_updater.py
+++ b/src/PyPiUpdater/pypi_updater.py
@@ -4,6 +4,7 @@ import sys
 import os
 import time
 import json
+import re
 from packaging import version
 from xml.etree import ElementTree as ET
 
@@ -59,6 +60,50 @@ class PyPiUpdater:
         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...")

From 09a37ae62886ae47a9acc50117a12ff2cd154622 Mon Sep 17 00:00:00 2001
From: CodeByMrFinchum <mr.finchum@pm.me>
Date: Sun, 9 Feb 2025 13:26:21 +0100
Subject: [PATCH 05/17] patch: Added classifiers for pypi

---
 CHANGELOG.md   | 6 +++++-
 pyproject.toml | 4 ++++
 2 files changed, 9 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 678af18..60dc87f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,10 @@
 # Changelog
 
-## 0.6.0: New Local Update Feature
+## 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.
diff --git a/pyproject.toml b/pyproject.toml
index 0456616..e0c386a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -11,13 +11,17 @@ readme = "README.md"
 requires-python = ">=3.8"
 dependencies = ["requests", "packaging"]
 classifiers = [
+    "Development Status :: 4 - Beta",
     "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"]

From 9680e33730059c55ee604eeafea640b54a15dfb0 Mon Sep 17 00:00:00 2001
From: CodeByMrFinchum <mr.finchum@pm.me>
Date: Sun, 9 Feb 2025 17:48:30 +0100
Subject: [PATCH 06/17] feat: function to install single package.

---
 CHANGELOG.md                    |  6 ++++++
 pyproject.toml                  |  2 +-
 src/PyPiUpdater/pypi_updater.py | 11 +++++++++++
 3 files changed, 18 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 60dc87f..f07c7b0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 # Changelog
 
+## 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
diff --git a/pyproject.toml b/pyproject.toml
index e0c386a..2aeb735 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -11,7 +11,7 @@ readme = "README.md"
 requires-python = ">=3.8"
 dependencies = ["requests", "packaging"]
 classifiers = [
-    "Development Status :: 4 - Beta",
+    "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+)",
diff --git a/src/PyPiUpdater/pypi_updater.py b/src/PyPiUpdater/pypi_updater.py
index 4c5b835..3203ddb 100644
--- a/src/PyPiUpdater/pypi_updater.py
+++ b/src/PyPiUpdater/pypi_updater.py
@@ -146,3 +146,14 @@ class PyPiUpdater:
         """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}"]

From 712d80a0aa17bb9ea2fc7b9caf98654d0999edce Mon Sep 17 00:00:00 2001
From: CodeByMrFinchum <mr.finchum@pm.me>
Date: Mon, 10 Feb 2025 13:16:21 +0100
Subject: [PATCH 07/17] fix: Fixed that prereleases version were listed

---
 src/PyPiUpdater/__init__.py                   |  3 ++-
 src/PyPiUpdater/multi_updater.py              |  4 ++++
 .../{pypi_updater.py => single_updater.py}    | 24 +++++++++++++++----
 3 files changed, 26 insertions(+), 5 deletions(-)
 create mode 100644 src/PyPiUpdater/multi_updater.py
 rename src/PyPiUpdater/{pypi_updater.py => single_updater.py} (86%)

diff --git a/src/PyPiUpdater/__init__.py b/src/PyPiUpdater/__init__.py
index 136d36c..621ee43 100644
--- a/src/PyPiUpdater/__init__.py
+++ b/src/PyPiUpdater/__init__.py
@@ -1,4 +1,5 @@
-from .pypi_updater import PyPiUpdater
+from .single_updater import PyPiUpdater
+#from .multi_updater import MultiPackageUpdater
 
 __all__ = ["PyPiUpdater"]
 
diff --git a/src/PyPiUpdater/multi_updater.py b/src/PyPiUpdater/multi_updater.py
new file mode 100644
index 0000000..71e55a9
--- /dev/null
+++ b/src/PyPiUpdater/multi_updater.py
@@ -0,0 +1,4 @@
+
+class MultiPackageUpdater:
+    def __init__(self, log_path):
+        print("Not ready yet...")
diff --git a/src/PyPiUpdater/pypi_updater.py b/src/PyPiUpdater/single_updater.py
similarity index 86%
rename from src/PyPiUpdater/pypi_updater.py
rename to src/PyPiUpdater/single_updater.py
index 3203ddb..851f69b 100644
--- a/src/PyPiUpdater/pypi_updater.py
+++ b/src/PyPiUpdater/single_updater.py
@@ -6,6 +6,7 @@ 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:
@@ -25,7 +26,7 @@ class PyPiUpdater:
         self.last_update_check = 0.1
 
     def _get_latest_version(self):
-        """Fetch the latest version from PyPI RSS feed."""
+        """Fetch the latest stable version from PyPI RSS feed."""
         rss_url = f"https://pypi.org/rss/project/{self.package_name.lower()}/releases.xml"
 
         try:
@@ -33,9 +34,24 @@ class PyPiUpdater:
             response.raise_for_status()
             root = ET.fromstring(response.content)
 
-            latest_version = root.find(".//item/title").text.strip()
-            self.latest_version = latest_version
-            return [latest_version, None]
+            # Extract all versions from the feed
+            versions = []
+            for item in root.findall(".//item/title"):
+                version_text = item.text.strip()
+                parsed_version = parse(version_text)
+                print(version_text,"\n",parsed_version)
+                # 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:

From 23214d7d32b4a81bd637dfb0c0a7cf190f71fb46 Mon Sep 17 00:00:00 2001
From: CodeByMrFinchum <mr.finchum@pm.me>
Date: Mon, 10 Feb 2025 13:21:38 +0100
Subject: [PATCH 08/17] fix: removed debug leftover

---
 CHANGELOG.md                      | 9 ++++++++-
 src/PyPiUpdater/single_updater.py | 1 -
 2 files changed, 8 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index f07c7b0..c7a8815 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,13 @@
 # Changelog
 
-## 0.7.0: Added Function to Install Packages
+## 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.
 
diff --git a/src/PyPiUpdater/single_updater.py b/src/PyPiUpdater/single_updater.py
index 851f69b..3e4e6d6 100644
--- a/src/PyPiUpdater/single_updater.py
+++ b/src/PyPiUpdater/single_updater.py
@@ -39,7 +39,6 @@ class PyPiUpdater:
             for item in root.findall(".//item/title"):
                 version_text = item.text.strip()
                 parsed_version = parse(version_text)
-                print(version_text,"\n",parsed_version)
                 # 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)

From 8f91f64d04a5a66145965ca13e2ee869089b0092 Mon Sep 17 00:00:00 2001
From: Mr Finchum <mr.finchum@pm.me>
Date: Thu, 10 Apr 2025 16:30:40 +0200
Subject: [PATCH 09/17] CI: woodpecker CI

---
 .gitlab-ci/git/create_tag.yml        | 15 ------
 .gitlab-ci/versioning/gitversion.yml | 31 ------------
 .woodpecker/woodpecker_ci.yml        | 75 ++++++++++++++++++++++++++++
 3 files changed, 75 insertions(+), 46 deletions(-)
 delete mode 100644 .gitlab-ci/git/create_tag.yml
 delete mode 100644 .gitlab-ci/versioning/gitversion.yml
 create mode 100644 .woodpecker/woodpecker_ci.yml

diff --git a/.gitlab-ci/git/create_tag.yml b/.gitlab-ci/git/create_tag.yml
deleted file mode 100644
index 2c1afd7..0000000
--- a/.gitlab-ci/git/create_tag.yml
+++ /dev/null
@@ -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
diff --git a/.gitlab-ci/versioning/gitversion.yml b/.gitlab-ci/versioning/gitversion.yml
deleted file mode 100644
index dbbc149..0000000
--- a/.gitlab-ci/versioning/gitversion.yml
+++ /dev/null
@@ -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
diff --git a/.woodpecker/woodpecker_ci.yml b/.woodpecker/woodpecker_ci.yml
new file mode 100644
index 0000000..01f2460
--- /dev/null
+++ b/.woodpecker/woodpecker_ci.yml
@@ -0,0 +1,75 @@
+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: build
+    depends_on: [gitversion]
+    when:
+      event: push
+      branch: main
+    image: python:3.9.21
+    commands:
+      - source gitversion.env
+      - 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: release
+    depends_on: [gitversion, 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: tag-release
+    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
+      - source gitversion.env
+      - git tag $GitVersion_SemVer
+      - git push origin tag $GitVersion_SemVer

From bf5d68061211b2e9208554378df99706588a1c4f Mon Sep 17 00:00:00 2001
From: Mr Finchum <mr.finchum@pm.me>
Date: Thu, 10 Apr 2025 16:48:17 +0200
Subject: [PATCH 10/17] fix: changed order

---
 .woodpecker/woodpecker_ci.yml | 59 ++++++++++++++++++-----------------
 1 file changed, 30 insertions(+), 29 deletions(-)

diff --git a/.woodpecker/woodpecker_ci.yml b/.woodpecker/woodpecker_ci.yml
index 01f2460..e015764 100644
--- a/.woodpecker/woodpecker_ci.yml
+++ b/.woodpecker/woodpecker_ci.yml
@@ -27,35 +27,7 @@ steps:
         echo "GitVersion_MajorMinorPatch=$(jq -r '.MajorMinorPatch' version.json)" >> gitversion.env
         echo "GitVersion_BuildMetaData=$(jq -r '.BuildMetaData' version.json)" >> gitversion.env
 
-  - name: build
-    depends_on: [gitversion]
-    when:
-      event: push
-      branch: main
-    image: python:3.9.21
-    commands:
-      - source gitversion.env
-      - 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: release
-    depends_on: [gitversion, 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: tag-release
+  - name: tagging
     depends_on: [gitversion]
     when:
       event: push
@@ -73,3 +45,32 @@ steps:
       - source 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
+      - source gitversion.env
+      - 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: release
+    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/*

From 2e2bba2aa5d5a91dce1724dcde6f8a2403af8937 Mon Sep 17 00:00:00 2001
From: Mr Finchum <mr.finchum@pm.me>
Date: Thu, 10 Apr 2025 16:59:59 +0200
Subject: [PATCH 11/17] fix: change source to dot

---
 .woodpecker/woodpecker_ci.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.woodpecker/woodpecker_ci.yml b/.woodpecker/woodpecker_ci.yml
index e015764..1b3d924 100644
--- a/.woodpecker/woodpecker_ci.yml
+++ b/.woodpecker/woodpecker_ci.yml
@@ -42,7 +42,7 @@ steps:
       - 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
-      - source gitversion.env
+      - . gitversion.env
       - git tag $GitVersion_SemVer
       - git push origin tag $GitVersion_SemVer
 
@@ -54,7 +54,7 @@ steps:
     image: python:3.9.21
     commands:
       - ls
-      - source gitversion.env
+      - . gitversion.env
       - sed -i "s/^__version__ = .*/__version__ = \"$GitVersion_SemVer\"/" src/PyPiUpdater/__init__.py
       - cat src/PyPiUpdater/__init__.py
       - python3 -m pip install build

From 03cf68b8731ca76ee52c08696fdd902d18704555 Mon Sep 17 00:00:00 2001
From: Mr Finchum <mr.finchum@pm.me>
Date: Thu, 10 Apr 2025 17:05:49 +0200
Subject: [PATCH 12/17] fix: use export for vars

---
 .woodpecker/woodpecker_ci.yml | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/.woodpecker/woodpecker_ci.yml b/.woodpecker/woodpecker_ci.yml
index 1b3d924..ede607d 100644
--- a/.woodpecker/woodpecker_ci.yml
+++ b/.woodpecker/woodpecker_ci.yml
@@ -54,7 +54,8 @@ steps:
     image: python:3.9.21
     commands:
       - ls
-      - . gitversion.env
+      - 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

From 364af1cb6ec378b62db94b7a0700b4f8d4b96b8f Mon Sep 17 00:00:00 2001
From: Mr Finchum <mr.finchum@pm.me>
Date: Fri, 11 Apr 2025 11:42:43 +0200
Subject: [PATCH 13/17] ci: now also upload to package on forgejo.

---
 .woodpecker/woodpecker_ci.yml | 26 +++++++++++++++++++++++++-
 1 file changed, 25 insertions(+), 1 deletion(-)

diff --git a/.woodpecker/woodpecker_ci.yml b/.woodpecker/woodpecker_ci.yml
index ede607d..ebb0ec7 100644
--- a/.woodpecker/woodpecker_ci.yml
+++ b/.woodpecker/woodpecker_ci.yml
@@ -61,7 +61,7 @@ steps:
       - python3 -m pip install build
       - python3 -m build
 
-  - name: release
+  - name: publish_pypi
     depends_on: [gitversion, tagging, build]
     when:
       event: push
@@ -75,3 +75,27 @@ steps:
       - 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:
+      CI_TOKEN:
+        from_secret: CI_TOKEN
+    commands:
+      - ls
+      - python3 -m pip install twine
+      - |
+        cat > ~/.pypirc <<EOF
+        [distutils]
+        index-servers = forgejo
+
+        [forgejo]
+        repository = https://code.boxyfoxy.net/api/packages/CodeByMrFinchum/pypi
+        username = CodeByMrFinchum
+        password = ${CI_TOKEN}
+        EOF
+      - python3 -m twine upload --repository forgejo dist/*

From f4ef58672c227515b08b8b2376243f9773cbdd1e Mon Sep 17 00:00:00 2001
From: Mr Finchum <mr.finchum@pm.me>
Date: Fri, 11 Apr 2025 11:56:12 +0200
Subject: [PATCH 14/17] fix: fixes publish to forgejo package

---
 .woodpecker/woodpecker_ci.yml | 6 +++---
 CHANGELOG.md                  | 3 +++
 2 files changed, 6 insertions(+), 3 deletions(-)

diff --git a/.woodpecker/woodpecker_ci.yml b/.woodpecker/woodpecker_ci.yml
index ebb0ec7..7822860 100644
--- a/.woodpecker/woodpecker_ci.yml
+++ b/.woodpecker/woodpecker_ci.yml
@@ -83,8 +83,8 @@ steps:
       branch: main
     image: python:3.9.21
     environment:
-      CI_TOKEN:
-        from_secret: CI_TOKEN
+      PKG_TOKEN:
+        from_secret: PKG_TOKEN
     commands:
       - ls
       - python3 -m pip install twine
@@ -96,6 +96,6 @@ steps:
         [forgejo]
         repository = https://code.boxyfoxy.net/api/packages/CodeByMrFinchum/pypi
         username = CodeByMrFinchum
-        password = ${CI_TOKEN}
+        password = ${PKG_TOKEN}
         EOF
       - python3 -m twine upload --repository forgejo dist/*
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c7a8815..3e847e5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,8 @@
 # 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.

From dc8b1ca9ed18fb9f6983211beeb4470d273bb531 Mon Sep 17 00:00:00 2001
From: Mr Finchum <mr.finchum@pm.me>
Date: Fri, 11 Apr 2025 12:09:32 +0200
Subject: [PATCH 15/17] fix: skip twine file

---
 .woodpecker/woodpecker_ci.yml | 15 +++------------
 1 file changed, 3 insertions(+), 12 deletions(-)

diff --git a/.woodpecker/woodpecker_ci.yml b/.woodpecker/woodpecker_ci.yml
index 7822860..c531675 100644
--- a/.woodpecker/woodpecker_ci.yml
+++ b/.woodpecker/woodpecker_ci.yml
@@ -83,19 +83,10 @@ steps:
       branch: main
     image: python:3.9.21
     environment:
-      PKG_TOKEN:
+      TWINE_PASSWORD:
         from_secret: PKG_TOKEN
+      TWINE_USERNAME: "CodeByMrFinchum"
     commands:
       - ls
       - python3 -m pip install twine
-      - |
-        cat > ~/.pypirc <<EOF
-        [distutils]
-        index-servers = forgejo
-
-        [forgejo]
-        repository = https://code.boxyfoxy.net/api/packages/CodeByMrFinchum/pypi
-        username = CodeByMrFinchum
-        password = ${PKG_TOKEN}
-        EOF
-      - python3 -m twine upload --repository forgejo dist/*
+      - python3 -m twine upload --verbose --repository-url https://code.boxyfoxy.net/api/packages/CodeByMrFinchum/pypi dist/*

From 014feb23c205f18b0d21c9e45e81772f46154ba7 Mon Sep 17 00:00:00 2001
From: Mr Finchum <mr.finchum@pm.me>
Date: Fri, 11 Apr 2025 12:16:59 +0200
Subject: [PATCH 16/17] Adding info about the migration to readme.

---
 README.md     | 2 ++
 pip_README.md | 2 +-
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index c3f04c1..ceb666a 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,6 @@
 # PyPiUpdater
+Developed on my [forgejo instance](https://code.boxyfoxy.net/CodeByMrFinchum), [GitLab](https://gitlab.com/CodeByMrFinchum) is used as backup.
+
 **UNFINISHED** Still early code, functions might change drasticly
 
 **PyPiUpdater** is a Python library for managing updates of packages installed via `pip`.
diff --git a/pip_README.md b/pip_README.md
index 2fb85ac..81c43b3 100644
--- a/pip_README.md
+++ b/pip_README.md
@@ -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#).

From 77d7092a24f1bccae7a3aa8ef1784806f2ef4a72 Mon Sep 17 00:00:00 2001
From: Mr Finchum <mr.finchum@pm.me>
Date: Fri, 11 Apr 2025 12:23:11 +0200
Subject: [PATCH 17/17] patch: removed old gitlab ci file.

---
 .gitlab-ci.yml | 69 --------------------------------------------------
 1 file changed, 69 deletions(-)
 delete mode 100644 .gitlab-ci.yml

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
deleted file mode 100644
index d93834d..0000000
--- a/.gitlab-ci.yml
+++ /dev/null
@@ -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/PyPiUpdater/__init__.py
-    - cat src/PyPiUpdater/__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