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
214 changes: 214 additions & 0 deletions .github/scripts/update_docs_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
#!/usr/bin/env python3
"""
Script to dynamically update the version number in Sphinx documentation.

This script updates the hardcoded version in docs/source/index.rst before
the Sphinx build runs. It reads the version from multiple sources in priority order:
1. VERSION file at project root (if exists)
2. setuptools_scm (if available)
3. pyproject.toml as fallback

Usage:
python .github/scripts/update_docs_version.py
"""

import os
import re
import sys
from pathlib import Path


def get_version_from_file(project_root):
"""
Read version from the VERSION file at project root.

Args:
project_root: Path to the project root directory

Returns:
str: Version string if found, None otherwise
"""
version_file = project_root / "VERSION"
if version_file.exists():
try:
version = version_file.read_text().strip()
print(f"✓ Found version in VERSION file: {version}")
return version
except Exception as e:
print(f"✗ Error reading VERSION file: {e}", file=sys.stderr)
return None


def get_version_from_setuptools_scm(project_root):
"""
Get version from setuptools_scm if available.

Args:
project_root: Path to the project root directory

Returns:
str: Version string if found, None otherwise
"""
try:
from setuptools_scm import get_version
version = get_version(root=str(project_root))
print(f"✓ Found version from setuptools_scm: {version}")
return version
except ImportError:
print("✗ setuptools_scm not available", file=sys.stderr)
except Exception as e:
print(f"✗ Error getting version from setuptools_scm: {e}", file=sys.stderr)
return None


def get_version_from_pyproject(project_root):
"""
Extract version from pyproject.toml as a fallback.

Args:
project_root: Path to the project root directory

Returns:
str: Version string if found, None otherwise
"""
pyproject_file = project_root / "pyproject.toml"
if pyproject_file.exists():
try:
content = pyproject_file.read_text()
# Look for version = "X.X.X" pattern
match = re.search(r'version\s*=\s*["\']([^"\']+)["\']', content)
if match:
version = match.group(1)
print(f"✓ Found version in pyproject.toml: {version}")
return version
Comment on lines +78 to +83
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex pattern for extracting version from pyproject.toml is too generic and may match unintended version fields (e.g., dependencies). Consider making it more specific by looking for version within the [project] section to avoid false matches.

Suggested change
# Look for version = "X.X.X" pattern
match = re.search(r'version\s*=\s*["\']([^"\']+)["\']', content)
if match:
version = match.group(1)
print(f"✓ Found version in pyproject.toml: {version}")
return version
# Extract the [project] section only
project_section_match = re.search(r'^\[project\](.*?)(^\[|\Z)', content, re.DOTALL | re.MULTILINE)
if project_section_match:
project_section = project_section_match.group(1)
# Look for version = "X.X.X" pattern within [project] section
match = re.search(r'version\s*=\s*["\']([^"\']+)["\']', project_section)
if match:
version = match.group(1)
print(f"✓ Found version in pyproject.toml [project] section: {version}")
return version

Copilot uses AI. Check for mistakes.

except Exception as e:
print(f"✗ Error reading pyproject.toml: {e}", file=sys.stderr)
return None


def get_version(project_root):
"""
Get version from available sources in priority order.

Priority:
1. VERSION file
2. setuptools_scm
3. pyproject.toml

Args:
project_root: Path to the project root directory

Returns:
str: Version string

Raises:
RuntimeError: If no version can be determined
"""
# Try VERSION file first
version = get_version_from_file(project_root)
if version:
return version

# Try setuptools_scm
version = get_version_from_setuptools_scm(project_root)
if version:
return version

# Fallback to pyproject.toml
version = get_version_from_pyproject(project_root)
if version:
return version

raise RuntimeError("Could not determine version from any source")


def update_index_rst(index_file, version):
"""
Update the version number in docs/source/index.rst.

Args:
index_file: Path to the index.rst file
version: Version string to insert

Returns:
bool: True if file was updated, False otherwise
"""
if not index_file.exists():
print(f"✗ File not found: {index_file}", file=sys.stderr)
return False

try:
# Read the file
content = index_file.read_text(encoding='utf-8')
lines = content.splitlines(keepends=True)

# Pattern to match the version line (line 20, 0-indexed as 19)
pattern = re.compile(
r'^(\s*Prompture is currently in development \(version )'
r'[^)]+'
r'(\)\. APIs may change between versions\.\s*)$'
)

# Update line 20 (index 19)
if len(lines) >= 20:
line_idx = 19 # Line 20 is at index 19
Comment on lines +152 to +154
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The magic number 19/20 is repeated multiple times without a named constant. Consider defining LINE_INDEX = 19 at the module level to improve maintainability and make the intent clearer.

Copilot uses AI. Check for mistakes.

original_line = lines[line_idx]

# Check if the line matches the expected pattern
if pattern.match(original_line):
# Replace with new version
new_line = pattern.sub(
rf'\g<1>{version}\g<2>',
original_line
)
lines[line_idx] = new_line

# Write back to file
index_file.write_text(''.join(lines), encoding='utf-8')
print(f"✓ Updated version in {index_file}")
print(f" Old: {original_line.strip()}")
print(f" New: {new_line.strip()}")
return True
else:
print(f"✗ Line 20 does not match expected pattern", file=sys.stderr)
print(f" Found: {original_line.strip()}", file=sys.stderr)
return False
else:
print(f"✗ File has fewer than 20 lines", file=sys.stderr)
Comment on lines +145 to +177
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard-coding line number 20 (index 19) makes the script fragile to changes in the documentation structure. Consider searching for the pattern throughout the file instead of relying on a specific line number.

