Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,17 @@ outdated:
$(PCU) pyproject.toml -t latest --extra dev --fail_on_update
@echo "✅ Dependency outdated check passed."

# compatibility: Checks each dependencies for python version compatibility
.PHONY: compatibility
compatibility:
@echo "🔍 Checking dependencies for python version compatibility..."
ifdef py_version
$(PYTHON) check_compatibility.py $(py_version)
else
$(PYTHON) check_compatibility.py
endif
@echo "✅ Compatibility check done."

# PIP Upgrade: upgrade PIP to its latest version
.PHONY: pip-upgrade
pip-upgrade:
Expand Down
52 changes: 28 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,31 @@ Check the [Makefile](./Makefile) for automation as the initial step, it defines

### Make Commands

| Make Command | Description |
|:----------------------|:----------------------------------------------------------------------------------------|
| `make venv` | Creates a virtual environment in `.venv`. |
| `make lock` | Generates `requirements.txt` from `pyproject.toml` using `pip-compile`. |
| `make upgrade` | Updates all packages in `requirements.txt` to the latest allowed versions. |
| `make install` | Syncs the environment with locked dependencies and installs the app in editable mode. |
| `make setup` | Installs dependencies and sets up git hooks (runs `install` and `pre-commit install`). |
| `make outdated` | Checks for newer versions of dependencies using `pip-check-updates`. |
| `make pip-upgrade` | Upgrades `pip` to its latest version. |
| `make lint` | Checks code style using `ruff` without modifying files. |
| `make format` | Automatically fixes code style issues using `ruff`. |
| `make security` | Runs `bandit` to check for security vulnerabilities. |
| `make test` | Runs unit and integration tests using `pytest` (also runs `security`). |
| `make sbom` | Generates a Software Bill of Materials (SBOM) in `sbom.json`. |
| `make audit` | Generates a security audit report in `audit.json`. |
| `make build` | Creates distribution files (Wheel & Tarball) in `dist/`. |
| `make publish` | Uploads artifacts to the repository using `twine`. |
| `make docker-build` | Builds the Docker image for the application. |
| `make docker-run` | Runs the Docker container with mounted volumes for testing. |
| `make aws-login` | Authenticates Docker with AWS ECR. |
| `make docker-publish` | Tags and pushes the Docker image to AWS ECR. |
| `make docs` | Generates documentation from docstrings into the `docs/` directory. |
| `make clean` | Removes build artifacts, caches, and generated files. |
| `make all` | Runs the full development cycle: `lock`, `install`, `upgrade`, `lint`, `test`, `build`. |
| Make Command | Description |
|:----------------------|:-----------------------------------------------------------------------------------------|
| `make venv` | Creates a virtual environment in `.venv`. |
| `make lock` | Generates `requirements.txt` from `pyproject.toml` using `pip-compile`. |
| `make upgrade` | Updates all packages in `requirements.txt` to the latest allowed versions. |
| `make install` | Syncs the environment with locked dependencies and installs the app in editable mode. |
| `make setup` | Installs dependencies and sets up git hooks (runs `install` and `pre-commit install`). |
| `make outdated` | Checks for newer versions of dependencies using `pip-check-updates`. |
| `make compatibility` | Checks each dependencies for python version compatibility. |
| `make pip-upgrade` | Upgrades `pip` to its latest version. |
| `make lint` | Checks code style using `ruff` without modifying files. |
| `make format` | Automatically fixes code style issues using `ruff`. |
| `make security` | Runs `bandit` to check for security vulnerabilities. |
| `make test` | Runs unit and integration tests using `pytest` (also runs `security`). |
| `make sbom` | Generates a Software Bill of Materials (SBOM) in `sbom.json`. |
| `make audit` | Generates a security audit report in `audit.json`. |
| `make build` | Creates distribution files (Wheel & Tarball) in `dist/`. |
| `make publish` | Uploads artifacts to the repository using `twine`. |
| `make docker-build` | Builds the Docker image for the application. |
| `make docker-run` | Runs the Docker container with mounted volumes for testing. |
| `make aws-login` | Authenticates Docker with AWS ECR. |
| `make docker-publish` | Tags and pushes the Docker image to AWS ECR. |
| `make docs` | Generates documentation from docstrings into the `docs/` directory. |
| `make clean` | Removes build artifacts, caches, and generated files. |
| `make all` | Runs the full development cycle: `lock`, `install`, `upgrade`, `lint`, `test`, `build`. |

The `make publish` require

