setup_template.pyβ’22.4 kB
#!/usr/bin/env python3
"""
FastAPI + MCP ν
νλ¦Ώ μ€μ μ€ν¬λ¦½νΈ
- κΈ°μ‘΄ ν
νλ¦Ώ 컀μ€ν°λ§μ΄μ§
- λ°±μ
λ° λ‘€λ°± κΈ°λ₯
"""
import argparse
import json
import os
import shutil
import subprocess
import sys
from datetime import datetime
from pathlib import Path
from typing import Dict, Any
class TemplateSetup:
def __init__(self, project_path: Path):
self.project_path = project_path
self.backup_dir = project_path / ".template_backup"
self.template_files = [
"pyproject.toml",
"README.md",
".cursor/mcp.json",
"src/core/config.py",
"src/core/models.py",
"src/api/app.py",
"src/mcp/server.py",
"src/mcp/tools.py",
"src/mcp/resources.py",
"run_api_server.py",
"run_mcp_server.py",
"run_server.py",
"run_docker.py",
"Dockerfile",
"docker-compose.yml",
".dockerignore",
"examples/api_usage.py",
"tests/test_api.py",
"docs/TEMPLATE_GUIDE.md"
]
def get_user_input(self, prompt: str, default: str = "") -> str:
"""μ¬μ©μ μ
λ ₯μ λ°λ ν¨μ"""
if default:
user_input = input(f"{prompt} [{default}]: ").strip()
return user_input if user_input else default
return input(f"{prompt}: ").strip()
def get_yes_no(self, prompt: str, default: bool = True) -> bool:
"""μ/μλμ€ μ
λ ₯μ λ°λ ν¨μ"""
default_str = "Y/n" if default else "y/N"
while True:
response = input(f"{prompt} [{default_str}]: ").strip().lower()
if not response:
return default
if response in ['y', 'yes', 'μ']:
return True
if response in ['n', 'no', 'μλμ€']:
return False
print("'y' λλ 'n'μ μ
λ ₯ν΄μ£ΌμΈμ.")
def create_backup(self) -> bool:
"""νμ¬ ν
νλ¦Ώ μνλ₯Ό λ°±μ
"""
try:
if self.backup_dir.exists():
shutil.rmtree(self.backup_dir)
self.backup_dir.mkdir(exist_ok=True)
print("πΎ νμ¬ ν
νλ¦Ώ μνλ₯Ό λ°±μ
ν©λλ€...")
for file_path in self.template_files:
source_file = self.project_path / file_path
if source_file.exists():
backup_file = self.backup_dir / file_path
backup_file.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(source_file, backup_file)
# λ©νλ°μ΄ν° μ μ₯
metadata = {
"backup_time": datetime.now().isoformat(),
"original_files": [str(f) for f in self.template_files if (self.project_path / f).exists()]
}
with open(self.backup_dir / "metadata.json", 'w', encoding='utf-8') as f:
json.dump(metadata, f, indent=2, ensure_ascii=False)
print(f"β
λ°±μ
μλ£: {self.backup_dir}")
return True
except Exception as e:
print(f"β λ°±μ
μ€ν¨: {e}")
return False
def restore_backup(self) -> bool:
"""λ°±μ
μμ 볡μ"""
try:
if not self.backup_dir.exists():
print("β λ°±μ
νμΌμ΄ μμ΅λλ€.")
return False
metadata_file = self.backup_dir / "metadata.json"
if not metadata_file.exists():
print("β λ°±μ
λ©νλ°μ΄ν°κ° μμ΅λλ€.")
return False
with open(metadata_file, 'r', encoding='utf-8') as f:
metadata = json.load(f)
print("π λ°±μ
μμ 볡μν©λλ€...")
for file_path in metadata["original_files"]:
backup_file = self.backup_dir / file_path
target_file = self.project_path / file_path
if backup_file.exists():
target_file.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(backup_file, target_file)
print("β
볡μ μλ£")
return True
except Exception as e:
print(f"β 볡μ μ€ν¨: {e}")
return False
def replace_in_file(self, file_path: Path, replacements: Dict[str, str]) -> bool:
"""νμΌ λ΄μ©μμ λ¬Έμμ΄ λμΉ"""
try:
if not file_path.exists():
return False
content = file_path.read_text(encoding='utf-8')
original_content = content
for old_text, new_text in replacements.items():
content = content.replace(old_text, new_text)
if content != original_content:
file_path.write_text(content, encoding='utf-8')
return True
return False
except Exception as e:
print(f"β νμΌ μμ μ€ν¨ {file_path}: {e}")
return False
def update_all_files(self, project_info: Dict[str, Any]) -> bool:
"""λͺ¨λ νμΌμμ ν
νλ¦Ώ μ 보λ₯Ό νλ‘μ νΈ μ λ³΄λ‘ λμΉ"""
# κΈ°λ³Έ λμΉ λ§΅ν
replacements = {
# νλ‘μ νΈ μ΄λ¦ κ΄λ ¨
"fastapi-mcp-template": project_info["name"],
"FastAPI + MCP Template": project_info["title"],
"FastAPI MCP Template": project_info["title"],
# μ€λͺ
κ΄λ ¨
"FastAPI + MCP Template - νλμ μΈ APIμ LLM ν΅ν©μ μν κ°λ° ν
νλ¦Ώ": project_info["description"],
"**FastAPI**μ **MCP(Model Context Protocol)**λ₯Ό κ²°ν©ν κ°λ° ν
νλ¦Ώμ
λλ€.": project_info["description"],
"νλμ μΈ API μλ²μ LLM ν΅ν©μ μν MCP μλ²λ₯Ό λμμ μ 곡νλ μμ ν κ°λ° νκ²½μ μ 곡ν©λλ€.": f"{project_info['description']} νλ‘μ νΈμ
λλ€.",
# μμ±μ κ΄λ ¨
'authors = ["Your Name <your.email@example.com>"]': f'authors = ["{project_info.get("author", "Your Name")} <{project_info.get("email", "your.email@example.com")}>"]',
"Your Name": project_info.get("author", "Your Name"),
"your.email@example.com": project_info.get("email", "your.email@example.com"),
# ν΄λμ€/λ³μλͺ
κ΄λ ¨ (Python μλ³μλ‘ μ¬μ© κ°λ₯ν νν)
"FastApiMcpTemplate": self.to_pascal_case(project_info["name"]),
"fastapi_mcp_template": self.to_snake_case(project_info["name"]),
"FASTAPI_MCP_TEMPLATE": self.to_upper_snake_case(project_info["name"]),
# Docker κ΄λ ¨ (컨ν
μ΄λ μ΄λ¦)
"fastapi-mcp-app": f"{self.to_snake_case(project_info['name'])}-app",
"fastapi-mcp-dev": f"{self.to_snake_case(project_info['name'])}-dev",
# λλ ν 리/ν¨ν€μ§λͺ
κ΄λ ¨
"fastapi-mcp-template/": f"{project_info['name']}/",
# λ¬Έμ κ΄λ ¨
"FastAPI + MCP ν
νλ¦Ώ": project_info["title"],
"ν
νλ¦Ώ": "νλ‘μ νΈ",
# URL/λλ©μΈ κ΄λ ¨ (μμ)
"fastapi-mcp-template.com": f"{project_info['name']}.com",
"fastapi-mcp-template.example.com": f"{project_info['name']}.example.com",
}
print("π νλ‘μ νΈ νμΌλ€μ μ
λ°μ΄νΈν©λλ€...")
updated_files = []
for file_path in self.template_files:
full_path = self.project_path / file_path
if self.replace_in_file(full_path, replacements):
updated_files.append(file_path)
# μΆκ° νμΌλ€λ κ²μ¬
additional_files = [
"setup_template.py",
"scripts/setup_template.py",
"scripts/init_blank_template.py"
]
for file_path in additional_files:
full_path = self.project_path / file_path
if self.replace_in_file(full_path, replacements):
updated_files.append(file_path)
if updated_files:
print("β
λ€μ νμΌλ€μ΄ μ
λ°μ΄νΈλμμ΅λλ€:")
for file_path in updated_files:
print(f" - {file_path}")
else:
print("βΉοΈ μ
λ°μ΄νΈν νμΌμ΄ μμ΅λλ€.")
return True
def to_pascal_case(self, text: str) -> str:
"""kebab-caseλ₯Ό PascalCaseλ‘ λ³ν"""
return ''.join(word.capitalize() for word in text.replace('-', '_').split('_'))
def to_snake_case(self, text: str) -> str:
"""kebab-caseλ₯Ό snake_caseλ‘ λ³ν"""
return text.replace('-', '_')
def to_upper_snake_case(self, text: str) -> str:
"""kebab-caseλ₯Ό UPPER_SNAKE_CASEλ‘ λ³ν"""
return text.replace('-', '_').upper()
def create_gitignore(self) -> bool:
"""κΈ°λ³Έ .gitignore νμΌ μμ±"""
try:
gitignore_path = self.project_path / ".gitignore"
# μ΄λ―Έ .gitignoreκ° μμΌλ©΄ 건λλ¦¬μ§ μμ
if gitignore_path.exists():
print("βΉοΈ .gitignore νμΌμ΄ μ΄λ―Έ μ‘΄μ¬ν©λλ€.")
return True
gitignore_content = """# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Project specific
logs/
*.log
.template_backup/
.template_archive/
# uv
.python-version
"""
gitignore_path.write_text(gitignore_content, encoding='utf-8')
print("β
.gitignore νμΌμ΄ μμ±λμμ΅λλ€.")
return True
except Exception as e:
print(f"β .gitignore νμΌ μμ± μ€ν¨: {e}")
return False
def run_git_command(self, command: list[str]) -> bool:
"""Git λͺ
λ Ήμ΄ μ€ν"""
try:
result = subprocess.run(
command,
cwd=self.project_path,
capture_output=True,
text=True,
check=True
)
return True
except subprocess.CalledProcessError as e:
print(f"β Git λͺ
λ Ήμ΄ μ€ν μ€ν¨: {' '.join(command)}")
print(f" μ€λ₯: {e.stderr.strip()}")
return False
except FileNotFoundError:
print("β Gitμ΄ μ€μΉλμ΄ μμ§ μμ΅λλ€.")
return False
def is_git_repository(self) -> bool:
"""νμ¬ λλ ν λ¦¬κ° Git μ μ₯μμΈμ§ νμΈ"""
return (self.project_path / ".git").exists()
def init_git_repository(self) -> bool:
"""Git μ μ₯μ μ΄κΈ°ν"""
try:
print("π§ Git μ μ₯μλ₯Ό μ΄κΈ°νν©λλ€...")
# μ΄λ―Έ Git μ μ₯μμΈμ§ νμΈ
if self.is_git_repository():
print("βΉοΈ μ΄λ―Έ Git μ μ₯μλ‘ μ΄κΈ°νλμ΄ μμ΅λλ€.")
return True
# Git μ΄κΈ°ν
if not self.run_git_command(["git", "init"]):
return False
# κΈ°λ³Έ λΈλμΉλ₯Ό mainμΌλ‘ μ€μ
if not self.run_git_command(["git", "branch", "-M", "main"]):
print("β οΈ κΈ°λ³Έ λΈλμΉ μ€μ μ€ν¨ (κ³μ μ§ν)")
# .gitignore μμ±
self.create_gitignore()
# λͺ¨λ νμΌ μΆκ°
if not self.run_git_command(["git", "add", "."]):
return False
# μ΄κΈ° μ»€λ° μμ±
commit_message = "Initial commit: FastAPI + MCP project setup"
if not self.run_git_command(["git", "commit", "-m", commit_message]):
return False
print("β
Git μ μ₯μ μ΄κΈ°ν μλ£!")
print(" - μ΄κΈ° μ»€λ° μμ±λ¨")
print(" - κΈ°λ³Έ λΈλμΉ: main")
print(" - .gitignore νμΌ μμ±λ¨")
return True
except Exception as e:
print(f"β Git μ μ₯μ μ΄κΈ°ν μ€ν¨: {e}")
return False
def move_template_files(self) -> bool:
"""ν
νλ¦Ώ κ΄λ ¨ νμΌλ€μ λ³λ ν΄λλ‘ μ΄λ"""
try:
template_archive = self.project_path / ".template_archive"
template_archive.mkdir(exist_ok=True)
files_to_move = [
"setup_template.py",
"scripts/setup_template.py",
"scripts/init_blank_template.py"
]
moved_files = []
for file_path in files_to_move:
source_file = self.project_path / file_path
if source_file.exists():
target_file = template_archive / file_path
target_file.parent.mkdir(parents=True, exist_ok=True)
shutil.move(str(source_file), str(target_file))
moved_files.append(file_path)
if moved_files:
print(f"π ν
νλ¦Ώ νμΌλ€μ {template_archive}λ‘ μ΄λνμ΅λλ€:")
for file_path in moved_files:
print(f" - {file_path}")
# 볡μ μ€ν¬λ¦½νΈ μμ±
restore_script = template_archive / "restore_template.py"
restore_code = f'''#!/usr/bin/env python3
"""
ν
νλ¦Ώ νμΌ λ³΅μ μ€ν¬λ¦½νΈ
"""
import shutil
from pathlib import Path
def main():
current_dir = Path(__file__).parent.parent
archive_dir = Path(__file__).parent
files_to_restore = {moved_files}
print("π ν
νλ¦Ώ νμΌλ€μ 볡μν©λλ€...")
for file_path in files_to_restore:
source_file = archive_dir / file_path
target_file = current_dir / file_path
if source_file.exists():
target_file.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(str(source_file), str(target_file))
print(f"β
볡μλ¨: {{file_path}}")
print("β
볡μ μλ£!")
if __name__ == "__main__":
main()
'''
restore_script.write_text(restore_code, encoding='utf-8')
restore_script.chmod(0o755)
print(f"π 볡μ μ€ν¬λ¦½νΈ μμ±: {restore_script}")
return True
except Exception as e:
print(f"β νμΌ μ΄λ μ€ν¨: {e}")
return False
def customize_project(self, skip_git: bool = False) -> bool:
"""νλ‘μ νΈ μ»€μ€ν°λ§μ΄μ§"""
print("π FastAPI + MCP νλ‘μ νΈ μ»€μ€ν°λ§μ΄μ§")
print("=" * 50)
# λ°±μ
μμ±
if not self.create_backup():
return False
# νλ‘μ νΈ μ 보 μμ§
project_info = {}
print("\nπ νλ‘μ νΈ μ 보λ₯Ό μ
λ ₯ν΄μ£ΌμΈμ:")
project_info["name"] = self.get_user_input(
"νλ‘μ νΈ μ΄λ¦ (ν¨ν€μ§λͺ
)",
"my-awesome-api"
)
project_info["title"] = self.get_user_input(
"νλ‘μ νΈ μ λͺ©",
f"{project_info['name'].replace('-', ' ').title()}"
)
project_info["description"] = self.get_user_input(
"νλ‘μ νΈ μ€λͺ
",
f"{project_info['title']} - FastAPIμ MCPλ₯Ό νμ©ν API μλ²"
)
project_info["author"] = self.get_user_input("μμ±μ μ΄λ¦", "")
if project_info["author"]:
project_info["email"] = self.get_user_input("μμ±μ μ΄λ©μΌ", "")
# νμΈ
print(f"\nπ μ€μ μμ½:")
print(f" - νλ‘μ νΈλͺ
: {project_info['name']}")
print(f" - μ λͺ©: {project_info['title']}")
print(f" - μ€λͺ
: {project_info['description']}")
if project_info.get("author"):
print(f" - μμ±μ: {project_info['author']}")
if project_info.get("email"):
print(f" - μ΄λ©μΌ: {project_info['email']}")
if not self.get_yes_no("\nβ
μ μ€μ μΌλ‘ νλ‘μ νΈλ₯Ό 컀μ€ν°λ§μ΄μ§νμκ² μ΅λκΉ?", True):
print("β 컀μ€ν°λ§μ΄μ§μ΄ μ·¨μλμμ΅λλ€.")
return False
# νμΌ μ
λ°μ΄νΈ
if not self.update_all_files(project_info):
return False
# Git μ μ₯μ μ΄κΈ°ν
if not skip_git and self.get_yes_no("\nπ§ Git μ μ₯μλ₯Ό μ΄κΈ°ννμκ² μ΅λκΉ?", True):
git_success = self.init_git_repository()
if not git_success:
print("β οΈ Git μ΄κΈ°νμ μ€ν¨νμ§λ§ νλ‘μ νΈ μ€μ μ μλ£λμμ΅λλ€.")
elif skip_git:
print("\nβΉοΈ Git μ΄κΈ°νλ₯Ό 건λλλλ€.")
# ν
νλ¦Ώ νμΌ μ 리
if self.get_yes_no("\nπ§Ή ν
νλ¦Ώ κ΄λ ¨ νμΌλ€μ μ 리νμκ² μ΅λκΉ?", True):
self.move_template_files()
print("\nπ νλ‘μ νΈ μ»€μ€ν°λ§μ΄μ§μ΄ μλ£λμμ΅λλ€!")
print("\nπ λ€μ λ¨κ³:")
print("1. uv sync - μμ‘΄μ± μ€μΉ")
print("2. python run_server.py - μλ² μ€ν")
print("3. μ½λ μμ λ° κ°λ° μμ")
if self.is_git_repository():
print("\nπ Git μ¬μ©λ²:")
print("- git status - λ³κ²½μ¬ν νμΈ")
print("- git add . && git commit -m 'message' - λ³κ²½μ¬ν 컀λ°")
print("- git remote add origin <repository-url> - μ격 μ μ₯μ μ°κ²°")
print("- git push -u origin main - μ격 μ μ₯μμ νΈμ")
print("\nπ 볡μ λ°©λ²:")
print("λ³κ²½μ¬νμ λλλ¦¬λ €λ©΄ λ€μ λͺ
λ Ήμ΄λ₯Ό μ¬μ©νμΈμ:")
print("python setup_template.py --restore")
return True
def main():
"""λ©μΈ ν¨μ"""
parser = argparse.ArgumentParser(
description="FastAPI + MCP ν
νλ¦Ώ μ€μ λꡬ",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
μ¬μ© μμ:
python setup_template.py # ν
νλ¦Ώ 컀μ€ν°λ§μ΄μ§ (κΈ°λ³Έ)
python setup_template.py --customize # λͺ
μμ μΌλ‘ 컀μ€ν°λ§μ΄μ§
python setup_template.py --no-git # Git μ΄κΈ°ν μμ΄ μ»€μ€ν°λ§μ΄μ§
python setup_template.py --restore # λ°±μ
μμ 볡μ
"""
)
parser.add_argument(
"--customize",
action="store_true",
help="νμ¬ ν
νλ¦Ώμ 컀μ€ν°λ§μ΄μ§ (κΈ°λ³Έ λμ)"
)
parser.add_argument(
"--restore",
action="store_true",
help="λ°±μ
μμ 볡μ"
)
parser.add_argument(
"--no-git",
action="store_true",
help="Git μ μ₯μ μ΄κΈ°ν 건λλ°κΈ°"
)
args = parser.parse_args()
current_path = Path.cwd()
setup = TemplateSetup(current_path)
try:
if args.restore:
# λ°±μ
μμ 볡μ
print("π λ°±μ
μμ 볡μ")
print("=" * 50)
success = setup.restore_backup()
else:
# κΈ°λ³Έ λμ: νμ¬ ν
νλ¦Ώ 컀μ€ν°λ§μ΄μ§
if not (current_path / "pyproject.toml").exists():
print("β μ€λ₯: ν
νλ¦Ώ λ£¨νΈ λλ ν 리μμ μ€νν΄μ£ΌμΈμ.")
sys.exit(1)
success = setup.customize_project(skip_git=args.no_git)
if success:
print("\nβ
μμ
μ΄ μ±κ³΅μ μΌλ‘ μλ£λμμ΅λλ€!")
else:
print("\nβ μμ
μ΄ μ€ν¨νμ΅λλ€.")
sys.exit(1)
except KeyboardInterrupt:
print("\nβ μ¬μ©μμ μν΄ μ·¨μλμμ΅λλ€.")
sys.exit(1)
except Exception as e:
print(f"\nβ μμμΉ λͺ»ν μ€λ₯ λ°μ: {e}")
sys.exit(1)
if __name__ == "__main__":
main()