Skip to content
Draft
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
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,6 @@
},
"[baml]": {
"editor.defaultFormatter": "Boundary.baml-extension"
}
},
"python.analysis.typeCheckingMode": "basic"
}
63 changes: 63 additions & 0 deletions engine/language_client_python_cffi/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# Virtual environments
.venv/
venv/
ENV/
env/

# Testing
.tox/
.coverage
.coverage.*
.cache
.pytest_cache/
nosetests.xml
coverage.xml
*.cover
.hypothesis/
htmlcov/

# Type checking
.mypy_cache/
.dmypy.json
dmypy.json

# Linting
.ruff_cache/

# IDEs
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store

# Project specific
test_manual.py
example.py

# uv
uv.lock
1 change: 1 addition & 0 deletions engine/language_client_python_cffi/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.9
74 changes: 74 additions & 0 deletions engine/language_client_python_cffi/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Makefile for BAML Python CFFI Client

# Default BAML library path
BAML_LIBRARY_PATH ?= /Users/greghale/code/baml-4/engine/target/debug/libbaml_cffi.dylib

.PHONY: sync test format lint typecheck clean all dev

# Default target
all: sync test

# Sync dependencies
sync:
@echo "Syncing dependencies..."
@uv sync --all-extras

# Run tests
test:
@echo "Running tests..."
@BAML_LIBRARY_PATH=$(BAML_LIBRARY_PATH) uv run --extra dev pytest tests/ -v

# Run tests with coverage
test-cov:
@echo "Running tests with coverage..."
@BAML_LIBRARY_PATH=$(BAML_LIBRARY_PATH) uv run --extra dev pytest tests/ --cov=baml_py_cffi --cov-report=html -v

# Format code
format:
@echo "Formatting code..."
@uv run --extra dev black baml_py_cffi tests

# Lint code
lint:
@echo "Linting code..."
@uv run --extra dev ruff check baml_py_cffi tests

# Type check
typecheck:
@echo "Type checking..."
@uv run --extra dev mypy baml_py_cffi

# Development workflow: format, lint, typecheck, test
dev: format lint typecheck test

# Clean up
clean:
@echo "Cleaning up..."
@rm -rf .venv
@rm -rf .pytest_cache
@rm -rf .mypy_cache
@rm -rf .ruff_cache
@rm -rf htmlcov
@rm -rf __pycache__
@rm -rf baml_py_cffi/__pycache__
@rm -rf tests/__pycache__
@find . -name "*.pyc" -delete
@find . -name "*.pyo" -delete

# Install pre-commit hooks (if using pre-commit)
install-hooks:
@echo "Installing pre-commit hooks..."
@uv run --extra dev pre-commit install

# Show help
help:
@echo "Available targets:"
@echo " make sync - Sync dependencies with uv"
@echo " make test - Run tests"
@echo " make test-cov - Run tests with coverage"
@echo " make format - Format code with black"
@echo " make lint - Lint code with ruff"
@echo " make typecheck - Type check with mypy"
@echo " make dev - Run full development workflow"
@echo " make clean - Clean up generated files"
@echo " make help - Show this help message"
98 changes: 98 additions & 0 deletions engine/language_client_python_cffi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# BAML Python CFFI Client

A new Python client for BAML using CFFI (C Foreign Function Interface) that provides direct access to the BAML runtime.

## Overview

This client is designed to replace the existing pyo3-based Python client and follows the architecture of the Go CFFI client, providing:

- Direct FFI bindings to the BAML runtime
- Async/await support using Python's asyncio
- Type-safe function calls
- Streaming support
- Media type handling

## Installation

```bash
# For users
uv add baml-py-cffi

# For development
uv sync
```

## Development

This package uses `uv` for dependency management and is currently under development as part of the BAML project.

### Setting up the development environment

```bash
# Sync dependencies (creates venv automatically)
uv sync --all-extras

# The virtual environment is created at .venv automatically
# No need to activate it when using uv run
```

### Phase 1 Status
- ✅ Package structure created
- ✅ Library loading logic implemented
- ✅ Basic FFI bindings for version() function
- ✅ Package metadata configured

### Running Tests

```bash
# Using Makefile (recommended)
make test # Run tests
make test-cov # Run tests with coverage
make dev # Format, lint, typecheck, and test

# Using shell script
./run_tests.sh

# Or manually with uv
uv sync --all-extras
BAML_LIBRARY_PATH=/path/to/libbaml_cffi.dylib uv run --extra dev pytest tests/
```