Suggested change
# Pattern to match the version line (line 20, 0-indexed as 19)
pattern = re.compile(
r'^(\s*Prompture is currently in development \(version )'
r'[^)]+'
r'(\)\. APIs may change between versions\.\s*)$'
)
# Update line 20 (index 19)
if len(lines) >= 20:
line_idx = 19 # Line 20 is at index 19
original_line = lines[line_idx]
# Check if the line matches the expected pattern
if pattern.match(original_line):
# Replace with new version
new_line = pattern.sub(
rf'\g<1>{version}\g<2>',
original_line
)
lines[line_idx] = new_line
# Write back to file
index_file.write_text(''.join(lines), encoding='utf-8')
print(f"✓ Updated version in {index_file}")
print(f" Old: {original_line.strip()}")
print(f" New: {new_line.strip()}")
return True
else:
print(f"✗ Line 20 does not match expected pattern", file=sys.stderr)
print(f" Found: {original_line.strip()}", file=sys.stderr)
return False
else:
print(f"✗ File has fewer than 20 lines", file=sys.stderr)
# Pattern to match the version line
pattern = re.compile(
r'^(\s*Prompture is currently in development \(version )'
r'[^)]+'
r'(\)\. APIs may change between versions\.\s*)$'
)
# Search for the version line by pattern
found = False
for idx, line in enumerate(lines):
if pattern.match(line):
new_line = pattern.sub(
rf'\g<1>{version}\g<2>',
line
)
lines[idx] = new_line
found = True
print(f"✓ Updated version in {index_file}")
print(f" Old: {line.strip()}")
print(f" New: {new_line.strip()}")
break
if found:
# Write back to file
index_file.write_text(''.join(lines), encoding='utf-8')
return True
else:
print(f"✗ No line matching the expected version pattern found", file=sys.stderr)

Copilot uses AI. Check for mistakes.

return False

except Exception as e:
print(f"✗ Error updating index.rst: {e}", file=sys.stderr)
return False


def main():
"""Main entry point for the script."""
# Determine project root (two levels up from this script)
script_path = Path(__file__).resolve()
project_root = script_path.parent.parent.parent

print(f"Project root: {project_root}")

# Get version
try:
version = get_version(project_root)
print(f"\n→ Using version: {version}\n")
except RuntimeError as e:
print(f"\n✗ Fatal error: {e}", file=sys.stderr)
sys.exit(1)

# Update index.rst
index_file = project_root / "docs" / "source" / "index.rst"
success = update_index_rst(index_file, version)

if success:
print("\n✓ Documentation version updated successfully")
sys.exit(0)
else:
print("\n✗ Failed to update documentation version", file=sys.stderr)
sys.exit(1)


if __name__ == "__main__":
main()
4 changes: 3 additions & 1 deletion .github/workflows/documentation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ on:
branches:
- main


permissions:
contents: write

Expand All @@ -18,6 +17,9 @@ jobs:
- name: Install dependencies
run: |
pip install -r docs/requirements.txt
- name: Update documentation version
run: |
python .github/scripts/update_docs_version.py
- name: Sphinx build
run: |
sphinx-build docs/source _build
Expand Down
66 changes: 65 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Prompture

[![PyPI version](https://badge.fury.io/py/prompture.svg)](https://badge.fury.io/py/prompture)
[![Python Versions](https://img.shields.io/pypi/pyversions/prompture.svg)](https://pypi.org/project/prompture/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Downloads](https://static.pepy.tech/badge/prompture)](https://pepy.tech/project/prompture)
![GitHub Repo stars](https://img.shields.io/github/stars/jhd3197/prompture?style=social)


**Prompture** is an API-first library for getting **structured JSON** (or any structure) from LLMs, validating it, and benchmarking multiple models with one spec.

## ✨ Features
Expand Down Expand Up @@ -94,7 +101,64 @@ person = extract_with_model(Person, text, model_name="ollama/gpt-oss:20b")
print(person.dict())
```

**Why start here?** It’s fast (one call), cost-efficient, and returns a validated Pydantic instance.
**Why start here?** It's fast (one call), cost-efficient, and returns a validated Pydantic instance.

---

## 📋 Field Definitions

Prompture includes a powerful **field definitions system** that provides a centralized registry of structured data extraction fields. This system enables consistent, reusable field configurations across your data extraction workflows with built-in fields for common use cases like personal info, contact details, professional data, and more.

**Key benefits:**
- 🎯 Pre-configured fields with descriptions and extraction instructions
- 🔄 Template variables like `{{current_year}}`, `{{current_date}}`, `{{current_datetime}}`
- 🔌 Seamless Pydantic integration via `field_from_registry()`
- ⚙️ Easy custom field registration

### Using Built-in Fields

```python
from pydantic import BaseModel
from prompture import field_from_registry, stepwise_extract_with_model

class Person(BaseModel):
name: str = field_from_registry("name")
age: int = field_from_registry("age")
email: str = field_from_registry("email")
occupation: str = field_from_registry("occupation")
company: str = field_from_registry("company")

# Built-in fields include: name, age, email, phone, address, city, country,
# occupation, company, education_level, salary, and many more!

result = stepwise_extract_with_model(
Person,
"John Smith is 25 years old, software engineer at TechCorp, [email protected]",
model_name="openai/gpt-4"
)
```

### Registering Custom Fields

```python
from prompture import register_field, field_from_registry

# Register a custom field with template variables
register_field("document_date", {
"type": "str",
"description": "Document creation or processing date",
"instructions": "Use {{current_date}} if not specified in document",
"default": "{{current_date}}",
"nullable": False
})

# Use custom field in your model
class Document(BaseModel):
title: str = field_from_registry("name")
created_date: str = field_from_registry("document_date")
```

📚 **[View Full Field Definitions Reference →](https://prompture.readthedocs.io/en/latest/field_definitions_reference.html)**

---

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.0.28
0.0.29.dev1
Loading