Files
iLSP/ilsp/bicep_lsp/modules.py

106 lines
3.6 KiB
Python
Raw Normal View History

"""
LRU Bicep module catalog fetched from DevOpsMCP at startup.
Provides completion items for LRU-internal Bicep modules with
higher sort priority than standard Azure modules.
"""
import asyncio
import logging
import os
import time
from typing import Any
import aiohttp
logger = logging.getLogger(__name__)
DEVOPS_MCP_URL = os.getenv("DEVOPS_MCP_URL", "https://devops-mcp.i80.dk")
REFRESH_INTERVAL = 3600 # 1 hour
class BicepModuleCatalog:
"""Fetches and caches LRU Bicep modules from DevOpsMCP."""
_modules: list[dict[str, Any]] = []
_last_refresh: float = 0
_lock = asyncio.Lock()
@classmethod
async def get_modules(cls) -> list[dict[str, Any]]:
if not cls._modules or time.time() - cls._last_refresh > REFRESH_INTERVAL:
await cls._refresh()
return cls._modules
@classmethod
async def start_background_refresh(cls) -> None:
asyncio.create_task(cls._refresh_loop())
@classmethod
async def _refresh_loop(cls) -> None:
while True:
await cls._refresh()
await asyncio.sleep(REFRESH_INTERVAL)
@classmethod
async def _refresh(cls) -> None:
async with cls._lock:
try:
modules = await cls._fetch_from_devops_mcp()
cls._modules = modules
cls._last_refresh = time.time()
logger.info("Bicep module catalog refreshed: %d modules", len(modules))
except Exception:
logger.exception("Failed to refresh Bicep module catalog — using stale cache")
@classmethod
async def _fetch_from_devops_mcp(cls) -> list[dict[str, Any]]:
"""Call DevOpsMCP list_bicep_modules tool via HTTP."""
url = f"{DEVOPS_MCP_URL}/call-tool"
payload = {"tool": "list_bicep_modules", "arguments": {}}
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=20)) as session:
async with session.post(url, json=payload) as resp:
if resp.status != 200:
raise RuntimeError(f"DevOpsMCP returned {resp.status}")
data = await resp.json()
modules = []
for m in data.get("modules", []):
path = m.get("path", "")
versions = m.get("versions", ["latest"])
name = path.split("/")[-1] if "/" in path else path
modules.append({
"name": name,
"path": path,
"versions": versions,
"latest": versions[-1] if versions else "latest",
"registry": data.get("registry", "iactemplatereg.azurecr.io"),
})
return modules
@classmethod
def as_completion_items(cls) -> list[dict[str, Any]]:
items = []
for mod in cls._modules:
ref = f"br/modules:{mod['path']}:{mod['latest']}"
items.append({
"label": mod["name"],
"kind": 9, # Module
"detail": f"LRU Bicep module — {mod['registry']}",
"insertText": ref,
"sortText": f"0_lru_{mod['name']}", # sorts above standard az modules
"documentation": {
"kind": "markdown",
"value": (
f"**{mod['name']}** (LRU internal)\n\n"
f"Registry: `{mod['registry']}`\n"
f"Versions: {', '.join(mod['versions'])}\n\n"
f"```bicep\nmodule {mod['name'].lower()} '{ref}' = {{\n"
f" name: '{mod['name'].lower()}'\n params: {{}}\n}}\n```"
),
},
})
return items