langmigrate 1.0.0__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.
- langmigrate-1.0.0/.github/workflows/ci.yml +75 -0
- langmigrate-1.0.0/.github/workflows/docs.yml +57 -0
- langmigrate-1.0.0/.github/workflows/publish.yml +100 -0
- langmigrate-1.0.0/.gitignore +36 -0
- langmigrate-1.0.0/.pre-commit-config.yaml +24 -0
- langmigrate-1.0.0/CHANGELOG.md +80 -0
- langmigrate-1.0.0/CLAUDE.md +86 -0
- langmigrate-1.0.0/CODE_OF_CONDUCT.md +65 -0
- langmigrate-1.0.0/CONTRIBUTING.md +99 -0
- langmigrate-1.0.0/LICENSE +21 -0
- langmigrate-1.0.0/PKG-INFO +222 -0
- langmigrate-1.0.0/README.md +174 -0
- langmigrate-1.0.0/SECURITY.md +35 -0
- langmigrate-1.0.0/docker-compose.yml +31 -0
- langmigrate-1.0.0/docs/Gemfile +4 -0
- langmigrate-1.0.0/docs/INTEGRATION.md +209 -0
- langmigrate-1.0.0/docs/_config.yml +41 -0
- langmigrate-1.0.0/docs/cookbook/index.md +637 -0
- langmigrate-1.0.0/docs/index.md +63 -0
- langmigrate-1.0.0/examples/README.md +36 -0
- langmigrate-1.0.0/examples/batch_migration/README.md +67 -0
- langmigrate-1.0.0/examples/batch_migration/demo.py +224 -0
- langmigrate-1.0.0/examples/batch_migration/migrations/a1c0_add_priority.py +23 -0
- langmigrate-1.0.0/examples/batch_migration/migrations/b2d1_normalise_status.py +32 -0
- langmigrate-1.0.0/examples/deep_research_agent/README.md +58 -0
- langmigrate-1.0.0/examples/deep_research_agent/demo.py +243 -0
- langmigrate-1.0.0/examples/deep_research_agent/migrations/a1c0_add_depth.py +23 -0
- langmigrate-1.0.0/examples/deep_research_agent/migrations/b2d1_add_findings.py +40 -0
- langmigrate-1.0.0/examples/deep_research_agent/migrations/c3e2_drop_debug.py +27 -0
- langmigrate-1.0.0/examples/evolving_agent/README.md +42 -0
- langmigrate-1.0.0/examples/evolving_agent/demo.py +63 -0
- langmigrate-1.0.0/examples/evolving_agent/migrations/a1c0_add_context.py +22 -0
- langmigrate-1.0.0/examples/evolving_agent/migrations/b2d1_rename_msgs.py +24 -0
- langmigrate-1.0.0/examples/middleware_agent/README.md +54 -0
- langmigrate-1.0.0/examples/middleware_agent/demo.py +222 -0
- langmigrate-1.0.0/examples/middleware_agent/migrations/a1c0_add_metadata.py +21 -0
- langmigrate-1.0.0/examples/middleware_agent/migrations/b2d1_add_confidence.py +23 -0
- langmigrate-1.0.0/examples/multi_tool_agent/README.md +50 -0
- langmigrate-1.0.0/examples/multi_tool_agent/demo.py +195 -0
- langmigrate-1.0.0/examples/multi_tool_agent/migrations/a1c0_add_session.py +25 -0
- langmigrate-1.0.0/examples/multi_tool_agent/migrations/b2d1_add_tool_count.py +26 -0
- langmigrate-1.0.0/examples/multi_tool_agent/migrations/c3e2_require_query.py +28 -0
- langmigrate-1.0.0/examples/quickstart/main.py +58 -0
- langmigrate-1.0.0/examples/quickstart/migrations/__init__.py +0 -0
- langmigrate-1.0.0/examples/quickstart/migrations/a1c0_add_context.py +24 -0
- langmigrate-1.0.0/pyproject.toml +96 -0
- langmigrate-1.0.0/src/langmigrate/__init__.py +62 -0
- langmigrate-1.0.0/src/langmigrate/adapters/__init__.py +5 -0
- langmigrate-1.0.0/src/langmigrate/adapters/base.py +48 -0
- langmigrate-1.0.0/src/langmigrate/adapters/postgres.py +126 -0
- langmigrate-1.0.0/src/langmigrate/adapters/redis.py +152 -0
- langmigrate-1.0.0/src/langmigrate/cli/__init__.py +1 -0
- langmigrate-1.0.0/src/langmigrate/cli/main.py +372 -0
- langmigrate-1.0.0/src/langmigrate/cli/templates/revision.py.tmpl +25 -0
- langmigrate-1.0.0/src/langmigrate/cli/templates/revision_auto.py.tmpl +30 -0
- langmigrate-1.0.0/src/langmigrate/config.py +68 -0
- langmigrate-1.0.0/src/langmigrate/core/__init__.py +1 -0
- langmigrate-1.0.0/src/langmigrate/core/engine.py +80 -0
- langmigrate-1.0.0/src/langmigrate/core/exceptions.py +143 -0
- langmigrate-1.0.0/src/langmigrate/core/migration.py +241 -0
- langmigrate-1.0.0/src/langmigrate/core/operations.py +123 -0
- langmigrate-1.0.0/src/langmigrate/core/registry.py +236 -0
- langmigrate-1.0.0/src/langmigrate/core/schema.py +180 -0
- langmigrate-1.0.0/src/langmigrate/core/topology.py +62 -0
- langmigrate-1.0.0/src/langmigrate/core/types.py +152 -0
- langmigrate-1.0.0/src/langmigrate/core/version.py +61 -0
- langmigrate-1.0.0/src/langmigrate/integrations/__init__.py +13 -0
- langmigrate-1.0.0/src/langmigrate/integrations/langchain.py +119 -0
- langmigrate-1.0.0/src/langmigrate/integrations/state.py +82 -0
- langmigrate-1.0.0/src/langmigrate/py.typed +2 -0
- langmigrate-1.0.0/src/langmigrate/runtime/__init__.py +1 -0
- langmigrate-1.0.0/src/langmigrate/runtime/batch.py +103 -0
- langmigrate-1.0.0/src/langmigrate/runtime/factory.py +53 -0
- langmigrate-1.0.0/src/langmigrate/runtime/interceptor.py +164 -0
- langmigrate-1.0.0/src/langmigrate/runtime/persistence.py +101 -0
- langmigrate-1.0.0/tests/__init__.py +0 -0
- langmigrate-1.0.0/tests/conftest.py +20 -0
- langmigrate-1.0.0/tests/integration/__init__.py +0 -0
- langmigrate-1.0.0/tests/integration/test_postgres_e2e.py +282 -0
- langmigrate-1.0.0/tests/integration/test_redis_e2e.py +221 -0
- langmigrate-1.0.0/tests/unit/__init__.py +0 -0
- langmigrate-1.0.0/tests/unit/test_batch.py +199 -0
- langmigrate-1.0.0/tests/unit/test_before_after_langmigrate.py +275 -0
- langmigrate-1.0.0/tests/unit/test_bugs.py +256 -0
- langmigrate-1.0.0/tests/unit/test_cli.py +167 -0
- langmigrate-1.0.0/tests/unit/test_cli_autogenerate.py +118 -0
- langmigrate-1.0.0/tests/unit/test_cli_db.py +230 -0
- langmigrate-1.0.0/tests/unit/test_config.py +37 -0
- langmigrate-1.0.0/tests/unit/test_engine.py +150 -0
- langmigrate-1.0.0/tests/unit/test_functional_migration.py +121 -0
- langmigrate-1.0.0/tests/unit/test_integrations_langchain.py +114 -0
- langmigrate-1.0.0/tests/unit/test_integrations_state.py +119 -0
- langmigrate-1.0.0/tests/unit/test_interceptor.py +275 -0
- langmigrate-1.0.0/tests/unit/test_operations.py +132 -0
- langmigrate-1.0.0/tests/unit/test_registry.py +147 -0
- langmigrate-1.0.0/tests/unit/test_schema.py +93 -0
- langmigrate-1.0.0/tests/unit/test_setup.py +93 -0
- langmigrate-1.0.0/tests/unit/test_topology.py +102 -0
- langmigrate-1.0.0/tests/unit/test_version.py +55 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
lint-and-unit:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
strategy:
|
|
11
|
+
matrix:
|
|
12
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v5
|
|
15
|
+
- name: Install uv
|
|
16
|
+
uses: astral-sh/setup-uv@v6
|
|
17
|
+
with:
|
|
18
|
+
python-version: ${{ matrix.python-version }}
|
|
19
|
+
cache-dependency-glob: "pyproject.toml"
|
|
20
|
+
- name: Sync (dev + langchain extras)
|
|
21
|
+
run: uv sync --extra dev --extra langchain
|
|
22
|
+
- name: Ruff lint
|
|
23
|
+
run: uv run ruff check src tests
|
|
24
|
+
- name: Ruff format check
|
|
25
|
+
run: uv run ruff format --check src tests
|
|
26
|
+
- name: Type check
|
|
27
|
+
run: uv run mypy src/langmigrate
|
|
28
|
+
- name: Unit tests (with coverage)
|
|
29
|
+
run: uv run pytest -m "not integration" --cov=langmigrate --cov-report=term --cov-report=xml
|
|
30
|
+
- name: Upload coverage artifact
|
|
31
|
+
if: matrix.python-version == '3.12'
|
|
32
|
+
uses: actions/upload-artifact@v4
|
|
33
|
+
with:
|
|
34
|
+
name: coverage
|
|
35
|
+
path: coverage.xml
|
|
36
|
+
|
|
37
|
+
integration:
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
services:
|
|
40
|
+
postgres:
|
|
41
|
+
image: postgres:16-alpine
|
|
42
|
+
env:
|
|
43
|
+
POSTGRES_USER: langmigrate
|
|
44
|
+
POSTGRES_PASSWORD: langmigrate
|
|
45
|
+
POSTGRES_DB: langmigrate
|
|
46
|
+
ports:
|
|
47
|
+
- 5432:5432
|
|
48
|
+
options: >-
|
|
49
|
+
--health-cmd "pg_isready -U langmigrate"
|
|
50
|
+
--health-interval 2s
|
|
51
|
+
--health-timeout 3s
|
|
52
|
+
--health-retries 15
|
|
53
|
+
redis:
|
|
54
|
+
image: redis/redis-stack-server:latest
|
|
55
|
+
ports:
|
|
56
|
+
- 6379:6379
|
|
57
|
+
options: >-
|
|
58
|
+
--health-cmd "redis-cli ping"
|
|
59
|
+
--health-interval 2s
|
|
60
|
+
--health-timeout 3s
|
|
61
|
+
--health-retries 15
|
|
62
|
+
env:
|
|
63
|
+
LANGMIGRATE_TEST_PG: postgresql://langmigrate:langmigrate@localhost:5432/langmigrate
|
|
64
|
+
LANGMIGRATE_TEST_REDIS: redis://localhost:6379
|
|
65
|
+
steps:
|
|
66
|
+
- uses: actions/checkout@v5
|
|
67
|
+
- name: Install uv
|
|
68
|
+
uses: astral-sh/setup-uv@v6
|
|
69
|
+
with:
|
|
70
|
+
python-version: "3.12"
|
|
71
|
+
cache-dependency-glob: "pyproject.toml"
|
|
72
|
+
- name: Sync (all extras)
|
|
73
|
+
run: uv sync --extra dev --extra postgres --extra redis --extra langchain
|
|
74
|
+
- name: Integration tests
|
|
75
|
+
run: uv run pytest -m integration
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
name: Deploy docs to GitHub Pages
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
paths:
|
|
7
|
+
- "docs/**"
|
|
8
|
+
- ".github/workflows/docs.yml"
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
pages: write
|
|
14
|
+
id-token: write
|
|
15
|
+
|
|
16
|
+
concurrency:
|
|
17
|
+
group: pages
|
|
18
|
+
cancel-in-progress: false
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
build:
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
defaults:
|
|
24
|
+
run:
|
|
25
|
+
working-directory: docs
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v5
|
|
28
|
+
|
|
29
|
+
- uses: actions/configure-pages@v5
|
|
30
|
+
with:
|
|
31
|
+
enablement: true
|
|
32
|
+
|
|
33
|
+
- name: Setup Ruby
|
|
34
|
+
uses: ruby/setup-ruby@v1
|
|
35
|
+
with:
|
|
36
|
+
ruby-version: "3.3"
|
|
37
|
+
bundler-cache: true
|
|
38
|
+
working-directory: ${{ github.workspace }}/docs
|
|
39
|
+
|
|
40
|
+
- name: Build with Jekyll
|
|
41
|
+
run: bundle exec jekyll build
|
|
42
|
+
env:
|
|
43
|
+
JEKYLL_ENV: production
|
|
44
|
+
|
|
45
|
+
- uses: actions/upload-pages-artifact@v3
|
|
46
|
+
with:
|
|
47
|
+
path: docs/_site
|
|
48
|
+
|
|
49
|
+
deploy:
|
|
50
|
+
needs: build
|
|
51
|
+
runs-on: ubuntu-latest
|
|
52
|
+
environment:
|
|
53
|
+
name: github-pages
|
|
54
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
55
|
+
steps:
|
|
56
|
+
- id: deployment
|
|
57
|
+
uses: actions/deploy-pages@v4
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
# PyPI trusted publishing requires id-token: write.
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
id-token: write
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
test-before-publish:
|
|
15
|
+
name: Run full test suite before publish
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
services:
|
|
18
|
+
postgres:
|
|
19
|
+
image: postgres:16-alpine
|
|
20
|
+
env:
|
|
21
|
+
POSTGRES_USER: langmigrate
|
|
22
|
+
POSTGRES_PASSWORD: langmigrate
|
|
23
|
+
POSTGRES_DB: langmigrate
|
|
24
|
+
ports: ["5432:5432"]
|
|
25
|
+
options: >-
|
|
26
|
+
--health-cmd "pg_isready -U langmigrate"
|
|
27
|
+
--health-interval 2s --health-timeout 3s --health-retries 15
|
|
28
|
+
redis:
|
|
29
|
+
image: redis/redis-stack-server:latest
|
|
30
|
+
ports: ["6379:6379"]
|
|
31
|
+
options: >-
|
|
32
|
+
--health-cmd "redis-cli ping"
|
|
33
|
+
--health-interval 2s --health-timeout 3s --health-retries 15
|
|
34
|
+
env:
|
|
35
|
+
LANGMIGRATE_TEST_PG: postgresql://langmigrate:langmigrate@localhost:5432/langmigrate
|
|
36
|
+
LANGMIGRATE_TEST_REDIS: redis://localhost:6379
|
|
37
|
+
steps:
|
|
38
|
+
- uses: actions/checkout@v5
|
|
39
|
+
- uses: astral-sh/setup-uv@v6
|
|
40
|
+
with:
|
|
41
|
+
cache-dependency-glob: "pyproject.toml"
|
|
42
|
+
- name: Sync (all extras)
|
|
43
|
+
run: uv sync --extra dev --extra postgres --extra redis --extra langchain
|
|
44
|
+
- name: Ruff lint
|
|
45
|
+
run: uv run ruff check src tests
|
|
46
|
+
- name: Ruff format check
|
|
47
|
+
run: uv run ruff format --check src tests
|
|
48
|
+
- name: Type check
|
|
49
|
+
run: uv run mypy src/langmigrate
|
|
50
|
+
- name: Unit tests (with coverage)
|
|
51
|
+
run: uv run pytest -m "not integration" --cov=langmigrate --cov-report=term
|
|
52
|
+
- name: Integration tests
|
|
53
|
+
run: uv run pytest -m integration
|
|
54
|
+
|
|
55
|
+
build:
|
|
56
|
+
name: Build sdist and wheel
|
|
57
|
+
needs: test-before-publish
|
|
58
|
+
runs-on: ubuntu-latest
|
|
59
|
+
steps:
|
|
60
|
+
- uses: actions/checkout@v5
|
|
61
|
+
- uses: astral-sh/setup-uv@v6
|
|
62
|
+
- name: Build
|
|
63
|
+
run: uv build
|
|
64
|
+
- uses: actions/upload-artifact@v4
|
|
65
|
+
with:
|
|
66
|
+
name: dist
|
|
67
|
+
path: dist/*
|
|
68
|
+
|
|
69
|
+
publish:
|
|
70
|
+
name: Publish to PyPI via trusted publishing
|
|
71
|
+
needs: build
|
|
72
|
+
runs-on: ubuntu-latest
|
|
73
|
+
environment:
|
|
74
|
+
name: pypi
|
|
75
|
+
url: https://pypi.org/p/langmigrate
|
|
76
|
+
steps:
|
|
77
|
+
- uses: actions/download-artifact@v4
|
|
78
|
+
with:
|
|
79
|
+
name: dist
|
|
80
|
+
path: dist/
|
|
81
|
+
- name: Publish
|
|
82
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
83
|
+
|
|
84
|
+
github-release:
|
|
85
|
+
name: Create GitHub Release
|
|
86
|
+
needs: publish
|
|
87
|
+
runs-on: ubuntu-latest
|
|
88
|
+
permissions:
|
|
89
|
+
contents: write
|
|
90
|
+
steps:
|
|
91
|
+
- uses: actions/checkout@v5
|
|
92
|
+
- uses: actions/download-artifact@v4
|
|
93
|
+
with:
|
|
94
|
+
name: dist
|
|
95
|
+
path: dist/
|
|
96
|
+
- name: Create release
|
|
97
|
+
uses: softprops/action-gh-release@v2
|
|
98
|
+
with:
|
|
99
|
+
files: dist/*
|
|
100
|
+
generate_release_notes: true
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
*.egg
|
|
9
|
+
|
|
10
|
+
# Virtual env / uv
|
|
11
|
+
.venv/
|
|
12
|
+
venv/
|
|
13
|
+
uv.lock
|
|
14
|
+
|
|
15
|
+
# Test / coverage
|
|
16
|
+
.pytest_cache/
|
|
17
|
+
.coverage
|
|
18
|
+
htmlcov/
|
|
19
|
+
.ruff_cache/
|
|
20
|
+
.mypy_cache/
|
|
21
|
+
|
|
22
|
+
# OS
|
|
23
|
+
.DS_Store
|
|
24
|
+
|
|
25
|
+
# IDE
|
|
26
|
+
.idea/
|
|
27
|
+
.vscode/
|
|
28
|
+
.cursor/
|
|
29
|
+
|
|
30
|
+
# AI assistant local config
|
|
31
|
+
.claude/
|
|
32
|
+
|
|
33
|
+
# Project
|
|
34
|
+
langmigrate.toml
|
|
35
|
+
migrations/
|
|
36
|
+
!examples/**/migrations/
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
3
|
+
rev: v0.11.0
|
|
4
|
+
hooks:
|
|
5
|
+
- id: ruff
|
|
6
|
+
- id: ruff-format
|
|
7
|
+
|
|
8
|
+
- repo: https://github.com/pre-commit/mirrors-mypy
|
|
9
|
+
rev: v1.15.0
|
|
10
|
+
hooks:
|
|
11
|
+
- id: mypy
|
|
12
|
+
additional_dependencies:
|
|
13
|
+
- pydantic>=2.5
|
|
14
|
+
- typer>=0.12
|
|
15
|
+
- langgraph-checkpoint>=2.0
|
|
16
|
+
|
|
17
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
18
|
+
rev: v5.0.0
|
|
19
|
+
hooks:
|
|
20
|
+
- id: trailing-whitespace
|
|
21
|
+
- id: end-of-file-fixer
|
|
22
|
+
- id: check-yaml
|
|
23
|
+
- id: check-toml
|
|
24
|
+
- id: check-added-large-files
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`setup_langmigrate(saver, migrations)` factory.** One-liner that builds the
|
|
13
|
+
registry, engine and `MigrationInterceptor` for you. Accepts a path, a
|
|
14
|
+
`MigrationRegistry`, or a `MigrationEngine`; forwards `write_back` / `target`.
|
|
15
|
+
- **`@migration` decorator and `FunctionMigration`.** Write inline function-pair
|
|
16
|
+
migrations without subclassing `BaseMigration`; attach the reverse with
|
|
17
|
+
`.reverse`. `MigrationRegistry.from_path` now discovers both decorator-built
|
|
18
|
+
instances and `BaseMigration` subclasses.
|
|
19
|
+
- **Fluent `StateEnvelope` helpers.** `state.add_field(...)`, `.drop_field(...)`,
|
|
20
|
+
`.rename_field(...)`, `.coerce_field(...)`, `.require_field(...)` — the same
|
|
21
|
+
pure operations as methods, for the function-pair style.
|
|
22
|
+
- **`BaseMigration.is_reversible`.** Used by `langmigrate check` to flag one-way
|
|
23
|
+
migrations (works for both authoring styles).
|
|
24
|
+
- **`langmigrate init` scaffolding.** Now also writes `migrations/__init__.py`
|
|
25
|
+
and a `migrations/README.md`; `--example` drops a first revision skeleton.
|
|
26
|
+
- **Quickstart example** (`examples/quickstart/`) type-checked with `mypy --strict`.
|
|
27
|
+
- Public exports: `setup_langmigrate`, `migration`, `FunctionMigration`,
|
|
28
|
+
`new_revision_id`.
|
|
29
|
+
|
|
30
|
+
## [1.0.0] — 2026-06-05
|
|
31
|
+
|
|
32
|
+
First stable release. LangMigrate brings declarative, Alembic-style schema
|
|
33
|
+
migrations to LangGraph state persistence — checkpointers (Postgres, Redis)
|
|
34
|
+
and stores.
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- **Migration engine and registry.** Alembic-style DAG with `revision` /
|
|
39
|
+
`down_revision`, path resolution, cycle and multiple-head detection.
|
|
40
|
+
- **Pure, idempotent operations.** `add_field`, `drop_field`, `rename_field`,
|
|
41
|
+
`coerce_field`, `require_field` — Safe vs Unsafe annotated, with
|
|
42
|
+
`IrreversibleMigrationError` for genuinely one-way migrations.
|
|
43
|
+
- **Declarative migrations.** `BaseMigration` ABC with helpers that delegate
|
|
44
|
+
to the pure operations module.
|
|
45
|
+
- **Topology repair.** `NodeRemap` helper for interrupted threads on
|
|
46
|
+
deleted/renamed graph nodes, applicable within any migration.
|
|
47
|
+
- **CLI (`langmigrate`).** `init`, `revision` (with `--autogenerate --schema`
|
|
48
|
+
for state-aware scaffolding), `history`, `current`, `check`, `upgrade`,
|
|
49
|
+
`downgrade`, `stamp`.
|
|
50
|
+
- **Online migration.** `MigrationInterceptor` — drop-in
|
|
51
|
+
`BaseCheckpointSaver` wrapper. Lazy upgrade on `get_tuple`/`aget_tuple`
|
|
52
|
+
with idempotent write-back; `list`/`alist` migrate in-memory only to
|
|
53
|
+
prevent write storms. `put`/`aput` stamp the HEAD revision.
|
|
54
|
+
- **Batch migration.** `run_batch_upgrade` / `run_batch_downgrade` for
|
|
55
|
+
proactive cure of every stored checkpoint.
|
|
56
|
+
- **State-level middleware.** `SchemaMigrationMiddleware` for managed
|
|
57
|
+
platforms where the checkpointer is owned by the framework
|
|
58
|
+
(LangGraph Server, deepagents). Hooks `before_agent`, `before_model` and
|
|
59
|
+
their async counterparts.
|
|
60
|
+
- **Pure helper.** `migrate_state_update` for hand-built `StateGraph`s or
|
|
61
|
+
custom entry nodes.
|
|
62
|
+
- **Adapters.** `PostgresAdapter` (indexed `metadata->>'langmigrate_rev'`
|
|
63
|
+
filter) and `RedisAdapter` (scan-based RedisJSON enumeration).
|
|
64
|
+
- **Version tag.** Stored in `checkpoint.metadata`, never in
|
|
65
|
+
`channel_values` — queryable at the DB level and safe from pruning.
|
|
66
|
+
- **Schema autogenerate.** `revision --autogenerate --schema <module>:<class>`
|
|
67
|
+
diffs the current state schema against the last revision and scaffolds
|
|
68
|
+
the migration body.
|
|
69
|
+
- **Test suite.** 114 unit tests + 8 integration tests covering Postgres
|
|
70
|
+
and Redis end-to-end, sync and async paths.
|
|
71
|
+
|
|
72
|
+
### Compatibility
|
|
73
|
+
|
|
74
|
+
- Python 3.10+
|
|
75
|
+
- Pydantic v2
|
|
76
|
+
- `langgraph-checkpoint` ≥ 2.0
|
|
77
|
+
- Optional: `psycopg` ≥ 3.1 (Postgres), `langgraph-checkpoint-redis` (Redis),
|
|
78
|
+
`langchain` ≥ 1.0 (state-level middleware)
|
|
79
|
+
|
|
80
|
+
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# CLAUDE.md — LangMigrate
|
|
2
|
+
|
|
3
|
+
Declarative schema migrations for LangGraph state persistence (checkpointers & stores).
|
|
4
|
+
This file defines the conventions every contributor (human or AI) must follow.
|
|
5
|
+
|
|
6
|
+
## What this project is
|
|
7
|
+
|
|
8
|
+
When a LangGraph application evolves its state schema (`TypedDict` / Pydantic), old or
|
|
9
|
+
interrupted threads persisted by a checkpointer (Postgres, Redis, ...) stop deserializing
|
|
10
|
+
cleanly. LangMigrate brings the **Alembic model** to LangGraph state: declarative
|
|
11
|
+
revisions with a cascade of transformation functions, applied either **offline** (proactive
|
|
12
|
+
batch CLI) or **online** (lazy runtime interceptor).
|
|
13
|
+
|
|
14
|
+
## Architecture — Clean Architecture, strictly enforced
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
cli/ runtime/ adapters/ ──────► core/
|
|
18
|
+
(DB clients live here) (pure: no DB client imports)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Dependency rule:** `cli`, `runtime`, and `adapters` may import from `core`. `core` must
|
|
22
|
+
NEVER import a database client (`psycopg`, `redis`, ...) nor anything from `adapters` /
|
|
23
|
+
`runtime` / `cli`. Keep migration business logic independent of any backend.
|
|
24
|
+
|
|
25
|
+
- `core/` — pure logic: `types`, `exceptions`, `operations`, `migration`, `registry`,
|
|
26
|
+
`engine`, `version`, `topology`. No I/O, no DB drivers.
|
|
27
|
+
- `adapters/` — DB-specific bulk access for the batch CLI. DB client imports are confined
|
|
28
|
+
here, ideally imported lazily inside methods so the core stays importable without extras.
|
|
29
|
+
- `runtime/` — `MigrationInterceptor`, a `BaseCheckpointSaver` wrapper for lazy online
|
|
30
|
+
migration. DB-agnostic: it delegates to whatever saver it wraps.
|
|
31
|
+
- `cli/` — Typer app.
|
|
32
|
+
|
|
33
|
+
## Core design decisions (do not change without discussion)
|
|
34
|
+
|
|
35
|
+
1. **Versioning = Alembic-style DAG.** Each revision has a `revision` hash and a
|
|
36
|
+
`down_revision` pointer. The engine resolves a path through the DAG, then applies it as a
|
|
37
|
+
linear cascade.
|
|
38
|
+
2. **Version tag lives ONLY in `checkpoint.metadata`** under the key `langmigrate_rev`.
|
|
39
|
+
Never store it inside `channel_values` (it is metadata, not application state, and would
|
|
40
|
+
risk being pruned by LangGraph). It must stay queryable at the DB level.
|
|
41
|
+
3. **Lazy write-back is ON by default, disableable, and idempotent.** Re-persisting a
|
|
42
|
+
migrated checkpoint must NOT change `checkpoint["id"]` nor break the `parent_config`
|
|
43
|
+
chain. Write-back happens only on `get_tuple`/`aget_tuple` (single checkpoint on
|
|
44
|
+
resume). `list`/`alist` migrate **in memory only, never writing back** — they
|
|
45
|
+
enumerate history (many checkpoints) and healing there would be a write storm and
|
|
46
|
+
would rewrite past checkpoints. The proactive "cure the DB" path is the batch
|
|
47
|
+
runner (`langmigrate upgrade`), not `list()`.
|
|
48
|
+
4. **Adapters:** Postgres and Redis are both implemented (batch enumeration + the
|
|
49
|
+
shared online interceptor). Postgres filters stale checkpoints with an indexed
|
|
50
|
+
`metadata->>'langmigrate_rev'` query; Redis scans `checkpoint:*` RedisJSON docs
|
|
51
|
+
(no server-side index on the tag).
|
|
52
|
+
|
|
53
|
+
## Migration rules (binding)
|
|
54
|
+
|
|
55
|
+
- Every `upgrade` MUST have a corresponding `downgrade`. Genuinely irreversible migrations
|
|
56
|
+
must be marked explicitly and raise `IrreversibleMigrationError` from `downgrade`.
|
|
57
|
+
- Migrations MUST be **idempotent** and **pure** — no hidden I/O, no network, no clocks.
|
|
58
|
+
Re-applying a migration to already-migrated state must be a no-op.
|
|
59
|
+
- Never introduce a breaking change without a downgrade script.
|
|
60
|
+
- Use the declarative helpers (`add_field`, `drop_field`, `rename_field`, `coerce_field`,
|
|
61
|
+
`require_field`) instead of hand-mutating dicts where possible.
|
|
62
|
+
|
|
63
|
+
## Commands
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
uv sync --extra dev --extra postgres --extra redis --extra langchain # set up the environment
|
|
67
|
+
uv run pytest # unit tests
|
|
68
|
+
uv run pytest -m integration # integration tests (needs Docker)
|
|
69
|
+
uv run ruff check . && uv run ruff format . # lint + format
|
|
70
|
+
docker compose up -d # local Postgres + Redis for integration
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Code style
|
|
74
|
+
|
|
75
|
+
- Python 3.10+, full type hints, Pydantic v2.
|
|
76
|
+
- `ruff` for lint + format (config in `pyproject.toml`). Line length 100.
|
|
77
|
+
- Docstrings on all public APIs.
|
|
78
|
+
- No heavy dependencies in `core`.
|
|
79
|
+
- Public exports go through `langmigrate/__init__.py`.
|
|
80
|
+
|
|
81
|
+
## Testing
|
|
82
|
+
|
|
83
|
+
- Every operation/primitive has unit tests, including Safe vs Unsafe behavior.
|
|
84
|
+
- Engine tests must cover the cascade, idempotency, and the no-op-at-HEAD case.
|
|
85
|
+
- Each adapter has integration tests behind `@pytest.mark.integration` (requires Docker).
|
|
86
|
+
- Prefer the in-memory saver for runtime/interceptor unit tests.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our Pledge
|
|
4
|
+
|
|
5
|
+
We as members, contributors, and leaders pledge to make participation in our
|
|
6
|
+
community a harassment-free experience for everyone, regardless of age, body
|
|
7
|
+
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
|
8
|
+
identity and expression, level of experience, education, socio-economic status,
|
|
9
|
+
nationality, personal appearance, race, caste, color, religion, or sexual
|
|
10
|
+
identity and orientation.
|
|
11
|
+
|
|
12
|
+
We pledge to act and interact in ways that contribute to an open, welcoming,
|
|
13
|
+
diverse, inclusive, and healthy community.
|
|
14
|
+
|
|
15
|
+
## Our Standards
|
|
16
|
+
|
|
17
|
+
Examples of behavior that contributes to a positive environment:
|
|
18
|
+
|
|
19
|
+
* Demonstrating empathy and kindness toward other people
|
|
20
|
+
* Being respectful of differing opinions, viewpoints, and experiences
|
|
21
|
+
* Giving and gracefully accepting constructive feedback
|
|
22
|
+
* Accepting responsibility and apologizing to those affected by our mistakes,
|
|
23
|
+
and learning from the experience
|
|
24
|
+
* Focusing on what is best not just for us as individuals, but for the overall
|
|
25
|
+
community
|
|
26
|
+
|
|
27
|
+
Examples of unacceptable behavior:
|
|
28
|
+
|
|
29
|
+
* The use of sexualized language or imagery, and sexual attention or advances of
|
|
30
|
+
any kind
|
|
31
|
+
* Trolling, insulting or derogatory comments, and personal or political attacks
|
|
32
|
+
* Public or private harassment
|
|
33
|
+
* Publishing others' private information, such as a physical or email address,
|
|
34
|
+
without their explicit permission
|
|
35
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
|
36
|
+
professional setting
|
|
37
|
+
|
|
38
|
+
## Enforcement Responsibilities
|
|
39
|
+
|
|
40
|
+
Community leaders are responsible for clarifying and enforcing our standards of
|
|
41
|
+
acceptable behavior and will take appropriate and fair corrective action in
|
|
42
|
+
response to any behavior that they deem inappropriate, threatening, offensive,
|
|
43
|
+
or harmful.
|
|
44
|
+
|
|
45
|
+
## Scope
|
|
46
|
+
|
|
47
|
+
This Code of Conduct applies within all community spaces, and also applies when
|
|
48
|
+
an individual is officially representing the community in public spaces.
|
|
49
|
+
|
|
50
|
+
## Enforcement
|
|
51
|
+
|
|
52
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
53
|
+
reported to the community leaders via the
|
|
54
|
+
[private vulnerability reporting](https://github.com/scinfu/langmigrate/security/advisories/new)
|
|
55
|
+
feature on GitHub or by contacting the maintainers directly. All complaints
|
|
56
|
+
will be reviewed and investigated promptly and fairly.
|
|
57
|
+
|
|
58
|
+
## Attribution
|
|
59
|
+
|
|
60
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
|
61
|
+
version 2.1, available at
|
|
62
|
+
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
|
63
|
+
|
|
64
|
+
[homepage]: https://www.contributor-covenant.org
|
|
65
|
+
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Contributing to LangMigrate
|
|
2
|
+
|
|
3
|
+
Thank you for your interest in contributing! This document explains how to
|
|
4
|
+
set up your development environment, run the tests, and submit changes that
|
|
5
|
+
fit the project's style and architecture.
|
|
6
|
+
|
|
7
|
+
## Development setup
|
|
8
|
+
|
|
9
|
+
LangMigrate uses [uv](https://docs.astral.sh/uv/) as its package manager
|
|
10
|
+
and Python 3.10+ as the language floor.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# Clone and install (all extras for full test coverage)
|
|
14
|
+
git clone https://github.com/scinfu/langmigrate.git
|
|
15
|
+
cd langmigrate
|
|
16
|
+
uv sync --extra dev --extra postgres --extra redis --extra langchain
|
|
17
|
+
|
|
18
|
+
# Bring up Postgres and Redis for integration tests
|
|
19
|
+
docker compose up -d
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Architecture
|
|
23
|
+
|
|
24
|
+
LangMigrate follows **Clean Architecture** strictly:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
cli/ runtime/ adapters/ ──────► core/
|
|
28
|
+
(DB clients live here) (pure: no DB client imports)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
- `core/` — pure logic. No I/O, no DB drivers. Nothing outside `core/` may
|
|
32
|
+
be imported from within `core/`.
|
|
33
|
+
- `adapters/` — DB-specific bulk access for the batch CLI. DB client
|
|
34
|
+
imports are confined here, ideally imported lazily inside methods.
|
|
35
|
+
- `runtime/` — `MigrationInterceptor` and batch runners. DB-agnostic;
|
|
36
|
+
delegates to whichever saver it wraps.
|
|
37
|
+
- `integrations/` — `SchemaMigrationMiddleware` and pure helpers for
|
|
38
|
+
hand-built `StateGraph`s.
|
|
39
|
+
- `cli/` — Typer application.
|
|
40
|
+
|
|
41
|
+
The dependency rule is non-negotiable: `core` must never import a database
|
|
42
|
+
client (`psycopg`, `redis`, …) nor anything from `adapters/`, `runtime/`,
|
|
43
|
+
`cli/` or `integrations/`. Keep migration logic independent of any backend.
|
|
44
|
+
|
|
45
|
+
## Testing
|
|
46
|
+
|
|
47
|
+
Every feature must have tests. Conventions:
|
|
48
|
+
|
|
49
|
+
- **Unit tests** (`tests/unit/`): pure logic, no services.
|
|
50
|
+
- **Integration tests** (`tests/integration/`): marked with
|
|
51
|
+
`@pytest.mark.integration`; require Docker. They cover end-to-end paths
|
|
52
|
+
on Postgres and Redis.
|
|
53
|
+
- Operations, engine, and interceptor changes require coverage of the
|
|
54
|
+
cascade, idempotency, async paths, and the no-op-at-HEAD case.
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
uv run pytest # unit tests only
|
|
58
|
+
uv run pytest -m integration # integration (needs Docker)
|
|
59
|
+
uv run pytest --cov=langmigrate # with coverage report
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Code style
|
|
63
|
+
|
|
64
|
+
- Python 3.10+, full type hints, Pydantic v2.
|
|
65
|
+
- `ruff` for lint and format (line length 100).
|
|
66
|
+
- Docstrings on all public APIs (Google style).
|
|
67
|
+
- `mypy` is run in CI — keep it happy.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
uv run ruff check . && uv run ruff format .
|
|
71
|
+
uv run mypy src/langmigrate
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Commit and PR conventions
|
|
75
|
+
|
|
76
|
+
- Keep commits atomic: one logical change per commit.
|
|
77
|
+
- Write imperative commit subjects ("Add feature", not "Added feature").
|
|
78
|
+
- PRs must link the issue they address (if any) and summarize the change.
|
|
79
|
+
- Run the full unit test suite, `ruff` and `mypy` before pushing.
|
|
80
|
+
|
|
81
|
+
## Migration authoring rules
|
|
82
|
+
|
|
83
|
+
Every migration in user code (and the examples in this repository) must:
|
|
84
|
+
|
|
85
|
+
- Provide both an `upgrade` and a `downgrade`. Genuinely one-way migrations
|
|
86
|
+
raise `IrreversibleMigrationError` from `downgrade`.
|
|
87
|
+
- Be **idempotent and pure**: no I/O, no network, no clocks. Re-applying a
|
|
88
|
+
migration to already-migrated state must be a no-op.
|
|
89
|
+
- Prefer the declarative helpers (`add_field`, `drop_field`, `rename_field`,
|
|
90
|
+
`coerce_field`, `require_field`) over hand-mutating dicts.
|
|
91
|
+
- Never store the version tag in `channel_values` — it lives in
|
|
92
|
+
`checkpoint.metadata` and the framework handles it for you.
|
|
93
|
+
|
|
94
|
+
## Releasing
|
|
95
|
+
|
|
96
|
+
Releases are published via PyPI trusted publishing. Pushing a `vX.Y.Z` tag
|
|
97
|
+
triggers the `publish.yml` workflow, which builds and uploads the
|
|
98
|
+
distribution after running the full test suite. See
|
|
99
|
+
[SECURITY.md](./SECURITY.md) for vulnerability reporting.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nabil Chatbi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|