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
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ name: qret-ci

on:
push:
paths:
- .github/workflows/release.yml
- quration
pull_request:
paths:
- .github/workflows/release.yml
- quration
release:
types: [published]

Expand Down
65 changes: 65 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: test

on:
push:
paths:
- src/**
- tests/**
- pyproject.toml
- .github/workflows/test.yml
pull_request:
paths:
- src/**
- tests/**
- pyproject.toml
- .github/workflows/test.yml

jobs:
pytest:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
- macos-latest
- macos-15-intel
- ubuntu-latest
- windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install package and test deps
run: |
python -m pip install --upgrade pip
python -m pip install -e ".[test]"

- name: Prepare qret stub for test (Unix)
if: runner.os != 'Windows'
run: |
mkdir -p .ci-bin
cat > .ci-bin/qret <<'EOF'
#!/usr/bin/env sh
echo "qret 0.0.0-ci"
EOF
chmod +x .ci-bin/qret
echo "$GITHUB_WORKSPACE/.ci-bin" >> "$GITHUB_PATH"

- name: Prepare qret stub for test (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path .ci-bin | Out-Null
@'
@echo off
echo qret 0.0.0-ci
'@ | Set-Content -Path .ci-bin/qret.cmd
"$env:GITHUB_WORKSPACE/.ci-bin" | Out-File -FilePath $env:GITHUB_PATH -Append

- name: Run pytest
run: pytest -q tests/test_qret_cli_bundle.py
7 changes: 1 addition & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
# qret-cli-bundle

This package downloads a platform-specific `qret` CLI archive from GitHub Releases and prepends its directory to `PATH` when imported.
This package downloads a platform-specific `qret` CLI archive from GitHub Releases and appends its directory to `PATH` when imported.

```python
import qret_cli_bundle
```

Environment variables:

- `QRET_BUNDLE_REPOSITORY` (default: `QunaSys/quration-cli-bundle`)
- `QRET_BUNDLE_TAG` (default: latest release)
196 changes: 196 additions & 0 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 6 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ readme = "README.md"
requires-python = ">=3.9"
authors = [{ name = "QunaSys" }]

[project.optional-dependencies]
test = ["pytest>=8.0"]

[tool.setuptools]
package-dir = { "" = "src" }
package-dir = { "qret_cli_bundle" = "." }

[tool.setuptools.packages.find]
where = ["src"]
[tool.poetry.group.dev.dependencies]
pytest = "^8.0"
57 changes: 38 additions & 19 deletions src/qret_cli_bundle/__init__.py → qret_cli_bundle/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,8 @@ def _platform_asset_name() -> str:
raise QretBundleError(f"Unsupported platform: {system} ({machine})")


def _release_json(repo: str, tag: str | None) -> dict:
if tag:
url = f"{API_BASE}/repos/{repo}/releases/tags/{tag}"
else:
url = f"{API_BASE}/repos/{repo}/releases/latest"

def _release_json() -> dict:
url = f"{API_BASE}/repos/{DEFAULT_REPO}/releases/latest"
req = urllib.request.Request(url, headers={"Accept": "application/vnd.github+json"})
with urllib.request.urlopen(req, timeout=60) as response:
return json.load(response)
Expand Down Expand Up @@ -79,21 +75,44 @@ def _find_qret_binary(root: Path) -> Path:
raise QretBundleError(f"Could not find {exe_name} under {root}")


def _append_env_path(var_name: str, entry: str) -> None:
current = os.environ.get(var_name, "")
os.environ[var_name] = current + os.pathsep + entry if current else entry


def _find_existing_qret_in_path() -> Path | None:
exe_name = "qret.exe" if platform.system() == "Windows" else "qret"
qret_path = shutil.which(exe_name)
if qret_path:
candidate = Path(qret_path).resolve()
if candidate.is_file():
return candidate
return None


def ensure_qret_on_path() -> Path:
"""Ensure qret is downloaded and available in PATH, returning its full path."""
repo = os.environ.get("QRET_BUNDLE_REPOSITORY", DEFAULT_REPO)
tag = os.environ.get("QRET_BUNDLE_TAG")
existing_qret = _find_existing_qret_in_path()
if existing_qret:
qret = existing_qret
bin_dir = qret.parent
lib_dir = qret.parent.parent / "lib"
if bin_dir.exists():
_append_env_path("PATH", str(bin_dir))
if platform.system() == "Linux" and lib_dir.exists():
_append_env_path("LD_LIBRARY_PATH", str(lib_dir))
return qret

asset_name = _platform_asset_name()

temp_root = Path(tempfile.gettempdir()) / "qret-cli-bundle"
release_key = tag or "latest"
install_root = temp_root / repo.replace("/", "__") / release_key / asset_name
install_root = temp_root / DEFAULT_REPO.replace("/", "__") / "latest" / asset_name
marker = install_root / ".ready"

if marker.exists():
qret = _find_qret_binary(install_root)
else:
release = _release_json(repo=repo, tag=tag)
release = _release_json()
assets = {asset["name"]: asset["browser_download_url"] for asset in release.get("assets", [])}
if asset_name not in assets:
available = ", ".join(sorted(assets.keys()))
Expand All @@ -113,15 +132,15 @@ def ensure_qret_on_path() -> Path:
qret = _find_qret_binary(install_root)
marker.write_text("ok", encoding="utf-8")

bin_dir = str(qret.parent)
current_path = os.environ.get("PATH", "")
parts = current_path.split(os.pathsep) if current_path else []
if bin_dir not in parts:
os.environ["PATH"] = bin_dir + (os.pathsep + current_path if current_path else "")
os.environ["QRET_PATH"] = str(qret)
bin_dir = qret.parent
lib_dir = qret.parent.parent / "lib"
if bin_dir.exists():
_append_env_path("PATH", str(bin_dir))
if platform.system() == "Linux" and lib_dir.exists():
_append_env_path("LD_LIBRARY_PATH", str(lib_dir))
return qret


QRET_PATH = ensure_qret_on_path()
ensure_qret_on_path()

__all__ = ["QRET_PATH", "QretBundleError", "ensure_qret_on_path"]
__all__ = ["QretBundleError", "ensure_qret_on_path"]
Loading
Loading