llmtivo 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- llmtivo-0.1.1/.bandit +26 -0
- llmtivo-0.1.1/.github/badges/llmtivo.json +7 -0
- llmtivo-0.1.1/.github/workflows/ci.yml +125 -0
- llmtivo-0.1.1/.github/workflows/llmtivo-badge.yml +115 -0
- llmtivo-0.1.1/.github/workflows/pages.yml +37 -0
- llmtivo-0.1.1/.github/workflows/release.yml +337 -0
- llmtivo-0.1.1/.github/workflows/test-release.yml +195 -0
- llmtivo-0.1.1/.gitignore +46 -0
- llmtivo-0.1.1/.python-version +1 -0
- llmtivo-0.1.1/CHANGELOG.md +29 -0
- llmtivo-0.1.1/CONTRIBUTING.md +247 -0
- llmtivo-0.1.1/LICENSE +21 -0
- llmtivo-0.1.1/MANIFEST.in +20 -0
- llmtivo-0.1.1/Makefile +288 -0
- llmtivo-0.1.1/PKG-INFO +354 -0
- llmtivo-0.1.1/README.md +312 -0
- llmtivo-0.1.1/codecov.yml +27 -0
- llmtivo-0.1.1/mypy.ini +24 -0
- llmtivo-0.1.1/pyproject.toml +135 -0
- llmtivo-0.1.1/ruff.toml +95 -0
- llmtivo-0.1.1/scripts/check_deps.py +97 -0
- llmtivo-0.1.1/src/llmtivo/__init__.py +49 -0
- llmtivo-0.1.1/src/llmtivo/cassette.py +131 -0
- llmtivo-0.1.1/src/llmtivo/guard.py +80 -0
- llmtivo-0.1.1/src/llmtivo/intercept.py +190 -0
- llmtivo-0.1.1/src/llmtivo/keys.py +120 -0
- llmtivo-0.1.1/src/llmtivo/langchain.py +170 -0
- llmtivo-0.1.1/src/llmtivo/litellm.py +146 -0
- llmtivo-0.1.1/src/llmtivo/modes.py +79 -0
- llmtivo-0.1.1/src/llmtivo/plugin.py +196 -0
- llmtivo-0.1.1/src/llmtivo/py.typed +0 -0
- llmtivo-0.1.1/src/llmtivo/recorder.py +398 -0
- llmtivo-0.1.1/src/llmtivo/store.py +165 -0
- llmtivo-0.1.1/tests/integration/__init__.py +1 -0
- llmtivo-0.1.1/tests/integration/cassettes/tests_integration_test_agent_live.py_test_an_agent_loop_with_a_real_search_tool.jsonl.zst +0 -0
- llmtivo-0.1.1/tests/integration/cassettes/tests_integration_test_agent_live.py_test_replaying_the_agent_touches_no_network.jsonl.zst +0 -0
- llmtivo-0.1.1/tests/integration/cassettes/tests_integration_test_langchain_live.py_test_a_real_async_invoke_records_the_awaited_value.jsonl.zst +0 -0
- llmtivo-0.1.1/tests/integration/cassettes/tests_integration_test_langchain_live.py_test_a_real_invoke_replays_as_an_AIMessage_anthropic.jsonl.zst +0 -0
- llmtivo-0.1.1/tests/integration/cassettes/tests_integration_test_langchain_live.py_test_a_real_invoke_replays_as_an_AIMessage_deepseek.jsonl.zst +0 -0
- llmtivo-0.1.1/tests/integration/cassettes/tests_integration_test_langchain_live.py_test_a_real_invoke_replays_as_an_AIMessage_gemini.jsonl.zst +0 -0
- llmtivo-0.1.1/tests/integration/cassettes/tests_integration_test_langchain_live.py_test_a_real_invoke_replays_as_an_AIMessage_openai.jsonl.zst +0 -0
- llmtivo-0.1.1/tests/integration/cassettes/tests_integration_test_langchain_live.py_test_a_real_stream_replays_chunk_by_chunk_anthropic.jsonl.zst +0 -0
- llmtivo-0.1.1/tests/integration/cassettes/tests_integration_test_langchain_live.py_test_a_real_stream_replays_chunk_by_chunk_openai.jsonl.zst +0 -0
- llmtivo-0.1.1/tests/integration/cassettes/tests_integration_test_langchain_live.py_test_four_providers_share_one_tape_in_call_order.jsonl.zst +0 -0
- llmtivo-0.1.1/tests/integration/cassettes/tests_integration_test_litellm_live.py_test_a_real_completion_records_and_replays_anthropic_claude-haiku-4-5-20251001.jsonl.zst +0 -0
- llmtivo-0.1.1/tests/integration/cassettes/tests_integration_test_litellm_live.py_test_a_real_completion_records_and_replays_deepseek_deepseek-chat.jsonl.zst +0 -0
- llmtivo-0.1.1/tests/integration/cassettes/tests_integration_test_litellm_live.py_test_a_real_completion_records_and_replays_gemini_gemini-2.5-flash.jsonl.zst +0 -0
- llmtivo-0.1.1/tests/integration/cassettes/tests_integration_test_litellm_live.py_test_a_real_completion_records_and_replays_openai_gpt-4o-mini.jsonl.zst +0 -0
- llmtivo-0.1.1/tests/integration/cassettes/tests_integration_test_litellm_live.py_test_an_async_completion_records_the_awaited_value.jsonl.zst +0 -0
- llmtivo-0.1.1/tests/integration/cassettes/tests_integration_test_litellm_live.py_test_one_tape_holds_two_providers_in_call_order.jsonl.zst +0 -0
- llmtivo-0.1.1/tests/integration/cassettes/tests_integration_test_mcp_live.py_test_a_real_model_drives_an_mcp_tool.jsonl.zst +0 -0
- llmtivo-0.1.1/tests/integration/cassettes/tests_integration_test_mcp_live.py_test_an_mcp_tool_execution_replays_without_reaching_the_server.jsonl.zst +0 -0
- llmtivo-0.1.1/tests/integration/cassettes/tests_integration_test_tool_calling_live.py_test_a_real_model_asks_for_a_tool_and_the_request_survives_anthropic.jsonl.zst +0 -0
- llmtivo-0.1.1/tests/integration/cassettes/tests_integration_test_tool_calling_live.py_test_a_real_model_asks_for_a_tool_and_the_request_survives_openai.jsonl.zst +0 -0
- llmtivo-0.1.1/tests/integration/cassettes/tests_integration_test_tool_calling_live.py_test_the_full_round_trip_model_tool_model_anthropic.jsonl.zst +0 -0
- llmtivo-0.1.1/tests/integration/cassettes/tests_integration_test_tool_calling_live.py_test_the_full_round_trip_model_tool_model_openai.jsonl.zst +0 -0
- llmtivo-0.1.1/tests/integration/conftest.py +82 -0
- llmtivo-0.1.1/tests/integration/mcp_server.py +33 -0
- llmtivo-0.1.1/tests/integration/test_agent_live.py +83 -0
- llmtivo-0.1.1/tests/integration/test_langchain_live.py +110 -0
- llmtivo-0.1.1/tests/integration/test_litellm_live.py +102 -0
- llmtivo-0.1.1/tests/integration/test_mcp_live.py +113 -0
- llmtivo-0.1.1/tests/integration/test_replay_needs_no_credentials.py +39 -0
- llmtivo-0.1.1/tests/integration/test_tool_calling_live.py +100 -0
- llmtivo-0.1.1/tests/test_guard.py +86 -0
- llmtivo-0.1.1/tests/test_intercept.py +165 -0
- llmtivo-0.1.1/tests/test_langchain.py +265 -0
- llmtivo-0.1.1/tests/test_litellm.py +169 -0
- llmtivo-0.1.1/tests/test_plugin.py +162 -0
- llmtivo-0.1.1/tests/test_recorder.py +359 -0
- llmtivo-0.1.1/tests/test_store.py +82 -0
- llmtivo-0.1.1/uv.lock +4296 -0
llmtivo-0.1.1/.bandit
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Bandit configuration for LLMTivo project
|
|
2
|
+
[bandit]
|
|
3
|
+
exclude_dirs = [
|
|
4
|
+
"tests",
|
|
5
|
+
".venv",
|
|
6
|
+
"build",
|
|
7
|
+
"dist"
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
# Skip certain tests that are not relevant for our use case or are false positives
|
|
11
|
+
skips = [
|
|
12
|
+
"B101", # Test for use of assert
|
|
13
|
+
"B110", # Test for a try/except pass - used for optional dependency handling
|
|
14
|
+
"B404", # Test for subprocess import - we use subprocess securely with absolute paths
|
|
15
|
+
"B601", # paramiko calls (not used)
|
|
16
|
+
"B602", # subprocess popen with shell=true (controlled usage)
|
|
17
|
+
"B603", # subprocess without shell equals true (controlled usage)
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
# Severity level
|
|
21
|
+
# LOW, MEDIUM, HIGH
|
|
22
|
+
severity = MEDIUM
|
|
23
|
+
|
|
24
|
+
# Confidence level
|
|
25
|
+
# LOW, MEDIUM, HIGH
|
|
26
|
+
confidence = MEDIUM
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ main ]
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
test:
|
|
12
|
+
runs-on: ${{ matrix.os }}
|
|
13
|
+
strategy:
|
|
14
|
+
fail-fast: false
|
|
15
|
+
matrix:
|
|
16
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
17
|
+
# the project declares requires-python = ">=3.11"; 3.10 was inherited from another
|
|
18
|
+
# project's config and could never have resolved
|
|
19
|
+
python-version: ['3.11', '3.12', '3.13']
|
|
20
|
+
|
|
21
|
+
steps:
|
|
22
|
+
- name: Checkout code
|
|
23
|
+
uses: actions/checkout@v4
|
|
24
|
+
|
|
25
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
26
|
+
uses: actions/setup-python@v5
|
|
27
|
+
with:
|
|
28
|
+
python-version: ${{ matrix.python-version }}
|
|
29
|
+
|
|
30
|
+
- name: Install UV
|
|
31
|
+
uses: astral-sh/setup-uv@v3
|
|
32
|
+
with:
|
|
33
|
+
enable-cache: true
|
|
34
|
+
|
|
35
|
+
- name: Install dependencies
|
|
36
|
+
run: |
|
|
37
|
+
uv sync --group dev --group integration
|
|
38
|
+
uv run python -c "import llmtivo; print('LLMTivo installed successfully')"
|
|
39
|
+
|
|
40
|
+
- name: Check formatting and types
|
|
41
|
+
shell: bash
|
|
42
|
+
run: |
|
|
43
|
+
uv run ruff format --check src/ tests/
|
|
44
|
+
uv run ruff check src/ tests/
|
|
45
|
+
uv run mypy --strict src/llmtivo/
|
|
46
|
+
|
|
47
|
+
# `pytest --cov` UNDER-REPORTS this project specifically: llmtivo registers a pytest11 plugin,
|
|
48
|
+
# so the package is imported while pytest loads plugins — before pytest-cov starts tracing.
|
|
49
|
+
# Every line executed at import time is then invisible (it measured 59% against a real 93%).
|
|
50
|
+
# Running coverage as the OUTER process starts tracing first and measures the truth.
|
|
51
|
+
# The integration tests run HERE, in replay, with no keys and no network: they are committed
|
|
52
|
+
# tapes of real OpenAI / Anthropic / Gemini / DeepSeek / Tavily / MCP traffic. Excluding them
|
|
53
|
+
# from CI is how a suite stops noticing that a provider changed a response shape.
|
|
54
|
+
- name: Run the suite under coverage
|
|
55
|
+
shell: bash
|
|
56
|
+
env:
|
|
57
|
+
ANTHROPIC_API_KEY: ""
|
|
58
|
+
OPENAI_API_KEY: ""
|
|
59
|
+
run: |
|
|
60
|
+
uv run coverage run --source=src/llmtivo -m pytest tests/ -v
|
|
61
|
+
uv run coverage xml
|
|
62
|
+
uv run coverage report --fail-under=85
|
|
63
|
+
|
|
64
|
+
- name: Upload coverage reports to Codecov
|
|
65
|
+
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'
|
|
66
|
+
uses: codecov/codecov-action@v5
|
|
67
|
+
with:
|
|
68
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
|
69
|
+
files: ./coverage.xml
|
|
70
|
+
flags: unittests
|
|
71
|
+
name: codecov-${{ matrix.os }}-py${{ matrix.python-version }}
|
|
72
|
+
fail_ci_if_error: false
|
|
73
|
+
|
|
74
|
+
# A quality gate that only prints a warning is not a gate. `--min-score` exits non-zero, so
|
|
75
|
+
# this fails the build rather than narrating the number and passing anyway.
|
|
76
|
+
- name: MFCQI quality gate
|
|
77
|
+
if: matrix.os == 'ubuntu-latest'
|
|
78
|
+
run: uv run mfcqi-py analyze src/llmtivo --skip-llm --min-score 0.75
|
|
79
|
+
|
|
80
|
+
- name: Upload test results
|
|
81
|
+
uses: actions/upload-artifact@v4
|
|
82
|
+
if: always()
|
|
83
|
+
with:
|
|
84
|
+
name: test-results-${{ matrix.os }}-py${{ matrix.python-version }}
|
|
85
|
+
path: |
|
|
86
|
+
coverage.xml
|
|
87
|
+
htmlcov/
|
|
88
|
+
|
|
89
|
+
security:
|
|
90
|
+
runs-on: ubuntu-latest
|
|
91
|
+
steps:
|
|
92
|
+
- uses: actions/checkout@v4
|
|
93
|
+
- uses: astral-sh/setup-uv@v3
|
|
94
|
+
- name: Install dependencies
|
|
95
|
+
run: uv sync --group dev
|
|
96
|
+
- name: Bandit
|
|
97
|
+
run: uv run bandit -r src/ -q
|
|
98
|
+
|
|
99
|
+
build:
|
|
100
|
+
runs-on: ubuntu-latest
|
|
101
|
+
needs: test
|
|
102
|
+
|
|
103
|
+
steps:
|
|
104
|
+
- name: Checkout code
|
|
105
|
+
uses: actions/checkout@v4
|
|
106
|
+
|
|
107
|
+
- name: Set up Python
|
|
108
|
+
uses: actions/setup-python@v5
|
|
109
|
+
with:
|
|
110
|
+
python-version: '3.12'
|
|
111
|
+
|
|
112
|
+
- name: Install build dependencies
|
|
113
|
+
run: pip install build twine
|
|
114
|
+
|
|
115
|
+
- name: Build package
|
|
116
|
+
run: python -m build
|
|
117
|
+
|
|
118
|
+
- name: Check package
|
|
119
|
+
run: python -m twine check dist/*
|
|
120
|
+
|
|
121
|
+
- name: Upload build artifacts
|
|
122
|
+
uses: actions/upload-artifact@v4
|
|
123
|
+
with:
|
|
124
|
+
name: python-package
|
|
125
|
+
path: dist/
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
name: Update LLMTivo Badge
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main, master ]
|
|
6
|
+
paths:
|
|
7
|
+
- 'src/**'
|
|
8
|
+
- 'tests/**'
|
|
9
|
+
- 'pyproject.toml'
|
|
10
|
+
pull_request:
|
|
11
|
+
branches: [ main, master ]
|
|
12
|
+
workflow_dispatch: # Allow manual trigger
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
update-badge:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
permissions:
|
|
18
|
+
contents: write # Required for git push to update badge file
|
|
19
|
+
issues: write # Required for PR comments
|
|
20
|
+
pull-requests: write
|
|
21
|
+
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v3
|
|
24
|
+
with:
|
|
25
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
26
|
+
|
|
27
|
+
- name: Set up Python
|
|
28
|
+
uses: actions/setup-python@v4
|
|
29
|
+
with:
|
|
30
|
+
python-version: '3.13'
|
|
31
|
+
|
|
32
|
+
- name: Install UV
|
|
33
|
+
uses: astral-sh/setup-uv@v3
|
|
34
|
+
with:
|
|
35
|
+
enable-cache: true
|
|
36
|
+
|
|
37
|
+
- name: Install dependencies
|
|
38
|
+
run: |
|
|
39
|
+
uv sync --frozen
|
|
40
|
+
|
|
41
|
+
- name: Calculate LLMTivo Score
|
|
42
|
+
id: llmtivo
|
|
43
|
+
env:
|
|
44
|
+
LLMTivo_DISABLE_KEYRING: "1"
|
|
45
|
+
run: |
|
|
46
|
+
# Save old score if badge exists
|
|
47
|
+
mkdir -p .github/badges
|
|
48
|
+
if [ -f .github/badges/llmtivo.json ]; then
|
|
49
|
+
OLD_SCORE=$(python -c "import json; data=json.load(open('.github/badges/llmtivo.json')); print(data['message'].split()[0])" 2>/dev/null || echo "0.0")
|
|
50
|
+
else
|
|
51
|
+
OLD_SCORE="0.0"
|
|
52
|
+
fi
|
|
53
|
+
echo "old_score=$OLD_SCORE" >> $GITHUB_OUTPUT
|
|
54
|
+
|
|
55
|
+
# Calculate new score and save to JSON
|
|
56
|
+
uv run mfcqi-py badge src/llmtivo -f json -o .github/badges/llmtivo.json
|
|
57
|
+
|
|
58
|
+
# Extract new score
|
|
59
|
+
NEW_SCORE=$(python -c "import json; data=json.load(open('.github/badges/llmtivo.json')); print(data['message'].split()[0])")
|
|
60
|
+
echo "score=$NEW_SCORE" >> $GITHUB_OUTPUT
|
|
61
|
+
|
|
62
|
+
# Calculate delta and determine arrow
|
|
63
|
+
DELTA=$(python -c "print(f'{float('$NEW_SCORE') - float('$OLD_SCORE'):.3f}')")
|
|
64
|
+
echo "delta=$DELTA" >> $GITHUB_OUTPUT
|
|
65
|
+
|
|
66
|
+
# Determine arrow and format delta
|
|
67
|
+
if (( $(echo "$DELTA > 0" | bc -l) )); then
|
|
68
|
+
echo "arrow=↑" >> $GITHUB_OUTPUT
|
|
69
|
+
echo "delta_formatted=(+$DELTA)" >> $GITHUB_OUTPUT
|
|
70
|
+
elif (( $(echo "$DELTA < 0" | bc -l) )); then
|
|
71
|
+
echo "arrow=↓" >> $GITHUB_OUTPUT
|
|
72
|
+
echo "delta_formatted=($DELTA)" >> $GITHUB_OUTPUT
|
|
73
|
+
else
|
|
74
|
+
echo "arrow=→" >> $GITHUB_OUTPUT
|
|
75
|
+
echo "delta_formatted=(±0.000)" >> $GITHUB_OUTPUT
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
# Determine if this is good enough
|
|
79
|
+
if (( $(echo "$NEW_SCORE >= 0.75" | bc -l) )); then
|
|
80
|
+
echo "status=passing" >> $GITHUB_OUTPUT
|
|
81
|
+
else
|
|
82
|
+
echo "status=failing" >> $GITHUB_OUTPUT
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
- name: Commit badge update
|
|
86
|
+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
87
|
+
run: |
|
|
88
|
+
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
|
89
|
+
git config --local user.name "github-actions[bot]"
|
|
90
|
+
git add .
|
|
91
|
+
git diff --quiet && git diff --staged --quiet || (git commit -m "chore: Update LLMTivo badge: ${{ steps.llmtivo.outputs.score }} ${{ steps.llmtivo.outputs.arrow }} ${{ steps.llmtivo.outputs.delta_formatted }}" && git push)
|
|
92
|
+
|
|
93
|
+
- name: Comment on PR
|
|
94
|
+
if: github.event_name == 'pull_request'
|
|
95
|
+
uses: actions/github-script@v6
|
|
96
|
+
with:
|
|
97
|
+
script: |
|
|
98
|
+
const score = '${{ steps.llmtivo.outputs.score }}';
|
|
99
|
+
const oldScore = '${{ steps.llmtivo.outputs.old_score }}';
|
|
100
|
+
const arrow = '${{ steps.llmtivo.outputs.arrow }}';
|
|
101
|
+
const deltaFormatted = '${{ steps.llmtivo.outputs.delta_formatted }}';
|
|
102
|
+
const status = '${{ steps.llmtivo.outputs.status }}';
|
|
103
|
+
const emoji = status === 'passing' ? '✅' : '⚠️';
|
|
104
|
+
const trendEmoji = arrow === '↑' ? '📈' : arrow === '↓' ? '📉' : '➡️';
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
await github.rest.issues.createComment({
|
|
108
|
+
issue_number: context.issue.number,
|
|
109
|
+
owner: context.repo.owner,
|
|
110
|
+
repo: context.repo.repo,
|
|
111
|
+
body: `## ${emoji} LLMTivo Score: ${score} ${arrow} ${deltaFormatted}\n\n${trendEmoji} **Previous:** ${oldScore}\n\nCode quality ${status === 'passing' ? 'meets' : 'does not meet'} the minimum threshold (0.75).`
|
|
112
|
+
});
|
|
113
|
+
} catch (error) {
|
|
114
|
+
core.warning(`Unable to post LLMTivo PR comment: ${error.message}`);
|
|
115
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: Deploy Docs Site
|
|
2
|
+
|
|
3
|
+
# Publishes the static landing page in docs/ to GitHub Pages. No build step — it's hand-crafted
|
|
4
|
+
# HTML/CSS/JS (see docs/). The install commands are version-less (pip/pipx/uv resolve the latest
|
|
5
|
+
# from PyPI), so nothing needs to be injected at deploy time.
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
branches: [main]
|
|
9
|
+
paths:
|
|
10
|
+
- "docs/**"
|
|
11
|
+
- ".github/workflows/pages.yml"
|
|
12
|
+
workflow_dispatch:
|
|
13
|
+
|
|
14
|
+
permissions:
|
|
15
|
+
contents: read
|
|
16
|
+
pages: write
|
|
17
|
+
id-token: write
|
|
18
|
+
|
|
19
|
+
# Allow one concurrent deployment; let an in-progress run finish rather than cancel it.
|
|
20
|
+
concurrency:
|
|
21
|
+
group: pages
|
|
22
|
+
cancel-in-progress: false
|
|
23
|
+
|
|
24
|
+
jobs:
|
|
25
|
+
deploy:
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
environment:
|
|
28
|
+
name: github-pages
|
|
29
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@v4
|
|
32
|
+
- uses: actions/configure-pages@v5
|
|
33
|
+
- uses: actions/upload-pages-artifact@v3
|
|
34
|
+
with:
|
|
35
|
+
path: docs
|
|
36
|
+
- id: deployment
|
|
37
|
+
uses: actions/deploy-pages@v4
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
name: Release to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
env:
|
|
8
|
+
PYTHON_VERSION: "3.12"
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
# Verify version matches release tag (fail fast)
|
|
12
|
+
verify-version:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
outputs:
|
|
15
|
+
version: ${{ steps.get-version.outputs.version }}
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Install tomli for version parsing
|
|
21
|
+
run: pip install tomli
|
|
22
|
+
|
|
23
|
+
- name: Get package version
|
|
24
|
+
id: get-version
|
|
25
|
+
run: |
|
|
26
|
+
VERSION=$(python -c "
|
|
27
|
+
try:
|
|
28
|
+
import tomllib
|
|
29
|
+
except ModuleNotFoundError:
|
|
30
|
+
import tomli as tomllib
|
|
31
|
+
print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])
|
|
32
|
+
")
|
|
33
|
+
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
34
|
+
echo "Package version: $VERSION"
|
|
35
|
+
|
|
36
|
+
- name: Verify version matches release tag
|
|
37
|
+
if: github.event_name == 'release'
|
|
38
|
+
env:
|
|
39
|
+
VERSION: ${{ steps.get-version.outputs.version }}
|
|
40
|
+
run: |
|
|
41
|
+
RELEASE_TAG=${GITHUB_REF#refs/tags/}
|
|
42
|
+
RELEASE_VERSION=${RELEASE_TAG#v}
|
|
43
|
+
|
|
44
|
+
echo "Package version: $VERSION"
|
|
45
|
+
echo "Release tag: $RELEASE_TAG"
|
|
46
|
+
echo "Release version: $RELEASE_VERSION"
|
|
47
|
+
|
|
48
|
+
if [ "$VERSION" != "$RELEASE_VERSION" ]; then
|
|
49
|
+
echo "❌ Error: Package version ($VERSION) does not match release version ($RELEASE_VERSION)"
|
|
50
|
+
echo "Please update the version in pyproject.toml to match the release tag"
|
|
51
|
+
exit 1
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
echo "✅ Version verification passed"
|
|
55
|
+
|
|
56
|
+
# Test the release on Test PyPI
|
|
57
|
+
test-release:
|
|
58
|
+
needs: verify-version
|
|
59
|
+
uses: ./.github/workflows/test-release.yml
|
|
60
|
+
permissions: write-all
|
|
61
|
+
secrets: inherit
|
|
62
|
+
|
|
63
|
+
# Run comprehensive pre-release validation
|
|
64
|
+
pre-release-checks:
|
|
65
|
+
needs:
|
|
66
|
+
- verify-version
|
|
67
|
+
- test-release
|
|
68
|
+
runs-on: ubuntu-latest
|
|
69
|
+
strategy:
|
|
70
|
+
fail-fast: false
|
|
71
|
+
matrix:
|
|
72
|
+
python-version: ['3.11', '3.12', '3.13']
|
|
73
|
+
os: [ubuntu-latest, macos-latest]
|
|
74
|
+
|
|
75
|
+
steps:
|
|
76
|
+
- uses: actions/checkout@v4
|
|
77
|
+
|
|
78
|
+
# No caching - we want to catch any dependency issues
|
|
79
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
80
|
+
uses: actions/setup-python@v5
|
|
81
|
+
with:
|
|
82
|
+
python-version: ${{ matrix.python-version }}
|
|
83
|
+
|
|
84
|
+
- name: Install UV
|
|
85
|
+
uses: astral-sh/setup-uv@v3
|
|
86
|
+
|
|
87
|
+
- name: Upgrade setuptools with UV
|
|
88
|
+
run: |
|
|
89
|
+
uv pip install --system "setuptools>=40.8.0" wheel
|
|
90
|
+
|
|
91
|
+
- name: Install from Test PyPI
|
|
92
|
+
env:
|
|
93
|
+
VERSION: ${{ needs.test-release.outputs.version }}
|
|
94
|
+
run: |
|
|
95
|
+
uv pip install --system \
|
|
96
|
+
--index-strategy unsafe-best-match \
|
|
97
|
+
--extra-index-url https://test.pypi.org/simple/ \
|
|
98
|
+
"llmtivo==$VERSION" || \
|
|
99
|
+
( \
|
|
100
|
+
sleep 5 && \
|
|
101
|
+
uv pip install --system \
|
|
102
|
+
--index-strategy unsafe-best-match \
|
|
103
|
+
--extra-index-url https://test.pypi.org/simple/ \
|
|
104
|
+
"llmtivo==$VERSION" \
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
- name: Install test dependencies
|
|
108
|
+
run: uv sync --group dev --group integration
|
|
109
|
+
|
|
110
|
+
- name: Reinstall package from Test PyPI
|
|
111
|
+
env:
|
|
112
|
+
VERSION: ${{ needs.test-release.outputs.version }}
|
|
113
|
+
run: |
|
|
114
|
+
uv pip install --system \
|
|
115
|
+
--extra-index-url https://test.pypi.org/simple/ \
|
|
116
|
+
--force-reinstall --no-deps \
|
|
117
|
+
"llmtivo==$VERSION"
|
|
118
|
+
|
|
119
|
+
- name: Run full test suite
|
|
120
|
+
env:
|
|
121
|
+
ANTHROPIC_API_KEY: ""
|
|
122
|
+
OPENAI_API_KEY: ""
|
|
123
|
+
run: |
|
|
124
|
+
uv run pytest tests/ -v --tb=short
|
|
125
|
+
echo "✅ All tests passed on Python ${{ matrix.python-version }}"
|
|
126
|
+
|
|
127
|
+
- name: Run type checking
|
|
128
|
+
run: |
|
|
129
|
+
uv run mypy --strict src/llmtivo/
|
|
130
|
+
echo "✅ Type checking passed"
|
|
131
|
+
|
|
132
|
+
- name: Run linting
|
|
133
|
+
run: |
|
|
134
|
+
uv run ruff check src/
|
|
135
|
+
echo "✅ Linting passed"
|
|
136
|
+
|
|
137
|
+
# `--min-score` exits non-zero. The previous version printed a warning and released anyway,
|
|
138
|
+
# which is a gate that cannot fail — worse than none, because the log says it was checked.
|
|
139
|
+
- name: MFCQI quality gate
|
|
140
|
+
run: uv run mfcqi-py analyze src/llmtivo --skip-llm --min-score 0.75
|
|
141
|
+
|
|
142
|
+
# Get minimum dependency versions and test with them
|
|
143
|
+
test-min-versions:
|
|
144
|
+
needs:
|
|
145
|
+
- verify-version
|
|
146
|
+
- test-release
|
|
147
|
+
runs-on: ubuntu-latest
|
|
148
|
+
|
|
149
|
+
steps:
|
|
150
|
+
- uses: actions/checkout@v4
|
|
151
|
+
|
|
152
|
+
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
|
153
|
+
uses: actions/setup-python@v5
|
|
154
|
+
with:
|
|
155
|
+
python-version: ${{ env.PYTHON_VERSION }}
|
|
156
|
+
|
|
157
|
+
- name: Install UV
|
|
158
|
+
uses: astral-sh/setup-uv@v3
|
|
159
|
+
|
|
160
|
+
- name: Upgrade setuptools with UV
|
|
161
|
+
run: |
|
|
162
|
+
uv pip install --system "setuptools>=40.8.0" wheel
|
|
163
|
+
|
|
164
|
+
- name: Install from Test PyPI
|
|
165
|
+
env:
|
|
166
|
+
VERSION: ${{ needs.test-release.outputs.version }}
|
|
167
|
+
run: |
|
|
168
|
+
uv pip install --system \
|
|
169
|
+
--index-strategy unsafe-best-match \
|
|
170
|
+
--extra-index-url https://test.pypi.org/simple/ \
|
|
171
|
+
"llmtivo==$VERSION"
|
|
172
|
+
|
|
173
|
+
- name: Install tomli for pyproject parsing
|
|
174
|
+
run: uv pip install --system tomli
|
|
175
|
+
|
|
176
|
+
- name: Get minimum versions from pyproject.toml
|
|
177
|
+
id: min-versions
|
|
178
|
+
run: |
|
|
179
|
+
# Extract minimum versions from dependencies
|
|
180
|
+
python -c "
|
|
181
|
+
try:
|
|
182
|
+
import tomllib
|
|
183
|
+
except ModuleNotFoundError:
|
|
184
|
+
import tomli as tomllib
|
|
185
|
+
import re
|
|
186
|
+
|
|
187
|
+
with open('pyproject.toml', 'rb') as f:
|
|
188
|
+
data = tomllib.load(f)
|
|
189
|
+
|
|
190
|
+
deps = data['project']['dependencies']
|
|
191
|
+
min_versions = []
|
|
192
|
+
|
|
193
|
+
for dep in deps:
|
|
194
|
+
# Parse 'package>=1.2.3' -> 'package==1.2.3'
|
|
195
|
+
match = re.match(r'([a-zA-Z0-9_-]+)>=([0-9.]+)', dep)
|
|
196
|
+
if match:
|
|
197
|
+
pkg, ver = match.groups()
|
|
198
|
+
# Skip some packages that have complex version constraints
|
|
199
|
+
if pkg not in ['networkx', 'scipy']:
|
|
200
|
+
min_versions.append(f'{pkg}=={ver}')
|
|
201
|
+
|
|
202
|
+
print(' '.join(min_versions))
|
|
203
|
+
" > /tmp/min_versions.txt
|
|
204
|
+
|
|
205
|
+
MIN_VERSIONS=$(cat /tmp/min_versions.txt)
|
|
206
|
+
echo "min-versions=$MIN_VERSIONS" >> $GITHUB_OUTPUT
|
|
207
|
+
echo "Minimum versions: $MIN_VERSIONS"
|
|
208
|
+
|
|
209
|
+
- name: Install test dependencies
|
|
210
|
+
run: uv sync --group dev --group integration
|
|
211
|
+
|
|
212
|
+
- name: Install minimum dependency versions
|
|
213
|
+
if: steps.min-versions.outputs.min-versions != ''
|
|
214
|
+
env:
|
|
215
|
+
MIN_VERSIONS: ${{ steps.min-versions.outputs.min-versions }}
|
|
216
|
+
run: |
|
|
217
|
+
if [ -n "$MIN_VERSIONS" ]; then
|
|
218
|
+
uv pip install --system $MIN_VERSIONS
|
|
219
|
+
echo "✅ Installed minimum versions"
|
|
220
|
+
fi
|
|
221
|
+
|
|
222
|
+
- name: Run tests with minimum versions
|
|
223
|
+
if: steps.min-versions.outputs.min-versions != ''
|
|
224
|
+
env:
|
|
225
|
+
ANTHROPIC_API_KEY: ""
|
|
226
|
+
OPENAI_API_KEY: ""
|
|
227
|
+
run: |
|
|
228
|
+
uv run pytest tests/ -v --tb=short
|
|
229
|
+
echo "✅ Tests passed with minimum dependency versions"
|
|
230
|
+
|
|
231
|
+
# Only publish to production PyPI after all validations pass
|
|
232
|
+
publish-pypi:
|
|
233
|
+
needs:
|
|
234
|
+
- verify-version
|
|
235
|
+
- test-release
|
|
236
|
+
- pre-release-checks
|
|
237
|
+
- test-min-versions
|
|
238
|
+
runs-on: ubuntu-latest
|
|
239
|
+
environment: release
|
|
240
|
+
|
|
241
|
+
steps:
|
|
242
|
+
- uses: actions/checkout@v4
|
|
243
|
+
|
|
244
|
+
- name: Download build artifacts
|
|
245
|
+
uses: actions/download-artifact@v4
|
|
246
|
+
with:
|
|
247
|
+
name: test-dist
|
|
248
|
+
path: dist/
|
|
249
|
+
|
|
250
|
+
- name: Publish to production PyPI
|
|
251
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
252
|
+
with:
|
|
253
|
+
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
254
|
+
verbose: true
|
|
255
|
+
print-hash: true
|
|
256
|
+
attestations: false
|
|
257
|
+
|
|
258
|
+
# Create or update GitHub release
|
|
259
|
+
mark-release:
|
|
260
|
+
needs:
|
|
261
|
+
- verify-version
|
|
262
|
+
- test-release
|
|
263
|
+
- pre-release-checks
|
|
264
|
+
- test-min-versions
|
|
265
|
+
- publish-pypi
|
|
266
|
+
runs-on: ubuntu-latest
|
|
267
|
+
permissions:
|
|
268
|
+
contents: write
|
|
269
|
+
|
|
270
|
+
steps:
|
|
271
|
+
- uses: actions/checkout@v4
|
|
272
|
+
|
|
273
|
+
- name: Download build artifacts
|
|
274
|
+
uses: actions/download-artifact@v4
|
|
275
|
+
with:
|
|
276
|
+
name: test-dist
|
|
277
|
+
path: dist/
|
|
278
|
+
|
|
279
|
+
- name: Update release with PyPI links
|
|
280
|
+
uses: actions/github-script@v7
|
|
281
|
+
env:
|
|
282
|
+
VERSION: ${{ needs.verify-version.outputs.version }}
|
|
283
|
+
with:
|
|
284
|
+
script: |
|
|
285
|
+
const version = process.env.VERSION;
|
|
286
|
+
const packageName = 'llmtivo';
|
|
287
|
+
|
|
288
|
+
// Get the release (either from the release event or find by tag)
|
|
289
|
+
let release;
|
|
290
|
+
if (context.eventName === 'release') {
|
|
291
|
+
release = context.payload.release;
|
|
292
|
+
} else {
|
|
293
|
+
// For workflow_dispatch, find the latest release
|
|
294
|
+
const { data: releases } = await github.rest.repos.listReleases({
|
|
295
|
+
owner: context.repo.owner,
|
|
296
|
+
repo: context.repo.repo,
|
|
297
|
+
per_page: 1
|
|
298
|
+
});
|
|
299
|
+
release = releases[0];
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (!release) {
|
|
303
|
+
console.log('No release found, skipping update');
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const pypiLinks = `
|
|
308
|
+
|
|
309
|
+
## 📦 PyPI Release
|
|
310
|
+
|
|
311
|
+
This release is now available on PyPI:
|
|
312
|
+
|
|
313
|
+
- **PyPI Package**: https://pypi.org/project/${packageName}/${version}/
|
|
314
|
+
- **Installation**: \`pip install ${packageName}==${version}\`
|
|
315
|
+
- **Upgrade**: \`pip install --upgrade ${packageName}\`
|
|
316
|
+
|
|
317
|
+
### Quick Start
|
|
318
|
+
|
|
319
|
+
\`\`\`bash
|
|
320
|
+
pip install ${packageName}[pytest]
|
|
321
|
+
pytest # replay — CI-safe, costs nothing
|
|
322
|
+
pytest --llmtivo=record-new # record only the tests with no tape yet
|
|
323
|
+
\`\`\`
|
|
324
|
+
|
|
325
|
+
### What's New in v${version}
|
|
326
|
+
|
|
327
|
+
See the full changelog above for details.
|
|
328
|
+
`;
|
|
329
|
+
|
|
330
|
+
await github.rest.repos.updateRelease({
|
|
331
|
+
owner: context.repo.owner,
|
|
332
|
+
repo: context.repo.repo,
|
|
333
|
+
release_id: release.id,
|
|
334
|
+
body: (release.body || '') + pypiLinks
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
console.log('✅ Updated release with PyPI links');
|