Skip to content

Commit c6a178e

Browse files
committed
[FEAT] add package managers option - uv, pdm, poetry
1 parent d5a739f commit c6a178e

File tree

13 files changed

+2083
-94
lines changed

13 files changed

+2083
-94
lines changed

src/fastapi_fastkit/backend/main.py

Lines changed: 68 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@
55
# --------------------------------------------------------------------------
66
import os
77
import re
8-
import subprocess
9-
import sys
10-
from typing import Dict, List, Optional
8+
from typing import Dict, List
119

1210
from fastapi_fastkit import console
11+
from fastapi_fastkit.backend.package_managers import PackageManagerFactory
1312
from fastapi_fastkit.backend.transducer import copy_and_convert_template_file
1413
from fastapi_fastkit.core.exceptions import BackendExceptions, TemplateExceptions
1514
from fastapi_fastkit.core.settings import settings
@@ -246,99 +245,94 @@ def _process_config_file(config_py: str, project_name: str) -> None:
246245
raise BackendExceptions(f"Failed to process config file: {e}")
247246

248247

248+
def create_venv_with_manager(project_dir: str, manager_type: str = "pip") -> str:
249+
"""
250+
Create a virtual environment using the specified package manager.
251+
252+
:param project_dir: Path to the project directory
253+
:param manager_type: Type of package manager to use
254+
:return: Path to the virtual environment
255+
:raises: BackendExceptions if virtual environment creation fails
256+
"""
257+
try:
258+
package_manager = PackageManagerFactory.create_manager(
259+
manager_type, project_dir, auto_detect=True
260+
)
261+
return package_manager.create_virtual_environment()
262+
except Exception as e:
263+
debug_log(
264+
f"Error creating virtual environment with {manager_type}: {e}", "error"
265+
)
266+
raise BackendExceptions(f"Failed to create virtual environment: {str(e)}")
267+
268+
249269
def create_venv(project_dir: str) -> str:
250270
"""
251271
Create a Python virtual environment in the project directory.
252272
273+
This is a backward compatibility wrapper that uses pip by default.
274+
253275
:param project_dir: Path to the project directory
254276
:return: Path to the virtual environment
255277
"""
256-
venv_path = os.path.join(project_dir, ".venv")
278+
return create_venv_with_manager(project_dir, "pip")
257279

258-
try:
259-
with console.status("[bold green]Creating virtual environment..."):
260-
subprocess.run(
261-
[sys.executable, "-m", "venv", venv_path],
262-
check=True,
263-
capture_output=True,
264-
text=True,
265-
)
266280

267-
debug_log(f"Virtual environment created at {venv_path}", "info")
268-
print_success("Virtual environment created successfully")
269-
return venv_path
281+
def install_dependencies_with_manager(
282+
project_dir: str, venv_path: str, manager_type: str = "pip"
283+
) -> None:
284+
"""
285+
Install dependencies using the specified package manager.
270286
271-
except subprocess.CalledProcessError as e:
272-
debug_log(f"Error creating virtual environment: {e.stderr}", "error")
273-
handle_exception(e, f"Error creating virtual environment: {str(e)}")
274-
raise BackendExceptions("Failed to create venv")
275-
except OSError as e:
276-
debug_log(f"System error creating virtual environment: {e}", "error")
277-
handle_exception(e, f"Error creating virtual environment: {str(e)}")
278-
raise BackendExceptions(f"Failed to create venv: {str(e)}")
287+
:param project_dir: Path to the project directory
288+
:param venv_path: Path to the virtual environment
289+
:param manager_type: Type of package manager to use
290+
:return: None
291+
:raises: BackendExceptions if dependency installation fails
292+
"""
293+
try:
294+
package_manager = PackageManagerFactory.create_manager(
295+
manager_type, project_dir, auto_detect=True
296+
)
297+
package_manager.install_dependencies(venv_path)
298+
except Exception as e:
299+
debug_log(f"Error installing dependencies with {manager_type}: {e}", "error")
300+
raise BackendExceptions(f"Failed to install dependencies: {str(e)}")
279301

280302

