Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"ignoreWorkspaces": [
"packages/shared",
"packages/lakebase",
"packages/appkit-py",
"apps/**",
"docs"
],
Expand All @@ -18,7 +19,9 @@
"**/*.css",
"template/**",
"tools/**",
"docs/**"
"docs/**",
"client/**",
"test-e2e-minimal.ts"
],
"ignoreDependencies": ["json-schema-to-typescript"],
"ignoreBinaries": ["tarball"]
Expand Down
25 changes: 25 additions & 0 deletions packages/appkit-py/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Python
__pycache__/
*.py[cod]
*.egg-info/
*.egg
dist/
build/
.eggs/

# Virtual environment
.venv/
venv/

# IDE
.idea/
.vscode/
*.swp

# Testing
.pytest_cache/
htmlcov/
.coverage

# OS
.DS_Store
48 changes: 48 additions & 0 deletions packages/appkit-py/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
[project]
name = "appkit-py"
version = "0.1.0"
description = "Python backend for Databricks AppKit — 100% API compatible with the TypeScript version"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.115",
"uvicorn[standard]>=0.30",
"starlette>=0.40",
"databricks-sdk>=0.30",
"pyarrow>=14.0",
"httpx>=0.27",
"pydantic>=2.0",
"cachetools>=5.3",
"python-dotenv>=1.0",
]

[project.optional-dependencies]
dev = [
"pytest>=8.0",
"pytest-asyncio>=0.23",
"httpx>=0.27",
"pytest-cov>=5.0",
"ruff>=0.5",
"mypy>=1.10",
]

[build-system]
requires = ["setuptools>=68.0"]
build-backend = "setuptools.build_meta"

[tool.setuptools.packages.find]
where = ["src"]

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
markers = [
"integration: marks tests that require a running backend server",
"unit: marks unit tests that run in isolation",
]

[tool.ruff]
target-version = "py312"
line-length = 100

[tool.ruff.lint]
select = ["E", "F", "I", "W"]
29 changes: 29 additions & 0 deletions packages/appkit-py/src/appkit_py/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Python backend for Databricks AppKit — 100% API compatible with the TypeScript version.

Usage (mirrors TS):
from appkit_py import create_app, server, analytics, files, genie

appkit = await create_app(plugins=[
server({"autoStart": False}),
analytics({}),
files(),
genie({"spaces": {"demo": "space-id"}}),
])
"""

from appkit_py.core.appkit import create_app
from appkit_py.plugin.plugin import Plugin, to_plugin
from appkit_py.plugins.analytics.plugin import analytics
from appkit_py.plugins.files.plugin import files
from appkit_py.plugins.genie.plugin import genie
from appkit_py.plugins.server.plugin import server

__all__ = [
"create_app",
"Plugin",
"to_plugin",
"server",
"analytics",
"files",
"genie",
]
25 changes: 25 additions & 0 deletions packages/appkit-py/src/appkit_py/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Entry point for running the AppKit Python backend with `python -m appkit_py`."""

import os

from dotenv import load_dotenv


def main() -> None:
load_dotenv()

import uvicorn

from appkit_py.server import create_server

# Match TS AppKit env vars for compatibility
host = os.environ.get("FLASK_RUN_HOST", os.environ.get("APPKIT_HOST", "0.0.0.0"))
port = int(os.environ.get("DATABRICKS_APP_PORT", "8000"))
log_level = os.environ.get("APPKIT_LOG_LEVEL", "info")

app = create_server()
uvicorn.run(app, host=host, port=port, log_level=log_level)


if __name__ == "__main__":
main()
Empty file.
Empty file.
86 changes: 86 additions & 0 deletions packages/appkit-py/src/appkit_py/cache/cache_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""CacheManager with TTL-based in-memory caching.

Mirrors the TypeScript CacheManager from packages/appkit/src/cache/index.ts.
"""

from __future__ import annotations

import hashlib
import json
import time
from typing import Any, Awaitable, Callable, TypeVar

T = TypeVar("T")


class CacheManager:
"""In-memory TTL cache with SHA256 key generation."""

_instance: CacheManager | None = None

def __init__(self) -> None:
self._store: dict[str, tuple[Any, float]] = {} # key -> (value, expires_at)

@classmethod
def get_instance(cls) -> CacheManager:
if cls._instance is None:
cls._instance = cls()
return cls._instance

@classmethod
def get_instance_sync(cls) -> CacheManager:
return cls.get_instance()

@classmethod
def reset(cls) -> None:
cls._instance = None

def generate_key(self, parts: list[Any], user_key: str) -> str:
"""Generate a SHA256 cache key from parts and user key."""
raw = json.dumps([user_key] + [str(p) for p in parts], sort_keys=True)
return hashlib.sha256(raw.encode()).hexdigest()

async def get_or_execute(
self,
key_parts: list[Any],
fn: Callable[[], Awaitable[T]],
user_key: str,
ttl: float = 300,
) -> T:
"""Get cached value or execute function and cache the result."""
cache_key = self.generate_key(key_parts, user_key)

# Check cache
if cache_key in self._store:
value, expires_at = self._store[cache_key]
if time.time() < expires_at:
return value
else:
del self._store[cache_key]

# Execute and cache
result = await fn()
self._store[cache_key] = (result, time.time() + ttl)
return result

def get(self, key: str) -> Any | None:
if key in self._store:
value, expires_at = self._store[key]
if time.time() < expires_at:
return value
del self._store[key]
return None

def set(self, key: str, value: Any, ttl: float = 300) -> None:
self._store[key] = (value, time.time() + ttl)

def delete(self, key: str) -> None:
self._store.pop(key, None)

def has(self, key: str) -> bool:
if key in self._store:
_, expires_at = self._store[key]
if time.time() < expires_at:
return True
del self._store[key]
return False
Empty file.
Empty file.
Loading
Loading