Migrate Keboola Component to uv Build System
You are an expert at migrating Keboola Python packages from legacy setup.py
- pip to modern pyproject.toml
- uv build system. You understand PEP 517/518/639 standards, GitHub Actions workflows, and Keboola's established migration patterns.
When to Use This Skill
Use this skill when:
-
Migrating a Keboola Python package from setup.py to pyproject.toml
-
Modernizing build system to use uv instead of pip
-
Adding deterministic dependency management with uv.lock
-
Updating CI/CD workflows to use uv
-
Following Keboola's python-http-client and python-component patterns
Prerequisites Check
Before starting, verify:
-
Repository uses setup.py and/or requirements.txt
-
Package has test suite with pytest
-
Git repository with clean working tree
-
Access to GitHub secrets configuration
-
PyPI and Test PyPI accounts available
Migration Philosophy
Flexible Commit Strategy
Guideline (not dogma): Use 3 logical commits
-
Linting baseline - Fix linting first to avoid noise in migration diffs
-
Package metadata - Migrate to pyproject.toml
-
CI/CD workflows - Update to uv + generate lock file
Why this works:
-
Clean diffs: Linting fixes don't pollute actual migration changes
-
Reviewable: Each commit has clear, focused purpose
-
Flexible: Could be 2 commits (combine lint+pyproject) or 4 (separate workflows)
Key principle: Logical, reviewable chunks that make sense independently
Lint-First Principle
Always fix linting BEFORE migrating metadata:
-
Establishes clean baseline
-
Reveals code quality issues early
-
Prevents attribution confusion (linting vs migration changes)
-
Makes review significantly easier
Version Strategy
Testing phase: Use next minor version
- Example: Current 1.6.13 → Test as 1.7.0, 1.7.1, 1.7.2
Production release: Use following minor version
- Example: After testing 1.7.x → Release 1.8.0
Flexibility: Could use 1.7.0a1, 1.7.0a2 instead - pattern matters, not exact format
Step-by-Step Migration Guide
Phase 1: Analysis
- Check current state:
What's in setup.py?
cat setup.py
What's in requirements.txt?
cat requirements.txt
What's the current version?
Check PyPI or setup.py
- Identify Python version support:
From setup.py python_requires
Determine: min_version = max(3.8, current_requires_python)
- Check for docs generation:
Does push_main.yml exist with pdoc?
cat .github/workflows/push_main.yml 2>/dev/null | grep pdoc
Phase 2: Commit 1 - Linting Baseline
Purpose: Establish clean linting baseline
Steps:
- Rename and update flake8 config:
Rename to standard name
mv flake8.cfg .flake8 # if it exists
Use cookiecutter template standard:
cat > .flake8 << 'EOF' [flake8] exclude = pycache, .git, .venv, venv, docs ignore = E203,W503 max-line-length = 120 EOF
- Run flake8 and fix ALL errors:
Install flake8
uv add --dev flake8
Run and fix errors
flake8 src/ tests/
Common fixes:
-
F403/F405: Replace star imports with explicit imports
-
F841: Remove unused variables
-
E501: Break long lines
-
E231: Add missing whitespace
-
E123: Fix bracket indentation
-
CRLF→LF: Normalize line endings (expected, good cleanup)
-
Commit:
git add .flake8 src/ tests/ git commit -m "flake8 config consistent with cookiecutter template 🍪"
Phase 3: Commit 2 - Package Metadata
Purpose: Migrate to modern pyproject.toml
Steps:
- Create pyproject.toml (see templates/pyproject.toml.template ):
Key points:
-
version = "0.0.0"
-
replaced by git tags in CI
-
Extract dependencies from setup.py install_requires
-
Extract dev dependencies from setup.py setup_requires and tests_require
-
Add pdoc3 to dev if docs workflow exists
-
requires-python = ">=MIN_VERSION" (≥3.8)
-
Remove License :: OSI Approved :: MIT License classifier (PEP 639)
-
Keep license = "MIT" field
-
Add TestPyPI index configuration
-
Delete old files:
git rm setup.py requirements.txt
- Update LICENSE copyright year:
Update to current year (2026)
sed -i 's/Copyright (c) 20[0-9][0-9]/Copyright (c) 2026/' LICENSE
- Commit:
git add pyproject.toml LICENSE git commit -m "migrate package configuration to pyproject.toml 📦"
Phase 4: Commit 3 - uv Workflows
Purpose: Update CI/CD to use uv
Steps:
-
Update all 3-4 workflows (see references/workflow-templates.md ):
-
push_dev.yml
-
Testing on dev branches
-
deploy.yml
-
Production PyPI deployment
-
deploy_to_test.yml
-
Test PyPI deployment
-
push_main.yml
-
Docs generation (OPTIONAL - only if docs exist)
Key changes per workflow:
-
Update action versions: @v4 → @v5 , @v6
-
Add uv installation: uses: astral-sh/setup-uv@v6
-
Add ruff action: uses: astral-sh/ruff-action@v3 (blocking, no continue-on-error)
-
Change: pip install → uv sync --all-groups --frozen
-
Change: flake8 --config=... → uv run flake8
-
Change: pytest tests → uv run pytest tests
-
Add version replacement: uv version $TAG_VERSION
-
Update secrets: UV_PUBLISH_TOKEN , UV_PUBLISH_TOKEN_TEST_PYPI
-
Python matrix: [MIN_VERSION, "3.13", "3.14"] (min + 2 latest)
-
Generate uv.lock:
uv sync --all-groups
- Verify build works:
uv build
Should create dist/.tar.gz and dist/.whl
uv version 1.7.0 --dry-run
Should show: package-name 0.0.0 => 1.7.0
- Commit:
git add .github/workflows/*.yml uv.lock git commit -m "uv 💜"
Phase 5: Testing on Test PyPI
- Push branch:
git push origin BRANCH_NAME
- Create test tag:
git tag 1.7.0 git push origin 1.7.0
Manually trigger Test PyPI workflow:
-
Go to GitHub Actions → "Build & Upload Python Package To Test PyPI"
-
Click "Run workflow"
-
Select branch or tag
-
Click "Run workflow"
Verify on Test PyPI:
-
Verify version appears
Test installation:
cd /tmp && mkdir test_install && cd test_install
uv init
uv add --index-url https://test.pypi.org/simple/
--extra-index-url https://pypi.org/simple/
--index-strategy unsafe-best-match
PACKAGE_NAME==1.7.0
uv run python -c "import PACKAGE; print('✅ Works!')"
cd .. && rm -rf test_install
Phase 6: Production Release
Create PR (see templates/pr-description.md.template )
Get approval and merge to main
Create production release:
-
Tag: 1.8.0
-
Target: main
-
Title: 1.8.0
-
Description: Migration summary
-
Click "Publish release"
Workflow auto-triggers → Publishes to PyPI
Verify production:
cd /tmp && mkdir test_prod && cd test_prod uv init uv add PACKAGE_NAME==1.8.0 uv run python -c "import PACKAGE; print('✅ Production works!')" cd .. && rm -rf test_prod
Python Matrix Strategy
Smart matrix logic:
python-version: [ "MIN_SUPPORTED", # max(3.8, current_requires_python) "3.13", # Second-latest stable "3.14" # Latest stable ]
Examples:
-
Package supports ≥3.8 → ["3.8", "3.13", "3.14"]
-
Package supports ≥3.10 → ["3.10", "3.13", "3.14"]
-
Package supports ≥3.12 → ["3.12", "3.13", "3.14"]
Rationale: Test minimum (compatibility floor) + 2 latest (future-proofing)
Docs Workflow Handling
Detection:
Check if push_main.yml exists AND contains pdoc
if [ -f .github/workflows/push_main.yml ] && grep -q pdoc .github/workflows/push_main.yml; then # Include docs workflow in migration # Add pdoc3 to [dependency-groups] dev fi
Migration for docs:
-
Old: pip install --user pdoc3 (ad-hoc)
-
New: Add pdoc3 to [dependency-groups] dev
- use uv run pdoc
- Improvement: Tracked dependency instead of ad-hoc install
Secret Configuration
Required GitHub secrets:
UV_PUBLISH_TOKEN - Production PyPI token
-
Create at: https://pypi.org/manage/account/token/
-
Scope: Entire account or specific project
-
Add at: GitHub repo → Settings → Secrets → UV_PUBLISH_TOKEN
UV_PUBLISH_TOKEN_TEST_PYPI - Test PyPI token
-
Create at: https://test.pypi.org/manage/account/token/
-
Scope: Entire account
-
Add at: GitHub repo → Settings → Secrets → UV_PUBLISH_TOKEN_TEST_PYPI
Note: uv automatically uses token as username when UV_PUBLISH_TOKEN env var is set
Workflow Trigger Strategy
Test PyPI: on: workflow_dispatch (manual)
-
Allows testing any version without tag naming constraints
-
Full control over when to test
Production PyPI: on: push: tags: ['*'] (automatic)
-
Triggers automatically when tag pushed to main
-
Simpler workflow: git tag X.Y.Z && git push origin X.Y.Z
Common Issues & Solutions
See references/troubleshooting.md for detailed troubleshooting guide.
Quick fixes:
License classifier conflict
-
Error: License classifiers have been superseded
-
Fix: Remove License :: OSI Approved :: MIT License from classifiers
CRLF line endings
-
Symptom: Huge diffs in unchanged files
-
Status: Expected and correct (CRLF → LF normalization)
Test PyPI installation fails
-
Error: No solution found
-
Fix: Add --index-strategy unsafe-best-match
uv version command not found
- Fix: Ensure uv ≥ 0.5.0
Modern Tooling Requirements
100% modern uv - ZERO pip mentions:
✅ CORRECT:
-
uv add PACKAGE
-
Add production dependency
-
uv add --dev PACKAGE
-
Add dev dependency
-
uv sync --all-groups --frozen
-
Install from lock
-
uv run COMMAND
-
Run command in environment
-
uv build
-
Build package
-
uv publish
-
Publish to PyPI
❌ NEVER USE:
-
pip install
-
OLD
-
pip
-
OLD
-
uv pip install
-
WRONG uv usage
References
-
references/migration-guide.md
-
Complete step-by-step guide
-
references/workflow-templates.md
-
All 4 workflow YAML files
-
references/troubleshooting.md
-
Common issues and solutions
-
references/examples.md
-
python-http-client and python-component migrations
-
templates/pyproject.toml.template
-
Template pyproject.toml
-
templates/flake8.template
-
Template .flake8
-
templates/pr-description.md.template
-
PR description template
Questions to Ask User
Before starting:
-
What's the current latest version on PyPI?
-
Do you have access to configure GitHub secrets?
-
What Python versions should we support? (check setup.py)
-
Does the package generate HTML docs with pdoc?
-
Any custom build requirements or special dependencies?
Success Criteria
Migration complete when:
-
✅ All tests pass with uv locally
-
✅ Package builds successfully (uv build )
-
✅ Test PyPI release installable and functional
-
✅ Production PyPI release installable and functional
-
✅ CI/CD workflows green on main branch
-
✅ Documentation updated (if needed)
-
✅ Team notified
Remember: This is a build system migration, NOT an API change. End users should see no difference except faster installs and more reliable dependency resolution.