281303
def install_dependencies(project_dir: str, venv_path: str) -> None:
282304
"""
283305
Install dependencies in the virtual environment.
284306
307+
This is a backward compatibility wrapper that uses pip by default.
308+
285309
:param project_dir: Path to the project directory
286310
:param venv_path: Path to the virtual environment
287311
:return: None
288312
"""
289-
try:
290-
if not os.path.exists(venv_path):
291-
debug_log(
292-
"Virtual environment does not exist. Creating it first.", "warning"
293-
)
294-
print_error("Virtual environment does not exist. Creating it first.")
295-
venv_path = create_venv(project_dir)
296-
if not venv_path:
297-
raise BackendExceptions("Failed to create venv")
298-
299-
requirements_path = os.path.join(project_dir, "requirements.txt")
300-
if not os.path.exists(requirements_path):
301-
debug_log(f"Requirements file not found at {requirements_path}", "error")
302-
print_error(f"Requirements file not found at {requirements_path}")
303-
raise BackendExceptions("Requirements file not found")
304-
305-
# Determine pip path based on OS
306-
if os.name == "nt": # Windows
307-
pip_path = os.path.join(venv_path, "Scripts", "pip")
308-
else: # Unix-based
309-
pip_path = os.path.join(venv_path, "bin", "pip")
310-
311-
# Upgrade pip first
312-
subprocess.run(
313-
[pip_path, "install", "--upgrade", "pip"],
314-
check=True,
315-
capture_output=True,
316-
text=True,
317-
)
313+
install_dependencies_with_manager(project_dir, venv_path, "pip")
318314

319-
# Install dependencies
320-
with console.status("[bold green]Installing dependencies..."):
321-
subprocess.run(
322-
[pip_path, "install", "-r", "requirements.txt"],
323-
cwd=project_dir,
324-
check=True,
325-
capture_output=True,
326-
text=True,
327-
)
328315

329-
debug_log("Dependencies installed successfully", "info")
330-
print_success("Dependencies installed successfully")
331-
332-
except subprocess.CalledProcessError as e:
333-
debug_log(f"Error during dependency installation: {e.stderr}", "error")
334-
handle_exception(e, f"Error during dependency installation: {str(e)}")
335-
if hasattr(e, "stderr"):
336-
print_error(f"Error details: {e.stderr}")
337-
raise BackendExceptions("Failed to install dependencies")
338-
except OSError as e:
339-
debug_log(f"System error during dependency installation: {e}", "error")
340-
handle_exception(e, f"Error during dependency installation: {str(e)}")
341-
raise BackendExceptions(f"Failed to install dependencies: {str(e)}")
316+
def generate_dependency_file_with_manager(
317+
project_dir: str, dependencies: List[str], manager_type: str = "pip"
318+
) -> None:
319+
"""
320+
Generate a dependency file using the specified package manager.
321+
322+
:param project_dir: Path to the project directory
323+
:param dependencies: List of dependency specifications
324+
:param manager_type: Type of package manager to use
325+
:return: None
326+
:raises: BackendExceptions if dependency file generation fails
327+
"""
328+
try:
329+
package_manager = PackageManagerFactory.create_manager(
330+
manager_type, project_dir, auto_detect=True
331+
)
332+
package_manager.generate_dependency_file(dependencies)
333+
except Exception as e:
334+
debug_log(f"Error generating dependency file with {manager_type}: {e}", "error")
335+
raise BackendExceptions(f"Failed to generate dependency file: {str(e)}")
342336

343337

