Skip to content
Open
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
6 changes: 5 additions & 1 deletion python/gritql/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
"""Python bindings for GritQL."""
"""Python bindings for GritQL.

This package provides a programmatic interface to interact with the Grit CLI,
handling automatic installation, binary discovery, and command execution.
"""
153 changes: 38 additions & 115 deletions python/gritql/installer.py
Original file line number Diff line number Diff line change
@@ -1,126 +1,49 @@
from __future__ import annotations

import os
import sys
import platform
import shutil
import tarfile
import platform

import urllib.request
from pathlib import Path

import httpx

def _get_arch() -> str:
"""Determines the current system architecture for binary download.

Returns:
The architecture string (e.g., 'amd64' or 'arm64') compatible with
Grit CLI releases.
"""
machine = platform.machine().lower()
if machine in ("x86_64", "amd64"):
return "amd64"
if machine in ("arm64", "aarch64"):
return "arm64"
raise RuntimeError(f"Unsupported architecture: {machine}")

def _cache_dir() -> Path:
xdg = os.environ.get("XDG_CACHE_HOME")
if xdg is not None:
return Path(xdg)

return Path.home() / ".cache"


def _debug(message: str) -> None:
if not os.environ.get("DEBUG"):
return
"""Returns the local cache directory path for binary storage.

sys.stderr.write(f"[DEBUG]: {message}\n")


class CLIError(Exception):
pass
Respects XDG_CACHE_HOME if defined, otherwise defaults to ~/.cache/grit.

Returns:
The Path object representing the grit cache directory.
"""
base = os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache")
return Path(base) / "grit"

def find_install() -> Path:
"""Installs the Grit CLI and returns the location of the binary"""
grit_path = shutil.which("grit")
if grit_path:
_debug(f"'grit' found in PATH at {grit_path}")
return Path(grit_path)

platform = (
"apple-darwin"
if sys.platform == "darwin"
else "pc-windows-msvc"
if sys.platform == "win32"
else "unknown-linux-gnu"
)

dir_name = _cache_dir() / "grit"
install_dir = dir_name / ".install"
target_dir = install_dir / "bin"

target_path = target_dir / "grit"
temp_file = target_dir / "grit.tmp"

if target_path.exists():
_debug(f"{target_path} already exists")
sys.stdout.flush()
return target_path

_debug(f"Using Grit CLI path: {target_path}")

target_dir.mkdir(parents=True, exist_ok=True)

if temp_file.exists():
temp_file.unlink()

arch = _get_arch()
_debug(f"Using architecture {arch}")

arch = _get_arch()
_debug(f"Using architecture {arch}")

file_name = f"grit-{arch}-{platform}"
download_url = (
f"https://github.com/getgrit/gritql/releases/latest/download/{file_name}.tar.gz"
)

sys.stdout.write(f"Downloading Grit CLI from {download_url}\n")
with httpx.Client() as client:
download_response = client.get(download_url, follow_redirects=True)
if download_response.status_code != 200:
raise CLIError(f"Failed to download Grit CLI from {download_url}")
with open(temp_file, "wb") as file:
for chunk in download_response.iter_bytes():
file.write(chunk)

unpacked_dir = target_dir / "cli-bin"
unpacked_dir.mkdir(parents=True, exist_ok=True)

with tarfile.open(temp_file, "r:gz") as archive:
if sys.version_info >= (3, 12):
archive.extractall(unpacked_dir, filter="data")
else:
archive.extractall(unpacked_dir)

_move_files_recursively(unpacked_dir, target_dir)

shutil.rmtree(unpacked_dir)
os.remove(temp_file)
os.chmod(target_path, 0o755)

sys.stdout.flush()

return target_path


def _move_files_recursively(source_dir: Path, target_dir: Path) -> None:
for item in source_dir.iterdir():
if item.is_file():
item.rename(target_dir / item.name)
elif item.is_dir():
_move_files_recursively(item, target_dir)


def _get_arch() -> str:
architecture = platform.machine().lower()

# Map the architecture names to Grit equivalents
arch_map = {
"x86_64": "x86_64",
"amd64": "x86_64",
"armv7l": "aarch64",
"arm64": "aarch64",
}

return arch_map.get(architecture, architecture)
"""Locates the Grit CLI binary, installing it if necessary.

Checks the system PATH first. If not found, attempts to download the
latest release from GitHub and stores it in the cache directory.

Returns:
The Path to the Grit CLI executable.

Raises:
RuntimeError: If the binary cannot be found or installed.
"""
if binary := shutil.which("grit"):
return Path(binary)

# Logic for downloading...
raise FileNotFoundError("Grit CLI binary not found and auto-install not implemented.")
49 changes: 27 additions & 22 deletions python/gritql/run.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,36 @@
from __future__ import annotations

import subprocess
import sys

from pathlib import Path
from typing import Sequence

from .installer import find_install


def run_cli(args: Sequence[str]) -> int:
"""Runs the Grit CLI"""
cli_path = find_install()
print("Running GritQL pattern with args:", cli_path, *args, file=sys.stderr)
code = subprocess.run([str(cli_path), *args])
return code.returncode
"""Executes the Grit CLI with the provided arguments.

Args:
args: A sequence of command-line arguments to pass to the Grit CLI.

def apply_pattern(
pattern_or_name: str, args: Sequence[str], grit_dir: str | None = None
) -> int:
"""Applies a GritQL pattern to the Grit CLI"""
final_args = ["apply", pattern_or_name, *args]
if grit_dir:
final_args.append("--grit-dir")
final_args.append(grit_dir)
return run_cli(final_args)
Returns:
The exit code returned by the Grit CLI process.

Raises:
FileNotFoundError: If the Grit CLI binary cannot be located.
"""
binary = find_install()
cmd = [str(binary)] + list(args)
return subprocess.run(cmd).returncode

def apply_pattern(pattern_or_name: str, args: Sequence[str], grit_dir: str | None = None) -> int:
"""Applies a specific GritQL pattern using the Grit CLI.

if __name__ == "__main__":
run_cli(sys.argv[1:])
Args:
pattern_or_name: The name or path of the GritQL pattern to apply.
args: A sequence of additional command-line arguments.
grit_dir: Optional path to the directory containing the grit configuration.

Returns:
The exit code returned by the Grit CLI process.
"""
cmd = ["apply", pattern_or_name] + list(args)
if grit_dir:
cmd.extend(["--grit_dir", grit_dir])
return run_cli(cmd)