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
4 changes: 2 additions & 2 deletions .github/workflows/claude.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ jobs:
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
fetch-depth: 1

- name: Run Claude PR Action
uses: anthropics/claude-code-action@beta
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
timeout_minutes: "60"
6 changes: 3 additions & 3 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v4.2.2
uses: actions/checkout@v7

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5.6.0
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}

Expand All @@ -44,7 +44,7 @@ jobs:
pip install pytest>=6.2.5 pytest-cov>=2.12.0 nose>=1.3.7

- name: setup-stackql
uses: stackql/setup-stackql@v2.2.3
uses: stackql/setup-stackql@v2.3.1
with:
use_wrapper: true

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ ENV/
# stackql
.stackql/
stackql
stackql.exe
stackql-*.sh
.env
nohup.out
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## v3.8.4 (2026-06-29)

### Updates

- Routed all binary downloads through `releases.stackql.io` (including macOS) and added a `pystackql/{version}` User-Agent so downloads can be identified
- Updated the test suite and test runners for Linux and Windows

## v3.8.2 (2025-11-09)

### New Features
Expand Down
35 changes: 35 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# CLAUDE.md

PyStackQL - a Python wrapper for the [StackQL](https://stackql.io) query engine, which runs SQL against cloud and SaaS providers. Published to PyPI as `pystackql`.

## Layout

- `pystackql/core/` - main logic
- `stackql.py` - `StackQL` class, the primary public interface (`execute`, `executeStmt`, `executeQueriesAsync`, `properties`, `upgrade`, `test_connection`)
- `query.py` - `QueryExecutor` / `AsyncQueryExecutor`
- `server.py` - `ServerConnection` (server mode via psycopg/postgres wire protocol)
- `output.py` - `OutputFormatter` (output formats: `dict`, `pandas`, `csv`, `markdownkv`)
- `binary.py` - `BinaryManager` (locates/downloads the stackql binary)
- `error_detector.py` - `ErrorDetector`, matches messages against patterns in `pystackql/errors.yaml`
- `pystackql/utils/` - platform, binary, download, auth, and param helpers (re-exported from `utils/__init__.py`)
- `pystackql/magic_ext/` - Jupyter magics: `StackqlMagic` (local) and `StackqlServerMagic` (server), sharing `base.py`
- `pystackql/__init__.py` - public API: `StackQL`, `StackqlMagic`, `StackqlServerMagic`

## Two execution modes

- **Local mode** (default): downloads/runs the stackql binary as a subprocess.
- **Server mode** (`server_mode=True`): connects to a running stackql server over the postgres wire protocol. `csv` output and several local-only options are unsupported here.

## Testing

Tests use the no-auth Homebrew provider and provider-agnostic literal queries (avoid adding auth-requiring tests).

- Non-server tests: `python run_tests.py` (optionally pass specific `tests/test_*.py` files, `-v`)
- Server tests: start a server first (`stackql srv --pgsrv.address 127.0.0.1 --pgsrv.port 5466`), then `python run_server_tests.py`
- CI (`.github/workflows/test.yaml`) runs both across Linux/macOS/Windows and Python 3.9-3.13.

## Conventions

- Supports Python 3.9-3.13 on Windows, macOS, and Linux - keep changes cross-platform.
- `README.rst` is reStructuredText (the PyPI readme); docs in `docs/` build to ReadTheDocs from Sphinx-style docstrings. Update the `StackQL.__init__` docstring when changing constructor params.
- Bump `version` in `pyproject.toml` for releases; record changes in `CHANGELOG.md`.
4 changes: 2 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -144,14 +144,14 @@ To build the package, you will need to install the following packages:

::

pip install build
pip install build twine

Then, from the root directory of the repository, run:

::

rm -rf dist/*
python3 -m build
python3 -m build

The package will be built in the ``dist`` directory.

Expand Down
140 changes: 0 additions & 140 deletions launch_venv.sh

This file was deleted.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "pystackql"
version = "3.8.2"
version = "3.8.4"
description = "A Python interface for StackQL"
readme = "README.rst"
authors = [
Expand Down
4 changes: 4 additions & 0 deletions pystackql/core/binary.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ def __init__(self, download_dir=None):
else:
# Use provided download_dir or default
self.download_dir = download_dir if download_dir else get_download_dir()
# Create a user-supplied download directory if it does not exist
# (get_download_dir already ensures the default location exists).
if not os.path.exists(self.download_dir):
os.makedirs(self.download_dir, exist_ok=True)
self.bin_path = os.path.join(self.download_dir, get_binary_name(self.system))

# Check if binary exists and get version
Expand Down
22 changes: 17 additions & 5 deletions pystackql/core/stackql.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,11 +170,23 @@ def __init__(self,
self.server_port = server_port
self.server_connection = ServerConnection(server_address, server_port)
else:
# Local mode - execute the binary locally
# Get all parameters from local variables (excluding 'self')
local_params = locals().copy()
local_params.pop('self')

# Local mode - execute the binary locally.
# Combine the explicit constructor arguments with any additional
# **kwargs (e.g. download_dir, api_timeout, proxy settings, custom_auth).
# kwargs is spread flat so these reach setup_local_mode as top-level
# keys rather than being nested under a 'kwargs' entry.
local_params = {
'server_mode': server_mode,
'server_address': server_address,
'server_port': server_port,
'output': output,
'sep': sep,
'header': header,
'debug': debug,
'debug_log_file': debug_log_file,
**kwargs,
}

# Set up local mode - this sets the instance attributes and returns params
self.params = setup_local_mode(self, **local_params)

Expand Down
28 changes: 24 additions & 4 deletions pystackql/utils/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,25 @@
import platform
import requests

from .package import get_package_version

# Base URL for all stackql binary downloads. This is a Cloudflare proxy in front
# of the GitHub releases, used so downloads can be attributed and cached.
RELEASES_BASE_URL = 'https://releases.stackql.io/stackql/latest'


def get_user_agent():
"""Builds the User-Agent string used for stackql binary downloads.

The versioned identifier (e.g. ``pystackql/3.8.2``) lets the proxy logs
attribute downloads to pystackql.

Returns:
str: The User-Agent header value
"""
version = get_package_version("pystackql") or "unknown"
return f"pystackql/{version}"


def get_download_dir():
"""Gets the directory to download the stackql binary.
Expand Down Expand Up @@ -38,11 +57,11 @@ def get_download_url():
machine_val = platform.machine()

if system_val == 'Linux' and machine_val == 'x86_64':
return 'https://releases.stackql.io/stackql/latest/stackql_linux_amd64.zip'
return f'{RELEASES_BASE_URL}/stackql_linux_amd64.zip'
elif system_val == 'Windows':
return 'https://releases.stackql.io/stackql/latest/stackql_windows_amd64.zip'
return f'{RELEASES_BASE_URL}/stackql_windows_amd64.zip'
elif system_val == 'Darwin':
return 'https://storage.googleapis.com/stackql-public-releases/latest/stackql_darwin_multiarch.pkg'
return f'{RELEASES_BASE_URL}/stackql_darwin_multiarch.pkg'
else:
raise Exception(f"ERROR: [get_download_url] unsupported OS type: {system_val} {machine_val}")

Expand All @@ -59,7 +78,8 @@ def download_file(url, path, showprogress=True):
Exception: If the download fails
"""
try:
r = requests.get(url, stream=True)
headers = {"User-Agent": get_user_agent()}
r = requests.get(url, stream=True, headers=headers)
r.raise_for_status()
total_size_in_bytes = int(r.headers.get('content-length', 0))
block_size = 1024
Expand Down
Loading
Loading