mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-17 21:25:46 +02:00
libs: add cli, sdk-py, sdk-js and move core langgraph
This commit is contained in:
@@ -133,7 +133,7 @@ jobs:
|
|||||||
"$PKG_NAME==$VERSION"
|
"$PKG_NAME==$VERSION"
|
||||||
|
|
||||||
- name: Run unit tests
|
- name: Run unit tests
|
||||||
run: make tests
|
run: make test
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
needs:
|
needs:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) LangChain, Inc.
|
Copyright (c) 2024 LangChain, Inc.
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -1,48 +1,5 @@
|
|||||||
.PHONY: all clean format lint test tests test_watch integration_tests docker_tests help extended_tests coverage spell_check spell_fix build-docs serve-docs serve-clean-docs clean-docs
|
.PHONY: all clean format lint test tests test_watch integration_tests docker_tests help extended_tests coverage spell_check spell_fix build-docs serve-docs serve-clean-docs clean-docs
|
||||||
|
|
||||||
# Default target executed when no arguments are given to make.
|
|
||||||
all: help
|
|
||||||
|
|
||||||
######################
|
|
||||||
# TESTING AND COVERAGE
|
|
||||||
######################
|
|
||||||
|
|
||||||
# Run unit tests and generate a coverage report.
|
|
||||||
coverage:
|
|
||||||
poetry run pytest --cov \
|
|
||||||
--cov-config=.coveragerc \
|
|
||||||
--cov-report xml \
|
|
||||||
--cov-report term-missing:skip-covered
|
|
||||||
|
|
||||||
test:
|
|
||||||
poetry run pytest
|
|
||||||
|
|
||||||
test_watch:
|
|
||||||
poetry run ptw .
|
|
||||||
|
|
||||||
######################
|
|
||||||
# LINTING AND FORMATTING
|
|
||||||
######################
|
|
||||||
|
|
||||||
# Define a variable for Python and notebook files.
|
|
||||||
PYTHON_FILES=.
|
|
||||||
MYPY_CACHE=.mypy_cache
|
|
||||||
lint format: PYTHON_FILES=.
|
|
||||||
lint_diff format_diff: PYTHON_FILES=$(shell git diff --name-only --diff-filter=d master | grep -E '\.py$$|\.ipynb$$')
|
|
||||||
lint_package: PYTHON_FILES=langgraph
|
|
||||||
lint_tests: PYTHON_FILES=tests
|
|
||||||
lint_tests: MYPY_CACHE=.mypy_cache_test
|
|
||||||
|
|
||||||
lint lint_diff lint_package lint_tests:
|
|
||||||
poetry run ruff .
|
|
||||||
[ "$(PYTHON_FILES)" = "" ] || poetry run ruff format $(PYTHON_FILES) --diff
|
|
||||||
[ "$(PYTHON_FILES)" = "" ] || poetry run ruff --select I $(PYTHON_FILES)
|
|
||||||
[ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE) || poetry run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE)
|
|
||||||
|
|
||||||
format format_diff:
|
|
||||||
poetry run ruff format $(PYTHON_FILES)
|
|
||||||
poetry run ruff --select I --fix $(PYTHON_FILES)
|
|
||||||
|
|
||||||
spell_check:
|
spell_check:
|
||||||
poetry run codespell --toml pyproject.toml
|
poetry run codespell --toml pyproject.toml
|
||||||
|
|
||||||
@@ -63,28 +20,4 @@ serve-docs:
|
|||||||
|
|
||||||
clean-docs:
|
clean-docs:
|
||||||
find ./docs/docs -name "*.ipynb" -type f -delete
|
find ./docs/docs -name "*.ipynb" -type f -delete
|
||||||
rm -rf docs/site
|
rm -rf docs/site
|
||||||
|
|
||||||
|
|
||||||
######################
|
|
||||||
# HELP
|
|
||||||
######################
|
|
||||||
|
|
||||||
help:
|
|
||||||
@echo '===================='
|
|
||||||
@echo '-- DOCUMENTATION --'
|
|
||||||
|
|
||||||
@echo '-- LINTING --'
|
|
||||||
@echo 'format - run code formatters'
|
|
||||||
@echo 'lint - run linters'
|
|
||||||
@echo 'spell_check - run codespell on the project'
|
|
||||||
@echo 'spell_fix - run codespell on the project and fix the errors'
|
|
||||||
@echo '-- TESTS --'
|
|
||||||
@echo 'coverage - run unit tests and generate coverage report'
|
|
||||||
@echo 'test - run unit tests'
|
|
||||||
@echo 'tests - run unit tests (alias for "make test")'
|
|
||||||
@echo 'test TEST_FILE=<test_file> - run all tests in file'
|
|
||||||
@echo 'extended_tests - run only extended unit tests'
|
|
||||||
@echo 'test_watch - run unit tests in watch mode'
|
|
||||||
@echo 'integration_tests - run integration tests'
|
|
||||||
@echo 'docker_tests - run unit tests in docker'
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 LangChain, Inc.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
.PHONY: lint format
|
||||||
|
|
||||||
|
lint:
|
||||||
|
poetry run ruff .
|
||||||
|
poetry run ruff format . --diff
|
||||||
|
poetry run mypy .
|
||||||
|
|
||||||
|
format:
|
||||||
|
poetry run ruff format .
|
||||||
|
poetry run ruff --select I --fix .
|
||||||
|
|
||||||
|
test:
|
||||||
|
poetry run pytest tests
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# langchain-cli
|
||||||
|
|
||||||
|
This package implements the official CLI for LangGraph API.
|
||||||
@@ -0,0 +1,476 @@
|
|||||||
|
import json
|
||||||
|
import pathlib
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import click
|
||||||
|
import click.exceptions
|
||||||
|
|
||||||
|
import langgraph_cli.config
|
||||||
|
import langgraph_cli.docker
|
||||||
|
from langgraph_cli.config import Config
|
||||||
|
from langgraph_cli.docker import DockerCapabilities
|
||||||
|
from langgraph_cli.exec import Runner, subp_exec
|
||||||
|
from langgraph_cli.progress import Progress
|
||||||
|
|
||||||
|
OPT_O = click.option(
|
||||||
|
"--docker-compose",
|
||||||
|
"-d",
|
||||||
|
help="Advanced: Path to docker-compose.yml file with additional services to launch",
|
||||||
|
type=click.Path(
|
||||||
|
exists=True,
|
||||||
|
file_okay=True,
|
||||||
|
dir_okay=False,
|
||||||
|
resolve_path=True,
|
||||||
|
path_type=pathlib.Path,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
OPT_C = click.option(
|
||||||
|
"--config",
|
||||||
|
"-c",
|
||||||
|
help="""Path to configuration file declaring dependencies, graphs and environment variables.
|
||||||
|
|
||||||
|
\b
|
||||||
|
Config file must be a JSON file that has the following keys:
|
||||||
|
- "dependencies": array of dependencies for langgraph API server. Dependencies can be one of the following:
|
||||||
|
- ".", which would look for local python packages, as well as pyproject.toml, setup.py or requirements.txt in the app directory
|
||||||
|
- "./local_package"
|
||||||
|
- "<package_name>
|
||||||
|
- "graphs": mapping from graph ID to path where the compiled graph is defined, i.e. ./your_package/your_file.py:variable, where
|
||||||
|
"variable" is an instance of langgraph.graph.graph.CompiledGraph
|
||||||
|
- "env": (optional) path to .env file or a mapping from environment variable to its value
|
||||||
|
- "python_version": (optional) 3.11 or 3.12. Defaults to 3.11
|
||||||
|
- "pip_config_file": (optional) path to pip config file
|
||||||
|
- "dockerfile_lines": (optional) array of additional lines to add to Dockerfile following the import from parent image
|
||||||
|
|
||||||
|
\b
|
||||||
|
Example:
|
||||||
|
langgraph up -c langgraph.json
|
||||||
|
|
||||||
|
\b
|
||||||
|
Example:
|
||||||
|
{
|
||||||
|
"dependencies": [
|
||||||
|
"langchain_openai",
|
||||||
|
"./your_package"
|
||||||
|
],
|
||||||
|
"graphs": {
|
||||||
|
"my_graph_id": "./your_package/your_file.py:variable"
|
||||||
|
},
|
||||||
|
"env": "./.env"
|
||||||
|
}
|
||||||
|
|
||||||
|
\b
|
||||||
|
Example:
|
||||||
|
{
|
||||||
|
"python_version": "3.11",
|
||||||
|
"dependencies": [
|
||||||
|
"langchain_openai",
|
||||||
|
"."
|
||||||
|
],
|
||||||
|
"graphs": {
|
||||||
|
"my_graph_id": "./your_package/your_file.py:variable"
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"OPENAI_API_KEY": "secret-key"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Defaults to looking for langgraph.json in the current directory.""",
|
||||||
|
default="langgraph.json",
|
||||||
|
type=click.Path(
|
||||||
|
exists=True,
|
||||||
|
file_okay=True,
|
||||||
|
dir_okay=False,
|
||||||
|
resolve_path=True,
|
||||||
|
path_type=pathlib.Path,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
OPT_PORT = click.option(
|
||||||
|
"--port",
|
||||||
|
"-p",
|
||||||
|
type=int,
|
||||||
|
default=8123,
|
||||||
|
show_default=True,
|
||||||
|
help="""
|
||||||
|
Port to expose.
|
||||||
|
|
||||||
|
\b
|
||||||
|
Example:
|
||||||
|
langgraph up --port 8000
|
||||||
|
\b
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
OPT_RECREATE = click.option(
|
||||||
|
"--recreate/--no-recreate",
|
||||||
|
default=False,
|
||||||
|
show_default=True,
|
||||||
|
help="Recreate containers even if their configuration and image haven't changed",
|
||||||
|
)
|
||||||
|
OPT_PULL = click.option(
|
||||||
|
"--pull/--no-pull",
|
||||||
|
default=True,
|
||||||
|
show_default=True,
|
||||||
|
help="""
|
||||||
|
Pull latest images. Use --no-pull for running the server with locally-built images.
|
||||||
|
|
||||||
|
\b
|
||||||
|
Example:
|
||||||
|
langgraph up --no-pull
|
||||||
|
\b
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
OPT_VERBOSE = click.option(
|
||||||
|
"--verbose",
|
||||||
|
is_flag=True,
|
||||||
|
default=False,
|
||||||
|
help="Show more output from the server logs",
|
||||||
|
)
|
||||||
|
OPT_DEBUGGER_PORT = click.option(
|
||||||
|
"--debugger-port",
|
||||||
|
type=int,
|
||||||
|
help="Pull the debugger image locally and serve the UI on specified port",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@click.group()
|
||||||
|
def cli():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@OPT_RECREATE
|
||||||
|
@OPT_PULL
|
||||||
|
@OPT_PORT
|
||||||
|
@OPT_O
|
||||||
|
@OPT_C
|
||||||
|
@OPT_VERBOSE
|
||||||
|
@OPT_DEBUGGER_PORT
|
||||||
|
@click.option("--watch", is_flag=True, help="Restart on file changes")
|
||||||
|
@click.option(
|
||||||
|
"--langgraph-api-path",
|
||||||
|
type=click.Path(exists=True, file_okay=False, dir_okay=True, resolve_path=True),
|
||||||
|
hidden=True,
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--wait",
|
||||||
|
is_flag=True,
|
||||||
|
help="Wait for services to start before returning. Implies --detach",
|
||||||
|
)
|
||||||
|
@cli.command(help="Start langgraph API server")
|
||||||
|
def up(
|
||||||
|
config: pathlib.Path,
|
||||||
|
docker_compose: Optional[pathlib.Path],
|
||||||
|
port: int,
|
||||||
|
recreate: bool,
|
||||||
|
pull: bool,
|
||||||
|
watch: bool,
|
||||||
|
langgraph_api_path: Optional[pathlib.Path],
|
||||||
|
wait: bool,
|
||||||
|
verbose: bool,
|
||||||
|
debugger_port: Optional[int],
|
||||||
|
):
|
||||||
|
with Runner() as runner, Progress(message="Pulling...") as set:
|
||||||
|
capabilities = langgraph_cli.docker.check_capabilities(runner)
|
||||||
|
args, stdin = prepare(
|
||||||
|
runner,
|
||||||
|
capabilities=capabilities,
|
||||||
|
config_path=config,
|
||||||
|
docker_compose=docker_compose,
|
||||||
|
port=port,
|
||||||
|
pull=pull,
|
||||||
|
watch=watch,
|
||||||
|
langgraph_api_path=langgraph_api_path,
|
||||||
|
verbose=verbose,
|
||||||
|
debugger_port=debugger_port,
|
||||||
|
)
|
||||||
|
# add up + options
|
||||||
|
args.extend(["up", "--remove-orphans"])
|
||||||
|
if recreate:
|
||||||
|
args.extend(["--force-recreate", "--renew-anon-volumes"])
|
||||||
|
shutil.rmtree(config.parent / ".langgraph-data", ignore_errors=True)
|
||||||
|
try:
|
||||||
|
runner.run(subp_exec("docker", "volume", "rm", "langgraph-data"))
|
||||||
|
except click.exceptions.Exit:
|
||||||
|
pass
|
||||||
|
if watch:
|
||||||
|
args.append("--watch")
|
||||||
|
if wait:
|
||||||
|
args.append("--wait")
|
||||||
|
# run docker compose
|
||||||
|
set("Building...")
|
||||||
|
|
||||||
|
def on_stdout(line: str):
|
||||||
|
if "unpacking to docker.io" in line:
|
||||||
|
set("Starting...")
|
||||||
|
elif "GET /ok" in line:
|
||||||
|
debugger_origin = (
|
||||||
|
f"http://localhost:{debugger_port}"
|
||||||
|
if debugger_port
|
||||||
|
else "https://dev.smith.langchain.com"
|
||||||
|
)
|
||||||
|
set("")
|
||||||
|
sys.stdout.write(
|
||||||
|
f"""Ready!
|
||||||
|
- API: http://localhost:{port}
|
||||||
|
- Docs: http://localhost:{port}/docs
|
||||||
|
- Debugger: {debugger_origin}/studio/?baseUrl=http://127.0.0.1:{port}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
sys.stdout.flush()
|
||||||
|
return True
|
||||||
|
|
||||||
|
if capabilities.compose_type == "plugin":
|
||||||
|
compose_cmd = ["docker", "compose"]
|
||||||
|
elif capabilities.compose_type == "standalone":
|
||||||
|
compose_cmd = ["docker-compose"]
|
||||||
|
|
||||||
|
runner.run(
|
||||||
|
subp_exec(
|
||||||
|
*compose_cmd,
|
||||||
|
*args,
|
||||||
|
input=stdin,
|
||||||
|
verbose=verbose,
|
||||||
|
on_stdout=on_stdout,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@OPT_PORT
|
||||||
|
@OPT_O
|
||||||
|
@OPT_C
|
||||||
|
@OPT_VERBOSE
|
||||||
|
@OPT_DEBUGGER_PORT
|
||||||
|
@cli.command(help="Stop langgraph API server")
|
||||||
|
def down(
|
||||||
|
config: pathlib.Path,
|
||||||
|
docker_compose: Optional[pathlib.Path],
|
||||||
|
port: int,
|
||||||
|
verbose: bool,
|
||||||
|
debugger_port: Optional[int],
|
||||||
|
):
|
||||||
|
with Runner() as runner:
|
||||||
|
capabilities = langgraph_cli.docker.check_capabilities(runner)
|
||||||
|
args, stdin = prepare(
|
||||||
|
runner,
|
||||||
|
capabilities=capabilities,
|
||||||
|
config_path=config,
|
||||||
|
docker_compose=docker_compose,
|
||||||
|
port=port,
|
||||||
|
pull=False,
|
||||||
|
watch=False,
|
||||||
|
langgraph_api_path=None,
|
||||||
|
verbose=verbose,
|
||||||
|
debugger_port=debugger_port,
|
||||||
|
)
|
||||||
|
# add down + options
|
||||||
|
args.append("down")
|
||||||
|
# run docker compose
|
||||||
|
if capabilities.compose_type == "plugin":
|
||||||
|
compose_cmd = ["docker", "compose"]
|
||||||
|
elif capabilities.compose_type == "standalone":
|
||||||
|
compose_cmd = ["docker-compose"]
|
||||||
|
|
||||||
|
runner.run(subp_exec(*compose_cmd, *args, input=stdin, verbose=verbose))
|
||||||
|
|
||||||
|
|
||||||
|
@OPT_O
|
||||||
|
@OPT_C
|
||||||
|
@click.option("--follow", "-f", is_flag=True, help="Follow logs")
|
||||||
|
@cli.command(help="Show langgraph API server logs")
|
||||||
|
def logs(
|
||||||
|
config: pathlib.Path,
|
||||||
|
docker_compose: Optional[pathlib.Path],
|
||||||
|
follow: bool,
|
||||||
|
):
|
||||||
|
with Runner() as runner:
|
||||||
|
capabilities = langgraph_cli.docker.check_capabilities(runner)
|
||||||
|
args, stdin = prepare(
|
||||||
|
runner,
|
||||||
|
capabilities=capabilities,
|
||||||
|
config_path=config,
|
||||||
|
docker_compose=docker_compose,
|
||||||
|
port=8123,
|
||||||
|
pull=False,
|
||||||
|
watch=False,
|
||||||
|
verbose=False,
|
||||||
|
langgraph_api_path=None,
|
||||||
|
)
|
||||||
|
# add logs + options
|
||||||
|
args.append("logs")
|
||||||
|
if follow:
|
||||||
|
args.extend(["-f"])
|
||||||
|
# run docker compose
|
||||||
|
if capabilities.compose_type == "plugin":
|
||||||
|
compose_cmd = ["docker", "compose"]
|
||||||
|
elif capabilities.compose_type == "standalone":
|
||||||
|
compose_cmd = ["docker-compose"]
|
||||||
|
|
||||||
|
runner.run(subp_exec(*compose_cmd, *args, input=stdin, verbose=True))
|
||||||
|
|
||||||
|
|
||||||
|
@OPT_C
|
||||||
|
@OPT_PULL
|
||||||
|
@click.option(
|
||||||
|
"--tag",
|
||||||
|
"-t",
|
||||||
|
help="""Tag for the docker image.
|
||||||
|
|
||||||
|
\b
|
||||||
|
Example:
|
||||||
|
langgraph build -t my-image
|
||||||
|
|
||||||
|
\b
|
||||||
|
""",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--platform",
|
||||||
|
help="""Target platform(s) to build the docker image for.
|
||||||
|
|
||||||
|
\b
|
||||||
|
Example:
|
||||||
|
langgraph build --platform linux/amd64,linux/arm64
|
||||||
|
\b
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
@cli.command(help="Build langgraph API server docker image")
|
||||||
|
def build(
|
||||||
|
config: pathlib.Path,
|
||||||
|
platform: Optional[str],
|
||||||
|
pull: bool,
|
||||||
|
tag: str,
|
||||||
|
):
|
||||||
|
with open(config) as f:
|
||||||
|
config_json = langgraph_cli.config.validate_config(json.load(f))
|
||||||
|
with Runner() as runner:
|
||||||
|
# check docker available
|
||||||
|
langgraph_cli.docker.check_capabilities(runner)
|
||||||
|
# pull latest images
|
||||||
|
if pull:
|
||||||
|
runner.run(
|
||||||
|
subp_exec(
|
||||||
|
"docker",
|
||||||
|
"pull",
|
||||||
|
f"langchain/langgraph-api:{config_json['python_version']}",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# apply options
|
||||||
|
args = [
|
||||||
|
"-f",
|
||||||
|
"-", # stdin
|
||||||
|
"-t",
|
||||||
|
tag,
|
||||||
|
]
|
||||||
|
if platform:
|
||||||
|
args.extend(["--platform", platform])
|
||||||
|
# apply config
|
||||||
|
stdin = langgraph_cli.config.config_to_docker(config, config_json)
|
||||||
|
# run docker build
|
||||||
|
runner.run(
|
||||||
|
subp_exec(
|
||||||
|
"docker", "build", *args, str(config.parent), input=stdin, verbose=True
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@OPT_PORT
|
||||||
|
@OPT_O
|
||||||
|
@OPT_C
|
||||||
|
@cli.command(help="Build a helm chart to deploy to a Kubernetes cluster", hidden=True)
|
||||||
|
def helm(
|
||||||
|
config: pathlib.Path,
|
||||||
|
docker_compose: Optional[pathlib.Path],
|
||||||
|
port: int,
|
||||||
|
):
|
||||||
|
with open(config) as f:
|
||||||
|
config_json = langgraph_cli.config.validate_config(json.load(f))
|
||||||
|
with Runner() as runner:
|
||||||
|
# check docker available
|
||||||
|
capabilities = langgraph_cli.docker.check_capabilities(runner)
|
||||||
|
# prepare args
|
||||||
|
stdin = langgraph_cli.docker.compose(capabilities, port=port)
|
||||||
|
args = [
|
||||||
|
"--chart",
|
||||||
|
"-o=./helm",
|
||||||
|
"-v",
|
||||||
|
"-f",
|
||||||
|
"-", # stdin
|
||||||
|
]
|
||||||
|
# apply options
|
||||||
|
if docker_compose:
|
||||||
|
args.extend(["-f", str(docker_compose)])
|
||||||
|
args.append("convert")
|
||||||
|
# apply config
|
||||||
|
stdin += langgraph_cli.config.config_to_compose(config, config_json)
|
||||||
|
# run kompose convert
|
||||||
|
runner.run(subp_exec("kompose", *args, input=stdin))
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_args_and_stdin(
|
||||||
|
*,
|
||||||
|
capabilities: DockerCapabilities,
|
||||||
|
config_path: pathlib.Path,
|
||||||
|
config: Config,
|
||||||
|
docker_compose: Optional[pathlib.Path],
|
||||||
|
port: int,
|
||||||
|
watch: bool,
|
||||||
|
langgraph_api_path: Optional[pathlib.Path],
|
||||||
|
debugger_port: Optional[int] = None,
|
||||||
|
):
|
||||||
|
# prepare args
|
||||||
|
stdin = langgraph_cli.docker.compose(
|
||||||
|
capabilities, port=port, debugger_port=debugger_port
|
||||||
|
)
|
||||||
|
args = [
|
||||||
|
"--project-directory",
|
||||||
|
str(config_path.parent),
|
||||||
|
]
|
||||||
|
# apply options
|
||||||
|
if docker_compose:
|
||||||
|
args.extend(["-f", str(docker_compose)])
|
||||||
|
args.extend(["-f", "-"]) # stdin
|
||||||
|
# apply config
|
||||||
|
stdin += langgraph_cli.config.config_to_compose(
|
||||||
|
config_path, config, watch=watch, langgraph_api_path=langgraph_api_path
|
||||||
|
)
|
||||||
|
return args, stdin
|
||||||
|
|
||||||
|
|
||||||
|
def prepare(
|
||||||
|
runner,
|
||||||
|
*,
|
||||||
|
capabilities: DockerCapabilities,
|
||||||
|
config_path: pathlib.Path,
|
||||||
|
docker_compose: Optional[pathlib.Path],
|
||||||
|
port: int,
|
||||||
|
pull: bool,
|
||||||
|
watch: bool,
|
||||||
|
langgraph_api_path: Optional[pathlib.Path],
|
||||||
|
verbose: bool,
|
||||||
|
debugger_port: Optional[int] = None,
|
||||||
|
):
|
||||||
|
with open(config_path) as f:
|
||||||
|
config = langgraph_cli.config.validate_config(json.load(f))
|
||||||
|
# pull latest images
|
||||||
|
if pull:
|
||||||
|
runner.run(
|
||||||
|
subp_exec(
|
||||||
|
"docker",
|
||||||
|
"pull",
|
||||||
|
f"langchain/langgraph-api:{config['python_version']}",
|
||||||
|
verbose=verbose,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
args, stdin = prepare_args_and_stdin(
|
||||||
|
capabilities=capabilities,
|
||||||
|
config_path=config_path,
|
||||||
|
config=config,
|
||||||
|
docker_compose=docker_compose,
|
||||||
|
port=port,
|
||||||
|
watch=watch,
|
||||||
|
langgraph_api_path=langgraph_api_path,
|
||||||
|
debugger_port=debugger_port,
|
||||||
|
)
|
||||||
|
return args, stdin
|
||||||
@@ -0,0 +1,306 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import textwrap
|
||||||
|
from typing import NamedTuple, Optional, TypedDict, Union
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
|
||||||
|
class Config(TypedDict):
|
||||||
|
python_version: str
|
||||||
|
pip_config_file: Optional[str]
|
||||||
|
dockerfile_lines: list[str]
|
||||||
|
dependencies: list[str]
|
||||||
|
graphs: dict[str, str]
|
||||||
|
env: Union[dict[str, str], str]
|
||||||
|
|
||||||
|
|
||||||
|
def validate_config(config: Config) -> Config:
|
||||||
|
config = {
|
||||||
|
"python_version": config.get("python_version", "3.11"),
|
||||||
|
"pip_config_file": config.get("pip_config_file"),
|
||||||
|
"dockerfile_lines": config.get("dockerfile_lines", []),
|
||||||
|
"dependencies": config.get("dependencies", []),
|
||||||
|
"graphs": config.get("graphs", {}),
|
||||||
|
"env": config.get("env", {}),
|
||||||
|
}
|
||||||
|
if config["python_version"] not in (
|
||||||
|
"3.11",
|
||||||
|
"3.12",
|
||||||
|
):
|
||||||
|
raise click.UsageError(
|
||||||
|
f"Unsupported Python version: {config['python_version']}. "
|
||||||
|
"Supported versions are 3.11 and 3.12."
|
||||||
|
)
|
||||||
|
if not config["dependencies"]:
|
||||||
|
raise click.UsageError(
|
||||||
|
"No dependencies found in config. "
|
||||||
|
"Add at least one dependency to 'dependencies' list."
|
||||||
|
)
|
||||||
|
if not config["graphs"]:
|
||||||
|
raise click.UsageError(
|
||||||
|
"No graphs found in config. "
|
||||||
|
"Add at least one graph to 'graphs' dictionary."
|
||||||
|
)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
class LocalDeps(NamedTuple):
|
||||||
|
pip_reqs: list[tuple[pathlib.Path, str]]
|
||||||
|
real_pkgs: dict[pathlib.Path, str]
|
||||||
|
faux_pkgs: dict[pathlib.Path, tuple[str, str]]
|
||||||
|
# if . is in dependencies, use it as working_dir
|
||||||
|
working_dir: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps:
|
||||||
|
# ensure reserved package names are not used
|
||||||
|
reserved = {
|
||||||
|
"src",
|
||||||
|
"langgraph-api",
|
||||||
|
"langgraph_api",
|
||||||
|
"langgraph",
|
||||||
|
"langchain-core",
|
||||||
|
"langchain_core",
|
||||||
|
"pydantic",
|
||||||
|
"orjson",
|
||||||
|
"fastapi",
|
||||||
|
"uvicorn",
|
||||||
|
"psycopg",
|
||||||
|
"httpx",
|
||||||
|
"langsmith",
|
||||||
|
}
|
||||||
|
|
||||||
|
def check_reserved(name: str, ref: str):
|
||||||
|
if name in reserved:
|
||||||
|
raise ValueError(
|
||||||
|
f"Package name '{name}' used in local dep '{ref}' is reserved. "
|
||||||
|
"Rename the directory."
|
||||||
|
)
|
||||||
|
reserved.add(name)
|
||||||
|
|
||||||
|
pip_reqs = []
|
||||||
|
real_pkgs = {}
|
||||||
|
faux_pkgs = {}
|
||||||
|
working_dir = None
|
||||||
|
|
||||||
|
for local_dep in config["dependencies"]:
|
||||||
|
if not local_dep.startswith("."):
|
||||||
|
continue
|
||||||
|
|
||||||
|
resolved = config_path.parent / local_dep
|
||||||
|
|
||||||
|
# validate local dependency
|
||||||
|
if not resolved.exists():
|
||||||
|
raise FileNotFoundError(f"Could not find local dependency: {resolved}")
|
||||||
|
elif not resolved.is_dir():
|
||||||
|
raise NotADirectoryError(
|
||||||
|
f"Local dependency must be a directory: {resolved}"
|
||||||
|
)
|
||||||
|
elif not resolved.is_relative_to(config_path.parent):
|
||||||
|
raise ValueError(
|
||||||
|
f"Local dependency '{resolved}' must be a subdirectory of '{config_path.parent}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
# if it's installable, add it to local_pkgs
|
||||||
|
# otherwise, add it to faux_pkgs, and create a pyproject.toml
|
||||||
|
files = os.listdir(resolved)
|
||||||
|
if "pyproject.toml" in files:
|
||||||
|
real_pkgs[resolved] = local_dep
|
||||||
|
if local_dep == ".":
|
||||||
|
working_dir = f"/deps/{resolved.name}"
|
||||||
|
elif "setup.py" in files:
|
||||||
|
real_pkgs[resolved] = local_dep
|
||||||
|
if local_dep == ".":
|
||||||
|
working_dir = f"/deps/{resolved.name}"
|
||||||
|
else:
|
||||||
|
if any(file == "__init__.py" for file in files):
|
||||||
|
# flat layout
|
||||||
|
if "-" in resolved.name:
|
||||||
|
raise ValueError(
|
||||||
|
f"Package name '{resolved.name}' contains a hyphen. "
|
||||||
|
"Rename the directory to use it as flat-layout package."
|
||||||
|
)
|
||||||
|
check_reserved(resolved.name, local_dep)
|
||||||
|
container_path = f"/deps/__outer_{resolved.name}/{resolved.name}"
|
||||||
|
else:
|
||||||
|
# src layout
|
||||||
|
container_path = f"/deps/__outer_{resolved.name}/src"
|
||||||
|
for file in files:
|
||||||
|
rfile = resolved / file
|
||||||
|
if (
|
||||||
|
rfile.is_dir()
|
||||||
|
and file != "__pycache__"
|
||||||
|
and not file.startswith(".")
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
for subfile in os.listdir(rfile):
|
||||||
|
if subfile.endswith(".py"):
|
||||||
|
check_reserved(file, local_dep)
|
||||||
|
break
|
||||||
|
except PermissionError:
|
||||||
|
pass
|
||||||
|
faux_pkgs[resolved] = (local_dep, container_path)
|
||||||
|
if local_dep == ".":
|
||||||
|
working_dir = container_path
|
||||||
|
if "requirements.txt" in files:
|
||||||
|
rfile = resolved / "requirements.txt"
|
||||||
|
pip_reqs.append(
|
||||||
|
(
|
||||||
|
rfile.relative_to(config_path.parent),
|
||||||
|
f"{container_path}/requirements.txt",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return LocalDeps(pip_reqs, real_pkgs, faux_pkgs, working_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def _update_graph_paths(
|
||||||
|
config_path: pathlib.Path, config: Config, local_deps: LocalDeps
|
||||||
|
) -> None:
|
||||||
|
for graph_id, import_str in config["graphs"].items():
|
||||||
|
module_str, _, attr_str = import_str.partition(":")
|
||||||
|
if not module_str or not attr_str:
|
||||||
|
message = (
|
||||||
|
'Import string "{import_str}" must be in format "<module>:<attribute>".'
|
||||||
|
)
|
||||||
|
raise ValueError(message.format(import_str=import_str))
|
||||||
|
if "/" in module_str:
|
||||||
|
resolved = config_path.parent / module_str
|
||||||
|
if not resolved.exists():
|
||||||
|
raise FileNotFoundError(f"Could not find local module: {resolved}")
|
||||||
|
elif not resolved.is_file():
|
||||||
|
raise IsADirectoryError(f"Local module must be a file: {resolved}")
|
||||||
|
else:
|
||||||
|
for path in local_deps.real_pkgs:
|
||||||
|
if resolved.is_relative_to(path):
|
||||||
|
module_str = f"/deps/{path.name}/{resolved.relative_to(path)}"
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
for faux_pkg, (_, destpath) in local_deps.faux_pkgs.items():
|
||||||
|
if resolved.is_relative_to(faux_pkg):
|
||||||
|
module_str = f"{destpath}/{resolved.relative_to(faux_pkg)}"
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"Module '{import_str}' not found in 'dependencies' list. "
|
||||||
|
"Add its containing package to 'dependencies' list."
|
||||||
|
)
|
||||||
|
# update the config
|
||||||
|
config["graphs"][graph_id] = f"{module_str}:{attr_str}"
|
||||||
|
|
||||||
|
|
||||||
|
def config_to_docker(config_path: pathlib.Path, config: Config):
|
||||||
|
# configure pip
|
||||||
|
pip_install = "pip install -c /api/constraints.txt"
|
||||||
|
if config.get("pip_config_file"):
|
||||||
|
pip_install = f"PIP_CONFIG_FILE=/pipconfig.txt {pip_install}"
|
||||||
|
pip_config_file_str = (
|
||||||
|
f"ADD {config['pip_config_file']} /pipconfig.txt"
|
||||||
|
if config.get("pip_config_file")
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
|
||||||
|
# collect dependencies
|
||||||
|
pypi_deps = [dep for dep in config["dependencies"] if not dep.startswith(".")]
|
||||||
|
local_deps = _assemble_local_deps(config_path, config)
|
||||||
|
|
||||||
|
# rewrite graph paths
|
||||||
|
_update_graph_paths(config_path, config, local_deps)
|
||||||
|
|
||||||
|
pip_pkgs_str = f"RUN {pip_install} {' '.join(pypi_deps)}" if pypi_deps else ""
|
||||||
|
if local_deps.pip_reqs:
|
||||||
|
pip_reqs_str = os.linesep.join(
|
||||||
|
f"ADD {reqpath} {destpath}" for reqpath, destpath in local_deps.pip_reqs
|
||||||
|
)
|
||||||
|
pip_reqs_str += f'{os.linesep}RUN {pip_install} {" ".join("-r " + r for _,r in local_deps.pip_reqs)}'
|
||||||
|
|
||||||
|
else:
|
||||||
|
pip_reqs_str = ""
|
||||||
|
|
||||||
|
# https://setuptools.pypa.io/en/latest/userguide/datafiles.html#package-data
|
||||||
|
# https://til.simonwillison.net/python/pyproject
|
||||||
|
faux_pkgs_str = f"{os.linesep}{os.linesep}".join(
|
||||||
|
f"""ADD {relpath} {destpath}
|
||||||
|
COPY <<EOF /deps/__outer_{fullpath.name}/pyproject.toml
|
||||||
|
[project]
|
||||||
|
name = "{fullpath.name}"
|
||||||
|
version = "0.1"
|
||||||
|
[tool.setuptools.package-data]
|
||||||
|
"*" = ["**/*"]
|
||||||
|
EOF"""
|
||||||
|
for fullpath, (relpath, destpath) in local_deps.faux_pkgs.items()
|
||||||
|
)
|
||||||
|
local_pkgs_str = os.linesep.join(
|
||||||
|
f"ADD {relpath} /deps/{fullpath.name}"
|
||||||
|
for fullpath, relpath in local_deps.real_pkgs.items()
|
||||||
|
)
|
||||||
|
|
||||||
|
return f"""FROM langchain/langgraph-api:{config['python_version']}
|
||||||
|
|
||||||
|
{os.linesep.join(config["dockerfile_lines"])}
|
||||||
|
|
||||||
|
{pip_config_file_str}
|
||||||
|
|
||||||
|
{pip_pkgs_str}
|
||||||
|
|
||||||
|
{pip_reqs_str}
|
||||||
|
|
||||||
|
{local_pkgs_str}
|
||||||
|
|
||||||
|
{faux_pkgs_str}
|
||||||
|
|
||||||
|
RUN {pip_install} -e /deps/*
|
||||||
|
|
||||||
|
ENV LANGSERVE_GRAPHS='{json.dumps(config["graphs"])}'
|
||||||
|
|
||||||
|
{f"WORKDIR {local_deps.working_dir}" if local_deps.working_dir else ""}"""
|
||||||
|
|
||||||
|
|
||||||
|
def config_to_compose(
|
||||||
|
config_path: pathlib.Path,
|
||||||
|
config: Config,
|
||||||
|
watch: bool = False,
|
||||||
|
langgraph_api_path: Optional[pathlib.Path] = None,
|
||||||
|
):
|
||||||
|
env_vars = config["env"].items() if isinstance(config["env"], dict) else {}
|
||||||
|
env_vars_str = "\n".join(f" {k}: {v}" for k, v in env_vars)
|
||||||
|
env_file_str = (
|
||||||
|
f"env_file: {config['env']}" if isinstance(config["env"], str) else ""
|
||||||
|
)
|
||||||
|
if watch:
|
||||||
|
watch_paths = [config_path] + [
|
||||||
|
config_path.parent / dep
|
||||||
|
for dep in config["dependencies"]
|
||||||
|
if dep.startswith(".")
|
||||||
|
]
|
||||||
|
watch_actions = "\n".join(
|
||||||
|
f"""- path: {path}
|
||||||
|
action: rebuild
|
||||||
|
ignore:
|
||||||
|
- .langgraph-data"""
|
||||||
|
for path in watch_paths
|
||||||
|
)
|
||||||
|
if langgraph_api_path:
|
||||||
|
watch_actions += f"""\n- path: {langgraph_api_path}
|
||||||
|
action: sync+restart
|
||||||
|
target: /api/langgraph_api"""
|
||||||
|
watch_str = f"""
|
||||||
|
develop:
|
||||||
|
watch:
|
||||||
|
{textwrap.indent(watch_actions, " ")}
|
||||||
|
"""
|
||||||
|
else:
|
||||||
|
watch_str = ""
|
||||||
|
|
||||||
|
return f"""
|
||||||
|
{textwrap.indent(env_vars_str, " ")}
|
||||||
|
{env_file_str}
|
||||||
|
pull_policy: build
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile_inline: |
|
||||||
|
{textwrap.indent(config_to_docker(config_path, config), " ")}
|
||||||
|
{watch_str}
|
||||||
|
"""
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import json
|
||||||
|
import pathlib
|
||||||
|
import shutil
|
||||||
|
from typing import Literal, NamedTuple, Optional
|
||||||
|
|
||||||
|
import click.exceptions
|
||||||
|
|
||||||
|
from langgraph_cli.exec import subp_exec
|
||||||
|
|
||||||
|
ROOT = pathlib.Path(__file__).parent.resolve()
|
||||||
|
DEFAULT_POSTGRES_URI = (
|
||||||
|
"postgres://postgres:postgres@langgraph-postgres:5432/postgres?sslmode=disable"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
DB = """
|
||||||
|
langgraph-postgres:
|
||||||
|
image: postgres:16
|
||||||
|
restart: on-failure
|
||||||
|
ports:
|
||||||
|
- "5433:5432"
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: postgres
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
volumes:
|
||||||
|
- langgraph-data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: pg_isready -U postgres
|
||||||
|
start_period: 10s
|
||||||
|
timeout: 1s
|
||||||
|
retries: 5
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
DEBUGGER = """
|
||||||
|
langgraph-debugger:
|
||||||
|
image: langchain/langgraph-debugger
|
||||||
|
restart: on-failure
|
||||||
|
ports:
|
||||||
|
- "{debugger_port}:80"
|
||||||
|
depends_on:
|
||||||
|
langgraph-postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Version(NamedTuple):
|
||||||
|
major: int
|
||||||
|
minor: int
|
||||||
|
patch: int
|
||||||
|
|
||||||
|
|
||||||
|
DockerComposeType = Literal["plugin", "standalone"]
|
||||||
|
|
||||||
|
|
||||||
|
class DockerCapabilities(NamedTuple):
|
||||||
|
version_docker: Version
|
||||||
|
version_compose: Version
|
||||||
|
healthcheck_start_interval: bool
|
||||||
|
compose_type: DockerComposeType = "plugin"
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_version(version: str) -> Version:
|
||||||
|
parts = version.split(".", 2)
|
||||||
|
if len(parts) == 1:
|
||||||
|
major = parts[0]
|
||||||
|
minor = "0"
|
||||||
|
patch = "0"
|
||||||
|
elif len(parts) == 2:
|
||||||
|
major, minor = parts
|
||||||
|
patch = "0"
|
||||||
|
else:
|
||||||
|
major, minor, patch = parts
|
||||||
|
return Version(int(major.lstrip("v")), int(minor), int(patch.split("-")[0]))
|
||||||
|
|
||||||
|
|
||||||
|
def check_capabilities(runner) -> DockerCapabilities:
|
||||||
|
# check docker available
|
||||||
|
try:
|
||||||
|
stdout, _ = runner.run(subp_exec("docker", "info", "-f", "json", collect=True))
|
||||||
|
info = json.loads(stdout)
|
||||||
|
except (click.exceptions.Exit, json.JSONDecodeError):
|
||||||
|
raise click.UsageError("Docker not installed or not running") from None
|
||||||
|
|
||||||
|
compose_type: DockerComposeType
|
||||||
|
try:
|
||||||
|
compose = next(
|
||||||
|
p for p in info["ClientInfo"]["Plugins"] if p["Name"] == "compose"
|
||||||
|
)
|
||||||
|
compose_type = "plugin"
|
||||||
|
except (KeyError, StopIteration):
|
||||||
|
if shutil.which("docker-compose") is None:
|
||||||
|
raise click.UsageError("Docker Compose not installed") from None
|
||||||
|
|
||||||
|
compose_type = "standalone"
|
||||||
|
|
||||||
|
# parse versions
|
||||||
|
docker_version = _parse_version(info["ServerVersion"])
|
||||||
|
compose_version = _parse_version(compose["Version"])
|
||||||
|
|
||||||
|
# check capabilities
|
||||||
|
return DockerCapabilities(
|
||||||
|
version_docker=docker_version,
|
||||||
|
version_compose=compose_version,
|
||||||
|
healthcheck_start_interval=docker_version >= Version(25, 0, 0),
|
||||||
|
compose_type=compose_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def compose(
|
||||||
|
capabilities: DockerCapabilities,
|
||||||
|
*,
|
||||||
|
port: int,
|
||||||
|
debugger_port: Optional[int] = None,
|
||||||
|
# postgres://user:password@host:port/database?option=value
|
||||||
|
postgres_uri: Optional[str] = None,
|
||||||
|
) -> str:
|
||||||
|
if postgres_uri is None:
|
||||||
|
include_db = True
|
||||||
|
postgres_uri = DEFAULT_POSTGRES_URI
|
||||||
|
else:
|
||||||
|
include_db = False
|
||||||
|
|
||||||
|
db = DB.format() if include_db else ""
|
||||||
|
volumes = (
|
||||||
|
"""volumes:
|
||||||
|
langgraph-data:
|
||||||
|
driver: local
|
||||||
|
"""
|
||||||
|
if include_db
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
if db:
|
||||||
|
if capabilities.healthcheck_start_interval:
|
||||||
|
db += """
|
||||||
|
interval: 60s
|
||||||
|
start_interval: 1s"""
|
||||||
|
else:
|
||||||
|
db += """
|
||||||
|
interval: 5s"""
|
||||||
|
|
||||||
|
compose_str = f"""{volumes}services:
|
||||||
|
{db}
|
||||||
|
{DEBUGGER.format(debugger_port=debugger_port) if debugger_port else ""}
|
||||||
|
langgraph-api:
|
||||||
|
restart: on-failure
|
||||||
|
ports:
|
||||||
|
- "{port}:8000"
|
||||||
|
depends_on:
|
||||||
|
langgraph-postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
POSTGRES_URI: {postgres_uri}
|
||||||
|
"""
|
||||||
|
if capabilities.healthcheck_start_interval:
|
||||||
|
compose_str += """ healthcheck:
|
||||||
|
interval: 60s
|
||||||
|
start_interval: 1s
|
||||||
|
start_period: 10s"""
|
||||||
|
|
||||||
|
return compose_str
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from typing import Callable, Optional, cast
|
||||||
|
|
||||||
|
import click.exceptions
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def Runner():
|
||||||
|
if hasattr(asyncio, "Runner"):
|
||||||
|
with asyncio.Runner() as runner:
|
||||||
|
yield runner
|
||||||
|
else:
|
||||||
|
|
||||||
|
class _Runner:
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *args):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def run(self, coro):
|
||||||
|
asyncio.run(coro)
|
||||||
|
|
||||||
|
yield _Runner()
|
||||||
|
|
||||||
|
|
||||||
|
async def subp_exec(
|
||||||
|
cmd: str,
|
||||||
|
*args: str,
|
||||||
|
input: Optional[str] = None,
|
||||||
|
wait: Optional[float] = None,
|
||||||
|
verbose: bool = False,
|
||||||
|
collect: bool = False,
|
||||||
|
on_stdout: Optional[Callable[[str], Optional[bool]]] = None,
|
||||||
|
) -> tuple[Optional[str], Optional[str]]:
|
||||||
|
if verbose:
|
||||||
|
cmd_str = f"+ {cmd} {' '.join(map(str, args))}"
|
||||||
|
if input:
|
||||||
|
print(cmd_str, " <\n", "\n".join(filter(None, input.splitlines())), sep="")
|
||||||
|
else:
|
||||||
|
print(cmd_str)
|
||||||
|
if wait:
|
||||||
|
await asyncio.sleep(wait)
|
||||||
|
|
||||||
|
try:
|
||||||
|
proc = await asyncio.create_subprocess_exec(
|
||||||
|
cmd,
|
||||||
|
*args,
|
||||||
|
stdin=asyncio.subprocess.PIPE if input else None,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE,
|
||||||
|
)
|
||||||
|
|
||||||
|
def signal_handler():
|
||||||
|
# make sure process exists, then terminate it
|
||||||
|
if proc.returncode is None:
|
||||||
|
proc.terminate()
|
||||||
|
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
loop.add_signal_handler(signal.SIGINT, signal_handler)
|
||||||
|
loop.add_signal_handler(signal.SIGTERM, signal_handler)
|
||||||
|
|
||||||
|
empty_fut: asyncio.Future = asyncio.Future()
|
||||||
|
empty_fut.set_result(None)
|
||||||
|
stdout, stderr, _ = await asyncio.gather(
|
||||||
|
monitor_stream(
|
||||||
|
cast(asyncio.StreamReader, proc.stdout),
|
||||||
|
collect=True,
|
||||||
|
display=verbose,
|
||||||
|
on_line=on_stdout,
|
||||||
|
),
|
||||||
|
monitor_stream(
|
||||||
|
cast(asyncio.StreamReader, proc.stderr),
|
||||||
|
collect=True,
|
||||||
|
display=verbose,
|
||||||
|
),
|
||||||
|
proc._feed_stdin(input.encode()) if input else empty_fut, # type: ignore[attr-defined]
|
||||||
|
)
|
||||||
|
returncode = await proc.wait()
|
||||||
|
if (
|
||||||
|
returncode is not None
|
||||||
|
and returncode != 0 # success
|
||||||
|
and returncode != 130 # user interrupt
|
||||||
|
):
|
||||||
|
sys.stdout.write(stdout.decode() if stdout else "")
|
||||||
|
sys.stderr.write(stderr.decode() if stderr else "")
|
||||||
|
raise click.exceptions.Exit(returncode)
|
||||||
|
if collect:
|
||||||
|
return (
|
||||||
|
stdout.decode() if stdout else None,
|
||||||
|
stderr.decode() if stderr else None,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return None, None
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
if proc.returncode is None:
|
||||||
|
try:
|
||||||
|
os.killpg(os.getpgid(proc.pid), signal.SIGINT)
|
||||||
|
except (ProcessLookupError, KeyboardInterrupt):
|
||||||
|
pass
|
||||||
|
|
||||||
|
loop.remove_signal_handler(signal.SIGINT)
|
||||||
|
loop.remove_signal_handler(signal.SIGTERM)
|
||||||
|
except UnboundLocalError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
async def monitor_stream(
|
||||||
|
stream: asyncio.StreamReader,
|
||||||
|
collect: bool = False,
|
||||||
|
display: bool = False,
|
||||||
|
on_line: Optional[Callable[[str], Optional[bool]]] = None,
|
||||||
|
) -> Optional[bytearray]:
|
||||||
|
if collect:
|
||||||
|
ba = bytearray()
|
||||||
|
|
||||||
|
def handle(line: bytes):
|
||||||
|
nonlocal on_line
|
||||||
|
nonlocal display
|
||||||
|
|
||||||
|
if collect:
|
||||||
|
ba.extend(line)
|
||||||
|
if display:
|
||||||
|
sys.stdout.write(line.decode())
|
||||||
|
if on_line:
|
||||||
|
if on_line(line.decode()):
|
||||||
|
on_line = None
|
||||||
|
display = True
|
||||||
|
|
||||||
|
async for line in stream:
|
||||||
|
await asyncio.to_thread(handle, line)
|
||||||
|
if collect:
|
||||||
|
return ba
|
||||||
|
else:
|
||||||
|
return None
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
|
||||||
|
class Progress:
|
||||||
|
delay: float = 0.1
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def spinning_cursor():
|
||||||
|
while True:
|
||||||
|
yield from "|/-\\"
|
||||||
|
|
||||||
|
def __init__(self, *, message=""):
|
||||||
|
self.message = message
|
||||||
|
self.spinner_generator = self.spinning_cursor()
|
||||||
|
|
||||||
|
def spinner_iteration(self):
|
||||||
|
message = self.message
|
||||||
|
sys.stdout.write(next(self.spinner_generator) + " " + message)
|
||||||
|
sys.stdout.flush()
|
||||||
|
time.sleep(self.delay)
|
||||||
|
# clear the spinner and message
|
||||||
|
sys.stdout.write(
|
||||||
|
"\b" * (len(message) + 2)
|
||||||
|
+ " " * (len(message) + 2)
|
||||||
|
+ "\b" * (len(message) + 2)
|
||||||
|
)
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
def spinner_task(self):
|
||||||
|
while self.message:
|
||||||
|
message = self.message
|
||||||
|
sys.stdout.write(next(self.spinner_generator) + " " + message)
|
||||||
|
sys.stdout.flush()
|
||||||
|
time.sleep(self.delay)
|
||||||
|
# clear the spinner and message
|
||||||
|
sys.stdout.write(
|
||||||
|
"\b" * (len(message) + 2)
|
||||||
|
+ " " * (len(message) + 2)
|
||||||
|
+ "\b" * (len(message) + 2)
|
||||||
|
)
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
def __enter__(self) -> Callable[[str], None]:
|
||||||
|
self.thread = threading.Thread(target=self.spinner_task)
|
||||||
|
self.thread.start()
|
||||||
|
|
||||||
|
def set_message(message):
|
||||||
|
self.message = message
|
||||||
|
if not message:
|
||||||
|
self.thread.join()
|
||||||
|
|
||||||
|
return set_message
|
||||||
|
|
||||||
|
def __exit__(self, exception, value, tb):
|
||||||
|
self.message = ""
|
||||||
|
try:
|
||||||
|
self.thread.join()
|
||||||
|
finally:
|
||||||
|
del self.thread
|
||||||
|
if exception is not None:
|
||||||
|
return False
|
||||||
Generated
+327
@@ -0,0 +1,327 @@
|
|||||||
|
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click"
|
||||||
|
version = "8.1.7"
|
||||||
|
description = "Composable command line interface toolkit"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
|
||||||
|
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "codespell"
|
||||||
|
version = "2.2.6"
|
||||||
|
description = "Codespell"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "codespell-2.2.6-py3-none-any.whl", hash = "sha256:9ee9a3e5df0990604013ac2a9f22fa8e57669c827124a2e961fe8a1da4cacc07"},
|
||||||
|
{file = "codespell-2.2.6.tar.gz", hash = "sha256:a8c65d8eb3faa03deabab6b3bbe798bea72e1799c7e9e955d57eca4096abcff9"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["Pygments", "build", "chardet", "pre-commit", "pytest", "pytest-cov", "pytest-dependency", "ruff", "tomli", "twine"]
|
||||||
|
hard-encoding-detection = ["chardet"]
|
||||||
|
toml = ["tomli"]
|
||||||
|
types = ["chardet (>=5.1.0)", "mypy", "pytest", "pytest-cov", "pytest-dependency"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
description = "Cross-platform colored terminal text."
|
||||||
|
optional = false
|
||||||
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||||
|
files = [
|
||||||
|
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||||
|
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "docopt"
|
||||||
|
version = "0.6.2"
|
||||||
|
description = "Pythonic argument parser, that will make you smile"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
files = [
|
||||||
|
{file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "exceptiongroup"
|
||||||
|
version = "1.2.0"
|
||||||
|
description = "Backport of PEP 654 (exception groups)"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"},
|
||||||
|
{file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
test = ["pytest (>=6)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.0.0"
|
||||||
|
description = "brain-dead simple config-ini parsing"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
|
||||||
|
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mypy"
|
||||||
|
version = "1.10.0"
|
||||||
|
description = "Optional static typing for Python"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "mypy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2"},
|
||||||
|
{file = "mypy-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99"},
|
||||||
|
{file = "mypy-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2"},
|
||||||
|
{file = "mypy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9"},
|
||||||
|
{file = "mypy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051"},
|
||||||
|
{file = "mypy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1"},
|
||||||
|
{file = "mypy-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee"},
|
||||||
|
{file = "mypy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de"},
|
||||||
|
{file = "mypy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7"},
|
||||||
|
{file = "mypy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53"},
|
||||||
|
{file = "mypy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b"},
|
||||||
|
{file = "mypy-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30"},
|
||||||
|
{file = "mypy-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e"},
|
||||||
|
{file = "mypy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5"},
|
||||||
|
{file = "mypy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda"},
|
||||||
|
{file = "mypy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0"},
|
||||||
|
{file = "mypy-1.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727"},
|
||||||
|
{file = "mypy-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4"},
|
||||||
|
{file = "mypy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061"},
|
||||||
|
{file = "mypy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f"},
|
||||||
|
{file = "mypy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976"},
|
||||||
|
{file = "mypy-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec"},
|
||||||
|
{file = "mypy-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821"},
|
||||||
|
{file = "mypy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746"},
|
||||||
|
{file = "mypy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a"},
|
||||||
|
{file = "mypy-1.10.0-py3-none-any.whl", hash = "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee"},
|
||||||
|
{file = "mypy-1.10.0.tar.gz", hash = "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
mypy-extensions = ">=1.0.0"
|
||||||
|
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
|
||||||
|
typing-extensions = ">=4.1.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dmypy = ["psutil (>=4.0)"]
|
||||||
|
install-types = ["pip"]
|
||||||
|
mypyc = ["setuptools (>=50)"]
|
||||||
|
reports = ["lxml"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mypy-extensions"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "Type system extensions for programs checked with the mypy type checker."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
files = [
|
||||||
|
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
|
||||||
|
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "packaging"
|
||||||
|
version = "23.2"
|
||||||
|
description = "Core utilities for Python packages"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
|
||||||
|
{file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pluggy"
|
||||||
|
version = "1.3.0"
|
||||||
|
description = "plugin and hook calling mechanisms for python"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"},
|
||||||
|
{file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["pre-commit", "tox"]
|
||||||
|
testing = ["pytest", "pytest-benchmark"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest"
|
||||||
|
version = "7.4.3"
|
||||||
|
description = "pytest: simple powerful testing with Python"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"},
|
||||||
|
{file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||||
|
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
|
||||||
|
iniconfig = "*"
|
||||||
|
packaging = "*"
|
||||||
|
pluggy = ">=0.12,<2.0"
|
||||||
|
tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-asyncio"
|
||||||
|
version = "0.21.1"
|
||||||
|
description = "Pytest support for asyncio"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "pytest-asyncio-0.21.1.tar.gz", hash = "sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d"},
|
||||||
|
{file = "pytest_asyncio-0.21.1-py3-none-any.whl", hash = "sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
pytest = ">=7.0.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
|
||||||
|
testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-mock"
|
||||||
|
version = "3.12.0"
|
||||||
|
description = "Thin-wrapper around the mock package for easier use with pytest"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "pytest-mock-3.12.0.tar.gz", hash = "sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9"},
|
||||||
|
{file = "pytest_mock-3.12.0-py3-none-any.whl", hash = "sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
pytest = ">=5.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["pre-commit", "pytest-asyncio", "tox"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-watch"
|
||||||
|
version = "4.2.0"
|
||||||
|
description = "Local continuous test runner with pytest and watchdog."
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
files = [
|
||||||
|
{file = "pytest-watch-4.2.0.tar.gz", hash = "sha256:06136f03d5b361718b8d0d234042f7b2f203910d8568f63df2f866b547b3d4b9"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
colorama = ">=0.3.3"
|
||||||
|
docopt = ">=0.4.0"
|
||||||
|
pytest = ">=2.6.4"
|
||||||
|
watchdog = ">=0.6.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ruff"
|
||||||
|
version = "0.1.6"
|
||||||
|
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "ruff-0.1.6-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:88b8cdf6abf98130991cbc9f6438f35f6e8d41a02622cc5ee130a02a0ed28703"},
|
||||||
|
{file = "ruff-0.1.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5c549ed437680b6105a1299d2cd30e4964211606eeb48a0ff7a93ef70b902248"},
|
||||||
|
{file = "ruff-0.1.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cf5f701062e294f2167e66d11b092bba7af6a057668ed618a9253e1e90cfd76"},
|
||||||
|
{file = "ruff-0.1.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:05991ee20d4ac4bb78385360c684e4b417edd971030ab12a4fbd075ff535050e"},
|
||||||
|
{file = "ruff-0.1.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87455a0c1f739b3c069e2f4c43b66479a54dea0276dd5d4d67b091265f6fd1dc"},
|
||||||
|
{file = "ruff-0.1.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:683aa5bdda5a48cb8266fcde8eea2a6af4e5700a392c56ea5fb5f0d4bfdc0240"},
|
||||||
|
{file = "ruff-0.1.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:137852105586dcbf80c1717facb6781555c4e99f520c9c827bd414fac67ddfb6"},
|
||||||
|
{file = "ruff-0.1.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd98138a98d48a1c36c394fd6b84cd943ac92a08278aa8ac8c0fdefcf7138f35"},
|
||||||
|
{file = "ruff-0.1.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a0cd909d25f227ac5c36d4e7e681577275fb74ba3b11d288aff7ec47e3ae745"},
|
||||||
|
{file = "ruff-0.1.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8fd1c62a47aa88a02707b5dd20c5ff20d035d634aa74826b42a1da77861b5ff"},
|
||||||
|
{file = "ruff-0.1.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fd89b45d374935829134a082617954120d7a1470a9f0ec0e7f3ead983edc48cc"},
|
||||||
|
{file = "ruff-0.1.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:491262006e92f825b145cd1e52948073c56560243b55fb3b4ecb142f6f0e9543"},
|
||||||
|
{file = "ruff-0.1.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ea284789861b8b5ca9d5443591a92a397ac183d4351882ab52f6296b4fdd5462"},
|
||||||
|
{file = "ruff-0.1.6-py3-none-win32.whl", hash = "sha256:1610e14750826dfc207ccbcdd7331b6bd285607d4181df9c1c6ae26646d6848a"},
|
||||||
|
{file = "ruff-0.1.6-py3-none-win_amd64.whl", hash = "sha256:4558b3e178145491e9bc3b2ee3c4b42f19d19384eaa5c59d10acf6e8f8b57e33"},
|
||||||
|
{file = "ruff-0.1.6-py3-none-win_arm64.whl", hash = "sha256:03910e81df0d8db0e30050725a5802441c2022ea3ae4fe0609b76081731accbc"},
|
||||||
|
{file = "ruff-0.1.6.tar.gz", hash = "sha256:1b09f29b16c6ead5ea6b097ef2764b42372aebe363722f1605ecbcd2b9207184"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tomli"
|
||||||
|
version = "2.0.1"
|
||||||
|
description = "A lil' TOML parser"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
||||||
|
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.12.0"
|
||||||
|
description = "Backported and Experimental Type Hints for Python 3.8+"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "typing_extensions-4.12.0-py3-none-any.whl", hash = "sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594"},
|
||||||
|
{file = "typing_extensions-4.12.0.tar.gz", hash = "sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "watchdog"
|
||||||
|
version = "3.0.0"
|
||||||
|
description = "Filesystem events monitoring"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:336adfc6f5cc4e037d52db31194f7581ff744b67382eb6021c868322e32eef41"},
|
||||||
|
{file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a70a8dcde91be523c35b2bf96196edc5730edb347e374c7de7cd20c43ed95397"},
|
||||||
|
{file = "watchdog-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:adfdeab2da79ea2f76f87eb42a3ab1966a5313e5a69a0213a3cc06ef692b0e96"},
|
||||||
|
{file = "watchdog-3.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b57a1e730af3156d13b7fdddfc23dea6487fceca29fc75c5a868beed29177ae"},
|
||||||
|
{file = "watchdog-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7ade88d0d778b1b222adebcc0927428f883db07017618a5e684fd03b83342bd9"},
|
||||||
|
{file = "watchdog-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7e447d172af52ad204d19982739aa2346245cc5ba6f579d16dac4bfec226d2e7"},
|
||||||
|
{file = "watchdog-3.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9fac43a7466eb73e64a9940ac9ed6369baa39b3bf221ae23493a9ec4d0022674"},
|
||||||
|
{file = "watchdog-3.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8ae9cda41fa114e28faf86cb137d751a17ffd0316d1c34ccf2235e8a84365c7f"},
|
||||||
|
{file = "watchdog-3.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:25f70b4aa53bd743729c7475d7ec41093a580528b100e9a8c5b5efe8899592fc"},
|
||||||
|
{file = "watchdog-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4f94069eb16657d2c6faada4624c39464f65c05606af50bb7902e036e3219be3"},
|
||||||
|
{file = "watchdog-3.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7c5f84b5194c24dd573fa6472685b2a27cc5a17fe5f7b6fd40345378ca6812e3"},
|
||||||
|
{file = "watchdog-3.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3aa7f6a12e831ddfe78cdd4f8996af9cf334fd6346531b16cec61c3b3c0d8da0"},
|
||||||
|
{file = "watchdog-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:233b5817932685d39a7896b1090353fc8efc1ef99c9c054e46c8002561252fb8"},
|
||||||
|
{file = "watchdog-3.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:13bbbb462ee42ec3c5723e1205be8ced776f05b100e4737518c67c8325cf6100"},
|
||||||
|
{file = "watchdog-3.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8f3ceecd20d71067c7fd4c9e832d4e22584318983cabc013dbf3f70ea95de346"},
|
||||||
|
{file = "watchdog-3.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c9d8c8ec7efb887333cf71e328e39cffbf771d8f8f95d308ea4125bf5f90ba64"},
|
||||||
|
{file = "watchdog-3.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0e06ab8858a76e1219e68c7573dfeba9dd1c0219476c5a44d5333b01d7e1743a"},
|
||||||
|
{file = "watchdog-3.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:d00e6be486affb5781468457b21a6cbe848c33ef43f9ea4a73b4882e5f188a44"},
|
||||||
|
{file = "watchdog-3.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:c07253088265c363d1ddf4b3cdb808d59a0468ecd017770ed716991620b8f77a"},
|
||||||
|
{file = "watchdog-3.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:5113334cf8cf0ac8cd45e1f8309a603291b614191c9add34d33075727a967709"},
|
||||||
|
{file = "watchdog-3.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:51f90f73b4697bac9c9a78394c3acbbd331ccd3655c11be1a15ae6fe289a8c83"},
|
||||||
|
{file = "watchdog-3.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:ba07e92756c97e3aca0912b5cbc4e5ad802f4557212788e72a72a47ff376950d"},
|
||||||
|
{file = "watchdog-3.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:d429c2430c93b7903914e4db9a966c7f2b068dd2ebdd2fa9b9ce094c7d459f33"},
|
||||||
|
{file = "watchdog-3.0.0-py3-none-win32.whl", hash = "sha256:3ed7c71a9dccfe838c2f0b6314ed0d9b22e77d268c67e015450a29036a81f60f"},
|
||||||
|
{file = "watchdog-3.0.0-py3-none-win_amd64.whl", hash = "sha256:4c9956d27be0bb08fc5f30d9d0179a855436e655f046d288e2bcc11adfae893c"},
|
||||||
|
{file = "watchdog-3.0.0-py3-none-win_ia64.whl", hash = "sha256:5d9f3a10e02d7371cd929b5d8f11e87d4bad890212ed3901f9b4d68767bee759"},
|
||||||
|
{file = "watchdog-3.0.0.tar.gz", hash = "sha256:4d98a320595da7a7c5a18fc48cb633c2e73cda78f93cac2ef42d42bf609a33f9"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
watchmedo = ["PyYAML (>=3.10)"]
|
||||||
|
|
||||||
|
[metadata]
|
||||||
|
lock-version = "2.0"
|
||||||
|
python-versions = "^3.9.0,<4.0"
|
||||||
|
content-hash = "5efa2f1ed4bd611a45e5d43d7c3fb907a8fa4447e2d1c30ce26b830411e189dd"
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
[tool.poetry]
|
||||||
|
name = "langgraph-cli"
|
||||||
|
version = "0.1.35"
|
||||||
|
description = "CLI for interacting with LangGraph API"
|
||||||
|
authors = ["Nuno Campos <nuno@langchain.dev>"]
|
||||||
|
readme = "README.md"
|
||||||
|
packages = [{include = "langgraph_cli"}]
|
||||||
|
|
||||||
|
[tool.poetry.scripts]
|
||||||
|
langgraph = "langgraph_cli.cli:cli"
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.9.0,<4.0"
|
||||||
|
click = "^8.1.7"
|
||||||
|
|
||||||
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
ruff = "^0.1.4"
|
||||||
|
codespell = "^2.2.0"
|
||||||
|
pytest = "^7.2.1"
|
||||||
|
pytest-asyncio = "^0.21.1"
|
||||||
|
pytest-mock = "^3.11.1"
|
||||||
|
pytest-watch = "^4.2.0"
|
||||||
|
mypy = "^1.10.0"
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
# --strict-markers will raise errors on unknown marks.
|
||||||
|
# https://docs.pytest.org/en/7.1.x/how-to/mark.html#raising-errors-on-unknown-marks
|
||||||
|
#
|
||||||
|
# https://docs.pytest.org/en/7.1.x/reference/reference.html
|
||||||
|
# --strict-config any warnings encountered while parsing the `pytest`
|
||||||
|
# section of the configuration file raise errors.
|
||||||
|
addopts = "--strict-markers --strict-config --durations=5 -vv"
|
||||||
|
asyncio_mode = "auto"
|
||||||
|
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
select = [
|
||||||
|
# pycodestyle
|
||||||
|
"E",
|
||||||
|
# Pyflakes
|
||||||
|
"F",
|
||||||
|
# pyupgrade
|
||||||
|
"UP",
|
||||||
|
# flake8-bugbear
|
||||||
|
"B",
|
||||||
|
# isort
|
||||||
|
"I",
|
||||||
|
]
|
||||||
|
ignore = [ "E501", "B008" ]
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
def clean_empty_lines(input_str: str):
|
||||||
|
return "\n".join(filter(None, input_str.splitlines()))
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import pathlib
|
||||||
|
|
||||||
|
from langgraph_cli.cli import prepare_args_and_stdin
|
||||||
|
from langgraph_cli.config import Config, validate_config
|
||||||
|
from langgraph_cli.docker import DEFAULT_POSTGRES_URI, DockerCapabilities, Version
|
||||||
|
|
||||||
|
from .helpers import clean_empty_lines
|
||||||
|
|
||||||
|
DEFAULT_DOCKER_CAPABILITIES = DockerCapabilities(
|
||||||
|
version_docker=Version(26, 1, 1),
|
||||||
|
version_compose=Version(2, 27, 0),
|
||||||
|
healthcheck_start_interval=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_prepare_args_and_stdin():
|
||||||
|
# this basically serves as an end-to-end test for using config and docker helpers
|
||||||
|
config_path = pathlib.Path("./langgraph.json")
|
||||||
|
config = validate_config(
|
||||||
|
Config(dependencies=["."], graphs={"agent": "agent.py:graph"})
|
||||||
|
)
|
||||||
|
port = 8000
|
||||||
|
debugger_port = 8001
|
||||||
|
|
||||||
|
actual_args, actual_stdin = prepare_args_and_stdin(
|
||||||
|
capabilities=DEFAULT_DOCKER_CAPABILITIES,
|
||||||
|
config_path=config_path,
|
||||||
|
config=config,
|
||||||
|
docker_compose="custom-docker-compose.yml",
|
||||||
|
port=port,
|
||||||
|
debugger_port=debugger_port,
|
||||||
|
watch=True,
|
||||||
|
langgraph_api_path="path/to/langgraph-api",
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_args = [
|
||||||
|
"--project-directory",
|
||||||
|
".",
|
||||||
|
"-f",
|
||||||
|
"custom-docker-compose.yml",
|
||||||
|
"-f",
|
||||||
|
"-",
|
||||||
|
]
|
||||||
|
expected_stdin = f"""volumes:
|
||||||
|
langgraph-data:
|
||||||
|
driver: local
|
||||||
|
services:
|
||||||
|
langgraph-postgres:
|
||||||
|
image: postgres:16
|
||||||
|
restart: on-failure
|
||||||
|
ports:
|
||||||
|
- "5433:5432"
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: postgres
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
volumes:
|
||||||
|
- langgraph-data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: pg_isready -U postgres
|
||||||
|
start_period: 10s
|
||||||
|
timeout: 1s
|
||||||
|
retries: 5
|
||||||
|
interval: 60s
|
||||||
|
start_interval: 1s
|
||||||
|
langgraph-debugger:
|
||||||
|
image: langchain/langgraph-debugger
|
||||||
|
restart: on-failure
|
||||||
|
ports:
|
||||||
|
- "{debugger_port}:80"
|
||||||
|
depends_on:
|
||||||
|
langgraph-postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
langgraph-api:
|
||||||
|
restart: on-failure
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
depends_on:
|
||||||
|
langgraph-postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
POSTGRES_URI: {DEFAULT_POSTGRES_URI}
|
||||||
|
healthcheck:
|
||||||
|
interval: 60s
|
||||||
|
start_interval: 1s
|
||||||
|
start_period: 10s
|
||||||
|
|
||||||
|
pull_policy: build
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile_inline: |
|
||||||
|
FROM langchain/langgraph-api:3.11
|
||||||
|
ADD . /deps/
|
||||||
|
RUN pip install -c /api/constraints.txt -e /deps/*
|
||||||
|
ENV LANGSERVE_GRAPHS='{{"agent": "agent.py:graph"}}'
|
||||||
|
WORKDIR /deps/
|
||||||
|
|
||||||
|
develop:
|
||||||
|
watch:
|
||||||
|
- path: langgraph.json
|
||||||
|
action: rebuild
|
||||||
|
ignore:
|
||||||
|
- .langgraph-data
|
||||||
|
- path: .
|
||||||
|
action: rebuild
|
||||||
|
ignore:
|
||||||
|
- .langgraph-data
|
||||||
|
- path: path/to/langgraph-api
|
||||||
|
action: sync+restart
|
||||||
|
target: /api/langgraph_api\
|
||||||
|
"""
|
||||||
|
assert actual_args == expected_args
|
||||||
|
assert clean_empty_lines(actual_stdin) == expected_stdin
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"python_version": "3.12",
|
||||||
|
"pip_config_file": "pipconfig.txt",
|
||||||
|
"dockerfile_lines": ["ARG meow"],
|
||||||
|
"dependencies": [
|
||||||
|
"langchain_openai",
|
||||||
|
"."
|
||||||
|
],
|
||||||
|
"graphs": {
|
||||||
|
"agent": "tests/unit_tests/agent.py:graph"
|
||||||
|
},
|
||||||
|
"env": ".env"
|
||||||
|
}
|
||||||
@@ -0,0 +1,396 @@
|
|||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
import click
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from langgraph_cli.config import config_to_compose, config_to_docker, validate_config
|
||||||
|
|
||||||
|
from .helpers import clean_empty_lines
|
||||||
|
|
||||||
|
PATH_TO_CONFIG = pathlib.Path("tests/unit_tests/test_config.json")
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_config():
|
||||||
|
# minimal config
|
||||||
|
expected_config = {
|
||||||
|
"dependencies": ["."],
|
||||||
|
"graphs": {
|
||||||
|
"agent": "./agent.py:graph",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
expected_config = {
|
||||||
|
"python_version": "3.11",
|
||||||
|
"pip_config_file": None,
|
||||||
|
"dockerfile_lines": [],
|
||||||
|
"env": {},
|
||||||
|
**expected_config,
|
||||||
|
}
|
||||||
|
actual_config = validate_config(expected_config)
|
||||||
|
assert actual_config == expected_config
|
||||||
|
|
||||||
|
# full config
|
||||||
|
env = ".env"
|
||||||
|
expected_config = {
|
||||||
|
"python_version": "3.12",
|
||||||
|
"pip_config_file": "pipconfig.txt",
|
||||||
|
"dockerfile_lines": ["ARG meow"],
|
||||||
|
"dependencies": [".", "langchain"],
|
||||||
|
"graphs": {
|
||||||
|
"agent": "./agent.py:graph",
|
||||||
|
},
|
||||||
|
"env": env,
|
||||||
|
}
|
||||||
|
actual_config = validate_config(expected_config)
|
||||||
|
assert actual_config == expected_config
|
||||||
|
|
||||||
|
# check wrong python version raises
|
||||||
|
with pytest.raises(click.UsageError):
|
||||||
|
validate_config(
|
||||||
|
{
|
||||||
|
"python_version": "3.9",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# check missing dependencies key raises
|
||||||
|
with pytest.raises(click.UsageError):
|
||||||
|
validate_config(
|
||||||
|
{"python_version": "3.9", "graphs": {"agent": "./agent.py:graph"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
# check missing graphs key raises
|
||||||
|
with pytest.raises(click.UsageError):
|
||||||
|
validate_config({"python_version": "3.9", "dependencies": ["."]})
|
||||||
|
|
||||||
|
|
||||||
|
# config_to_docker
|
||||||
|
def test_config_to_docker_simple():
|
||||||
|
graphs = {"agent": "./agent.py:graph"}
|
||||||
|
actual_docker_stdin = config_to_docker(
|
||||||
|
PATH_TO_CONFIG, validate_config({"dependencies": ["."], "graphs": graphs})
|
||||||
|
)
|
||||||
|
expected_docker_stdin = """\
|
||||||
|
FROM langchain/langgraph-api:3.11
|
||||||
|
ADD . /deps/__outer_unit_tests/unit_tests
|
||||||
|
COPY <<EOF /deps/__outer_unit_tests/pyproject.toml
|
||||||
|
[project]
|
||||||
|
name = "unit_tests"
|
||||||
|
version = "0.1"
|
||||||
|
[tool.setuptools.package-data]
|
||||||
|
"*" = ["**/*"]
|
||||||
|
EOF
|
||||||
|
RUN pip install -c /api/constraints.txt -e /deps/*
|
||||||
|
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
|
||||||
|
WORKDIR /deps/__outer_unit_tests/unit_tests\
|
||||||
|
"""
|
||||||
|
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_to_docker_pipconfig():
|
||||||
|
graphs = {"agent": "./agent.py:graph"}
|
||||||
|
actual_docker_stdin = config_to_docker(
|
||||||
|
PATH_TO_CONFIG,
|
||||||
|
validate_config(
|
||||||
|
{
|
||||||
|
"dependencies": ["."],
|
||||||
|
"graphs": graphs,
|
||||||
|
"pip_config_file": "pipconfig.txt",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
expected_docker_stdin = """\
|
||||||
|
FROM langchain/langgraph-api:3.11
|
||||||
|
ADD pipconfig.txt /pipconfig.txt
|
||||||
|
ADD . /deps/__outer_unit_tests/unit_tests
|
||||||
|
COPY <<EOF /deps/__outer_unit_tests/pyproject.toml
|
||||||
|
[project]
|
||||||
|
name = "unit_tests"
|
||||||
|
version = "0.1"
|
||||||
|
[tool.setuptools.package-data]
|
||||||
|
"*" = ["**/*"]
|
||||||
|
EOF
|
||||||
|
RUN PIP_CONFIG_FILE=/pipconfig.txt pip install -c /api/constraints.txt -e /deps/*
|
||||||
|
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
|
||||||
|
WORKDIR /deps/__outer_unit_tests/unit_tests\
|
||||||
|
"""
|
||||||
|
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_to_docker_invalid_inputs():
|
||||||
|
# test missing local dependencies
|
||||||
|
with pytest.raises(FileNotFoundError):
|
||||||
|
graphs = {"agent": "tests/unit_tests/agent.py:graph"}
|
||||||
|
config_to_docker(
|
||||||
|
PATH_TO_CONFIG,
|
||||||
|
validate_config({"dependencies": ["./missing"], "graphs": graphs}),
|
||||||
|
)
|
||||||
|
|
||||||
|
# test missing local module
|
||||||
|
with pytest.raises(FileNotFoundError):
|
||||||
|
graphs = {"agent": "./missing_agent.py:graph"}
|
||||||
|
config_to_docker(
|
||||||
|
PATH_TO_CONFIG, validate_config({"dependencies": ["."], "graphs": graphs})
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_to_docker_local_deps():
|
||||||
|
graphs = {"agent": "./graphs/agent.py:graph"}
|
||||||
|
actual_docker_stdin = config_to_docker(
|
||||||
|
PATH_TO_CONFIG,
|
||||||
|
validate_config(
|
||||||
|
{
|
||||||
|
"dependencies": ["./graphs"],
|
||||||
|
"graphs": graphs,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
expected_docker_stdin = """\
|
||||||
|
FROM langchain/langgraph-api:3.11
|
||||||
|
ADD ./graphs /deps/__outer_graphs/src
|
||||||
|
COPY <<EOF /deps/__outer_graphs/pyproject.toml
|
||||||
|
[project]
|
||||||
|
name = "graphs"
|
||||||
|
version = "0.1"
|
||||||
|
[tool.setuptools.package-data]
|
||||||
|
"*" = ["**/*"]
|
||||||
|
EOF
|
||||||
|
RUN pip install -c /api/constraints.txt -e /deps/*
|
||||||
|
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_graphs/src/agent.py:graph"}'\
|
||||||
|
"""
|
||||||
|
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_to_docker_pyproject():
|
||||||
|
pyproject_str = """[project]
|
||||||
|
name = "custom"
|
||||||
|
version = "0.1"
|
||||||
|
dependencies = ["langchain"]"""
|
||||||
|
pyproject_path = "tests/unit_tests/pyproject.toml"
|
||||||
|
with open(pyproject_path, "w") as f:
|
||||||
|
f.write(pyproject_str)
|
||||||
|
|
||||||
|
graphs = {"agent": "./graphs/agent.py:graph"}
|
||||||
|
actual_docker_stdin = config_to_docker(
|
||||||
|
PATH_TO_CONFIG,
|
||||||
|
validate_config(
|
||||||
|
{
|
||||||
|
"dependencies": ["."],
|
||||||
|
"graphs": graphs,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
os.remove(pyproject_path)
|
||||||
|
expected_docker_stdin = """FROM langchain/langgraph-api:3.11
|
||||||
|
ADD . /deps/unit_tests
|
||||||
|
RUN pip install -c /api/constraints.txt -e /deps/*
|
||||||
|
ENV LANGSERVE_GRAPHS='{"agent": "/deps/unit_tests/graphs/agent.py:graph"}'
|
||||||
|
WORKDIR /deps/unit_tests"""
|
||||||
|
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_to_docker_end_to_end():
|
||||||
|
graphs = {"agent": "./graphs/agent.py:graph"}
|
||||||
|
actual_docker_stdin = config_to_docker(
|
||||||
|
PATH_TO_CONFIG,
|
||||||
|
validate_config(
|
||||||
|
{
|
||||||
|
"python_version": "3.12",
|
||||||
|
"dependencies": ["./graphs/", "langchain", "langchain_openai"],
|
||||||
|
"graphs": graphs,
|
||||||
|
"pip_config_file": "pipconfig.txt",
|
||||||
|
"dockerfile_lines": ["ARG meow", "ARG foo"],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
expected_docker_stdin = """FROM langchain/langgraph-api:3.12
|
||||||
|
ARG meow
|
||||||
|
ARG foo
|
||||||
|
ADD pipconfig.txt /pipconfig.txt
|
||||||
|
RUN PIP_CONFIG_FILE=/pipconfig.txt pip install -c /api/constraints.txt langchain langchain_openai
|
||||||
|
ADD ./graphs/ /deps/__outer_graphs/src
|
||||||
|
COPY <<EOF /deps/__outer_graphs/pyproject.toml
|
||||||
|
[project]
|
||||||
|
name = "graphs"
|
||||||
|
version = "0.1"
|
||||||
|
[tool.setuptools.package-data]
|
||||||
|
"*" = ["**/*"]
|
||||||
|
EOF
|
||||||
|
RUN PIP_CONFIG_FILE=/pipconfig.txt pip install -c /api/constraints.txt -e /deps/*
|
||||||
|
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_graphs/src/agent.py:graph"}'"""
|
||||||
|
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
||||||
|
|
||||||
|
|
||||||
|
# config_to_compose
|
||||||
|
def test_config_to_compose_simple_config():
|
||||||
|
graphs = {"agent": "./agent.py:graph"}
|
||||||
|
expected_compose_stdin = """\
|
||||||
|
|
||||||
|
pull_policy: build
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile_inline: |
|
||||||
|
FROM langchain/langgraph-api:3.11
|
||||||
|
ADD . /deps/__outer_unit_tests/unit_tests
|
||||||
|
COPY <<EOF /deps/__outer_unit_tests/pyproject.toml
|
||||||
|
[project]
|
||||||
|
name = "unit_tests"
|
||||||
|
version = "0.1"
|
||||||
|
[tool.setuptools.package-data]
|
||||||
|
"*" = ["**/*"]
|
||||||
|
EOF
|
||||||
|
RUN pip install -c /api/constraints.txt -e /deps/*
|
||||||
|
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
|
||||||
|
WORKDIR /deps/__outer_unit_tests/unit_tests
|
||||||
|
"""
|
||||||
|
actual_compose_stdin = config_to_compose(
|
||||||
|
PATH_TO_CONFIG, validate_config({"dependencies": ["."], "graphs": graphs})
|
||||||
|
)
|
||||||
|
assert clean_empty_lines(actual_compose_stdin) == expected_compose_stdin
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_to_compose_env_vars():
|
||||||
|
graphs = {"agent": "./agent.py:graph"}
|
||||||
|
expected_compose_stdin = """ OPENAI_API_KEY: key
|
||||||
|
|
||||||
|
pull_policy: build
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile_inline: |
|
||||||
|
FROM langchain/langgraph-api:3.11
|
||||||
|
ADD . /deps/__outer_unit_tests/unit_tests
|
||||||
|
COPY <<EOF /deps/__outer_unit_tests/pyproject.toml
|
||||||
|
[project]
|
||||||
|
name = "unit_tests"
|
||||||
|
version = "0.1"
|
||||||
|
[tool.setuptools.package-data]
|
||||||
|
"*" = ["**/*"]
|
||||||
|
EOF
|
||||||
|
RUN pip install -c /api/constraints.txt -e /deps/*
|
||||||
|
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
|
||||||
|
WORKDIR /deps/__outer_unit_tests/unit_tests
|
||||||
|
"""
|
||||||
|
openai_api_key = "key"
|
||||||
|
actual_compose_stdin = config_to_compose(
|
||||||
|
PATH_TO_CONFIG,
|
||||||
|
validate_config(
|
||||||
|
{
|
||||||
|
"dependencies": ["."],
|
||||||
|
"graphs": graphs,
|
||||||
|
"env": {"OPENAI_API_KEY": openai_api_key},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assert clean_empty_lines(actual_compose_stdin) == expected_compose_stdin
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_to_compose_env_file():
|
||||||
|
graphs = {"agent": "./agent.py:graph"}
|
||||||
|
expected_compose_stdin = """\
|
||||||
|
env_file: .env
|
||||||
|
pull_policy: build
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile_inline: |
|
||||||
|
FROM langchain/langgraph-api:3.11
|
||||||
|
ADD . /deps/__outer_unit_tests/unit_tests
|
||||||
|
COPY <<EOF /deps/__outer_unit_tests/pyproject.toml
|
||||||
|
[project]
|
||||||
|
name = "unit_tests"
|
||||||
|
version = "0.1"
|
||||||
|
[tool.setuptools.package-data]
|
||||||
|
"*" = ["**/*"]
|
||||||
|
EOF
|
||||||
|
RUN pip install -c /api/constraints.txt -e /deps/*
|
||||||
|
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
|
||||||
|
WORKDIR /deps/__outer_unit_tests/unit_tests
|
||||||
|
"""
|
||||||
|
actual_compose_stdin = config_to_compose(
|
||||||
|
PATH_TO_CONFIG,
|
||||||
|
validate_config({"dependencies": ["."], "graphs": graphs, "env": ".env"}),
|
||||||
|
)
|
||||||
|
assert clean_empty_lines(actual_compose_stdin) == expected_compose_stdin
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_to_compose_watch():
|
||||||
|
graphs = {"agent": "./agent.py:graph"}
|
||||||
|
expected_compose_stdin = """\
|
||||||
|
|
||||||
|
pull_policy: build
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile_inline: |
|
||||||
|
FROM langchain/langgraph-api:3.11
|
||||||
|
ADD . /deps/__outer_unit_tests/unit_tests
|
||||||
|
COPY <<EOF /deps/__outer_unit_tests/pyproject.toml
|
||||||
|
[project]
|
||||||
|
name = "unit_tests"
|
||||||
|
version = "0.1"
|
||||||
|
[tool.setuptools.package-data]
|
||||||
|
"*" = ["**/*"]
|
||||||
|
EOF
|
||||||
|
RUN pip install -c /api/constraints.txt -e /deps/*
|
||||||
|
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
|
||||||
|
WORKDIR /deps/__outer_unit_tests/unit_tests
|
||||||
|
|
||||||
|
develop:
|
||||||
|
watch:
|
||||||
|
- path: tests/unit_tests/test_config.json
|
||||||
|
action: rebuild
|
||||||
|
ignore:
|
||||||
|
- .langgraph-data
|
||||||
|
- path: tests/unit_tests
|
||||||
|
action: rebuild
|
||||||
|
ignore:
|
||||||
|
- .langgraph-data\
|
||||||
|
"""
|
||||||
|
actual_compose_stdin = config_to_compose(
|
||||||
|
PATH_TO_CONFIG,
|
||||||
|
validate_config({"dependencies": ["."], "graphs": graphs}),
|
||||||
|
watch=True,
|
||||||
|
)
|
||||||
|
assert clean_empty_lines(actual_compose_stdin) == expected_compose_stdin
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_to_compose_end_to_end():
|
||||||
|
# test all of the above + langgraph API path
|
||||||
|
graphs = {"agent": "./agent.py:graph"}
|
||||||
|
expected_compose_stdin = """\
|
||||||
|
env_file: .env
|
||||||
|
pull_policy: build
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile_inline: |
|
||||||
|
FROM langchain/langgraph-api:3.11
|
||||||
|
ADD . /deps/__outer_unit_tests/unit_tests
|
||||||
|
COPY <<EOF /deps/__outer_unit_tests/pyproject.toml
|
||||||
|
[project]
|
||||||
|
name = "unit_tests"
|
||||||
|
version = "0.1"
|
||||||
|
[tool.setuptools.package-data]
|
||||||
|
"*" = ["**/*"]
|
||||||
|
EOF
|
||||||
|
RUN pip install -c /api/constraints.txt -e /deps/*
|
||||||
|
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
|
||||||
|
WORKDIR /deps/__outer_unit_tests/unit_tests
|
||||||
|
|
||||||
|
develop:
|
||||||
|
watch:
|
||||||
|
- path: tests/unit_tests/test_config.json
|
||||||
|
action: rebuild
|
||||||
|
ignore:
|
||||||
|
- .langgraph-data
|
||||||
|
- path: tests/unit_tests
|
||||||
|
action: rebuild
|
||||||
|
ignore:
|
||||||
|
- .langgraph-data
|
||||||
|
- path: path/to/langgraph/api
|
||||||
|
action: sync+restart
|
||||||
|
target: /api/langgraph_api\
|
||||||
|
"""
|
||||||
|
actual_compose_stdin = config_to_compose(
|
||||||
|
PATH_TO_CONFIG,
|
||||||
|
validate_config({"dependencies": ["."], "graphs": graphs, "env": ".env"}),
|
||||||
|
watch=True,
|
||||||
|
langgraph_api_path="path/to/langgraph/api",
|
||||||
|
)
|
||||||
|
assert clean_empty_lines(actual_compose_stdin) == expected_compose_stdin
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
from langgraph_cli.docker import (
|
||||||
|
DEFAULT_POSTGRES_URI,
|
||||||
|
DockerCapabilities,
|
||||||
|
Version,
|
||||||
|
compose,
|
||||||
|
)
|
||||||
|
from tests.unit_tests.helpers import clean_empty_lines
|
||||||
|
|
||||||
|
DEFAULT_DOCKER_CAPABILITIES = DockerCapabilities(
|
||||||
|
version_docker=Version(26, 1, 1),
|
||||||
|
version_compose=Version(2, 27, 0),
|
||||||
|
healthcheck_start_interval=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_compose_with_no_debugger_and_custom_db():
|
||||||
|
port = 8123
|
||||||
|
custom_postgres_uri = "custom_postgres_uri"
|
||||||
|
actual_compose_str = compose(
|
||||||
|
DEFAULT_DOCKER_CAPABILITIES, port=port, postgres_uri=custom_postgres_uri
|
||||||
|
)
|
||||||
|
expected_compose_str = f"""services:
|
||||||
|
langgraph-api:
|
||||||
|
restart: on-failure
|
||||||
|
ports:
|
||||||
|
- "{port}:8000"
|
||||||
|
depends_on:
|
||||||
|
langgraph-postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
POSTGRES_URI: {custom_postgres_uri}"""
|
||||||
|
assert clean_empty_lines(actual_compose_str) == expected_compose_str
|
||||||
|
|
||||||
|
|
||||||
|
def test_compose_with_no_debugger_and_custom_db_with_healthcheck():
|
||||||
|
port = 8123
|
||||||
|
custom_postgres_uri = "custom_postgres_uri"
|
||||||
|
actual_compose_str = compose(
|
||||||
|
DEFAULT_DOCKER_CAPABILITIES._replace(healthcheck_start_interval=True),
|
||||||
|
port=port,
|
||||||
|
postgres_uri=custom_postgres_uri,
|
||||||
|
)
|
||||||
|
expected_compose_str = f"""services:
|
||||||
|
langgraph-api:
|
||||||
|
restart: on-failure
|
||||||
|
ports:
|
||||||
|
- "{port}:8000"
|
||||||
|
depends_on:
|
||||||
|
langgraph-postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
POSTGRES_URI: {custom_postgres_uri}
|
||||||
|
healthcheck:
|
||||||
|
interval: 60s
|
||||||
|
start_interval: 1s
|
||||||
|
start_period: 10s"""
|
||||||
|
assert clean_empty_lines(actual_compose_str) == expected_compose_str
|
||||||
|
|
||||||
|
|
||||||
|
def test_compose_with_debugger_and_custom_db():
|
||||||
|
port = 8123
|
||||||
|
custom_postgres_uri = "custom_postgres_uri"
|
||||||
|
actual_compose_str = compose(
|
||||||
|
DEFAULT_DOCKER_CAPABILITIES,
|
||||||
|
port=port,
|
||||||
|
postgres_uri=custom_postgres_uri,
|
||||||
|
)
|
||||||
|
expected_compose_str = f"""services:
|
||||||
|
langgraph-api:
|
||||||
|
restart: on-failure
|
||||||
|
ports:
|
||||||
|
- "{port}:8000"
|
||||||
|
depends_on:
|
||||||
|
langgraph-postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
POSTGRES_URI: {custom_postgres_uri}"""
|
||||||
|
assert clean_empty_lines(actual_compose_str) == expected_compose_str
|
||||||
|
|
||||||
|
|
||||||
|
def test_compose_with_debugger_and_default_db():
|
||||||
|
port = 8123
|
||||||
|
actual_compose_str = compose(DEFAULT_DOCKER_CAPABILITIES, port=port)
|
||||||
|
expected_compose_str = f"""volumes:
|
||||||
|
langgraph-data:
|
||||||
|
driver: local
|
||||||
|
services:
|
||||||
|
langgraph-postgres:
|
||||||
|
image: postgres:16
|
||||||
|
restart: on-failure
|
||||||
|
ports:
|
||||||
|
- "5433:5432"
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: postgres
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
volumes:
|
||||||
|
- langgraph-data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: pg_isready -U postgres
|
||||||
|
start_period: 10s
|
||||||
|
timeout: 1s
|
||||||
|
retries: 5
|
||||||
|
interval: 5s
|
||||||
|
langgraph-api:
|
||||||
|
restart: on-failure
|
||||||
|
ports:
|
||||||
|
- "{port}:8000"
|
||||||
|
depends_on:
|
||||||
|
langgraph-postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
POSTGRES_URI: {DEFAULT_POSTGRES_URI}"""
|
||||||
|
assert clean_empty_lines(actual_compose_str) == expected_compose_str
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 LangChain, Inc.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
.PHONY: all format lint test test_watch integration_tests spell_check spell_fix
|
||||||
|
|
||||||
|
# Default target executed when no arguments are given to make.
|
||||||
|
all: help
|
||||||
|
|
||||||
|
######################
|
||||||
|
# TESTING AND COVERAGE
|
||||||
|
######################
|
||||||
|
|
||||||
|
# Run unit tests and generate a coverage report.
|
||||||
|
coverage:
|
||||||
|
poetry run pytest --cov \
|
||||||
|
--cov-config=.coveragerc \
|
||||||
|
--cov-report xml \
|
||||||
|
--cov-report term-missing:skip-covered
|
||||||
|
|
||||||
|
test:
|
||||||
|
poetry run pytest
|
||||||
|
|
||||||
|
test_watch:
|
||||||
|
poetry run ptw .
|
||||||
|
|
||||||
|
######################
|
||||||
|
# LINTING AND FORMATTING
|
||||||
|
######################
|
||||||
|
|
||||||
|
# Define a variable for Python and notebook files.
|
||||||
|
PYTHON_FILES=.
|
||||||
|
MYPY_CACHE=.mypy_cache
|
||||||
|
lint format: PYTHON_FILES=.
|
||||||
|
lint_diff format_diff: PYTHON_FILES=$(shell git diff --name-only --diff-filter=d master | grep -E '\.py$$|\.ipynb$$')
|
||||||
|
lint_package: PYTHON_FILES=langgraph
|
||||||
|
lint_tests: PYTHON_FILES=tests
|
||||||
|
lint_tests: MYPY_CACHE=.mypy_cache_test
|
||||||
|
|
||||||
|
lint lint_diff lint_package lint_tests:
|
||||||
|
poetry run ruff .
|
||||||
|
[ "$(PYTHON_FILES)" = "" ] || poetry run ruff format $(PYTHON_FILES) --diff
|
||||||
|
[ "$(PYTHON_FILES)" = "" ] || poetry run ruff --select I $(PYTHON_FILES)
|
||||||
|
[ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE) || poetry run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE)
|
||||||
|
|
||||||
|
format format_diff:
|
||||||
|
poetry run ruff format $(PYTHON_FILES)
|
||||||
|
poetry run ruff --select I --fix $(PYTHON_FILES)
|
||||||
|
|
||||||
|
spell_check:
|
||||||
|
poetry run codespell --toml pyproject.toml
|
||||||
|
|
||||||
|
spell_fix:
|
||||||
|
poetry run codespell --toml pyproject.toml -w
|
||||||
|
|
||||||
|
|
||||||
|
######################
|
||||||
|
# HELP
|
||||||
|
######################
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo '===================='
|
||||||
|
@echo '-- DOCUMENTATION --'
|
||||||
|
|
||||||
|
@echo '-- LINTING --'
|
||||||
|
@echo 'format - run code formatters'
|
||||||
|
@echo 'lint - run linters'
|
||||||
|
@echo 'spell_check - run codespell on the project'
|
||||||
|
@echo 'spell_fix - run codespell on the project and fix the errors'
|
||||||
|
@echo '-- TESTS --'
|
||||||
|
@echo 'coverage - run unit tests and generate coverage report'
|
||||||
|
@echo 'test - run unit tests'
|
||||||
|
@echo 'test TEST_FILE=<test_file> - run all tests in file'
|
||||||
|
@echo 'test_watch - run unit tests in watch mode'
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 LangChain, Inc.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
module.exports = require('./dist/client.cjs');
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './dist/client.cjs'
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user