diff --git a/.github/workflows/template-inspection.yml b/.github/workflows/template-inspection.yml index 8246148..9710a01 100644 --- a/.github/workflows/template-inspection.yml +++ b/.github/workflows/template-inspection.yml @@ -33,6 +33,17 @@ jobs: - name: Install dependencies run: pdm install -G dev + - name: Install UV package manager + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + + - name: Verify UV installation + run: | + export PATH="$HOME/.cargo/bin:$PATH" + uv --version + which uv + - name: Set up Docker Compose run: | if ! command -v docker-compose &> /dev/null; then @@ -46,6 +57,8 @@ jobs: - name: Run template inspection run: | + export PATH="$HOME/.cargo/bin:$PATH" + uv --version # Verify UV is available pdm run python scripts/inspect-templates.py --templates "${{ github.event.inputs.templates }}" --output template_inspection_results.json --verbose - name: Upload inspection results @@ -109,8 +122,8 @@ jobs: labels: ['bug', 'template-inspection', 'automated'] }); - - name: Comment on Success - if: success() + - name: Report Results + if: always() uses: actions/github-script@v7 with: script: | @@ -119,11 +132,45 @@ jobs: try { const results = JSON.parse(fs.readFileSync('template_inspection_results.json', 'utf8')); - console.log(`✅ Weekly template inspection completed successfully!`); - console.log(`📊 Results: ${results.passed_templates}/${results.total_templates} templates passed`); - console.log(`📄 Detailed results available in workflow artifacts`); + let summary = `## 📊 Template Inspection Summary\n\n`; + summary += `**Inspection Date:** ${results.inspection_date}\n`; + summary += `**Total Templates:** ${results.total_templates}\n`; + summary += `**✅ Passed:** ${results.passed_templates}\n`; + summary += `**❌ Failed:** ${results.failed_templates}\n\n`; + + if (results.passed_templates > 0) { + summary += `### ✅ Passed Templates:\n`; + results.results.forEach(result => { + if (result.is_valid) { + summary += `- ${result.template_name}\n`; + } + }); + summary += `\n`; + } + + if (results.failed_templates > 0) { + summary += `### ❌ Failed Templates:\n`; + results.results.forEach(result => { + if (!result.is_valid) { + summary += `- ${result.template_name}`; + if (result.errors && result.errors.length > 0) { + summary += `: ${result.errors[0]}`; + } + summary += `\n`; + } + }); + } + + console.log(summary); + + if (results.failed_templates === 0) { + console.log(`🎉 All templates passed inspection!`); + } else { + console.log(`⚠️ ${results.failed_templates} template(s) failed inspection`); + } } catch (error) { - console.log(`✅ Weekly template inspection completed successfully!`); + console.log(`📊 Template inspection completed`); console.log(`📄 Results available in workflow artifacts`); + console.log(`❌ Error reading results: ${error.message}`); } diff --git a/CHANGELOG.md b/CHANGELOG.md index 070d633..ae5dc28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## v1.1.1 (2025-08-15) + +### Improvements + +- fix template inspection workflow & script + - fixing uv supportation compatibility + - for now, template inspection is running with `uv` package manager + ## v1.1.0 (2025-08-08) ### Features diff --git a/src/fastapi_fastkit/__init__.py b/src/fastapi_fastkit/__init__.py index 849114f..cfe29f6 100644 --- a/src/fastapi_fastkit/__init__.py +++ b/src/fastapi_fastkit/__init__.py @@ -1,4 +1,4 @@ -__version__ = "1.1.0" +__version__ = "1.1.1" import os diff --git a/src/fastapi_fastkit/backend/inspector.py b/src/fastapi_fastkit/backend/inspector.py index f324f55..be1cfc7 100644 --- a/src/fastapi_fastkit/backend/inspector.py +++ b/src/fastapi_fastkit/backend/inspector.py @@ -31,7 +31,9 @@ from fastapi_fastkit.backend.main import ( create_venv, find_template_core_modules, + inject_project_metadata, install_dependencies, + install_dependencies_with_manager, ) from fastapi_fastkit.backend.transducer import copy_and_convert_template from fastapi_fastkit.core.settings import settings @@ -61,6 +63,10 @@ def __enter__(self) -> "TemplateInspector": try: os.makedirs(self.temp_dir, exist_ok=True) copy_and_convert_template(str(self.template_path), self.temp_dir) + + # Inject dummy metadata for inspection + self._inject_dummy_metadata() + self._cleanup_needed = True self.template_config = self._load_template_config() debug_log(f"Created temporary directory at {self.temp_dir}", "debug") @@ -112,6 +118,43 @@ def _load_template_config(self) -> Optional[Dict[str, Any]]: debug_log(f"Failed to load template configuration: {e}", "warning") return None + def _inject_dummy_metadata(self) -> None: + """Inject dummy metadata for template inspection.""" + try: + # Use dummy metadata for inspection + dummy_metadata = { + "project_name": "test-template", + "author": "Template Inspector", + "author_email": "inspector@fastapi-fastkit.dev", + "description": "Test project for template inspection", + } + + inject_project_metadata( + self.temp_dir, + dummy_metadata["project_name"], + dummy_metadata["author"], + dummy_metadata["author_email"], + dummy_metadata["description"], + ) + debug_log("Injected dummy metadata for template inspection", "info") + + except Exception as e: + debug_log(f"Failed to inject dummy metadata: {e}", "warning") + self.warnings.append(f"Failed to inject metadata: {str(e)}") + + def _detect_package_manager(self) -> str: + """Detect the appropriate package manager for the template.""" + # Check for pyproject.toml (modern Python packaging) + if os.path.exists(os.path.join(self.temp_dir, "pyproject.toml")): + return "uv" # Use UV for pyproject.toml based projects + + # Check for requirements.txt (traditional pip) + if os.path.exists(os.path.join(self.temp_dir, "requirements.txt")): + return "pip" + + # Default to pip if no dependency file found + return "pip" + def _check_docker_available(self) -> bool: """Check if Docker and Docker Compose are available.""" try: @@ -393,7 +436,21 @@ def _test_with_standard_strategy(self) -> bool: try: # Create virtual environment for testing venv_path = create_venv(self.temp_dir) - install_dependencies(self.temp_dir, venv_path) + + # Detect and use appropriate package manager + package_manager = self._detect_package_manager() + debug_log(f"Using package manager: {package_manager}", "info") + + try: + install_dependencies_with_manager( + self.temp_dir, venv_path, package_manager + ) + except Exception as dep_error: + # Capture detailed dependency installation error + error_msg = f"Failed to install dependencies: {str(dep_error)}" + debug_log(f"Dependency installation failed: {dep_error}", "error") + self.errors.append(error_msg) + return False # Check if scripts/test.sh exists test_script_path = os.path.join(self.temp_dir, "scripts", "test.sh") @@ -557,7 +614,21 @@ def _test_with_fallback_strategy(self) -> bool: try: # Create virtual environment for testing venv_path = create_venv(self.temp_dir) - install_dependencies(self.temp_dir, venv_path) + + # Detect and use appropriate package manager + package_manager = self._detect_package_manager() + debug_log(f"Using package manager: {package_manager}", "info") + + try: + install_dependencies_with_manager( + self.temp_dir, venv_path, package_manager + ) + except Exception as dep_error: + # Capture detailed dependency installation error + error_msg = f"Failed to install dependencies: {str(dep_error)}" + debug_log(f"Dependency installation failed: {dep_error}", "error") + self.errors.append(error_msg) + return False # Set up fallback environment (e.g., SQLite database) fallback_config = self.template_config["fallback_testing"] @@ -683,6 +754,14 @@ def _wait_for_services_healthy(self, compose_file: str, timeout: int) -> None: # Check if all services are running all_running = True for service in services: + # Ensure service is a dictionary before calling .get() + if not isinstance(service, dict): + debug_log( + f"Service info is not a dictionary: {service}", + "warning", + ) + continue + if service.get("State") != "running": all_running = False debug_log( @@ -738,6 +817,11 @@ def _verify_services_running(self, compose_file: str) -> bool: app_running = False for service in services: + # Ensure service is a dictionary before calling .get() + if not isinstance(service, dict): + debug_log(f"Service info is not a dictionary: {service}", "warning") + continue + service_name = service.get("Name", "") service_state = service.get("State", "") diff --git a/src/fastapi_fastkit/backend/main.py b/src/fastapi_fastkit/backend/main.py index 7dec3af..8e3ff53 100644 --- a/src/fastapi_fastkit/backend/main.py +++ b/src/fastapi_fastkit/backend/main.py @@ -32,12 +32,12 @@ def find_template_core_modules(project_dir: str) -> Dict[str, str]: """ Find core module files in the template project structure. - Returns a dictionary with paths to main.py, setup.py, and config files. + Returns a dictionary with paths to main.py, setup.py, pyproject.toml and config files. :param project_dir: Path to the project directory :return: Dictionary with paths to core modules """ - core_modules = {"main": "", "setup": "", "config": ""} + core_modules = {"main": "", "setup": "", "pyproject": "", "config": ""} template_paths = settings.TEMPLATE_PATHS # Find main.py @@ -54,6 +54,13 @@ def find_template_core_modules(project_dir: str) -> Dict[str, str]: core_modules["setup"] = full_path break + # Find pyproject.toml + for pyproject_path in template_paths["pyproject"]: + full_path = os.path.join(project_dir, pyproject_path) + if os.path.exists(full_path): + core_modules["pyproject"] = full_path + break + # Find config file config_info = template_paths["config"] if isinstance(config_info, dict): @@ -166,6 +173,13 @@ def inject_project_metadata( author_email, description, ) + _process_pyproject_file( + core_modules.get("pyproject", ""), + project_name, + author, + author_email, + description, + ) _process_config_file(core_modules.get("config", ""), project_name) print_success("Project metadata injected successfully") @@ -245,6 +259,51 @@ def _process_config_file(config_py: str, project_name: str) -> None: raise BackendExceptions(f"Failed to process config file: {e}") +def _process_pyproject_file( + pyproject_toml: str, + project_name: str, + author: str, + author_email: str, + description: str, +) -> None: + """ + Process pyproject.toml file and inject metadata. + + :param pyproject_toml: Path to pyproject.toml file + :param project_name: Project name + :param author: Author name + :param author_email: Author email + :param description: Project description + """ + if not pyproject_toml or not os.path.exists(pyproject_toml): + return + + try: + with open(pyproject_toml, "r", encoding="utf-8") as f: + content = f.read() + + # Replace placeholders + replacements = { + "": project_name, + "": author, + "": author_email, + "": description, + } + + for placeholder, value in replacements.items(): + content = content.replace(placeholder, value) + + with open(pyproject_toml, "w", encoding="utf-8") as f: + f.write(content) + + debug_log("Injected metadata into pyproject.toml", "info") + print_info("Injected metadata into pyproject.toml") + + except (OSError, UnicodeDecodeError) as e: + debug_log(f"Error processing pyproject.toml: {e}", "error") + raise BackendExceptions(f"Failed to process pyproject.toml: {e}") + + def create_venv_with_manager(project_dir: str, manager_type: str = "pip") -> str: """ Create a virtual environment using the specified package manager. diff --git a/src/fastapi_fastkit/backend/package_managers/uv_manager.py b/src/fastapi_fastkit/backend/package_managers/uv_manager.py index ff2f532..7e93170 100644 --- a/src/fastapi_fastkit/backend/package_managers/uv_manager.py +++ b/src/fastapi_fastkit/backend/package_managers/uv_manager.py @@ -93,16 +93,27 @@ def install_dependencies(self, venv_path: str) -> None: print_error(f"pyproject.toml file not found at {pyproject_path}") raise BackendExceptions("pyproject.toml file not found") - # Install dependencies using UV sync + # Install dependencies using UV sync (including dev dependencies) + cmd = ["uv", "sync", "--group", "dev"] + debug_log( + f"Running UV command: {' '.join(cmd)} in {self.project_dir}", "info" + ) + with console.status("[bold green]Installing dependencies with UV..."): - subprocess.run( - ["uv", "sync"], + result = subprocess.run( + cmd, cwd=str(self.project_dir), check=True, capture_output=True, text=True, ) + # Log UV output for debugging + if result.stdout: + debug_log(f"UV stdout: {result.stdout}", "debug") + if result.stderr: + debug_log(f"UV stderr: {result.stderr}", "debug") + debug_log("Dependencies installed successfully with UV", "info") print_success("Dependencies installed successfully with UV") diff --git a/src/fastapi_fastkit/core/settings.py b/src/fastapi_fastkit/core/settings.py index a2917d9..577fe43 100644 --- a/src/fastapi_fastkit/core/settings.py +++ b/src/fastapi_fastkit/core/settings.py @@ -30,6 +30,9 @@ class FastkitConfig: "setup.py", "src/setup.py", ], + "pyproject": [ + "pyproject.toml", + ], "config": { "files": ["settings.py", "config.py"], "paths": [ diff --git a/src/fastapi_fastkit/fastapi_project_template/fastapi-async-crud/pyproject.toml-tpl b/src/fastapi_fastkit/fastapi_project_template/fastapi-async-crud/pyproject.toml-tpl new file mode 100644 index 0000000..d91d894 --- /dev/null +++ b/src/fastapi_fastkit/fastapi_project_template/fastapi-async-crud/pyproject.toml-tpl @@ -0,0 +1,85 @@ +[project] +name = "" +version = "0.1.0" +description = "" +authors = [ + {name = "", email = ""}, +] +readme = "README.md" +license = "MIT" +requires-python = ">=3.9" +dependencies = [ + "fastapi>=0.115.8", + "uvicorn[standard]>=0.34.0", + "pydantic>=2.10.6", + "pydantic-settings>=2.7.1", + "SQLAlchemy>=2.0.38", + "python-dotenv>=1.0.1", + "aiofiles>=24.1.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.3.4", + "pytest-asyncio>=0.25.3", + "httpx>=0.28.1", + "black>=25.1.0", + "isort>=6.0.0", + "mypy>=1.15.0", + "PyYAML>=6.0.2", +] + +[dependency-groups] +dev = [ + "pytest>=8.3.4", + "pytest-asyncio>=0.25.3", + "httpx>=0.28.1", + "black>=25.1.0", + "isort>=6.0.0", + "mypy>=1.15.0", + "PyYAML>=6.0.2", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src"] + +[tool.black] +line-length = 88 +target-version = ["py39"] +include = '\.pyi?$' +exclude = ''' +/( + \.git + | \.venv + | \.mypy_cache + | \.pytest_cache + | __pycache__ + | build + | dist +)/ +''' + +[tool.isort] +profile = "black" +multi_line_output = 3 +line_length = 88 +known_first_party = ["src"] + +[tool.mypy] +python_version = "3.9" +strict = true +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = "-v --tb=short" +asyncio_mode = "auto" diff --git a/src/fastapi_fastkit/fastapi_project_template/fastapi-async-crud/requirements.txt-tpl b/src/fastapi_fastkit/fastapi_project_template/fastapi-async-crud/requirements.txt-tpl index 97bb5eb..6f20863 100644 --- a/src/fastapi_fastkit/fastapi_project_template/fastapi-async-crud/requirements.txt-tpl +++ b/src/fastapi_fastkit/fastapi_project_template/fastapi-async-crud/requirements.txt-tpl @@ -33,4 +33,4 @@ uvicorn==0.34.0 uvloop==0.21.0 watchfiles==1.0.4 websockets==15.0 -aiofiles>=23.2.1 +aiofiles>=24.1.0 diff --git a/src/fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/core/config.py-tpl b/src/fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/core/config.py-tpl index 7224095..edc204c 100644 --- a/src/fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/core/config.py-tpl +++ b/src/fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/core/config.py-tpl @@ -3,17 +3,17 @@ # -------------------------------------------------------------------------- import secrets import warnings -from typing import Annotated, Any, Literal +from typing import Annotated, Any, List, Literal, Union from pydantic import AnyUrl, BeforeValidator, computed_field, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict from typing_extensions import Self -def parse_cors(v: Any) -> list[str] | str: +def parse_cors(v: Any) -> Union[List[str], str]: if isinstance(v, str) and not v.startswith("["): return [i.strip() for i in v.split(",")] - elif isinstance(v, list | str): + elif isinstance(v, (list, str)): return v raise ValueError(v) @@ -30,20 +30,20 @@ class Settings(BaseSettings): CLIENT_ORIGIN: str = "" - BACKEND_CORS_ORIGINS: Annotated[list[AnyUrl] | str, BeforeValidator(parse_cors)] = ( + BACKEND_CORS_ORIGINS: Annotated[Union[List[AnyUrl], str], BeforeValidator(parse_cors)] = ( [] ) @computed_field # type: ignore[prop-decorator] @property - def all_cors_origins(self) -> list[str]: + def all_cors_origins(self) -> List[str]: return [str(origin).rstrip("/") for origin in self.BACKEND_CORS_ORIGINS] + [ self.CLIENT_ORIGIN ] PROJECT_NAME: str = "" - def _check_default_secret(self, var_name: str, value: str | None) -> None: + def _check_default_secret(self, var_name: str, value: Union[str, None]) -> None: if value == "changethis": message = ( f'The value of {var_name} is "changethis", ' diff --git a/src/fastapi_fastkit/fastapi_project_template/fastapi-custom-response/pyproject.toml-tpl b/src/fastapi_fastkit/fastapi_project_template/fastapi-custom-response/pyproject.toml-tpl new file mode 100644 index 0000000..d91d894 --- /dev/null +++ b/src/fastapi_fastkit/fastapi_project_template/fastapi-custom-response/pyproject.toml-tpl @@ -0,0 +1,85 @@ +[project] +name = "" +version = "0.1.0" +description = "" +authors = [ + {name = "", email = ""}, +] +readme = "README.md" +license = "MIT" +requires-python = ">=3.9" +dependencies = [ + "fastapi>=0.115.8", + "uvicorn[standard]>=0.34.0", + "pydantic>=2.10.6", + "pydantic-settings>=2.7.1", + "SQLAlchemy>=2.0.38", + "python-dotenv>=1.0.1", + "aiofiles>=24.1.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.3.4", + "pytest-asyncio>=0.25.3", + "httpx>=0.28.1", + "black>=25.1.0", + "isort>=6.0.0", + "mypy>=1.15.0", + "PyYAML>=6.0.2", +] + +[dependency-groups] +dev = [ + "pytest>=8.3.4", + "pytest-asyncio>=0.25.3", + "httpx>=0.28.1", + "black>=25.1.0", + "isort>=6.0.0", + "mypy>=1.15.0", + "PyYAML>=6.0.2", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src"] + +[tool.black] +line-length = 88 +target-version = ["py39"] +include = '\.pyi?$' +exclude = ''' +/( + \.git + | \.venv + | \.mypy_cache + | \.pytest_cache + | __pycache__ + | build + | dist +)/ +''' + +[tool.isort] +profile = "black" +multi_line_output = 3 +line_length = 88 +known_first_party = ["src"] + +[tool.mypy] +python_version = "3.9" +strict = true +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = "-v --tb=short" +asyncio_mode = "auto" diff --git a/src/fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/api/routes/items.py-tpl b/src/fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/api/routes/items.py-tpl index 40707aa..58f1a5c 100644 --- a/src/fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/api/routes/items.py-tpl +++ b/src/fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/api/routes/items.py-tpl @@ -1,7 +1,7 @@ # -------------------------------------------------------------------------- # Item CRUD Endpoint # -------------------------------------------------------------------------- -from datetime import UTC, datetime +from datetime import datetime, timezone from typing import Any, List, Union from fastapi import APIRouter, Request @@ -29,7 +29,7 @@ async def read_all_items(request: Request): try: items = await read_items() return ResponseSchema( - timestamp=datetime.now(UTC) + timestamp=datetime.now(timezone.utc) .isoformat(timespec="milliseconds") .replace("+00:00", "Z"), status=200, @@ -60,7 +60,7 @@ async def read_item(item_id: int, request: Request): message="Item not found", error_code=ErrorCode.NOT_FOUND ) return ResponseSchema( - timestamp=datetime.now(UTC) + timestamp=datetime.now(timezone.utc) .isoformat(timespec="milliseconds") .replace("+00:00", "Z"), status=200, @@ -92,7 +92,7 @@ async def create_item(item: ItemCreate, request: Request): items.append(new_item) await write_items(items) return ResponseSchema( - timestamp=datetime.now(UTC) + timestamp=datetime.now(timezone.utc) .isoformat(timespec="milliseconds") .replace("+00:00", "Z"), status=201, @@ -124,7 +124,7 @@ async def update_item(item_id: int, item: ItemCreate, request: Request): items[index] = updated_item await write_items(items) return ResponseSchema( - timestamp=datetime.now(UTC) + timestamp=datetime.now(timezone.utc) .isoformat(timespec="milliseconds") .replace("+00:00", "Z"), status=200, @@ -156,7 +156,7 @@ async def delete_item(item_id: int, request: Request): ) await write_items(new_items) return ResponseSchema( - timestamp=datetime.now(UTC) + timestamp=datetime.now(timezone.utc) .isoformat(timespec="milliseconds") .replace("+00:00", "Z"), status=200, diff --git a/src/fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/core/config.py-tpl b/src/fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/core/config.py-tpl index 7224095..edc204c 100644 --- a/src/fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/core/config.py-tpl +++ b/src/fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/core/config.py-tpl @@ -3,17 +3,17 @@ # -------------------------------------------------------------------------- import secrets import warnings -from typing import Annotated, Any, Literal +from typing import Annotated, Any, List, Literal, Union from pydantic import AnyUrl, BeforeValidator, computed_field, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict from typing_extensions import Self -def parse_cors(v: Any) -> list[str] | str: +def parse_cors(v: Any) -> Union[List[str], str]: if isinstance(v, str) and not v.startswith("["): return [i.strip() for i in v.split(",")] - elif isinstance(v, list | str): + elif isinstance(v, (list, str)): return v raise ValueError(v) @@ -30,20 +30,20 @@ class Settings(BaseSettings): CLIENT_ORIGIN: str = "" - BACKEND_CORS_ORIGINS: Annotated[list[AnyUrl] | str, BeforeValidator(parse_cors)] = ( + BACKEND_CORS_ORIGINS: Annotated[Union[List[AnyUrl], str], BeforeValidator(parse_cors)] = ( [] ) @computed_field # type: ignore[prop-decorator] @property - def all_cors_origins(self) -> list[str]: + def all_cors_origins(self) -> List[str]: return [str(origin).rstrip("/") for origin in self.BACKEND_CORS_ORIGINS] + [ self.CLIENT_ORIGIN ] PROJECT_NAME: str = "" - def _check_default_secret(self, var_name: str, value: str | None) -> None: + def _check_default_secret(self, var_name: str, value: Union[str, None]) -> None: if value == "changethis": message = ( f'The value of {var_name} is "changethis", ' diff --git a/src/fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/helper/exceptions.py-tpl b/src/fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/helper/exceptions.py-tpl index 466bcda..7871aa9 100644 --- a/src/fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/helper/exceptions.py-tpl +++ b/src/fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/helper/exceptions.py-tpl @@ -1,7 +1,7 @@ # -------------------------------------------------------------------------- # The module defines custom Backend Application Exception class, overrides basic Exception. # -------------------------------------------------------------------------- -from datetime import UTC, datetime +from datetime import datetime, timezone from enum import Enum from pydantic import BaseModel, Field @@ -87,7 +87,7 @@ class ExceptionSchema(BaseModel): class InternalException(Exception): def __init__(self, message: str, error_code: ErrorCode): self.timestamp = ( - datetime.now(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z") + datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z") ) self.status = error_code.status_code self.error_code = error_code.code diff --git a/src/fastapi_fastkit/fastapi_project_template/fastapi-default/pyproject.toml-tpl b/src/fastapi_fastkit/fastapi_project_template/fastapi-default/pyproject.toml-tpl new file mode 100644 index 0000000..53d8959 --- /dev/null +++ b/src/fastapi_fastkit/fastapi_project_template/fastapi-default/pyproject.toml-tpl @@ -0,0 +1,81 @@ +[project] +name = "" +version = "0.1.0" +description = "" +authors = [ + {name = "", email = ""}, +] +readme = "README.md" +license = "MIT" +requires-python = ">=3.9" +dependencies = [ + "fastapi>=0.115.8", + "uvicorn[standard]>=0.34.0", + "pydantic>=2.10.6", + "pydantic-settings>=2.7.1", + "SQLAlchemy>=2.0.38", + "python-dotenv>=1.0.1", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.3.4", + "httpx>=0.28.1", + "black>=25.1.0", + "isort>=6.0.0", + "mypy>=1.15.0", + "PyYAML>=6.0.2", +] + +[dependency-groups] +dev = [ + "pytest>=8.3.4", + "httpx>=0.28.1", + "black>=25.1.0", + "isort>=6.0.0", + "mypy>=1.15.0", + "PyYAML>=6.0.2", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src"] + +[tool.black] +line-length = 88 +target-version = ["py39"] +include = '\.pyi?$' +exclude = ''' +/( + \.git + | \.venv + | \.mypy_cache + | \.pytest_cache + | __pycache__ + | build + | dist +)/ +''' + +[tool.isort] +profile = "black" +multi_line_output = 3 +line_length = 88 +known_first_party = ["src"] + +[tool.mypy] +python_version = "3.9" +strict = true +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = "-v --tb=short" diff --git a/src/fastapi_fastkit/fastapi_project_template/fastapi-default/src/core/config.py-tpl b/src/fastapi_fastkit/fastapi_project_template/fastapi-default/src/core/config.py-tpl index 7224095..edc204c 100644 --- a/src/fastapi_fastkit/fastapi_project_template/fastapi-default/src/core/config.py-tpl +++ b/src/fastapi_fastkit/fastapi_project_template/fastapi-default/src/core/config.py-tpl @@ -3,17 +3,17 @@ # -------------------------------------------------------------------------- import secrets import warnings -from typing import Annotated, Any, Literal +from typing import Annotated, Any, List, Literal, Union from pydantic import AnyUrl, BeforeValidator, computed_field, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict from typing_extensions import Self -def parse_cors(v: Any) -> list[str] | str: +def parse_cors(v: Any) -> Union[List[str], str]: if isinstance(v, str) and not v.startswith("["): return [i.strip() for i in v.split(",")] - elif isinstance(v, list | str): + elif isinstance(v, (list, str)): return v raise ValueError(v) @@ -30,20 +30,20 @@ class Settings(BaseSettings): CLIENT_ORIGIN: str = "" - BACKEND_CORS_ORIGINS: Annotated[list[AnyUrl] | str, BeforeValidator(parse_cors)] = ( + BACKEND_CORS_ORIGINS: Annotated[Union[List[AnyUrl], str], BeforeValidator(parse_cors)] = ( [] ) @computed_field # type: ignore[prop-decorator] @property - def all_cors_origins(self) -> list[str]: + def all_cors_origins(self) -> List[str]: return [str(origin).rstrip("/") for origin in self.BACKEND_CORS_ORIGINS] + [ self.CLIENT_ORIGIN ] PROJECT_NAME: str = "" - def _check_default_secret(self, var_name: str, value: str | None) -> None: + def _check_default_secret(self, var_name: str, value: Union[str, None]) -> None: if value == "changethis": message = ( f'The value of {var_name} is "changethis", ' diff --git a/src/fastapi_fastkit/fastapi_project_template/fastapi-dockerized/pyproject.toml-tpl b/src/fastapi_fastkit/fastapi_project_template/fastapi-dockerized/pyproject.toml-tpl new file mode 100644 index 0000000..9227a4c --- /dev/null +++ b/src/fastapi_fastkit/fastapi_project_template/fastapi-dockerized/pyproject.toml-tpl @@ -0,0 +1,84 @@ +[project] +name = "" +version = "0.1.0" +description = "" +authors = [ + {name = "", email = ""}, +] +readme = "README.md" +license = "MIT" +requires-python = ">=3.9" +dependencies = [ + "fastapi>=0.115.8", + "uvicorn[standard]>=0.34.0", + "pydantic>=2.10.6", + "pydantic-settings>=2.7.1", + "SQLAlchemy>=2.0.38", + "sqlmodel>=0.0.22", + "python-dotenv>=1.0.1", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.3.4", + "httpx>=0.28.1", + "black>=25.1.0", + "isort>=6.0.0", + "mypy>=1.15.0", + "PyYAML>=6.0.2", + "setuptools-scm>=8.1.0", +] + +[dependency-groups] +dev = [ + "pytest>=8.3.4", + "httpx>=0.28.1", + "black>=25.1.0", + "isort>=6.0.0", + "mypy>=1.15.0", + "PyYAML>=6.0.2", + "setuptools-scm>=8.1.0", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src"] + +[tool.black] +line-length = 88 +target-version = ["py39"] +include = '\.pyi?$' +exclude = ''' +/( + \.git + | \.venv + | \.mypy_cache + | \.pytest_cache + | __pycache__ + | build + | dist +)/ +''' + +[tool.isort] +profile = "black" +multi_line_output = 3 +line_length = 88 +known_first_party = ["src"] + +[tool.mypy] +python_version = "3.9" +strict = true +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = "-v --tb=short" diff --git a/src/fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/core/config.py-tpl b/src/fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/core/config.py-tpl index 7224095..edc204c 100644 --- a/src/fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/core/config.py-tpl +++ b/src/fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/core/config.py-tpl @@ -3,17 +3,17 @@ # -------------------------------------------------------------------------- import secrets import warnings -from typing import Annotated, Any, Literal +from typing import Annotated, Any, List, Literal, Union from pydantic import AnyUrl, BeforeValidator, computed_field, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict from typing_extensions import Self -def parse_cors(v: Any) -> list[str] | str: +def parse_cors(v: Any) -> Union[List[str], str]: if isinstance(v, str) and not v.startswith("["): return [i.strip() for i in v.split(",")] - elif isinstance(v, list | str): + elif isinstance(v, (list, str)): return v raise ValueError(v) @@ -30,20 +30,20 @@ class Settings(BaseSettings): CLIENT_ORIGIN: str = "" - BACKEND_CORS_ORIGINS: Annotated[list[AnyUrl] | str, BeforeValidator(parse_cors)] = ( + BACKEND_CORS_ORIGINS: Annotated[Union[List[AnyUrl], str], BeforeValidator(parse_cors)] = ( [] ) @computed_field # type: ignore[prop-decorator] @property - def all_cors_origins(self) -> list[str]: + def all_cors_origins(self) -> List[str]: return [str(origin).rstrip("/") for origin in self.BACKEND_CORS_ORIGINS] + [ self.CLIENT_ORIGIN ] PROJECT_NAME: str = "" - def _check_default_secret(self, var_name: str, value: str | None) -> None: + def _check_default_secret(self, var_name: str, value: Union[str, None]) -> None: if value == "changethis": message = ( f'The value of {var_name} is "changethis", ' diff --git a/src/fastapi_fastkit/fastapi_project_template/fastapi-empty/pyproject.toml-tpl b/src/fastapi_fastkit/fastapi_project_template/fastapi-empty/pyproject.toml-tpl new file mode 100644 index 0000000..e366663 --- /dev/null +++ b/src/fastapi_fastkit/fastapi_project_template/fastapi-empty/pyproject.toml-tpl @@ -0,0 +1,77 @@ +[project] +name = "" +version = "0.1.0" +description = "" +authors = [ + {name = "", email = ""}, +] +readme = "README.md" +license = "MIT" +requires-python = ">=3.9" +dependencies = [ + "fastapi>=0.115.8", + "uvicorn[standard]>=0.34.0", + "pydantic>=2.10.6", + "pydantic-settings>=2.7.1", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.3.4", + "httpx>=0.28.1", + "black>=25.1.0", + "isort>=6.0.0", + "mypy>=1.15.0", +] + +[dependency-groups] +dev = [ + "pytest>=8.3.4", + "httpx>=0.28.1", + "black>=25.1.0", + "isort>=6.0.0", + "mypy>=1.15.0", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src"] + +[tool.black] +line-length = 88 +target-version = ["py39"] +include = '\.pyi?$' +exclude = ''' +/( + \.git + | \.venv + | \.mypy_cache + | \.pytest_cache + | __pycache__ + | build + | dist +)/ +''' + +[tool.isort] +profile = "black" +multi_line_output = 3 +line_length = 88 +known_first_party = ["src"] + +[tool.mypy] +python_version = "3.9" +strict = true +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = "-v --tb=short" diff --git a/src/fastapi_fastkit/fastapi_project_template/fastapi-empty/src/core/config.py-tpl b/src/fastapi_fastkit/fastapi_project_template/fastapi-empty/src/core/config.py-tpl index 695f74a..4583cfc 100644 --- a/src/fastapi_fastkit/fastapi_project_template/fastapi-empty/src/core/config.py-tpl +++ b/src/fastapi_fastkit/fastapi_project_template/fastapi-empty/src/core/config.py-tpl @@ -3,17 +3,17 @@ # -------------------------------------------------------------------------- import secrets import warnings -from typing import Annotated, Any, Literal +from typing import Annotated, Any, List, Literal, Union from pydantic import AnyUrl, BeforeValidator, computed_field, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict from typing_extensions import Self -def parse_cors(v: Any) -> list[str] | str: +def parse_cors(v: Any) -> Union[List[str], str]: if isinstance(v, str) and not v.startswith("["): return [i.strip() for i in v.split(",")] - elif isinstance(v, list | str): + elif isinstance(v, (list, str)): return v raise ValueError(v) @@ -41,20 +41,20 @@ class Settings(BaseSettings): CLIENT_ORIGIN: str = "" - BACKEND_CORS_ORIGINS: Annotated[list[AnyUrl] | str, BeforeValidator(parse_cors)] = ( + BACKEND_CORS_ORIGINS: Annotated[Union[List[AnyUrl], str], BeforeValidator(parse_cors)] = ( [] ) @computed_field # type: ignore[prop-decorator] @property - def all_cors_origins(self) -> list[str]: + def all_cors_origins(self) -> List[str]: return [str(origin).rstrip("/") for origin in self.BACKEND_CORS_ORIGINS] + [ self.CLIENT_ORIGIN ] PROJECT_NAME: str = "" - def _check_default_secret(self, var_name: str, value: str | None) -> None: + def _check_default_secret(self, var_name: str, value: Union[str, None]) -> None: if value == "changethis": message = ( f'The value of {var_name} is "changethis", ' diff --git a/src/fastapi_fastkit/fastapi_project_template/fastapi-mcp/pyproject.toml-tpl b/src/fastapi_fastkit/fastapi_project_template/fastapi-mcp/pyproject.toml-tpl new file mode 100644 index 0000000..56096fa --- /dev/null +++ b/src/fastapi_fastkit/fastapi_project_template/fastapi-mcp/pyproject.toml-tpl @@ -0,0 +1,88 @@ +[project] +name = "" +version = "0.1.0" +description = "" +authors = [ + {name = "", email = ""}, +] +readme = "README.md" +license = "MIT" +requires-python = ">=3.10" +dependencies = [ + "fastapi>=0.115.8", + "uvicorn[standard]>=0.34.0", + "pydantic>=2.10.6", + "pydantic-settings>=2.7.1", + "SQLAlchemy>=2.0.38", + "python-dotenv>=1.0.1", + "fastapi-mcp>=0.3.4", + "bcrypt>=3.2.2", + "passlib>=1.7.4", + "python-jose>=3.3.0", + "python-multipart>=0.0.17", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.3.4", + "pytest-cov>=4.0.0", + "httpx>=0.28.1", + "black>=25.1.0", + "isort>=6.0.0", + "mypy>=1.15.0", + "PyYAML>=6.0.2", +] + +[dependency-groups] +dev = [ + "pytest>=8.3.4", + "pytest-cov>=4.0.0", + "httpx>=0.28.1", + "black>=25.1.0", + "isort>=6.0.0", + "mypy>=1.15.0", + "PyYAML>=6.0.2", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src"] + +[tool.black] +line-length = 88 +target-version = ["py310"] +include = '\.pyi?$' +exclude = ''' +/( + \.git + | \.venv + | \.mypy_cache + | \.pytest_cache + | __pycache__ + | build + | dist +)/ +''' + +[tool.isort] +profile = "black" +multi_line_output = 3 +line_length = 88 +known_first_party = ["src"] + +[tool.mypy] +python_version = "3.10" +strict = true +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = "-v --tb=short --cov=src --cov-report=term-missing" diff --git a/src/fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/pyproject.toml-tpl b/src/fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/pyproject.toml-tpl new file mode 100644 index 0000000..6a8bee4 --- /dev/null +++ b/src/fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/pyproject.toml-tpl @@ -0,0 +1,90 @@ +[project] +name = "" +version = "0.1.0" +description = "" +authors = [ + {name = "", email = ""}, +] +readme = "README.md" +license = "MIT" +requires-python = ">=3.9" +dependencies = [ + "fastapi>=0.115.8", + "uvicorn[standard]>=0.34.0", + "pydantic>=2.10.6", + "pydantic-settings>=2.7.1", + "SQLAlchemy>=2.0.38", + "sqlmodel>=0.0.22", + "python-dotenv>=1.0.1", + "alembic>=1.14.1", + "psycopg[binary]>=3.2.5", + "tenacity>=9.0.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.3.4", + "httpx>=0.28.1", + "black>=25.1.0", + "isort>=6.0.0", + "mypy>=1.15.0", + "PyYAML>=6.0.2", +] + +[dependency-groups] +dev = [ + "pytest>=8.3.4", + "httpx>=0.28.1", + "black>=25.1.0", + "isort>=6.0.0", + "mypy>=1.15.0", + "PyYAML>=6.0.2", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src"] + +[tool.black] +line-length = 88 +target-version = ["py39"] +include = '\.pyi?$' +exclude = ''' +/( + \.git + | \.venv + | \.mypy_cache + | \.pytest_cache + | __pycache__ + | build + | dist + | src/alembic/versions +)/ +''' + +[tool.isort] +profile = "black" +multi_line_output = 3 +line_length = 88 +known_first_party = ["src"] + +[tool.mypy] +python_version = "3.9" +strict = true +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = "-v --tb=short" + +[tool.alembic] +script_location = "src/alembic" +sqlalchemy.url = "postgresql://postgres:postgres@localhost/test_db" diff --git a/tests/test_backends/test_inspector.py b/tests/test_backends/test_inspector.py index e0ece35..644526d 100644 --- a/tests/test_backends/test_inspector.py +++ b/tests/test_backends/test_inspector.py @@ -219,7 +219,7 @@ def test_check_fastapi_implementation_no_main( assert any("main.py not found" in error for error in inspector.errors) @patch("fastapi_fastkit.backend.inspector.create_venv") - @patch("fastapi_fastkit.backend.inspector.install_dependencies") + @patch("fastapi_fastkit.backend.inspector.install_dependencies_with_manager") @patch("subprocess.run") def test_test_template_success( self, @@ -238,6 +238,10 @@ def test_test_template_success( inspector = TemplateInspector(str(self.template_path)) # Create tests directory in temp_dir os.makedirs(os.path.join(inspector.temp_dir, "tests"), exist_ok=True) + # Create a requirements.txt file to avoid package manager detection issues + requirements_file = os.path.join(inspector.temp_dir, "requirements.txt") + with open(requirements_file, "w") as f: + f.write("fastapi>=0.100.0\n") result = inspector._test_template() # then @@ -981,7 +985,7 @@ def test_test_with_fallback_strategy_success(self, mock_run: MagicMock) -> None: with ( patch("fastapi_fastkit.backend.inspector.create_venv") as mock_create_venv, patch( - "fastapi_fastkit.backend.inspector.install_dependencies" + "fastapi_fastkit.backend.inspector.install_dependencies_with_manager" ) as mock_install, patch.object(inspector, "_run_test_script_with_env") as mock_run_script, ): @@ -1000,6 +1004,11 @@ def test_test_with_fallback_strategy_success(self, mock_run: MagicMock) -> None: inspector.temp_dir = str(self.template_path) + # Create a requirements.txt file to avoid package manager detection issues + requirements_file = os.path.join(inspector.temp_dir, "requirements.txt") + with open(requirements_file, "w") as f: + f.write("fastapi>=0.100.0\n") + # when result = inspector._test_with_fallback_strategy() @@ -1030,7 +1039,7 @@ def test_test_with_fallback_strategy_no_config(self) -> None: # ===== ADDITIONAL TESTS FOR BETTER COVERAGE ===== @patch("fastapi_fastkit.backend.inspector.create_venv") - @patch("fastapi_fastkit.backend.inspector.install_dependencies") + @patch("fastapi_fastkit.backend.inspector.install_dependencies_with_manager") def test_test_with_standard_strategy_success( self, mock_install: MagicMock, mock_create_venv: MagicMock ) -> None: @@ -1049,6 +1058,11 @@ def test_test_with_standard_strategy_success( inspector.temp_dir = str(self.template_path) + # Create a requirements.txt file to avoid package manager detection issues + requirements_file = os.path.join(inspector.temp_dir, "requirements.txt") + with open(requirements_file, "w") as f: + f.write("fastapi>=0.100.0\n") + # Mock dependencies mock_create_venv.return_value = "/fake/venv" mock_install.return_value = True @@ -1069,7 +1083,7 @@ def test_test_with_standard_strategy_success( mock_run_script.assert_called_once() @patch("fastapi_fastkit.backend.inspector.create_venv") - @patch("fastapi_fastkit.backend.inspector.install_dependencies") + @patch("fastapi_fastkit.backend.inspector.install_dependencies_with_manager") def test_test_with_standard_strategy_no_test_script( self, mock_install: MagicMock, mock_create_venv: MagicMock ) -> None: @@ -1082,6 +1096,11 @@ def test_test_with_standard_strategy_no_test_script( inspector.temp_dir = str(self.template_path) + # Create a requirements.txt file to avoid package manager detection issues + requirements_file = os.path.join(inspector.temp_dir, "requirements.txt") + with open(requirements_file, "w") as f: + f.write("fastapi>=0.100.0\n") + # Mock dependencies mock_create_venv.return_value = "/fake/venv" mock_install.return_value = True