Expand All @@ -45,6 +46,9 @@ export TWINE_REPOSITORY_URL="https://nexus.mycompany.com/repository/pypi-interna

environment variables.

The `make compatibility` accepts a parameter example `make compatibility py_version=3.9` to mark dependencies
that are not compatible with the given target version.

## Usage

Build and run the job as:
Expand Down
139 changes: 139 additions & 0 deletions check_compatibility.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import json
import re
import sys
import urllib.request
from typing import List, Tuple


def parse_dependencies(file_path: str) -> List[Tuple[str, str]]:
dependencies = []
try:
with open(file_path, "r") as f:
content = f.read()
except FileNotFoundError:
print(f"Error: File '{file_path}' not found.")
sys.exit(1)

# Extract [project] dependencies
project_deps_match = re.search(r"dependencies\s*=\s*\[(.*?)\]", content, re.DOTALL)
if project_deps_match:
raw_deps = project_deps_match.group(1)
dependencies.extend(extract_deps_from_string(raw_deps))

# Extract [project.optional-dependencies] dev
dev_deps_match = re.search(r"dev\s*=\s*\[(.*?)\]", content, re.DOTALL)
if dev_deps_match:
raw_deps = dev_deps_match.group(1)
dependencies.extend(extract_deps_from_string(raw_deps))

return dependencies


def extract_deps_from_string(raw_string: str) -> List[Tuple[str, str]]:
deps = []
matches = re.findall(r'"(.*?)"', raw_string)
for match in matches:
# Split package name and version specifier
parts = re.split(r"==|>=|<=|~=", match)
name = parts[0].strip()
version = parts[1].strip() if len(parts) > 1 else "latest"
deps.append((name, version))
return deps


def get_python_requires(package: str, version: str) -> str:
if version == "latest":
url = f"https://pypi.org/pypi/{package}/json"
else:
url = f"https://pypi.org/pypi/{package}/{version}/json"

try:
with urllib.request.urlopen(url) as response: # nosec B310
data = json.loads(response.read().decode())
return data["info"].get("requires_python") or "Unknown"
except Exception:
# Fallback to latest if specific version fails
try:
url = f"https://pypi.org/pypi/{package}/json"
with urllib.request.urlopen(url) as response: # nosec B310
data = json.loads(response.read().decode())
return data["info"].get("requires_python") or "Unknown"
except Exception as e:
return f"Error: {e}"


def is_compatible(requires_python: str, target_version: str) -> bool:
if requires_python == "Unknown" or requires_python.startswith("Error"):
return True

# Clean up the requires_python string
req = requires_python.replace(" ", "")
conditions = req.split(",")

try:
target_ver_tuple = tuple(map(int, target_version.split(".")))
except ValueError:
return True # Invalid target version format, assume compatible

def to_tuple(v_str):
return tuple(map(int, v_str.split(".")))

for condition in conditions:
try:
if condition.startswith(">="):
v_tuple = to_tuple(condition[2:])
if target_ver_tuple < v_tuple:
return False
elif condition.startswith(">"):
v_tuple = to_tuple(condition[1:])
if target_ver_tuple <= v_tuple:
return False
elif condition.startswith("<="):
v_tuple = to_tuple(condition[2:])
if target_ver_tuple > v_tuple:
return False
elif condition.startswith("<"):
v_tuple = to_tuple(condition[1:])
if target_ver_tuple >= v_tuple:
return False
# Ignoring ==, !=, ~= for simplicity as they are less common for python_requires
except ValueError:
continue # Skip malformed version strings in requires_python

return True


def main():
"""
Check the minimum python version compatibility for each dependency in pyproject.toml.
Accepts an optional target version parameter example "3.9" and marks dependencies that
are not compatible with the given target version.
If the optional parameter is not provided then marker is not displayed.

Usage:
python3 check_compatibility.py
python3 check_compatibility.py 3.9
"""
target_version = None
if len(sys.argv) > 1:
target_version = sys.argv[1]
print(f"Checking compatibility for Python {target_version}...\n")

print(f"{'':<2} {'Dependency':<25} | {'Version':<15} | {'Min Python Version'}")
print("-" * 70)

deps = parse_dependencies("pyproject.toml")

for name, version in deps:
requires_python = get_python_requires(name, version)

marker = " "
if target_version:
if not is_compatible(requires_python, target_version):
marker = "* "

print(f"{marker}{name:<25} | {version:<15} | {requires_python}")


if __name__ == "__main__":
main()