diff --git a/.circleci/check_compatibility.py b/.circleci/check_compatibility.py
new file mode 100755
index 00000000..650f24dd
--- /dev/null
+++ b/.circleci/check_compatibility.py
@@ -0,0 +1,128 @@
+#!/usr/bin/env python3
+
+import sys
+import requests
+from packaging.specifiers import SpecifierSet
+from packaging.version import Version, InvalidVersion, parse
+import re
+
+
+def main():
+ if len(sys.argv) != 2:
+ print("Usage: python check_compatibility.py ")
+ sys.exit(1)
+
+ python_version = sys.argv[1]
+ all_passed = True
+
+ GREEN = "\033[0;32m"
+ RED = "\033[0;31m"
+ NC = "\033[0m" # No Color
+
+ def check_compatibility():
+ nonlocal all_passed
+ try:
+ with open("/Users/ibraheem/Desktop/btcli/btcli/requirements.txt", "r") as f:
+ requirements = f.readlines()
+ except FileNotFoundError:
+ print(f"{RED}requirements.txt file not found.{NC}")
+ sys.exit(1)
+
+ for line in requirements:
+ line = line.strip()
+ # Skip empty lines and comments
+ if not line or line.startswith("#"):
+ continue
+ # Skip lines starting with git+
+ if line.startswith("git+"):
+ continue
+
+ # Extract package name and version specifier
+ package_name_and_specifier = re.split("[;]", line)[0].strip()
+ package_name = re.split("[!=<>~]", package_name_and_specifier)[0]
+ package_name = package_name.split("[")[0] # Remove extras
+ version_specifier = package_name_and_specifier[len(package_name) :].strip()
+
+ # Request PyPi for package details
+ print(f"Checking {package_name}... ", end="")
+ url = f"https://pypi.org/pypi/{package_name}/json"
+ response = requests.get(url)
+ if response.status_code != 200:
+ print(
+ f"{RED}Information not available for {package_name}. Failure.{NC}"
+ )
+ all_passed = False
+ continue
+
+ # Parse the data
+ data = response.json()
+ requires_python = data["info"]["requires_python"]
+
+ # Parse the version specifier from requirements.txt
+ requirement_specifier = (
+ SpecifierSet(version_specifier) if version_specifier else None
+ )
+
+ # Get all available versions of the package
+ available_versions = [parse(v) for v in data["releases"].keys()]
+ available_versions.sort(reverse=True)
+
+ # Filter versions that satisfy the requirement specifier
+ if requirement_specifier:
+ matching_versions = [
+ v for v in available_versions if requirement_specifier.contains(v)
+ ]
+ else:
+ matching_versions = available_versions
+
+ # Check for versions compatible with the specified Python version
+ compatible_versions = []
+ for version in matching_versions:
+ releases = data["releases"].get(str(version), [])
+ for release in releases:
+ # Check if the release has a 'requires_python' field
+ release_requires_python = (
+ release.get("requires_python") or requires_python
+ )
+ if release_requires_python:
+ try:
+ specifier = SpecifierSet(release_requires_python)
+ if specifier.contains(Version(python_version)):
+ compatible_versions.append(version)
+ break # No need to check other files for this version
+ except InvalidVersion as e:
+ print(f"{RED}Invalid version in requires_python: {e}{NC}")
+ all_passed = False
+ break
+ else:
+ # If no requires_python, assume compatible
+ compatible_versions.append(version)
+ break
+ if compatible_versions:
+ break # Found the highest compatible version
+
+ if compatible_versions:
+ print(
+ f"{GREEN}Supported (compatible version: {compatible_versions[0]}){NC}"
+ )
+ else:
+ print(f"{RED}Not compatible with Python {python_version}.{NC}")
+ all_passed = False
+
+ check_compatibility()
+
+ if all_passed:
+ print(
+ f"{GREEN}All requirements are compatible with Python {python_version}.{NC}"
+ )
+ print(f"{GREEN}All tests passed.{NC}")
+ else:
+ print(
+ f"{RED}Some requirements are NOT compatible with Python {python_version}.{NC}"
+ )
+ print(f"{RED}Some tests did not pass.{NC}")
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/.circleci/check_pr_status.sh b/.circleci/check_pr_status.sh
new file mode 100755
index 00000000..4b31a296
--- /dev/null
+++ b/.circleci/check_pr_status.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+# Extract the repository owner
+REPO_OWNER=$(echo $CIRCLE_PULL_REQUEST | awk -F'/' '{print $(NF-3)}')
+
+# Extract the repository name
+REPO_NAME=$(echo $CIRCLE_PULL_REQUEST | awk -F'/' '{print $(NF-2)}')
+
+# Extract the pull request number
+PR_NUMBER=$(echo $CIRCLE_PULL_REQUEST | awk -F'/' '{print $NF}')
+
+
+PR_DETAILS=$(curl -s \
+ "https://api.github.com/repos/$REPO_OWNER/$REPO_NAME/pulls/$PR_NUMBER")
+
+
+IS_DRAFT=$(echo "$PR_DETAILS" | jq -r .draft)
+echo $IS_DRAFT
+
+if [ "$IS_DRAFT" == "true" ]; then
+ echo "This PR is a draft. Skipping the workflow."
+ exit 1
+else
+ echo "This PR is not a draft. Proceeding with the workflow."
+ exit 0
+fi
diff --git a/.circleci/check_requirements.sh b/.circleci/check_requirements.sh
new file mode 100755
index 00000000..5fcd27ea
--- /dev/null
+++ b/.circleci/check_requirements.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+# Check if requirements files have changed in the last commit
+if git diff --name-only HEAD~1 | grep -E 'requirements/prod.txt|requirements/dev.txt'; then
+ echo "Requirements files have changed. Running compatibility checks..."
+ echo 'export REQUIREMENTS_CHANGED="true"' >> $BASH_ENV
+else
+ echo "Requirements files have not changed. Skipping compatibility checks..."
+ echo 'export REQUIREMENTS_CHANGED="false"' >> $BASH_ENV
+fi
diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 00000000..7d38f2f6
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,102 @@
+version: 2.1
+
+orbs:
+ python: circleci/python@2.1.1
+ python-lib: dialogue/python-lib@0.1.55
+
+jobs:
+ # Check if the PR is a draft
+ check-if-pr-is-draft:
+ docker:
+ - image: cimg/python:3.10
+ steps:
+ - checkout
+ - run:
+ name: Install jq and curl
+ command: sudo apt-get update && sudo apt-get install -y jq curl
+ - run:
+ name: Check if PR is a draft
+ command: ./check_pr_status.sh
+
+ # Compatibility check across all supported versions
+ check_compatibility:
+ parameters:
+ python_version:
+ type: string
+ docker:
+ - image: cimg/python:<< parameters.python_version >>
+ steps:
+ - checkout
+ - run:
+ name: Install dependencies
+ command: |
+ sudo apt-get update
+ sudo apt-get install -y jq curl
+ python -m pip install --upgrade pip
+ pip install requests packaging
+ - run:
+ name: Check compatibility for Python << parameters.python_version >>
+ command: |
+ python .circleci/check_compatibility.py << parameters.python_version >>
+
+ # Linting with Ruff (Run once on the latest Python version)
+ ruff:
+ resource_class: small
+ docker:
+ - image: cimg/python:3.12.0
+ steps:
+ - checkout
+ - restore_cache:
+ name: Restore cached Ruff venv
+ keys:
+ - v2-pypi-py-ruff-3.12.0
+ - run:
+ name: Update & Activate Ruff venv
+ command: |
+ python -m venv .venv
+ . .venv/bin/activate
+ python -m pip install --upgrade pip
+ pip install ruff
+ - save_cache:
+ name: Save cached Ruff venv
+ paths:
+ - ".venv/"
+ key: v2-pypi-py-ruff-3.12.0
+ - run:
+ name: Ruff linting
+ command: |
+ . .venv/bin/activate
+ ruff check .
+
+workflows:
+ version: 2
+ main_workflow:
+ jobs:
+ - check-if-pr-is-draft
+
+ # Compatibility checks for multiple Python versions
+ - check_compatibility:
+ requires:
+ - check-if-pr-is-draft
+ python_version: "3.9.0"
+ name: check-compatibility-3.9.0
+ - check_compatibility:
+ requires:
+ - check-if-pr-is-draft
+ python_version: "3.10.0"
+ name: check-compatibility-3.10.0
+ - check_compatibility:
+ requires:
+ - check-if-pr-is-draft
+ python_version: "3.11.0"
+ name: check-compatibility-3.11.0
+ - check_compatibility:
+ requires:
+ - check-if-pr-is-draft
+ python_version: "3.12.0"
+ name: check-compatibility-3.12.0
+
+ # Linting workflow using Ruff
+ - ruff:
+ requires:
+ - check-if-pr-is-draft
diff --git a/requirements.txt b/requirements.txt
index 7ec75c94..524bc9f7 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -7,7 +7,7 @@ fuzzywuzzy~=0.18.0
netaddr~=1.3.0
numpy>=2.0.1
Jinja2
-pycryptodome # Crypto
+pycryptodome
PyYAML~=6.0.1
pytest
python-Levenshtein