diff --git a/poetry.lock b/poetry.lock index fa151335..c64f506f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -301,6 +301,17 @@ typing = ["build[uv]", "importlib-metadata (>=5.1)", "mypy (>=1.9.0,<1.10.0)", " uv = ["uv (>=0.1.18)"] virtualenv = ["virtualenv (>=20.0.35)"] +[[package]] +name = "cachetools" +version = "5.5.1" +description = "Extensible memoizing collections and decorators" +optional = false +python-versions = ">=3.7" +files = [ + {file = "cachetools-5.5.1-py3-none-any.whl", hash = "sha256:b76651fdc3b24ead3c648bbdeeb940c1b04d365b38b4af66788f9ec4a81d42bb"}, + {file = "cachetools-5.5.1.tar.gz", hash = "sha256:70f238fbba50383ef62e55c6aff6d9673175fe59f7c6782c7a0b9e38f4a9df95"}, +] + [[package]] name = "certifi" version = "2024.12.14" @@ -604,6 +615,7 @@ files = [ {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb"}, {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b"}, {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:60eb32934076fa07e4316b7b2742fa52cbb190b42c2df2863dbc4230a0a9b385"}, {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e"}, {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e"}, {file = "cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053"}, @@ -614,6 +626,7 @@ files = [ {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289"}, {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7"}, {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9abcc2e083cbe8dde89124a47e5e53ec38751f0d7dfd36801008f316a127d7ba"}, {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64"}, {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285"}, {file = "cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417"}, @@ -3083,4 +3096,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.12,<4.0" -content-hash = "479762cfb2027723fecb196828874a9fa5cbfe152e67187f42e07a5181930231" +content-hash = "f1907d1d44405e2e671a9584c7150823dc573468452eea2cc5d66df55f904f03" diff --git a/pyproject.toml b/pyproject.toml index 5d74c6a0..9e2d55c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ alembic = "==1.14.1" pygments = "==2.19.1" sqlite-vec-sl-tmp = "==0.0.4" greenlet = "==3.1.1" +cachetools = "==5.5.1" [tool.poetry.group.dev.dependencies] pytest = "==8.3.4" diff --git a/src/codegate/pipeline/cli/commands.py b/src/codegate/pipeline/cli/commands.py index da52c42d..5b101400 100644 --- a/src/codegate/pipeline/cli/commands.py +++ b/src/codegate/pipeline/cli/commands.py @@ -1,6 +1,7 @@ from abc import ABC, abstractmethod -from typing import Awaitable, Callable, Dict, List, Tuple +from typing import Awaitable, Callable, Dict, List, Optional, Tuple +from cachetools import TTLCache from pydantic import ValidationError from codegate import __version__ @@ -16,6 +17,11 @@ class NoSubcommandError(Exception): pass +# 1 second cache. 1 second is to be short enough to not affect UX but long enough to +# reply the same to concurrent requests. Needed for Copilot. +command_cache = TTLCache(maxsize=10, ttl=1) + + class CodegateCommand(ABC): @abstractmethod async def run(self, args: List[str]) -> str: @@ -31,10 +37,46 @@ def command_name(self) -> str: def help(self) -> str: pass + async def _get_full_command(self, args: List[str]) -> str: + """ + Get the full command string with the command name and args. + """ + joined_args = " ".join(args) + return f"{self.command_name} {joined_args}" + + async def _record_in_cache(self, args: List[str], cmd_out: str) -> None: + """ + Record the command in the cache. + """ + full_command = await self._get_full_command(args) + command_cache[full_command] = cmd_out + + async def _cache_lookup(self, args: List[str]) -> Optional[str]: + """ + Look up the command in the cache. If the command was executed less than 1 second ago, + return the cached output. + """ + full_command = await self._get_full_command(args) + cmd_out = command_cache.get(full_command) + return cmd_out + async def exec(self, args: List[str]) -> str: + """ + Execute the command and cache the output. The cache is invalidated after 1 second. + + 1. Check if the command is help. If it is, return the help text. + 2. Check if the command is in the cache. If it is, return the cached output. + 3. Run the command and cache the output. + 4. Return the output. + """ if args and args[0] == "-h": return self.help - return await self.run(args) + cached_out = await self._cache_lookup(args) + if cached_out: + return cached_out + cmd_out = await self.run(args) + await self._record_in_cache(args, cmd_out) + return cmd_out class Version(CodegateCommand):