**Note**: The tests require a built BAML CFFI library. You can build it with:
```bash
cd ../../ && cargo build
```

### Code Quality

```bash
# Using Makefile
make format # Format code with black
make lint # Lint code with ruff
make typecheck # Type check with mypy
make dev # Run all checks and tests

# Or manually with uv
uv sync --all-extras
uv run --extra dev black baml_py_cffi tests
uv run --extra dev ruff check baml_py_cffi tests
uv run --extra dev mypy baml_py_cffi
```

### Available Make Commands

```bash
make help # Show all available commands
make sync # Sync dependencies
make test # Run tests
make test-cov # Run tests with coverage
make format # Format code
make lint # Lint code
make typecheck # Type check
make dev # Full development workflow
make clean # Clean up generated files
```

## License

Apache-2.0
18 changes: 18 additions & 0 deletions engine/language_client_python_cffi/baml_py_cffi/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""BAML Python CFFI Client - A new Python client using CFFI for BAML runtime"""

from typing import Dict
from ._ffi import version
from ._lib import set_shared_library_path
from .runtime import BamlRuntime
from .client import ScopedClient

__version__ = "0.205.0"
__all__ = ["BamlRuntime", "version", "create_runtime", "set_shared_library_path", "ScopedClient"]


# Simple creation function matching Go API
def create_runtime(
root_path: str, src_files: Dict[str, str], env_vars: Dict[str, str]
) -> BamlRuntime:
"""Create a new BAML runtime"""
return BamlRuntime(root_path, src_files, env_vars)
105 changes: 105 additions & 0 deletions engine/language_client_python_cffi/baml_py_cffi/_async_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import asyncio
import random
from typing import Optional, Any, AsyncIterator, TypeVar
from ._callbacks import CallbackState, _active_callbacks, _callback_lock
from ._result_types import ResultCallback

T = TypeVar('T')

async def queue_to_single_result(queue: asyncio.Queue[ResultCallback]) -> T:
"""Convert a queue-based callback to a single result"""
result_cb = await queue.get()

if result_cb.error:
raise result_cb.error

# For single results, we expect has_data to be True
if result_cb.has_data:
return result_cb.data

# Handle edge case - shouldn't happen in normal flow
raise RuntimeError("Expected single result but got stream data or empty result")

async def queue_to_stream(queue: asyncio.Queue[ResultCallback]) -> AsyncIterator[T]:
"""Convert a queue-based callback to an async iterator"""
while True:
result_cb = await queue.get()

if result_cb.error:
raise result_cb.error

# Check if this is stream data or final data
if result_cb.has_stream_data:
yield result_cb.stream_data
elif result_cb.has_data:
yield result_cb.data
break # Final data received
else:
break # No more data

async def make_async_call(call_fn, *args) -> Any:
"""Make an async CFFI call and wait for result using queue-based approach"""
# Generate unique ID
call_id = random.randint(1, 1000000)

# Create queue and register callback state
loop = asyncio.get_event_loop()
queue = asyncio.Queue()

with _callback_lock:
_active_callbacks[call_id] = CallbackState(
queue=queue,
loop=loop,
type_map={} # Will be populated in later phases
)

try:
# Make the C call with our ID
error = call_fn(*args, call_id)
if error:
# Synchronous error
error_msg = error.decode('utf-8')
raise RuntimeError(f"Call failed: {error_msg}")

# Use wrapper to get single result from queue
return await queue_to_single_result(queue)
except Exception:
# Cleanup on error
with _callback_lock:
_active_callbacks.pop(call_id, None)
raise

async def make_async_call_with_type_map(call_fn, *args) -> Any:
"""Make an async CFFI call with a type map for encoding/decoding"""
# Extract type_map from the last argument
*call_args, type_map = args

# Generate unique ID
call_id = random.randint(1, 1000000)

# Create queue and register callback state with type map
loop = asyncio.get_event_loop()
queue = asyncio.Queue()

with _callback_lock:
_active_callbacks[call_id] = CallbackState(
queue=queue,
loop=loop,
type_map=type_map # Store the type map for callback processing
)

try:
# Make the C call with our ID
error = call_fn(*call_args, call_id)
if error:
# Synchronous error
error_msg = error.decode('utf-8')
raise RuntimeError(f"Call failed: {error_msg}")

# Use wrapper to get single result from queue
return await queue_to_single_result(queue)
except Exception:
# Cleanup on error
with _callback_lock:
_active_callbacks.pop(call_id, None)
raise
Loading
Loading