diff --git a/.gitignore b/.gitignore index 6a2480b85..99db54086 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea +__pycache__ qdrant-landing/.DS_Store .DS_Store diff --git a/automation/snippets/.gitignore b/automation/snippets/.gitignore new file mode 100644 index 000000000..f1d77e09f --- /dev/null +++ b/automation/snippets/.gitignore @@ -0,0 +1,4 @@ +.gradle +/cache +/clients +/tmp diff --git a/automation/snippets/README.md b/automation/snippets/README.md new file mode 100644 index 000000000..13e09cad7 --- /dev/null +++ b/automation/snippets/README.md @@ -0,0 +1,116 @@ +# Snippet tools + +This is a tool to work with code snippets. + +1. Automated CI type checking of snippets. +2. Writing new snippets and checking/running/testing them locally. + This tool hides the complexity of setting up every build system/dev env for each client. + Also, it supports testing snippets against unreleased versions of clients (from git branches/PRs). + + +## Dependencies + +To convert runnable snippets into markdown ([`generate-md.py`](./generate-md.py)) you need only python with no extra dependencies. + +To typecheck/build snippets, you need a bunch of tools installed. +The all-in-one [`Dockerfile`](./docker/Dockerfile) and [`shell.nix`](./shell.nix) are provided. +Run [`./docker.sh`](./docker.sh) to start a shell in the docker container. + +On ARM-based macOS, ensure that the Docker Desktop setting "General" > "Virtual Machine Options" > "Use Rosetta for x86\_64/amd64 emulation on Apple Silicon" is selected. +Note: on macOS, if you see Java compile failures related to `protobuf`, re-running the test will often solve those. + + +## Workflow: typechecking and running + +Clone and build clients (latest git versions). +```bash +./sync-clients.py fetch --csharp --java --typescript +``` + +(optional) You might specify particular tags/branches/commits instead. +```bash +./sync-clients.py fetch --csharp=1.16.0 --java=b290b14892534fc0d76893d41b8f656855cd589b --typescript=main +``` + +(optional) Update the lockfiles for templates (see ./sync-clients.py lock --help for syntax). +```bash +./sync-clients.py lock --go=v1.16.0 --rust=branch=master --python=tag=v1.16.0 +``` + +Typecheck/build all snippets or a particular language/snippet. +```bash +./check.py build +./check.py build -l csharp,java +./check.py build ./snippets/create-collection/simple +./check.py build ./snippets/create-collection/simple/go.go +``` + +Test all testable snippets against localhost Qdrant instance. (you start it yourself). +Testable snippets are the ones that have `test.py` file in the same directory. +```bash +./check.py test +``` + +Run a particular snippet without running a test. +```bash +./check.py run ./snippets/create-collection/simple/go.go +``` + + +## Workflow: writing new snippets/converting old ones + +Convert existing old markdown snippets. +```bash +./migrate-snippet.py ./snippets/create-collection/simple/go.md +``` +Or create new snippets from scratch. + +To reduce noise, you can hide boilerplate code using special comment: +```go +if err != nil { panic(err) } // @hide +``` +The usual wrapping boileplate like `public static void run() throws Exception {` is already hidden by default. + +Typecheck/build the snippet. +```bash + ./check.py build ./snippets/create-collection/simple +``` + +(Optional) Start Qdrant and run the snippet. +```bash +./check.py run ./snippets/create-collection/simple/go.go +``` + +(Optional) Write `test.py` and test it. +```bash +./check.py test ./snippets/create-collection/simple +``` + +Before committing, regenerate markdown files. +If migrated, also delete old markdown files. +```bash +./generate-md.py +git rm ./snippets/create-collection/simple/go.md # if migrated +git add ./snippets/create-collection/simple/generated/go.md # made by generate-md.py +git add ./snippets/create-collection/simple/go.go +``` + + +## Architecture + +For each language, the snippet tester collects all code snippets from the [`snippets/`](../../qdrant-landing/content/documentation/headless/snippets) directory and puts them in a single scratch project in a temporary directory. +Then those projects are compiled/typechecked. +Putting all snippets in a single project rather than compiling them one by one saves time: some build systems like `gradle` take a lot of time to spin up. + +Each supported language has: +- A template project in [`templates/`](./templates) directory. +- A Python subclass of `Language` in [`lib/languages/`](./lib/languages). It is responsible for: + - Creating the scratch project from snippets and the template. + - Building/typechecking the project. + - Running a particular snippet for testing. +- A list of build dependencies in [`docker/Dockerfile`](./docker/Dockerfile) and [`shell.nix`](./shell.nix). +- A handler in [`sync-clients.py`](./sync-clients.py) to fetch the particular git revision of the client. + - For Java, C#, Typescript it fetches the client sources to the `clients/` directory, and builds them. + This directory is gitignored. + - For Go, Rust, Python it updates the lockfile in the [`templates/`](./templates) directory. + These lockfiles are checked in this repo. diff --git a/automation/snippets/check.py b/automation/snippets/check.py new file mode 100755 index 000000000..4f7ec1c5b --- /dev/null +++ b/automation/snippets/check.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 + +import argparse +import importlib.util +import shutil +import subprocess +import sys +import traceback +import types +import typing +from pathlib import Path + +from lib import SNIPPETS_DIR, CollectedSnippetsType, collect_snippets, log +from lib.languages import ALL_LANGUAGES, CompileResult, Language, parse_languages + + +def main() -> None: + parser = argparse.ArgumentParser() + default_langs = ",".join([lang.NAME for lang in ALL_LANGUAGES]) + + parser.add_argument( + "-l", + "--langs", + help=f"comma-separated list of languages, default is {default_langs}", + default=default_langs, + ) + parser.add_argument( + "command", + help="command to execute", + choices=["build", "run", "test"], + ) + parser.add_argument( + "snippets", + nargs="*", + help=f"Snippet filenames/directories to process, default: {SNIPPETS_DIR}", + default=[SNIPPETS_DIR], + type=Path, + metavar="SNIPPET_PATH", + ) + + args = parser.parse_args() + + languages = parse_languages(args.langs) + snippets = collect_snippets(args.snippets, languages) + assert args.command in ("build", "run", "test") + + # Q: Why not `tempfile.TemporaryDirectory()`? + # A: Harder to debug/inspect generated files. + tmpdir = Path(__file__).parent / "tmp" + shutil.rmtree(tmpdir, ignore_errors=True) + tmpdir.mkdir(parents=True) + build_and_run(tmpdir=tmpdir, snippets=snippets, mode=args.command) + + +def build_and_run( + tmpdir: Path, + snippets: CollectedSnippetsType, + mode: typing.Literal[ + "build", # just build + "run", # build and run each snippet + "test", # build, then run/test all snippets that have a test.py file + ], +) -> None: + snippets_by_lang: dict[type[Language], list[Path]] = {} + + for snippets2 in snippets.values(): + for lang, fname in snippets2.items(): + snippets_by_lang.setdefault(lang, []).append(fname) + + compile_results: dict[type[Language], CompileResult] = {} + errors: list[str] = [] + + # Load test modules before compilation. + # We want them to crash early. + test_modules: dict[Path, types.ModuleType] = {} + if mode == "test": + for snippet_dir in snippets: + test_file = snippet_dir / "test.py" + if not test_file.exists(): + continue + spec = importlib.util.spec_from_file_location("test_module", test_file) + assert spec is not None + mod = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(mod) + test_modules[snippet_dir] = mod + + log("Compile stage") + for lang, fnames in snippets_by_lang.items(): + log(f"· Compiling {lang.NAME} snippets") + try: + res = lang.compile(tmpdir / lang.NAME, fnames) + if res.has_issues: + log(f"· · Compilation had issues for {lang.NAME}") + errors.append(f"Compilation had issues for {lang.NAME}") + compile_results[lang] = res + except Exception as e: + log(f"· · Compilation failed for {lang.NAME}") + errors.append(f"Compilation failed for {lang.NAME}") + if not isinstance(e, subprocess.CalledProcessError): + traceback.print_exc() + + if mode in ("run", "test"): + log("Run stage") + for snippet_dir, snippets2 in snippets.items(): + if mode == "run": + for lang, snippet_fname in snippets2.items(): + if (compile_result := compile_results.get(lang)) is None: + continue + + log(f"· Running {snippet_fname}") + p = subprocess.run( + compile_result.run_args[snippet_fname], + text=True, + ) + if p.returncode != 0: + log(f"· · Exit code {p.returncode}") + + if mode == "test" and snippet_dir in test_modules: + log(f"· Testing snippets in {snippet_dir}") + mod = test_modules[snippet_dir] + + for lang, snippet_fname in snippets2.items(): + compile_result = compile_results.get(lang) + if compile_result is None: + continue + + log(f"· · Testing {snippet_fname}") + + output = None + try: + if hasattr(mod, "prepare"): + mod.prepare() + + p = subprocess.run( + compile_result.run_args[snippet_fname], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + + output = p.stdout + if p.returncode != 0: + msg = f"Process exited with code {p.returncode}" + raise RuntimeError(msg) + + if hasattr(mod, "check"): + mod.check() + except Exception: + log(f"· · · Testing {snippet_fname} failed") + if output: + print(output.rstrip()) + traceback.print_exc() + errors.append(f"Testing {snippet_fname} failed") + finally: + try: + if hasattr(mod, "cleanup"): + mod.cleanup() + except Exception: + log(f"· · · Teardown for {snippet_fname} failed") + errors.append(f"Teardown for {snippet_fname} failed") + traceback.print_exc() + + if errors: + log("Errors encountered:") + for err in errors: + log(f"· {err}") + sys.exit(1) + elif mode == "test": + log("All tests passed successfully.") + + +if __name__ == "__main__": + main() diff --git a/automation/snippets/docker.sh b/automation/snippets/docker.sh new file mode 100755 index 000000000..e899b551c --- /dev/null +++ b/automation/snippets/docker.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env sh + +set -e + +IMAGE_NAME="snippet-checker" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +REPO_ROOT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" + +# check if on macos + +if ! docker image inspect "$IMAGE_NAME" > /dev/null 2>&1; then + # TODO: comment why + [ "$(uname)" != Darwin ] || export DOCKER_DEFAULT_PLATFORM=linux/amd64 + docker build -t "$IMAGE_NAME" "$SCRIPT_DIR/docker" +fi + +[ $# -ne 0 ] || set -- bash + +[ -t 0 ] && TTY_FLAG=--tty || TTY_FLAG= + +docker run \ + --rm \ + --interactive \ + $TTY_FLAG \ + --network host \ + --user "$(id -u):$(id -g)" \ + --volume "$REPO_ROOT_DIR:/workspace" \ + "$IMAGE_NAME" \ + uv run "$@" diff --git a/automation/snippets/docker/Dockerfile b/automation/snippets/docker/Dockerfile new file mode 100644 index 000000000..633d7fc7e --- /dev/null +++ b/automation/snippets/docker/Dockerfile @@ -0,0 +1,51 @@ +FROM ubuntu:24.04 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + git \ + software-properties-common \ + # for rust client + gcc \ + libc6-dev \ + # for go client + golang \ + # for java client + openjdk-21-jdk-headless \ + # for python client + python3 \ + # for typescript client + nodejs \ + npm \ + && rm -rf /var/lib/apt/lists/* + +# for csharp client +RUN add-apt-repository ppa:dotnet/backports && \ + apt-get update && \ + apt-get install -y dotnet-sdk-8.0 --no-install-recommends && \ + rm -rf /var/lib/apt/lists/* + +# for rust client +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \ + CARGO_HOME=/opt/rust RUSTUP_HOME=/opt/rust \ + sh -s -- -y --default-toolchain 1.91.0 + +# for typescript client +RUN npm install --global --prefix /usr/local pnpm +# Without this, typescript snippets tries to connect to ::1 instead of 127.1 +RUN echo "precedence ::ffff:0:0/96 100" >> /etc/gai.conf + +# for python client +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ + +ENV RUSTUP_HOME=/opt/rust \ + PATH="/opt/rust/bin:${PATH}" + +# /workspace is the repo root +WORKDIR /workspace/automation/snippets + +# Build tools put their caches in $HOME +ENV HOME=/workspace/automation/snippets/cache \ + # gradle ignores $HOME + GRADLE_USER_HOME=/workspace/automation/snippets/cache/.gradle + +ENTRYPOINT ["/bin/bash", "-c", "mkdir -p $HOME && \"$@\"", "--"] diff --git a/automation/snippets/generate-md.py b/automation/snippets/generate-md.py new file mode 100755 index 000000000..e4d4a0a13 --- /dev/null +++ b/automation/snippets/generate-md.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Converts all runnable snippets to markdown files. + +E.g., converts + `snippets/create-collection/simple/python.py` +to + `snippets/create-collection/simple/generated/python.md` + +For using on CI and running before committing changes. +The reverse is `./migrate-snippet.py`. +""" + +import difflib +import shutil +import sys +import traceback +import typing + +from lib import SNIPPETS_DIR, collect_snippets +from lib.languages import ALL_LANGUAGES + + +def main() -> None: + snippets = collect_snippets(bases=[SNIPPETS_DIR], languages=ALL_LANGUAGES) + + issues = 0 + + for snippet_dir, snippets2 in snippets.items(): + generated_dir = snippet_dir / "generated" + + shutil.rmtree(generated_dir, ignore_errors=True) + generated_dir.mkdir() + + for lang, snippet_fname in snippets2.items(): + try: + shortened = lang.shorten(snippet_fname.read_text()) + except Exception as e: + issues += 1 + print(f"Warning: failed to shorten snippet {snippet_fname}: {e}") + traceback.print_exc() + continue + generated = f"```{lang.NAME}\n{shortened}```\n" + + generated_fname = generated_dir / f"{lang.NAME}.md" + handwritten_fname = snippet_dir / f"{lang.NAME}.md" + generated_fname.write_text(generated) + + if handwritten_fname.exists(): + issues += 1 + print( + "Warning: both snippet and generated file exist:", + snippet_dir / f"{lang.NAME}.md", + ) + handwritten = (snippet_dir / f"{lang.NAME}.md").read_text() + if handwritten != generated: + print_and_colorize_diff( + difflib.unified_diff( + handwritten.splitlines(keepends=True), + generated.splitlines(keepends=True), + fromfile=str(handwritten_fname), + tofile=str(generated_fname), + ), + ) + print() + + if issues: + print(f"Total issues found: {issues}") + sys.exit(1) + + +def print_and_colorize_diff(diff: typing.Iterable[str]) -> None: + for line in diff: + if line.startswith("+"): + print(end=f"\x1b[32m{line}\x1b[0m", file=sys.stderr) + elif line.startswith("-"): + print(end=f"\x1b[31m{line}\x1b[0m", file=sys.stderr) + elif line.startswith("@"): + print(end=f"\x1b[36m{line}\x1b[0m", file=sys.stderr) + else: + print(end=line, file=sys.stderr) + + +if __name__ == "__main__": + main() diff --git a/automation/snippets/lib/__init__.py b/automation/snippets/lib/__init__.py new file mode 100644 index 000000000..0122c7ec2 --- /dev/null +++ b/automation/snippets/lib/__init__.py @@ -0,0 +1,74 @@ +import os +import sys +import typing +from pathlib import Path + +from .languages import Language + +SNIPPETS_DIR = Path(os.path.normpath(Path(__file__) / "../../snippets")).relative_to( + Path.cwd() +) + + +def log(msg: str) -> None: + """Print a message. + + Use this instead of `print()` to make the output more visible compared to + the output of builders/snippet runs. + """ + print(f"\x1b[30;43m + \x1b[m {msg}", file=sys.stderr) + + +type CollectedSnippetsType = dict[ + Path, # directory path + dict[type[Language], Path], # language -> snippet path +] +"""Snippet filenames grouped by directory then by language. + +Example: +{ + Path("snippets/example1"): { + LanguageGo: Path("snippets/example1/go.go"), + LanguagePython: Path("snippets/example1/python.py"), + }, + Path("snippets/example2"): { + LanguageGo: Path("snippets/example2/go.go"), + LanguagePython: Path("snippets/example2/python.py"), + LanguageRust: Path("snippets/example2/rust.rs"), + }, +} +""" + + +def collect_snippets( + bases: list[Path], + languages: typing.Sequence[type[Language]], +) -> CollectedSnippetsType: + """Locate snippets in the given base paths.""" + + result: CollectedSnippetsType = {} + + for base in bases: + found = False + if base.is_file(): + for lang in languages: + if base.name == lang.SNIPPET_FILENAME: + result.setdefault(base.parent, {})[lang] = base + found = True + break + elif base.is_dir(): + for root, dirs, files in os.walk(base): + dirs[:] = [d for d in dirs if d != "__pycache__"] + dirs.sort() + dir_path = Path(root) + + for lang in languages: + if lang.SNIPPET_FILENAME in files: + found = True + result.setdefault(dir_path, {})[lang] = ( + dir_path / lang.SNIPPET_FILENAME + ) + if not found: + log(f"Warning: No snippets found in {base}") + + return result diff --git a/automation/snippets/lib/languages/__init__.py b/automation/snippets/lib/languages/__init__.py new file mode 100644 index 000000000..2366d965c --- /dev/null +++ b/automation/snippets/lib/languages/__init__.py @@ -0,0 +1,53 @@ +from .base import CompileResult, Language +from .bash import LanguageBash +from .csharp import LanguageCsharp +from .go import LanguageGo +from .java import LanguageJava +from .python import LanguagePython +from .rust import LanguageRust +from .typescript import LanguageTypescript + +__all__ = [ + "ALL_LANGUAGES", + "CompileResult", + "Language", + "LanguageBash", + "LanguageCsharp", + "LanguageGo", + "LanguageJava", + "LanguagePython", + "LanguageRust", + "LanguageTypescript", + "parse_languages", +] + +ALL_LANGUAGES: tuple[type[Language], ...] = ( + LanguageBash, + LanguageCsharp, + LanguageGo, + LanguageJava, + LanguagePython, + LanguageRust, + LanguageTypescript, +) + + +def parse_languages(langs: str) -> list[type[Language]]: + """Parse comma-separated list of languages into a list of Language classes. + + Accepts either language names or snippet file extensions. + Example: "python,ts,go" -> [LanguagePython, LanguageTypescript, LanguageGo] + """ + result: list[type[Language]] = [] + for lang_name in langs.split(","): + lang_name = lang_name.strip() + for lang in ALL_LANGUAGES: + if lang in result: + continue + if lang_name in (lang.NAME, lang.SNIPPET_FILENAME.split(".")[1]): + result.append(lang) + break + else: + msg = f"Unknown language: {lang_name}" + raise ValueError(msg) + return result diff --git a/automation/snippets/lib/languages/base.py b/automation/snippets/lib/languages/base.py new file mode 100644 index 000000000..984ca3285 --- /dev/null +++ b/automation/snippets/lib/languages/base.py @@ -0,0 +1,121 @@ +import dataclasses +import os +import re +import typing +from pathlib import Path + +if typing.TYPE_CHECKING: + from _typeshed import StrOrBytesPath + + +class Language: + """Base class for a programming language snippet tester.""" + + NAME: str + """Name of the programming language, e.g., 'python'.""" + + SNIPPET_FILENAME: str + """Filename of the snippet in this language, e.g., 'python.py'.""" + + @classmethod + def compile(cls, tmpdir: Path, fnames: list[Path]) -> "CompileResult": + """Compile, typecheck, and prepare all snippets listed in fnames. + + Might create scratch projects in tmpdir. + """ + raise NotImplementedError + + @classmethod + def shorten(cls, contents: str) -> str: + """Shorten the snippet contents into a form suitable for inclusion in + documentation. + + Removes boilerplate code, e.g. class wrappers, main functions, etc. + """ + return generic_shorten(contents) + + @classmethod + def unshorten(cls, contents: str) -> str: + """Convert the shortened snippet into runnable code. + + Not reliable, might require manual adjustments. + """ + return contents + + @classmethod + def format(cls, fnames: list[str]) -> None: + """Format the snippets specified by fnames in place.""" + pass + + +@dataclasses.dataclass +class CompileResult: + run_args: dict[Path, typing.Sequence["StrOrBytesPath"]] = dataclasses.field( + default_factory=dict + ) + """Mapping from the snippet filename to the command that runs the + snippet.""" + + has_issues: bool = False + """If true, indicates that there were issues during + compilation/typechecking, but the snippets can still be run. + """ + + +def copy_template(src: Path, dst: Path) -> None: + dst.parent.mkdir(parents=True, exist_ok=True) + dst.write_text(src.read_text().replace("{collection_name}", "collection_name")) + + +def template( + original_fname: Path, + target_fname: Path, + replacements: dict[str, str], +) -> None: + result = [] + for line in original_fname.read_text().splitlines(keepends=True): + if (r := replacements.get(line.strip())) is not None: + result.append(r + "\n") + else: + result.append(line) + target_fname.write_text("".join(result)) + + +_RE_COMMENT = re.compile(r".*\s(?://|#)\s*(@.*)$") + + +def generic_shorten(text: str) -> str: + """Generic implementation of Language.shorten(). + + Removes comments with @hide annotation and trims excessive newlines. + """ + result = [] + for line in text.splitlines(): + if (m := _RE_COMMENT.match(line)) is not None: + if m[1] == "@hide": + continue + else: + msg = f"Unknown annotation: {m[1]}" + raise ValueError(msg) + result.append(line + "\n") + text = "".join(result) + text = text.lstrip("\n").rstrip("\n") + text = re.sub(r"\n{3,}", "\n\n", text) + if text: + text += "\n" + return text + + +def trim_commonpath(fnames: list[Path]) -> dict[Path, Path]: + """Trim the common path prefix from a list of file paths. + + Returns a mapping from original paths to trimmed paths. + """ + common_prefix = os.path.commonpath( + [(Path.cwd() / fname.parent) for fname in fnames] + ) + result = {} + for fname in fnames: + trimmed = Path(os.path.relpath(fname, common_prefix)) + result[fname] = trimmed + return result diff --git a/automation/snippets/lib/languages/bash.py b/automation/snippets/lib/languages/bash.py new file mode 100644 index 000000000..267f74c4a --- /dev/null +++ b/automation/snippets/lib/languages/bash.py @@ -0,0 +1,17 @@ +from pathlib import Path + +from .base import CompileResult, Language, copy_template + + +class LanguageBash(Language): + NAME = "bash" + SNIPPET_FILENAME = "bash.sh" + + @classmethod + def compile(cls, tmpdir: Path, fnames: list[Path]) -> CompileResult: + tmpdir.mkdir(parents=True) + result = CompileResult() + for no, fname in enumerate(fnames): + copy_template(fname, tmpdir / f"snippet{no}.sh") + result.run_args[fname] = ["bash", "-ex", f"{tmpdir}/snippet{no}.sh"] + return result diff --git a/automation/snippets/lib/languages/csharp.py b/automation/snippets/lib/languages/csharp.py new file mode 100644 index 000000000..23f464fd6 --- /dev/null +++ b/automation/snippets/lib/languages/csharp.py @@ -0,0 +1,125 @@ +import re +import shutil +import subprocess +import textwrap +from pathlib import Path + +from .base import CompileResult, Language, generic_shorten, template, trim_commonpath + + +class LanguageCsharp(Language): + NAME = "csharp" + SNIPPET_FILENAME = "csharp.cs" + + @classmethod + def compile(cls, tmpdir: Path, fnames: list[Path]) -> CompileResult: + shutil.copytree("templates/csharp", tmpdir) + + csproj_path = tmpdir / "csharp.csproj" + csproj_path.write_text( + csproj_path.read_text().replace( + "../../clients/csharp/", + f"{Path('clients/csharp').absolute()}/", + ) + ) + + trimmed_fnames = trim_commonpath(fnames) + + src_cases = [] + + result = CompileResult() + + for no, (snippet_fname, trimmed_fname) in enumerate(trimmed_fnames.items()): + content = ( + snippet_fname.read_text() + .replace("public class Snippet", f"public class Snippet{no}") + .replace("{collection_name}", "collection_name") + ) + dest_path = tmpdir / "s" / trimmed_fname.parent / f"Snippet{no}.cs" + dest_path.parent.mkdir(parents=True, exist_ok=True) + dest_path.write_text(content) + src_cases.append( + f'case "{snippet_fname}": await Snippet{no}.Run(); break;\n' + ) + result.run_args[snippet_fname] = [ + "dotnet", + f"{tmpdir}/bin/snippets-amalgamation.dll", + snippet_fname, + ] + + template( + original_fname=Path("templates/csharp/Program.cs"), + target_fname=tmpdir / "Program.cs", + replacements={"// %cases%": "".join(src_cases)}, + ) + + subprocess.run(["dotnet", "build", "-o", "bin"], cwd=tmpdir, check=True) + + return result + + RE_CODE = re.compile( + r""" + (?P .*? ) + \s*public\ class\ Snippet\s*\{\s* + public\ static\ async\ Task\ Run\(\)\s*\{\n + (?P .*? ) + \s*\}\s*}\s* + $ + """, + re.DOTALL | re.VERBOSE, + ) + + EXAMPLE_CODE = textwrap.dedent( + """\ + using Qdrant.Client; + using Qdrant.Client.Grpc; + + public class Snippet + { + public static async Task Run() + { + // Your code here + } + } + """ + ) + assert RE_CODE.match(EXAMPLE_CODE) is not None + + @classmethod + def shorten(cls, contents: str) -> str: + if (m := LanguageCsharp.RE_CODE.match(contents)) is None: + msg = "Invalid snippet format" + raise ValueError(msg) + return generic_shorten( + m["imports"].strip() + "\n\n" + textwrap.dedent(m["body"]).strip() + ) + + RE_RENDERED = re.compile( + r""" + (?P (?:using\s[^;]+;\n|\n)* ) + (?P .* ) + $ + """, + re.DOTALL | re.VERBOSE, + ) + + @classmethod + def unshorten(cls, contents: str) -> str: + if m := LanguageCsharp.RE_RENDERED.match(contents): + return textwrap.dedent( + """\ + {imports} + + public class Snippet + {{ + \tpublic static async Task Run() + \t{{ + {body} + \t}} + }} + """ + ).format( + imports=m["imports"].strip(), + body=textwrap.indent(m["body"].strip(), "\t" * 2), + ) + return contents diff --git a/automation/snippets/lib/languages/go.py b/automation/snippets/lib/languages/go.py new file mode 100644 index 000000000..39a258df9 --- /dev/null +++ b/automation/snippets/lib/languages/go.py @@ -0,0 +1,119 @@ +import re +import shutil +import subprocess +import textwrap +from pathlib import Path + +from .base import ( + CompileResult, + Language, + copy_template, + generic_shorten, + template, + trim_commonpath, +) + + +class LanguageGo(Language): + NAME = "go" + SNIPPET_FILENAME = "go.go" + + @classmethod + def compile(cls, tmpdir: Path, fnames: list[Path]) -> CompileResult: + shutil.copytree("templates/go", tmpdir) + + trimmed_fnames = trim_commonpath(fnames) + + src_imports = [] + src_cases = [] + + result = CompileResult() + + for no, (snippet_fname, trimmed_fname) in enumerate(trimmed_fnames.items()): + dest_path = tmpdir / "s" / trimmed_fname + copy_template(snippet_fname, dest_path) + src_imports.append( + f's{no} "example.com/snippets-amalgamation/{("s" / trimmed_fname).parent}"' + ) + src_cases.append(f'case "{snippet_fname}": s{no}.Main()\n') + result.run_args[snippet_fname] = [tmpdir / "tester", snippet_fname] + + template( + original_fname=Path("templates/go/main.go"), + target_fname=tmpdir / "main.go", + replacements={ + "// %imports%": "\n".join(src_imports), + "// %cases%": "\n".join(src_cases), + }, + ) + + subprocess.run(["go", "build", "-o", "tester", "."], cwd=tmpdir, check=True) + + return result + + RE_CODE = re.compile( + r""" + package\ [a-zA-Z0-9_]+\n + (?P .*? ) + func\ Main\(\)\ \{\n + (?P .*? ) + \}\s*$ + """, + re.DOTALL | re.VERBOSE, + ) + + EXAMPLE_CODE = textwrap.dedent( + """\ + package main + + import ( + "github.com/qdrant/go-client/qdrant" + ) + + func Main() { + // Your code here + } + """ + ) + assert RE_CODE.match(EXAMPLE_CODE) is not None + + @classmethod + def shorten(cls, contents: str) -> str: + if (m := LanguageGo.RE_CODE.match(contents)) is None: + msg = "Invalid snippet format" + raise ValueError(msg) + return generic_shorten( + m["imports"].strip() + "\n\n" + textwrap.dedent(m["body"]).strip() + ) + + @classmethod + def format(cls, fnames: list[str]) -> None: + subprocess.run(["gofmt", "-w", *fnames], check=True) + + RE_RENDERED = re.compile( + r""" + (?P (?:import\s*\([^)]+\)\n|\n)* ) + (?P .* ) + $ + """, + re.DOTALL | re.VERBOSE, + ) + + @classmethod + def unshorten(cls, contents: str) -> str: + if m := LanguageGo.RE_RENDERED.match(contents): + return textwrap.dedent( + """\ + package snippet + + {imports} + + func Main() {{ + {body} + }} + """ + ).format( + imports=m["imports"].strip(), + body=textwrap.indent(m["body"].strip(), "\t"), + ) + return contents diff --git a/automation/snippets/lib/languages/java.py b/automation/snippets/lib/languages/java.py new file mode 100644 index 000000000..8bbb699f4 --- /dev/null +++ b/automation/snippets/lib/languages/java.py @@ -0,0 +1,128 @@ +import os +import re +import shutil +import subprocess +import textwrap +from pathlib import Path + +from .base import CompileResult, Language, generic_shorten, template, trim_commonpath + + +class LanguageJava(Language): + NAME = "java" + SNIPPET_FILENAME = "java.java" + + @classmethod + def compile(cls, tmpdir: Path, fnames: list[Path]) -> CompileResult: + (tmpdir / "src/main/java/s").mkdir(parents=True) + shutil.copyfile("templates/java/build.gradle", f"{tmpdir}/build.gradle") + shutil.copyfile("templates/java/settings.gradle", f"{tmpdir}/settings.gradle") + + trimmed_fnames = trim_commonpath(fnames) + + result = CompileResult() + src_cases = [] + + for no, (snippet_fname, trimmed_fname) in enumerate(trimmed_fnames.items()): + content = ( + snippet_fname.read_text() + .replace("public class Snippet", f"public class Snippet{no}") + .replace("{collection_name}", "collection_name") + ) + dest_path = ( + tmpdir / "src/main/java/s" / trimmed_fname.parent / f"Snippet{no}.java" + ) + dest_path.parent.mkdir(parents=True, exist_ok=True) + dest_path.write_text(content) + src_cases.append(f'case "{snippet_fname}": Snippet{no}.run(); break;\n') + result.run_args[snippet_fname] = [ + "java", + "-jar", + f"{tmpdir}/build/libs/app-1.0.0-all.jar", + snippet_fname, + ] + + template( + original_fname=Path("templates/java/src/main/java/Main.java"), + target_fname=tmpdir / "src/main/java/Main.java", + replacements={ + "// %cases%": "\n".join(src_cases), + }, + ) + + gradlew = os.path.abspath("templates/java/gradlew") + client = os.path.abspath("clients/java") + subprocess.run( + [gradlew, "--no-daemon", f"-PclientPath={client}", "shadowJar"], + cwd=tmpdir, + check=True, + ) + + return result + + RE_CODE = re.compile( + r""" + package\s[a-zA-Z0-9_.]+;\n + (?P .*? ) + \s*public\ class\ Snippet\ \{\n + \s*public\ static\ void\ run\(\)\ throws\ Exception\ \{\n + (?P .*? ) + \s*\}\s*}\s* + $ + """, + re.DOTALL | re.VERBOSE, + ) + + EXAMPLE_CODE = textwrap.dedent( + """\ + package com.example.snippets_amalgamation; + + import io.qdrant.client.QdrantClient; + + public class Snippet { + public static void run() throws Exception { + // Your code here + } + } + """ + ) + assert RE_CODE.match(EXAMPLE_CODE) is not None + + @classmethod + def shorten(cls, contents: str) -> str: + if (m := LanguageJava.RE_CODE.match(contents)) is None: + msg = "Invalid snippet format" + raise ValueError(msg) + return generic_shorten( + m["imports"].strip() + "\n\n" + textwrap.dedent(m["body"]).strip() + ) + + RE_RENDERED = re.compile( + r""" + (?P (?: import\s[^;]+;\n|\n)* ) + (?P .* ) + $ + """, + re.DOTALL | re.VERBOSE, + ) + + @classmethod + def unshorten(cls, contents: str) -> str: + if m := LanguageJava.RE_RENDERED.match(contents): + return textwrap.dedent( + """\ + package com.example.snippets_amalgamation; + + {imports} + + public class Snippet {{ + public static void run() throws Exception {{ + {body} + }} + }} + """ + ).format( + imports=m["imports"].strip(), + body=textwrap.indent(m["body"].strip(), " " * 16), + ) + return contents diff --git a/automation/snippets/lib/languages/python.py b/automation/snippets/lib/languages/python.py new file mode 100644 index 000000000..00cadacb6 --- /dev/null +++ b/automation/snippets/lib/languages/python.py @@ -0,0 +1,34 @@ +import subprocess +from pathlib import Path + +from .base import CompileResult, Language, copy_template, trim_commonpath + + +class LanguagePython(Language): + NAME = "python" + SNIPPET_FILENAME = "python.py" + + @classmethod + def compile(cls, tmpdir: Path, fnames: list[Path]) -> CompileResult: + trimmed_fnames = trim_commonpath(fnames) + result = CompileResult() + + for snippet_fname, trimmed_fname in trimmed_fnames.items(): + target_path = tmpdir / str(trimmed_fname).replace("-", "_") + target_path.parent.mkdir(parents=True, exist_ok=True) + dir = tmpdir + for component in Path(str(trimmed_fname).replace("-", "_")).parts: + (dir / "__init__.py").touch() + dir /= component + copy_template(snippet_fname, target_path) + result.run_args[snippet_fname] = [ + "uv", + "--project=templates/python", + "run", + target_path, + ] + + p = subprocess.run(["uv", "--project=templates/python", "run", "mypy", tmpdir]) + result.has_issues = p.returncode != 0 + + return result diff --git a/automation/snippets/lib/languages/rust.py b/automation/snippets/lib/languages/rust.py new file mode 100644 index 000000000..94b33f266 --- /dev/null +++ b/automation/snippets/lib/languages/rust.py @@ -0,0 +1,128 @@ +import os +import re +import shutil +import subprocess +import textwrap +from pathlib import Path + +from .base import ( + CompileResult, + Language, + copy_template, + generic_shorten, + template, + trim_commonpath, +) + + +class LanguageRust(Language): + NAME = "rust" + SNIPPET_FILENAME = "rust.rs" + + @classmethod + def compile(cls, tmpdir: Path, fnames: list[Path]) -> CompileResult: + tmpdir.mkdir() + (tmpdir / "src").mkdir() + shutil.copyfile("templates/rust/Cargo.toml", f"{tmpdir}/Cargo.toml") + shutil.copyfile("templates/rust/Cargo.lock", f"{tmpdir}/Cargo.lock") + + src_mods = [] + src_cases = [] + + trimmed_fnames = trim_commonpath(fnames) + + result = CompileResult() + binary_path = Path.cwd() / "templates/rust/target/debug/snippets-amalgamation" + + for no, (snippet_fname, trimmed_fname) in enumerate(trimmed_fnames.items()): + copy_template(snippet_fname, tmpdir / "src" / "s" / trimmed_fname) + src_mods.append(f'#[path = "s/{trimmed_fname}"] mod s{no};') + src_cases.append(f'"{snippet_fname}" => s{no}::main().await.unwrap(),\n') + result.run_args[snippet_fname] = [binary_path, snippet_fname] + + template( + original_fname=Path("templates/rust/src/main.rs"), + target_fname=tmpdir / "src/main.rs", + replacements={ + "// %mods%": "\n".join(src_mods), + "// %cases%": "\n".join(src_cases), + }, + ) + + subprocess.run( + ["cargo", "build"], + cwd=tmpdir, + env={ + **os.environ, + "CARGO_TARGET_DIR": str(Path.cwd() / "templates/rust/target"), + }, + check=True, + ) + + return result + + @classmethod + def format(cls, fnames: list[str]) -> None: + subprocess.run(["rustfmt", "--edition=2024", *fnames], check=True) + + RE_CODE = re.compile( + r""" + (?P (?:use\s[^;]+;\n|\n)* ) + pub\s+async\s+fn\s+main\(\)\s*->\s*anyhow::Result<\(\)>\s*\{\n + (?P .*? ) + \s*Ok\(\(\)\)\n + \}\n? + $ + """, + re.DOTALL | re.VERBOSE, + ) + + EXAMPLE_CODE = textwrap.dedent( + """\ + use qdrant_client::Qdrant; + + pub async fn main() -> anyhow::Result<()> { + // Your code here + + Ok(()) + } + """ + ) + assert RE_CODE.match(EXAMPLE_CODE) is not None + + @classmethod + def shorten(cls, contents: str) -> str: + if (m := LanguageRust.RE_CODE.match(contents)) is None: + msg = "Invalid snippet format" + raise ValueError(msg) + return generic_shorten( + m["uses"].strip() + "\n\n" + textwrap.dedent(m["body"]).strip() + ) + + RE_RENDERED = re.compile( + r""" + (?P (?:use\s[^;]+;\n|\n)* ) + (?P .* ) + $ + """, + re.DOTALL | re.VERBOSE, + ) + + @classmethod + def unshorten(cls, contents: str) -> str: + if m := LanguageRust.RE_RENDERED.match(contents): + return textwrap.dedent( + """\ + {uses} + + pub async fn main() -> anyhow::Result<()> {{ + {body} + + Ok(()) + }} + """ + ).format( + uses=m["uses"].strip(), + body=textwrap.indent(m["body"].strip(), " " * 4), + ) + return contents diff --git a/automation/snippets/lib/languages/typescript.py b/automation/snippets/lib/languages/typescript.py new file mode 100644 index 000000000..50d4aba26 --- /dev/null +++ b/automation/snippets/lib/languages/typescript.py @@ -0,0 +1,53 @@ +import json +import shutil +import subprocess +from pathlib import Path + +from .base import CompileResult, Language, copy_template, trim_commonpath + + +class LanguageTypescript(Language): + NAME = "typescript" + SNIPPET_FILENAME = "typescript.ts" + + @classmethod + def compile(cls, tmpdir: Path, fnames: list[Path]) -> CompileResult: + tmpdir.mkdir() + + package_data = json.loads(Path("templates/typescript/package.json").read_text()) + + package_data["dependencies"]["@qdrant/js-client-rest"] = "file:" + str( + Path("clients/typescript/packages/js-client-rest").resolve().absolute() + ) + (tmpdir / "package.json").write_text(json.dumps(package_data, indent=2)) + + shutil.copyfile( + "templates/typescript/package-lock.json", f"{tmpdir}/package-lock.json" + ) + + trimmed_fnames = trim_commonpath(fnames) + result = CompileResult() + + for snippet_fname, trimmed_fname in trimmed_fnames.items(): + dest_path = tmpdir / "s" / trimmed_fname + copy_template(snippet_fname, dest_path) + result.run_args[snippet_fname] = ["node", dest_path.with_suffix(".js")] + + p = subprocess.run(["npm", "install"], cwd=tmpdir) + if p.returncode != 0: + result.has_issues = True + return result + + p = subprocess.run( + ["node_modules/.bin/tsc"] + + ["s" / trimmed_fname for trimmed_fname in trimmed_fnames.values()], + cwd=tmpdir, + ) + result.has_issues = p.returncode != 0 + + return result + + @classmethod + def format(cls, fnames: list[str]) -> None: + # subprocess.run(["npx", "prettier", "--write", *fnames], check=True) + pass diff --git a/automation/snippets/migrate-snippet.py b/automation/snippets/migrate-snippet.py new file mode 100755 index 000000000..d03e36f28 --- /dev/null +++ b/automation/snippets/migrate-snippet.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +"""Migrate the specified `.md` snippet to the runnable snippet file. + +E.g., converts + `snippets/create-collection/simple/python.md` +to + `snippets/create-collection/simple/python.py` + +Not reliable, might require manual adjustments. +Intended to be manually used to migrate non-runnable snippets into runnable +ones. +""" + +import argparse +from pathlib import Path + +from lib import log +from lib.languages import ALL_LANGUAGES + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("MD_FILE", nargs="+", type=Path) + args = parser.parse_args() + + for md_file in args.MD_FILE: + if md_file.suffix != ".md": + log(f"Skipping non-markdown file: {md_file}") + continue + for lang in ALL_LANGUAGES: + if md_file.name == f"{lang.NAME}.md": + break + else: + log(f"Skipping unknown language file: {md_file}") + continue + + lines = md_file.read_text().rstrip().splitlines() + if not lines[0].startswith("```") or lines[-1] != "```": + continue + content = "\n".join(lines[1:-1]) + "\n" + content = lang.unshorten(content) + + log(f"Converting {md_file} to {lang.SNIPPET_FILENAME}") + (md_file.parent / lang.SNIPPET_FILENAME).write_text(content) + + +if __name__ == "__main__": + main() diff --git a/automation/snippets/pyproject.toml b/automation/snippets/pyproject.toml new file mode 100644 index 000000000..fcba95d25 --- /dev/null +++ b/automation/snippets/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "snippet-checker" +version = "0.1.0" +requires-python = ">=3.12" +dependencies = [ + "requests>=2.32.5", +] + +[dependency-groups] +dev = [ + "mypy>=1.18.2", +] diff --git a/automation/snippets/shell.nix b/automation/snippets/shell.nix new file mode 100644 index 000000000..fb3b79c83 --- /dev/null +++ b/automation/snippets/shell.nix @@ -0,0 +1,26 @@ +with import { }; +mkShell { + buildInputs = [ + # for ./sync-clients.py + git + + # for csharp client + dotnetCorePackages.sdk_8_0-bin + + # for go client + go + + # for java client + openjdk + + # for python client + uv + + # for rust client + cargo + rustfmt + + # for typescript client + pnpm + ]; +} diff --git a/automation/snippets/snippets b/automation/snippets/snippets new file mode 120000 index 000000000..26fac97b6 --- /dev/null +++ b/automation/snippets/snippets @@ -0,0 +1 @@ +../../qdrant-landing/content/documentation/headless/snippets \ No newline at end of file diff --git a/automation/snippets/sync-clients.py b/automation/snippets/sync-clients.py new file mode 100755 index 000000000..a7e2dba07 --- /dev/null +++ b/automation/snippets/sync-clients.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 + +import argparse +import re +import subprocess +import sys +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent + +# Change these to use different revisions by default/on CI +CSHARP_CLIENT_REVISION = "main" +JAVA_CLIENT_REVISION = "master" +TYPESCRIPT_CLIENT_REVISION = "master" + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Fetch and prepare Qdrant client libraries." + ) + subparsers = parser.add_subparsers(dest="command", required=True) + + fetch_parser = subparsers.add_parser( + "fetch", + help="""Fetch client libraries and put them in the clients/ directory. + This is for java and C# clients.""", + ) + fetch_parser.add_argument( + "--csharp", + help="Fetch and build the C# client.", + nargs="?", + const=CSHARP_CLIENT_REVISION, + metavar="GIT_REV", + ) + fetch_parser.add_argument( + "--java", + help="Fetch the Java client.", + nargs="?", + const=JAVA_CLIENT_REVISION, + metavar="GIT_REV", + ) + fetch_parser.add_argument( + "--typescript", + help="Fetch the TypeScript client.", + nargs="?", + const=TYPESCRIPT_CLIENT_REVISION, + metavar="GIT_REV", + ) + + lock_parser = subparsers.add_parser( + "lock", + help="Update lockfiles for templates/ directories.", + ) + lock_parser.add_argument( + "--go", + help="Set Go client version", + nargs="?", + const="master", + metavar="GIT_REV", + ) + lock_parser.add_argument( + "--rust", + help="""Set Rust client version. + Format: branch=BRANCH_NAME or tag=TAG_NAME or rev=COMMIT_HASH""", + nargs="?", + const="branch=master", + metavar="KIND=REV", + ) + lock_parser.add_argument( + "--python", + help="""Set Python client version. + Format: branch=BRANCH_NAME or tag=TAG_NAME or rev=COMMIT_HASH""", + nargs="?", + const="branch=master", + metavar="KIND=REV", + ) + + args = parser.parse_args() + + ok = False + if args.command == "fetch": + if args.csharp is not None: + url = "https://github.com/qdrant/qdrant-dotnet" + dir = BASE_DIR / "clients" / "csharp" + checkout_repo(url, dir, args.csharp) + prepare_csharp(dir) + ok = True + if args.java is not None: + url = "https://github.com/qdrant/java-client" + dir = BASE_DIR / "clients" / "java" + checkout_repo(url, dir, "master") + ok = True + if args.typescript is not None: + url = "https://github.com/qdrant/qdrant-js" + dir = BASE_DIR / "clients" / "typescript" + checkout_repo(url, dir, args.typescript) + prepare_typescript(dir) + ok = True + if not ok: + print("Specify at least one of --csharp, --java, --typescript to fetch.") + elif args.command == "lock": + if args.go is not None: + lock_go(args.go) + ok = True + if args.rust is not None: + lock_rust(args.rust) + ok = True + if args.python is not None: + lock_python(args.python) + ok = True + if not ok: + print("Specify at least one of --go, --rust to lock.") + + +def prepare_csharp(dir: Path) -> None: + subprocess.run(["git", "-C", dir, "clean", "-fxd"], check=True) + subprocess.run( + ["dotnet", "run", "--project", "build", "--", "download-protos", "restore"], + check=True, + cwd=dir, + ) + subprocess.run( + ["dotnet", "build", "-c", "Release", "--nologo"], + check=True, + cwd=dir, + ) + + +def prepare_typescript(dir: Path) -> None: + subprocess.run(["pnpm", "install"], cwd=dir, check=True) + subprocess.run(["pnpm", "-r", "build"], cwd=dir, check=True) + + +def lock_go(rev: str) -> None: + dir = BASE_DIR / "templates" / "go" + subprocess.run( + ["go", "get", f"github.com/qdrant/go-client@{rev}"], cwd=dir, check=True + ) + subprocess.run(["go", "mod", "tidy"], cwd=dir, check=True) + + +def lock_rust(spec: str) -> None: + dir = BASE_DIR / "templates" / "rust" + update_toml_spec(dir / "Cargo.toml", spec) + subprocess.run(["cargo", "update", "-p", "qdrant-client"], cwd=dir, check=True) + + +def lock_python(spec: str) -> None: + dir = BASE_DIR / "templates" / "python" + update_toml_spec(dir / "pyproject.toml", spec) + subprocess.run(["uv", "lock"], cwd=dir, check=True) + + +def update_toml_spec(path: Path, spec: str) -> None: + try: + kind, rev = spec.split("=", 1) + if kind not in ("branch", "tag", "rev"): + raise ValueError + except ValueError: + print("Invalid format for --rust/--python.") + print("Use branch=BRANCH_NAME or tag=TAG_NAME or rev=COMMIT_HASH.") + sys.exit(1) + + toml_content = path.read_text().splitlines() + re_line = re.compile(r'^(qdrant-client = \{ git = "[^"]+").*$') + idx, m = next( + (i, m) + for i, line in enumerate(toml_content) + if (m := re_line.match(line)) is not None + ) + + toml_content[idx] = f'{m[1]}, {kind} = "{rev}" }}' + path.write_text("\n".join(toml_content) + "\n") + + +def checkout_repo( + repo_url: str, + dest_dir: Path, + rev: str, +) -> None: + if not dest_dir.exists(): + print(f"Cloning repository from {repo_url} to {dest_dir}...") + subprocess.run(["git", "clone", repo_url, dest_dir], check=True) + else: + print(f"Repository already exists at {dest_dir}. Fetching latest changes...") + subprocess.run(["git", "-C", dest_dir, "fetch", "-p", "origin"], check=True) + + # Try remote branch first (ignore local branches) + # If this check fails, it's either a commit hash, local branch, or tag + p = subprocess.run( + ["git", "-C", dest_dir, "rev-parse", "--verify", f"refs/remotes/origin/{rev}"], + stdout=subprocess.PIPE, + text=True, + ) + if p.returncode == 0: + rev = p.stdout.strip() + + subprocess.run(["git", "-C", dest_dir, "checkout", "--detach", rev], check=True) + + +if __name__ == "__main__": + main() diff --git a/automation/snippets/templates/csharp/.gitignore b/automation/snippets/templates/csharp/.gitignore new file mode 100644 index 000000000..4c7473ded --- /dev/null +++ b/automation/snippets/templates/csharp/.gitignore @@ -0,0 +1,2 @@ +/bin +/obj diff --git a/automation/snippets/templates/csharp/Program.cs b/automation/snippets/templates/csharp/Program.cs new file mode 100644 index 000000000..874fac37b --- /dev/null +++ b/automation/snippets/templates/csharp/Program.cs @@ -0,0 +1,22 @@ +try +{ + foreach (var arg in args) + { + switch (arg) + { + // %cases% + default: + Console.WriteLine($"Unknown argument: {arg}"); + Environment.Exit(1); + break; + } + } +} +catch (Exception e) +{ + Console.Error.WriteLine($"Error occurred: {e.Message}"); + Console.Error.WriteLine(e.StackTrace); + Environment.Exit(1); +} + +await Task.CompletedTask; diff --git a/automation/snippets/templates/csharp/csharp.csproj b/automation/snippets/templates/csharp/csharp.csproj new file mode 100644 index 000000000..8adfe06bf --- /dev/null +++ b/automation/snippets/templates/csharp/csharp.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + enable + snippets-amalgamation + + + + + + + diff --git a/automation/snippets/templates/go/go.mod b/automation/snippets/templates/go/go.mod new file mode 100644 index 000000000..45dfb9c09 --- /dev/null +++ b/automation/snippets/templates/go/go.mod @@ -0,0 +1,14 @@ +module example.com/snippets-amalgamation + +go 1.25.2 + +require github.com/qdrant/go-client v1.16.0 + +require ( + golang.org/x/net v0.47.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect + google.golang.org/grpc v1.76.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect +) diff --git a/automation/snippets/templates/go/go.sum b/automation/snippets/templates/go/go.sum new file mode 100644 index 000000000..305406d06 --- /dev/null +++ b/automation/snippets/templates/go/go.sum @@ -0,0 +1,38 @@ +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/qdrant/go-client v1.16.0 h1:DTkC3eppWXKhjQs+IgA9LFKOkSjJ4sTHs2jJPCni7dY= +github.com/qdrant/go-client v1.16.0/go.mod h1:I+EL3h4HRoRTeHtbfOd/4kDXwCukZfkd41j/9wryGkw= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= diff --git a/automation/snippets/templates/go/main.go b/automation/snippets/templates/go/main.go new file mode 100644 index 000000000..00188cf5c --- /dev/null +++ b/automation/snippets/templates/go/main.go @@ -0,0 +1,19 @@ +package main + +import ( + "fmt" + _ "github.com/qdrant/go-client/qdrant" + "os" + // %imports% +) + +func main() { + for _, arg := range os.Args[1:] { + switch arg { + // %cases% + default: + fmt.Printf("Unknown argument: %s\n", arg) + os.Exit(1) + } + } +} diff --git a/automation/snippets/templates/java/.gitignore b/automation/snippets/templates/java/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/automation/snippets/templates/java/.gitignore @@ -0,0 +1 @@ +/build diff --git a/automation/snippets/templates/java/build.gradle b/automation/snippets/templates/java/build.gradle new file mode 100644 index 000000000..925ef6646 --- /dev/null +++ b/automation/snippets/templates/java/build.gradle @@ -0,0 +1,51 @@ +plugins { + id 'java' + id 'application' + id 'com.github.johnrengelman.shadow' version '8.1.1' +} + +group = 'com.example.snippets_amalgamation' +version = '1.0.0' + +repositories { + mavenCentral() +} + +dependencies { + implementation 'io.qdrant:client' + + // gRPC dependencies - use the same version as Qdrant client + implementation 'io.grpc:grpc-netty-shaded:1.65.1' + implementation 'io.grpc:grpc-protobuf:1.65.1' + implementation 'io.grpc:grpc-stub:1.65.1' + implementation 'io.grpc:grpc-services:1.65.1' + + // Protobuf runtime + implementation 'com.google.protobuf:protobuf-java:3.25.4' + + // Logging + implementation 'org.slf4j:slf4j-simple:2.0.14' +} + +application { + mainClass = 'com.example.snippets_amalgamation.Main' +} + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +// Create a proper fat jar and merge service descriptor files (required by gRPC) +shadowJar { + mergeServiceFiles() + archiveBaseName.set('app') + archiveClassifier.set('all') +} + +// Keep the plain jar minimal (no dependency unpacking) +jar { + manifest { + attributes 'Main-Class': 'com.example.snippets_amalgamation.Main' + } +} diff --git a/automation/snippets/templates/java/gradle/wrapper/gradle-wrapper.jar b/automation/snippets/templates/java/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..1b33c55ba Binary files /dev/null and b/automation/snippets/templates/java/gradle/wrapper/gradle-wrapper.jar differ diff --git a/automation/snippets/templates/java/gradle/wrapper/gradle-wrapper.properties b/automation/snippets/templates/java/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..d4081da47 --- /dev/null +++ b/automation/snippets/templates/java/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/automation/snippets/templates/java/gradlew b/automation/snippets/templates/java/gradlew new file mode 100755 index 000000000..23d15a936 --- /dev/null +++ b/automation/snippets/templates/java/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/automation/snippets/templates/java/gradlew.bat b/automation/snippets/templates/java/gradlew.bat new file mode 100644 index 000000000..5eed7ee84 --- /dev/null +++ b/automation/snippets/templates/java/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/automation/snippets/templates/java/settings.gradle b/automation/snippets/templates/java/settings.gradle new file mode 100644 index 000000000..925cecc57 --- /dev/null +++ b/automation/snippets/templates/java/settings.gradle @@ -0,0 +1 @@ +includeBuild(clientPath) diff --git a/automation/snippets/templates/java/src/main/java/Main.java b/automation/snippets/templates/java/src/main/java/Main.java new file mode 100644 index 000000000..2b593523c --- /dev/null +++ b/automation/snippets/templates/java/src/main/java/Main.java @@ -0,0 +1,19 @@ +package com.example.snippets_amalgamation; + +public class Main { + public static void main(String[] args) { + try { + for (String arg : args) { + switch (arg) { + // %cases% + default: + System.out.println("Unknown argument: " + arg); + } + } + } catch (Exception e) { + System.err.println("Error occurred: " + e.getMessage()); + e.printStackTrace(); + System.exit(1); + } + } +} diff --git a/automation/snippets/templates/python/main.py b/automation/snippets/templates/python/main.py new file mode 100755 index 000000000..36e26eaed --- /dev/null +++ b/automation/snippets/templates/python/main.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python3 + +from qdrant_client import QdrantClient + +print(f"Hello, {QdrantClient}!") diff --git a/automation/snippets/templates/python/pyproject.toml b/automation/snippets/templates/python/pyproject.toml new file mode 100644 index 000000000..40402ae70 --- /dev/null +++ b/automation/snippets/templates/python/pyproject.toml @@ -0,0 +1,15 @@ +[project] +name = "snippets-amalgamation" +version = "0.1.0" +requires-python = ">=3.12" +dependencies = [ + "qdrant-client", +] + +[tool.uv.sources] +qdrant-client = { git = "https://github.com/qdrant/qdrant-client", tag = "v1.16.0" } + +[dependency-groups] +dev = [ + "mypy>=1.18.2", +] diff --git a/automation/snippets/templates/rust/.gitignore b/automation/snippets/templates/rust/.gitignore new file mode 100644 index 000000000..ea8c4bf7f --- /dev/null +++ b/automation/snippets/templates/rust/.gitignore @@ -0,0 +1 @@ +/target diff --git a/automation/snippets/templates/rust/Cargo.toml b/automation/snippets/templates/rust/Cargo.toml new file mode 100644 index 000000000..faf99f445 --- /dev/null +++ b/automation/snippets/templates/rust/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "snippets-amalgamation" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = "1.0.100" +qdrant-client = { git = "https://github.com/qdrant/rust-client", branch = "master" } +tokio = { version = "1.48.0", features = ["rt-multi-thread", "macros"] } diff --git a/automation/snippets/templates/rust/src/main.rs b/automation/snippets/templates/rust/src/main.rs new file mode 100644 index 000000000..af070cbc7 --- /dev/null +++ b/automation/snippets/templates/rust/src/main.rs @@ -0,0 +1,14 @@ +// %mods% + +#[tokio::main] +async fn main() { + for arg in std::env::args().skip(1) { + match arg.as_str() { + // %cases% + _ => { + eprintln!("Unknown argument: {}", arg); + std::process::exit(1); + } + } + } +} diff --git a/automation/snippets/templates/typescript/.gitignore b/automation/snippets/templates/typescript/.gitignore new file mode 100644 index 000000000..07e6e472c --- /dev/null +++ b/automation/snippets/templates/typescript/.gitignore @@ -0,0 +1 @@ +/node_modules diff --git a/automation/snippets/templates/typescript/package.json b/automation/snippets/templates/typescript/package.json new file mode 100644 index 000000000..1477aa428 --- /dev/null +++ b/automation/snippets/templates/typescript/package.json @@ -0,0 +1,17 @@ +{ + "name": "snippets-amalgamation", + "version": "1.0.0", + "description": "", + "main": "1.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "devDependencies": { + "typescript": "^5.9.3" + }, + "dependencies": { + "@qdrant/js-client-rest": "file:../../clients/typescript/packages/js-client-rest" + } +}