344338
# ------------------------------------------------------------
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# --------------------------------------------------------------------------
2+
# Package Managers Module - FastAPI-fastkit
3+
# Provides abstraction layer for different package managers (pip, uv, pdm, poetry)
4+
#
5+
# @author bnbong [email protected]
6+
# --------------------------------------------------------------------------
7+
8+
from .base import BasePackageManager
9+
from .factory import PackageManagerFactory
10+
from .pdm_manager import PdmManager
11+
from .pip_manager import PipManager
12+
from .poetry_manager import PoetryManager
13+
from .uv_manager import UvManager
14+
15+
__all__ = [
16+
"BasePackageManager",
17+
"PackageManagerFactory",
18+
"PipManager",
19+
"PdmManager",
20+
"UvManager",
21+
"PoetryManager",
22+
]
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# --------------------------------------------------------------------------
2+
# Base Package Manager - Abstract class for package manager implementations
3+
#
4+
# @author bnbong [email protected]
5+
# --------------------------------------------------------------------------
6+
import subprocess
7+
from abc import ABC, abstractmethod
8+
from pathlib import Path
9+
from typing import Any, Dict, List, Optional
10+
11+
12+
class BasePackageManager(ABC):
13+
"""
14+
Abstract base class for package managers.
15+
16+
All package manager implementations must inherit from this class
17+
and implement the required abstract methods.
18+
"""
19+
20+
def __init__(self, project_dir: str):
21+
"""
22+
Initialize package manager for a specific project.
23+
24+
:param project_dir: Path to the project directory
25+
"""
26+
self.project_dir = Path(project_dir)
27+
self.name = self.__class__.__name__.replace("Manager", "").lower()
28+
29+
@abstractmethod
30+
def is_available(self) -> bool:
31+
"""
32+
Check if the package manager is available on the system.
33+
34+
:return: True if package manager is installed and available
35+
"""
36+
pass
37+
38+
@abstractmethod
39+
def get_dependency_file_name(self) -> str:
40+
"""
41+
Get the name of the dependency file for this package manager.
42+
43+
:return: Dependency file name (e.g., 'requirements.txt', 'pyproject.toml')
44+
"""
45+
pass
46+
47+
@abstractmethod
48+
def create_virtual_environment(self) -> str:
49+
"""
50+
Create a virtual environment for the project.
51+
52+
:return: Path to the created virtual environment
53+
:raises: Exception if virtual environment creation fails
54+
"""
55+
pass
56+
57+
@abstractmethod
58+
def install_dependencies(self, venv_path: str) -> None:
59+
"""
60+
Install dependencies using the package manager.
61+
62+
:param venv_path: Path to the virtual environment
63+
:raises: Exception if dependency installation fails
64+
"""
65+
pass
66+
67+
@abstractmethod
68+
def generate_dependency_file(self, dependencies: List[str]) -> None:
69+
"""
70+
Generate a dependency file with the given dependencies.
71+
72+
:param dependencies: List of dependency specifications
73+
"""
74+
pass
75+
76+
@abstractmethod
77+
def add_dependency(self, dependency: str, dev: bool = False) -> None:
78+
"""
79+
Add a new dependency to the project.
80+
81+
:param dependency: Dependency specification
82+
:param dev: Whether this is a development dependency
83+
"""
84+
pass
85+
86+
def get_executable_path(
87+
self, executable_name: str, venv_path: Optional[str] = None
88+
) -> str:
89+
"""
90+
Get the full path to an executable, considering virtual environment.
91+
92+
:param executable_name: Name of the executable
93+
:param venv_path: Path to virtual environment (optional)
94+
:return: Full path to the executable
95+
"""
96+
import os
97+
98+
if venv_path:
99+
if os.name == "nt": # Windows
100+
return os.path.join(venv_path, "Scripts", f"{executable_name}.exe")
101+
else: # Unix-based
102+
return os.path.join(venv_path, "bin", executable_name)
103+
else:
104+
return executable_name
105+
106+
def run_command(
107+
self, command: List[str], **kwargs: Any
108+
) -> subprocess.CompletedProcess[str]:
109+
"""
110+
Run a command with proper error handling.
111+
112+
:param command: Command to run as list of strings
113+
:param kwargs: Additional keyword arguments for subprocess.run
114+
:return: CompletedProcess instance
115+
:raises: subprocess.CalledProcessError on failure
116+
"""
117+
default_kwargs: Dict[str, Any] = {
118+
"check": True,
119+
"capture_output": True,
120+
"text": True,
121+
"cwd": str(self.project_dir),
122+
}
123+
default_kwargs.update(kwargs)
124+
125+
return subprocess.run(command, **default_kwargs)
126+
127+
def get_dependency_file_path(self) -> Path:
128+
"""
129+
Get the full path to the dependency file.
130+
131+
:return: Path to the dependency file
132+
"""
133+
return self.project_dir / self.get_dependency_file_name()
134+
135+
def __str__(self) -> str:
136+
return f"{self.__class__.__name__}({self.project_dir})"
137+
138+
def __repr__(self) -> str:
139+
return self.__str__()

0 commit comments

Comments
 (0)