From dba43ff4ef8e1f2c90b549ca300f0ca5e8d136b1 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 7 Feb 2026 05:38:49 +0000 Subject: [PATCH] Refactor project setup: Remove setup.py, Optimize Dockerfile, Simplify Config - Removed redundant `setup.py` as `pyproject.toml` is sufficient. - Updated `Dockerfile` to use `python:3.11-slim-bookworm` for smaller size and better security. - Removed `sudo` from Docker image and run as non-root user. - Simplified `find_claude_binary` in `config.py` to rely on PATH and env var. - Updated `Makefile` to be more robust and consistent. - Added `requests` to test dependencies. - Fixed `black` configuration in `pyproject.toml`. Co-authored-by: Mehdi-Bl <8613869+Mehdi-Bl@users.noreply.github.com> --- Makefile | 21 ++++------- claude_code_api/core/config.py | 48 +++++------------------ claude_code_api/core/security.py | 8 +++- docker/Dockerfile | 17 +++------ pyproject.toml | 2 +- setup.py | 65 -------------------------------- tests/test_config.py | 32 ---------------- 7 files changed, 30 insertions(+), 163 deletions(-) delete mode 100644 setup.py diff --git a/Makefile b/Makefile index 9bda0d0..9ba1987 100644 --- a/Makefile +++ b/Makefile @@ -70,7 +70,7 @@ kill: echo "Error: PORT parameter is required. Usage: make kill PORT=8001"; \ else \ echo "Looking for processes on port $(PORT)..."; \ - if [ "$$(uname)" = "Darwin" ] || [ "$$(uname)" = "Linux" ]; then \ + if command -v lsof >/dev/null 2>&1; then \ PID=$$(lsof -iTCP:$(PORT) -sTCP:LISTEN -t); \ if [ -n "$$PID" ]; then \ echo "Found process(es) with PID(s): $$PID"; \ @@ -79,7 +79,12 @@ kill: echo "No process found listening on port $(PORT)."; \ fi; \ else \ - echo "This command is only supported on Unix-like systems (Linux/macOS)."; \ + echo "lsof not found. Trying fuser..."; \ + if command -v fuser >/dev/null 2>&1; then \ + fuser -k $(PORT)/tcp && echo "Process(es) killed successfully."; \ + else \ + echo "Neither lsof nor fuser found. Please kill the process manually."; \ + fi; \ fi; \ fi @@ -163,15 +168,6 @@ help: @echo " make start - Start Python API server (development with reload)" @echo " make start-prod - Start Python API server (production)" @echo "" - @echo "TypeScript API:" - @echo " make install-js - Install TypeScript dependencies" - @echo " make test-js - Run TypeScript unit tests" - @echo " make test-js-real - Run Python test suite against TypeScript API" - @echo " make start-js - Start TypeScript API server (production)" - @echo " make start-js-dev - Start TypeScript API server (development with reload)" - @echo " make start-js-prod - Build and start TypeScript API server (production)" - @echo " make build-js - Build TypeScript project" - @echo "" @echo "Quality & Security:" @echo " make sonar - Run SonarQube analysis (generates coverage + scans)" @echo " make sonar-cloud - Run SonarCloud scanner (uses SONAR_CLOUD_TOKEN)" @@ -186,6 +182,3 @@ help: @echo "General:" @echo " make clean - Clean up Python cache files" @echo " make kill PORT=X - Kill process on specific port" - @echo "" - @echo "IMPORTANT: Both implementations are functionally equivalent!" - @echo "Use Python or TypeScript - both provide the same OpenAI-compatible API." diff --git a/claude_code_api/core/config.py b/claude_code_api/core/config.py index 6c0b31e..fd09b8e 100644 --- a/claude_code_api/core/config.py +++ b/claude_code_api/core/config.py @@ -2,6 +2,7 @@ import os import shutil +from pathlib import Path from typing import List from pydantic import Field, field_validator @@ -11,62 +12,33 @@ def find_claude_binary() -> str: """Find Claude binary path automatically.""" # First check environment variable - if "CLAUDE_BINARY_PATH" in os.environ: - claude_path = os.environ["CLAUDE_BINARY_PATH"] - if os.path.exists(claude_path): - return claude_path + env_path = os.environ.get("CLAUDE_BINARY_PATH") + if env_path: + path = Path(env_path) + if path.exists(): + return str(path) - # Try to find claude in PATH - this should work for npm global installs + # Try to find claude in PATH claude_path = shutil.which("claude") if claude_path: return claude_path - # Import npm environment if needed - try: - import subprocess - - # Try to get npm global bin path - result = subprocess.run(["npm", "bin", "-g"], capture_output=True, text=True) - if result.returncode == 0: - npm_bin_path = result.stdout.strip() - claude_npm_path = os.path.join(npm_bin_path, "claude") - if os.path.exists(claude_npm_path): - return claude_npm_path - except Exception: - pass - - # Fallback to common npm/nvm locations - import glob - - common_patterns = [ - "/usr/local/bin/claude", - "/usr/local/share/nvm/versions/node/*/bin/claude", - "~/.nvm/versions/node/*/bin/claude", - ] - - for pattern in common_patterns: - expanded_pattern = os.path.expanduser(pattern) - matches = glob.glob(expanded_pattern) - if matches: - # Return the most recent version - return sorted(matches)[-1] - return "claude" # Final fallback def default_project_root() -> str: """Default project root under the current working directory.""" - return os.path.join(os.getcwd(), "claude_projects") + return str(Path.cwd() / "claude_projects") def default_session_map_path() -> str: """Default path for CLI-to-API session mapping.""" - return os.path.join(os.getcwd(), "claude_sessions", "session_map.json") + return str(Path.cwd() / "claude_sessions" / "session_map.json") def default_log_file_path() -> str: """Default path for application logs.""" - return os.path.join(os.getcwd(), "dist", "logs", "claude-code-api.log") + return str(Path.cwd() / "dist" / "logs" / "claude-code-api.log") def _is_shell_script_line(line: str) -> bool: diff --git a/claude_code_api/core/security.py b/claude_code_api/core/security.py index 4b60937..6608714 100644 --- a/claude_code_api/core/security.py +++ b/claude_code_api/core/security.py @@ -78,7 +78,9 @@ def resolve_path_within_base(path: str, base_path: str) -> str: path_value = os.fspath(path) normalized_path = os.path.normpath(path_value) if not os.path.isabs(normalized_path): - if normalized_path == ".." or normalized_path.startswith(f"..{os.path.sep}"): + if normalized_path == ".." or normalized_path.startswith( + f"..{os.path.sep}" + ): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=PATH_TRAVERSAL_MSG, @@ -86,7 +88,9 @@ def resolve_path_within_base(path: str, base_path: str) -> str: if os.path.isabs(normalized_path): resolved_path = os.path.realpath(normalized_path) else: - resolved_path = os.path.realpath(os.path.join(abs_base_path, normalized_path)) + resolved_path = os.path.realpath( + os.path.join(abs_base_path, normalized_path) + ) try: common_path = os.path.commonpath([abs_base_path, resolved_path]) diff --git a/docker/Dockerfile b/docker/Dockerfile index 0f9aa03..895e663 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,22 +1,18 @@ -FROM ubuntu:24.04 +FROM python:3.11-slim-bookworm ENV DEBIAN_FRONTEND=noninteractive +ENV PYTHONUNBUFFERED=1 -RUN apt-get update && apt-get install -y \ +RUN apt-get update && apt-get install -y --no-install-recommends \ bash \ ca-certificates \ curl \ git \ jq \ - python3 \ - python3-pip \ - python3-venv \ - sudo \ && rm -rf /var/lib/apt/lists/* # Create non-root user -RUN useradd -m -s /bin/bash claudeuser && \ - echo "claudeuser ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers +RUN useradd -m -s /bin/bash claudeuser # Set up application directory WORKDIR /home/claudeuser/app @@ -25,16 +21,15 @@ RUN chown -R claudeuser:claudeuser /home/claudeuser/app USER claudeuser -# Install Claude CLI using the official installer (no npm required) +# Install Claude CLI using the official installer RUN curl -fsSL https://claude.ai/install.sh | bash # Create virtualenv and install dependencies RUN python3 -m venv /home/claudeuser/venv && \ /home/claudeuser/venv/bin/pip install --upgrade pip setuptools wheel && \ - /home/claudeuser/venv/bin/pip install -e . --use-pep517 || \ /home/claudeuser/venv/bin/pip install -e . -ENV PATH="/home/claudeuser/venv/bin:/home/claudeuser/.local/bin:/home/claudeuser/.bun/bin:${PATH}" +ENV PATH="/home/claudeuser/venv/bin:/home/claudeuser/.local/bin:${PATH}" # Create Claude config and workspace directories RUN mkdir -p /home/claudeuser/.config/claude /home/claudeuser/app/workspace diff --git a/pyproject.toml b/pyproject.toml index 80cee6b..f89d787 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,7 @@ test = [ "pytest-cov>=4.1.0", "httpx>=0.25.0", "pytest-mock>=3.12.0", + "requests>=2.31.0", ] dev = [ "black>=23.0.0", @@ -108,7 +109,6 @@ exclude_lines = [ [tool.black] line-length = 88 target-version = ['py311'] -include = '\\.pyi?$' extend-exclude = ''' /( # directories diff --git a/setup.py b/setup.py deleted file mode 100644 index bb7c14d..0000000 --- a/setup.py +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env python -"""Setup script for claude-code-api package.""" - -import os -from setuptools import setup, find_packages - -setup( - name="claude-code-api", - version="1.0.0", - description="OpenAI-compatible API gateway for Claude Code with streaming support", - long_description=open("README.md").read() if os.path.exists("README.md") else "", - long_description_content_type="text/markdown", - author="Claude Code API Team", - url="https://github.com/claude-code-api/claude-code-api", - packages=find_packages(), - python_requires=">=3.11", - install_requires=[ - "fastapi>=0.115.0", - "uvicorn[standard]>=0.32.0", - "pydantic>=2.9.0", - "httpx>=0.25.0", - "aiofiles>=23.2.1", - "structlog>=23.2.0", - "python-multipart>=0.0.6", - "pydantic-settings>=2.1.0", - "sqlalchemy>=2.0.23", - "aiosqlite>=0.19.0", - "alembic>=1.13.0", - "passlib[bcrypt]>=1.7.4", - "python-jose[cryptography]>=3.3.0", - "python-dotenv>=1.0.0", - ], - extras_require={ - "test": [ - "pytest>=7.4.0", - "pytest-asyncio>=0.21.0", - "pytest-cov>=4.1.0", - "httpx>=0.25.0", - "pytest-mock>=3.12.0", - ], - "dev": [ - "black>=23.0.0", - "isort>=5.12.0", - "flake8>=6.0.0", - "mypy>=1.7.0", - "pre-commit>=3.5.0", - ], - }, - entry_points={ - "console_scripts": [ - "claude-code-api=claude_code_api.main:main", - ], - }, - classifiers=[ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: Internet :: WWW/HTTP :: HTTP Servers", - ], -) diff --git a/tests/test_config.py b/tests/test_config.py index 42995be..260f088 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,8 +1,6 @@ """Tests for configuration helpers.""" -import glob import os -import subprocess from claude_code_api.core import config as config_module @@ -22,39 +20,9 @@ def test_find_claude_binary_shutil(monkeypatch): assert config_module.find_claude_binary() == "/usr/local/bin/claude" -def test_find_claude_binary_npm(monkeypatch, tmp_path): - monkeypatch.delenv("CLAUDE_BINARY_PATH", raising=False) - monkeypatch.setattr(config_module.shutil, "which", lambda _name: None) - - npm_bin = tmp_path / "bin" - npm_bin.mkdir() - (npm_bin / "claude").write_text("bin") - - class Result: - returncode = 0 - stdout = str(npm_bin) - - monkeypatch.setattr(subprocess, "run", lambda *_a, **_k: Result()) - assert config_module.find_claude_binary() == str(npm_bin / "claude") - - -def test_find_claude_binary_glob(monkeypatch): - monkeypatch.delenv("CLAUDE_BINARY_PATH", raising=False) - monkeypatch.setattr(config_module.shutil, "which", lambda _name: None) - monkeypatch.setattr( - subprocess, "run", lambda *_a, **_k: (_ for _ in ()).throw(OSError("no")) - ) - monkeypatch.setattr(glob, "glob", lambda _pattern: ["/a/claude", "/b/claude"]) - assert config_module.find_claude_binary() == "/b/claude" - - def test_find_claude_binary_fallback(monkeypatch): monkeypatch.delenv("CLAUDE_BINARY_PATH", raising=False) monkeypatch.setattr(config_module.shutil, "which", lambda _name: None) - monkeypatch.setattr( - subprocess, "run", lambda *_a, **_k: (_ for _ in ()).throw(OSError("no")) - ) - monkeypatch.setattr(glob, "glob", lambda _pattern: []) assert config_module.find_claude_binary() == "claude"