FastAPI-fastkit 1.0.2__py3-none-any.whl → 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastapi_fastkit/__init__.py +1 -10
- fastapi_fastkit/backend/inspector.py +933 -83
- fastapi_fastkit/backend/main.py +640 -482
- fastapi_fastkit/backend/package_managers/__init__.py +22 -0
- fastapi_fastkit/backend/package_managers/base.py +150 -0
- fastapi_fastkit/backend/package_managers/factory.py +151 -0
- fastapi_fastkit/backend/package_managers/pdm_manager.py +275 -0
- fastapi_fastkit/backend/package_managers/pip_manager.py +205 -0
- fastapi_fastkit/backend/package_managers/poetry_manager.py +378 -0
- fastapi_fastkit/backend/package_managers/uv_manager.py +327 -0
- fastapi_fastkit/backend/transducer.py +210 -106
- fastapi_fastkit/cli.py +702 -664
- fastapi_fastkit/core/exceptions.py +14 -3
- fastapi_fastkit/core/settings.py +181 -155
- fastapi_fastkit/fastapi_project_template/README.md +90 -90
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/.env-tpl +1 -1
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/.gitignore-tpl +30 -30
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/README.md-tpl +107 -107
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/requirements.txt-tpl +36 -36
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/scripts/format.sh-tpl +5 -5
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/scripts/lint.sh-tpl +5 -5
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/scripts/run-server.sh-tpl +8 -8
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/scripts/test.sh-tpl +6 -6
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/setup.cfg-tpl +14 -14
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/setup.py-tpl +25 -25
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/api/api.py-tpl +9 -9
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/api/routes/items.py-tpl +57 -57
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/core/config.py-tpl +64 -64
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/crud/items.py-tpl +21 -21
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/main.py-tpl +26 -26
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/mocks/mock_items.json-tpl +8 -8
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/schemas/items.py-tpl +22 -22
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/tests/conftest.py-tpl +42 -42
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/tests/test_items.py-tpl +48 -48
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/.env-tpl +1 -1
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/.gitignore-tpl +191 -191
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/README.md-tpl +156 -156
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/requirements.txt-tpl +36 -36
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/scripts/format.sh-tpl +5 -5
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/scripts/lint.sh-tpl +5 -5
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/scripts/run-server.sh-tpl +8 -8
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/scripts/test.sh-tpl +6 -6
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/setup.cfg-tpl +12 -12
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/setup.py-tpl +27 -27
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/api/api.py-tpl +9 -9
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/api/routes/items.py-tpl +172 -172
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/core/config.py-tpl +64 -64
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/crud/items.py-tpl +21 -21
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/helper/exceptions.py-tpl +103 -103
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/helper/pagination.py-tpl +72 -72
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/main.py-tpl +39 -39
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/mocks/mock_items.json-tpl +8 -8
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/schemas/base.py-tpl +49 -49
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/schemas/items.py-tpl +22 -22
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/utils/documents.py-tpl +20 -20
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/tests/conftest.py-tpl +42 -42
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/tests/test_items.py-tpl +61 -61
- fastapi_fastkit/fastapi_project_template/fastapi-default/README.md-tpl +101 -101
- fastapi_fastkit/fastapi_project_template/fastapi-default/scripts/run-server.sh-tpl +8 -8
- fastapi_fastkit/fastapi_project_template/fastapi-default/setup.py-tpl +22 -22
- fastapi_fastkit/fastapi_project_template/fastapi-default/src/mocks/mock_items.json-tpl +8 -8
- fastapi_fastkit/fastapi_project_template/fastapi-default/tests/conftest.py-tpl +30 -30
- fastapi_fastkit/fastapi_project_template/fastapi-default/tests/test_items.py-tpl +44 -44
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/.env-tpl +1 -1
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/.gitignore-tpl +30 -30
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/Dockerfile-tpl +23 -23
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/README.md-tpl +119 -119
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/requirements.txt-tpl +36 -36
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/scripts/format.sh-tpl +5 -5
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/scripts/lint.sh-tpl +5 -5
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/scripts/run-server.sh-tpl +8 -8
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/scripts/test.sh-tpl +6 -6
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/setup.cfg-tpl +13 -13
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/setup.py-tpl +22 -22
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/api/api.py-tpl +6 -6
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/api/routes/items.py-tpl +54 -54
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/core/config.py-tpl +64 -64
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/crud/items.py-tpl +15 -15
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/main.py-tpl +26 -26
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/mocks/mock_items.json-tpl +8 -8
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/schemas/items.py-tpl +19 -19
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/tests/conftest.py-tpl +30 -30
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/tests/test_items.py-tpl +44 -44
- fastapi_fastkit/fastapi_project_template/fastapi-empty/.env-tpl +1 -1
- fastapi_fastkit/fastapi_project_template/fastapi-empty/.gitignore-tpl +30 -0
- fastapi_fastkit/fastapi_project_template/fastapi-empty/README.md-tpl +85 -0
- fastapi_fastkit/fastapi_project_template/fastapi-empty/requirements.txt-tpl +9 -0
- fastapi_fastkit/fastapi_project_template/fastapi-empty/setup.cfg-tpl +13 -0
- fastapi_fastkit/fastapi_project_template/fastapi-empty/setup.py-tpl +22 -22
- fastapi_fastkit/fastapi_project_template/fastapi-empty/src/core/config.py-tpl +75 -75
- fastapi_fastkit/fastapi_project_template/fastapi-empty/src/main.py-tpl +48 -36
- fastapi_fastkit/fastapi_project_template/fastapi-empty/tests/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-empty/tests/conftest.py-tpl +16 -0
- fastapi_fastkit/fastapi_project_template/fastapi-empty/tests/test_main.py-tpl +37 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/.env-tpl +29 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/README.md-tpl +249 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/requirements.txt-tpl +40 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/scripts/format.sh-tpl +12 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/scripts/lint.sh-tpl +18 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/scripts/run-server.sh-tpl +9 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/scripts/test.sh-tpl +9 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/setup.cfg-tpl +90 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/setup.py-tpl +51 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/__init__.py-tpl +4 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/__init__.py-tpl +3 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/api.py-tpl +14 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/routes/__init__.py-tpl +3 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/routes/auth.py-tpl +134 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/routes/items.py-tpl +189 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/auth/__init__.py-tpl +3 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/auth/dependencies.py-tpl +88 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/core/__init__.py-tpl +3 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/core/config.py-tpl +60 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/main.py-tpl +66 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/mcp/__init__.py-tpl +3 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/mcp/router.py-tpl +100 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/schemas/__init__.py-tpl +3 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/schemas/items.py-tpl +67 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/__init__.py-tpl +3 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/conftest.py-tpl +49 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/test_auth.py-tpl +101 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/test_items.py-tpl +96 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/test_mcp.py-tpl +81 -0
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/.env-tpl +9 -9
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/.gitignore-tpl +30 -30
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/Dockerfile-tpl +23 -23
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/README.md-tpl +144 -144
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/alembic.ini-tpl +119 -119
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/docker-compose.yml-tpl +44 -44
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/requirements.txt-tpl +41 -41
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/scripts/format.sh-tpl +5 -5
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/scripts/lint.sh-tpl +5 -5
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/scripts/pre-start.sh-tpl +16 -16
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/scripts/run-server.sh-tpl +8 -8
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/scripts/test.sh-tpl +10 -10
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/setup.cfg-tpl +13 -13
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/setup.py-tpl +25 -25
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/alembic/README-tpl +1 -1
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/alembic/env.py-tpl +85 -85
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/alembic/script.py.mako-tpl +26 -26
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/alembic/versions/bedcdc35b64a_first_alembic.py-tpl +37 -37
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/api/api.py-tpl +11 -11
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/api/deps.py-tpl +19 -19
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/api/routes/items.py-tpl +109 -109
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/core/config.py-tpl +88 -88
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/core/db.py-tpl +53 -53
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/crud/items.py-tpl +102 -102
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/main.py-tpl +45 -45
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/schemas/items.py-tpl +54 -54
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/utils/backend_pre_start.py-tpl +39 -39
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/utils/init_data.py-tpl +23 -23
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/utils/tests_pre_start.py-tpl +39 -39
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/template-config.yml-tpl +24 -0
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/tests/conftest.py-tpl +63 -63
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/tests/test_items.py-tpl +44 -44
- fastapi_fastkit/fastapi_project_template/modules/api/routes/new_route.py-tpl +64 -64
- fastapi_fastkit/fastapi_project_template/modules/crud/new_route.py-tpl +29 -29
- fastapi_fastkit/fastapi_project_template/modules/schemas/new_route.py-tpl +23 -23
- fastapi_fastkit/utils/logging.py +64 -28
- fastapi_fastkit/utils/main.py +180 -140
- fastapi_fastkit-1.1.0.dist-info/METADATA +182 -0
- {fastapi_fastkit-1.0.2.dist-info → fastapi_fastkit-1.1.0.dist-info}/RECORD +165 -129
- {fastapi_fastkit-1.0.2.dist-info → fastapi_fastkit-1.1.0.dist-info}/WHEEL +1 -1
- {fastapi_fastkit-1.0.2.dist-info → fastapi_fastkit-1.1.0.dist-info}/licenses/LICENSE +21 -21
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/.DS_Store +0 -0
- fastapi_fastkit/logs/fastkit_debug_20250702_161423.log +0 -21
- fastapi_fastkit/logs/fastkit_debug_20250702_162706.log +0 -20
- fastapi_fastkit/logs/fastkit_debug_20250702_163010.log +0 -21
- fastapi_fastkit/logs/fastkit_debug_20250702_163526.log +0 -21
- fastapi_fastkit/logs/fastkit_debug_20250702_164053.log +0 -21
- fastapi_fastkit/logs/fastkit_debug_20250702_165220.log +0 -21
- fastapi_fastkit/logs/fastkit_debug_20250702_165647.log +0 -21
- fastapi_fastkit-1.0.2.dist-info/METADATA +0 -320
- {fastapi_fastkit-1.0.2.dist-info → fastapi_fastkit-1.1.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# --------------------------------------------------------------------------
|
|
2
|
+
# Pip Package Manager Implementation
|
|
3
|
+
#
|
|
4
|
+
# @author bnbong bbbong9@gmail.com
|
|
5
|
+
# --------------------------------------------------------------------------
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from typing import List
|
|
10
|
+
|
|
11
|
+
from fastapi_fastkit import console
|
|
12
|
+
from fastapi_fastkit.core.exceptions import BackendExceptions
|
|
13
|
+
from fastapi_fastkit.utils.logging import debug_log, get_logger
|
|
14
|
+
from fastapi_fastkit.utils.main import handle_exception, print_error, print_success
|
|
15
|
+
|
|
16
|
+
from .base import BasePackageManager
|
|
17
|
+
|
|
18
|
+
logger = get_logger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PipManager(BasePackageManager):
|
|
22
|
+
"""Pip package manager implementation."""
|
|
23
|
+
|
|
24
|
+
def is_available(self) -> bool:
|
|
25
|
+
"""Check if pip is available on the system."""
|
|
26
|
+
try:
|
|
27
|
+
subprocess.run(
|
|
28
|
+
[sys.executable, "-m", "pip", "--version"],
|
|
29
|
+
check=True,
|
|
30
|
+
capture_output=True,
|
|
31
|
+
text=True,
|
|
32
|
+
)
|
|
33
|
+
return True
|
|
34
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
def get_dependency_file_name(self) -> str:
|
|
38
|
+
"""Get the dependency file name for pip."""
|
|
39
|
+
return "requirements.txt"
|
|
40
|
+
|
|
41
|
+
def create_virtual_environment(self) -> str:
|
|
42
|
+
"""
|
|
43
|
+
Create a Python virtual environment using venv module.
|
|
44
|
+
|
|
45
|
+
:return: Path to the virtual environment
|
|
46
|
+
:raises: BackendExceptions if virtual environment creation fails
|
|
47
|
+
"""
|
|
48
|
+
venv_path = str(self.project_dir / ".venv")
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
with console.status("[bold green]Creating virtual environment..."):
|
|
52
|
+
subprocess.run(
|
|
53
|
+
[sys.executable, "-m", "venv", venv_path],
|
|
54
|
+
check=True,
|
|
55
|
+
capture_output=True,
|
|
56
|
+
text=True,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
debug_log(f"Virtual environment created at {venv_path}", "info")
|
|
60
|
+
print_success("Virtual environment created successfully")
|
|
61
|
+
return venv_path
|
|
62
|
+
|
|
63
|
+
except subprocess.CalledProcessError as e:
|
|
64
|
+
debug_log(f"Error creating virtual environment: {e.stderr}", "error")
|
|
65
|
+
handle_exception(e, f"Error creating virtual environment: {str(e)}")
|
|
66
|
+
raise BackendExceptions("Failed to create venv")
|
|
67
|
+
except OSError as e:
|
|
68
|
+
debug_log(f"System error creating virtual environment: {e}", "error")
|
|
69
|
+
handle_exception(e, f"Error creating virtual environment: {str(e)}")
|
|
70
|
+
raise BackendExceptions(f"Failed to create venv: {str(e)}")
|
|
71
|
+
|
|
72
|
+
def install_dependencies(self, venv_path: str) -> None:
|
|
73
|
+
"""
|
|
74
|
+
Install dependencies using pip in the virtual environment.
|
|
75
|
+
|
|
76
|
+
:param venv_path: Path to the virtual environment
|
|
77
|
+
:raises: BackendExceptions if dependency installation fails
|
|
78
|
+
"""
|
|
79
|
+
try:
|
|
80
|
+
if not os.path.exists(venv_path):
|
|
81
|
+
debug_log(
|
|
82
|
+
"Virtual environment does not exist. Creating it first.", "warning"
|
|
83
|
+
)
|
|
84
|
+
print_error("Virtual environment does not exist. Creating it first.")
|
|
85
|
+
venv_path = self.create_virtual_environment()
|
|
86
|
+
if not venv_path:
|
|
87
|
+
raise BackendExceptions("Failed to create venv")
|
|
88
|
+
|
|
89
|
+
requirements_path = self.get_dependency_file_path()
|
|
90
|
+
if not requirements_path.exists():
|
|
91
|
+
debug_log(
|
|
92
|
+
f"Requirements file not found at {requirements_path}", "error"
|
|
93
|
+
)
|
|
94
|
+
print_error(f"Requirements file not found at {requirements_path}")
|
|
95
|
+
raise BackendExceptions("Requirements file not found")
|
|
96
|
+
|
|
97
|
+
# Get pip path
|
|
98
|
+
pip_path = self.get_executable_path("pip", venv_path)
|
|
99
|
+
|
|
100
|
+
# Upgrade pip first
|
|
101
|
+
subprocess.run(
|
|
102
|
+
[pip_path, "install", "--upgrade", "pip"],
|
|
103
|
+
check=True,
|
|
104
|
+
capture_output=True,
|
|
105
|
+
text=True,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Install dependencies
|
|
109
|
+
with console.status("[bold green]Installing dependencies..."):
|
|
110
|
+
subprocess.run(
|
|
111
|
+
[pip_path, "install", "-r", str(requirements_path.name)],
|
|
112
|
+
cwd=str(self.project_dir),
|
|
113
|
+
check=True,
|
|
114
|
+
capture_output=True,
|
|
115
|
+
text=True,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
debug_log("Dependencies installed successfully", "info")
|
|
119
|
+
print_success("Dependencies installed successfully")
|
|
120
|
+
|
|
121
|
+
except subprocess.CalledProcessError as e:
|
|
122
|
+
debug_log(f"Error during dependency installation: {e.stderr}", "error")
|
|
123
|
+
handle_exception(e, f"Error during dependency installation: {str(e)}")
|
|
124
|
+
if hasattr(e, "stderr"):
|
|
125
|
+
print_error(f"Error details: {e.stderr}")
|
|
126
|
+
raise BackendExceptions("Failed to install dependencies")
|
|
127
|
+
except OSError as e:
|
|
128
|
+
debug_log(f"System error during dependency installation: {e}", "error")
|
|
129
|
+
handle_exception(e, f"Error during dependency installation: {str(e)}")
|
|
130
|
+
raise BackendExceptions(f"Failed to install dependencies: {str(e)}")
|
|
131
|
+
|
|
132
|
+
def generate_dependency_file(
|
|
133
|
+
self,
|
|
134
|
+
dependencies: List[str],
|
|
135
|
+
project_name: str = "",
|
|
136
|
+
author: str = "",
|
|
137
|
+
author_email: str = "",
|
|
138
|
+
description: str = "",
|
|
139
|
+
) -> None:
|
|
140
|
+
"""
|
|
141
|
+
Generate a requirements.txt file with the given dependencies.
|
|
142
|
+
|
|
143
|
+
:param dependencies: List of dependency specifications
|
|
144
|
+
:param project_name: Name of the project (not used for pip)
|
|
145
|
+
:param author: Author name (not used for pip)
|
|
146
|
+
:param author_email: Author email (not used for pip)
|
|
147
|
+
:param description: Project description (not used for pip)
|
|
148
|
+
"""
|
|
149
|
+
requirements_path = self.get_dependency_file_path()
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
with open(requirements_path, "w", encoding="utf-8") as f:
|
|
153
|
+
for dep in dependencies:
|
|
154
|
+
f.write(f"{dep}\n")
|
|
155
|
+
|
|
156
|
+
debug_log(
|
|
157
|
+
f"Generated {requirements_path} with {len(dependencies)} dependencies",
|
|
158
|
+
"info",
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
except (OSError, UnicodeEncodeError) as e:
|
|
162
|
+
debug_log(f"Error generating requirements.txt: {e}", "error")
|
|
163
|
+
raise BackendExceptions(f"Failed to generate requirements.txt: {str(e)}")
|
|
164
|
+
|
|
165
|
+
def add_dependency(self, dependency: str, dev: bool = False) -> None:
|
|
166
|
+
"""
|
|
167
|
+
Add a new dependency to requirements.txt.
|
|
168
|
+
|
|
169
|
+
Note: pip doesn't have built-in support for dev dependencies,
|
|
170
|
+
so we'll add them to the main requirements.txt file.
|
|
171
|
+
|
|
172
|
+
:param dependency: Dependency specification
|
|
173
|
+
:param dev: Whether this is a development dependency (ignored for pip)
|
|
174
|
+
"""
|
|
175
|
+
requirements_path = self.get_dependency_file_path()
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
# Read existing dependencies
|
|
179
|
+
existing_deps = []
|
|
180
|
+
if requirements_path.exists():
|
|
181
|
+
with open(requirements_path, "r", encoding="utf-8") as f:
|
|
182
|
+
existing_deps = [
|
|
183
|
+
line.strip() for line in f.readlines() if line.strip()
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
# Add new dependency if not already present
|
|
187
|
+
if dependency not in existing_deps:
|
|
188
|
+
existing_deps.append(dependency)
|
|
189
|
+
|
|
190
|
+
with open(requirements_path, "w", encoding="utf-8") as f:
|
|
191
|
+
for dep in existing_deps:
|
|
192
|
+
f.write(f"{dep}\n")
|
|
193
|
+
|
|
194
|
+
debug_log(
|
|
195
|
+
f"Added dependency '{dependency}' to requirements.txt", "info"
|
|
196
|
+
)
|
|
197
|
+
else:
|
|
198
|
+
debug_log(
|
|
199
|
+
f"Dependency '{dependency}' already exists in requirements.txt",
|
|
200
|
+
"info",
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
except (OSError, UnicodeEncodeError, UnicodeDecodeError) as e:
|
|
204
|
+
debug_log(f"Error adding dependency: {e}", "error")
|
|
205
|
+
raise BackendExceptions(f"Failed to add dependency: {str(e)}")
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
# --------------------------------------------------------------------------
|
|
2
|
+
# Poetry Package Manager Implementation
|
|
3
|
+
#
|
|
4
|
+
# @author bnbong bbbong9@gmail.com
|
|
5
|
+
# --------------------------------------------------------------------------
|
|
6
|
+
import subprocess
|
|
7
|
+
from typing import List
|
|
8
|
+
|
|
9
|
+
from fastapi_fastkit import console
|
|
10
|
+
from fastapi_fastkit.core.exceptions import BackendExceptions
|
|
11
|
+
from fastapi_fastkit.utils.logging import debug_log, get_logger
|
|
12
|
+
from fastapi_fastkit.utils.main import handle_exception, print_error, print_success
|
|
13
|
+
|
|
14
|
+
from .base import BasePackageManager
|
|
15
|
+
|
|
16
|
+
logger = get_logger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PoetryManager(BasePackageManager):
|
|
20
|
+
"""Poetry package manager implementation."""
|
|
21
|
+
|
|
22
|
+
def is_available(self) -> bool:
|
|
23
|
+
"""Check if Poetry is available on the system."""
|
|
24
|
+
try:
|
|
25
|
+
subprocess.run(
|
|
26
|
+
["poetry", "--version"],
|
|
27
|
+
check=True,
|
|
28
|
+
capture_output=True,
|
|
29
|
+
text=True,
|
|
30
|
+
)
|
|
31
|
+
return True
|
|
32
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
def get_dependency_file_name(self) -> str:
|
|
36
|
+
"""Get the dependency file name for Poetry."""
|
|
37
|
+
return "pyproject.toml"
|
|
38
|
+
|
|
39
|
+
def create_virtual_environment(self) -> str:
|
|
40
|
+
"""
|
|
41
|
+
Create a virtual environment using Poetry.
|
|
42
|
+
|
|
43
|
+
:return: Path to the virtual environment
|
|
44
|
+
:raises: BackendExceptions if virtual environment creation fails
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
with console.status(
|
|
48
|
+
"[bold green]Creating virtual environment with Poetry..."
|
|
49
|
+
):
|
|
50
|
+
# Poetry automatically creates virtual environment when installing
|
|
51
|
+
# First ensure we have a basic pyproject.toml
|
|
52
|
+
pyproject_path = self.get_dependency_file_path()
|
|
53
|
+
if not pyproject_path.exists():
|
|
54
|
+
# Create minimal pyproject.toml for Poetry
|
|
55
|
+
self._create_minimal_pyproject()
|
|
56
|
+
|
|
57
|
+
# Get the virtual environment path from Poetry
|
|
58
|
+
result = subprocess.run(
|
|
59
|
+
["poetry", "env", "info", "--path"],
|
|
60
|
+
cwd=str(self.project_dir),
|
|
61
|
+
capture_output=True,
|
|
62
|
+
text=True,
|
|
63
|
+
check=False, # Don't fail if venv doesn't exist yet
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if result.returncode != 0:
|
|
67
|
+
# Create virtual environment with Poetry
|
|
68
|
+
subprocess.run(
|
|
69
|
+
["poetry", "install", "--no-deps"],
|
|
70
|
+
cwd=str(self.project_dir),
|
|
71
|
+
check=True,
|
|
72
|
+
capture_output=True,
|
|
73
|
+
text=True,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Get the path again
|
|
77
|
+
result = subprocess.run(
|
|
78
|
+
["poetry", "env", "info", "--path"],
|
|
79
|
+
cwd=str(self.project_dir),
|
|
80
|
+
capture_output=True,
|
|
81
|
+
text=True,
|
|
82
|
+
check=True,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
venv_path = result.stdout.strip()
|
|
86
|
+
|
|
87
|
+
debug_log(f"Virtual environment created at {venv_path}", "info")
|
|
88
|
+
print_success("Virtual environment created successfully with Poetry")
|
|
89
|
+
return venv_path
|
|
90
|
+
|
|
91
|
+
except subprocess.CalledProcessError as e:
|
|
92
|
+
debug_log(f"Error creating virtual environment: {e.stderr}", "error")
|
|
93
|
+
handle_exception(e, f"Error creating virtual environment: {str(e)}")
|
|
94
|
+
raise BackendExceptions("Failed to create venv with Poetry")
|
|
95
|
+
except OSError as e:
|
|
96
|
+
debug_log(f"System error creating virtual environment: {e}", "error")
|
|
97
|
+
handle_exception(e, f"Error creating virtual environment: {str(e)}")
|
|
98
|
+
raise BackendExceptions(f"Failed to create venv with Poetry: {str(e)}")
|
|
99
|
+
|
|
100
|
+
def _create_minimal_pyproject(self) -> None:
|
|
101
|
+
"""Create a minimal pyproject.toml for Poetry."""
|
|
102
|
+
pyproject_content = """[tool.poetry]
|
|
103
|
+
name = "temp-project"
|
|
104
|
+
version = "0.1.0"
|
|
105
|
+
description = ""
|
|
106
|
+
authors = ["Author <author@example.com>"]
|
|
107
|
+
|
|
108
|
+
[tool.poetry.dependencies]
|
|
109
|
+
python = "^3.8"
|
|
110
|
+
|
|
111
|
+
[tool.poetry.group.dev.dependencies]
|
|
112
|
+
|
|
113
|
+
[build-system]
|
|
114
|
+
requires = ["poetry-core"]
|
|
115
|
+
build-backend = "poetry.core.masonry.api"
|
|
116
|
+
"""
|
|
117
|
+
pyproject_path = self.get_dependency_file_path()
|
|
118
|
+
with open(pyproject_path, "w", encoding="utf-8") as f:
|
|
119
|
+
f.write(pyproject_content)
|
|
120
|
+
|
|
121
|
+
def install_dependencies(self, venv_path: str) -> None:
|
|
122
|
+
"""
|
|
123
|
+
Install dependencies using Poetry.
|
|
124
|
+
|
|
125
|
+
:param venv_path: Path to the virtual environment
|
|
126
|
+
:raises: BackendExceptions if dependency installation fails
|
|
127
|
+
"""
|
|
128
|
+
try:
|
|
129
|
+
pyproject_path = self.get_dependency_file_path()
|
|
130
|
+
if not pyproject_path.exists():
|
|
131
|
+
debug_log(f"pyproject.toml file not found at {pyproject_path}", "error")
|
|
132
|
+
print_error(f"pyproject.toml file not found at {pyproject_path}")
|
|
133
|
+
raise BackendExceptions("pyproject.toml file not found")
|
|
134
|
+
|
|
135
|
+
# Install dependencies using Poetry
|
|
136
|
+
with console.status("[bold green]Installing dependencies with Poetry..."):
|
|
137
|
+
subprocess.run(
|
|
138
|
+
["poetry", "install"],
|
|
139
|
+
cwd=str(self.project_dir),
|
|
140
|
+
check=True,
|
|
141
|
+
capture_output=True,
|
|
142
|
+
text=True,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
debug_log("Dependencies installed successfully with Poetry", "info")
|
|
146
|
+
print_success("Dependencies installed successfully with Poetry")
|
|
147
|
+
|
|
148
|
+
except subprocess.CalledProcessError as e:
|
|
149
|
+
debug_log(f"Error during dependency installation: {e.stderr}", "error")
|
|
150
|
+
handle_exception(e, f"Error during dependency installation: {str(e)}")
|
|
151
|
+
if hasattr(e, "stderr"):
|
|
152
|
+
print_error(f"Error details: {e.stderr}")
|
|
153
|
+
raise BackendExceptions("Failed to install dependencies with Poetry")
|
|
154
|
+
except OSError as e:
|
|
155
|
+
debug_log(f"System error during dependency installation: {e}", "error")
|
|
156
|
+
handle_exception(e, f"Error during dependency installation: {str(e)}")
|
|
157
|
+
raise BackendExceptions(
|
|
158
|
+
f"Failed to install dependencies with Poetry: {str(e)}"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def generate_dependency_file(
|
|
162
|
+
self,
|
|
163
|
+
dependencies: List[str],
|
|
164
|
+
project_name: str = "",
|
|
165
|
+
author: str = "",
|
|
166
|
+
author_email: str = "",
|
|
167
|
+
description: str = "",
|
|
168
|
+
) -> None:
|
|
169
|
+
"""
|
|
170
|
+
Generate a pyproject.toml file with the given dependencies and metadata.
|
|
171
|
+
|
|
172
|
+
:param dependencies: List of dependency specifications
|
|
173
|
+
:param project_name: Name of the project
|
|
174
|
+
:param author: Author name
|
|
175
|
+
:param author_email: Author email
|
|
176
|
+
:param description: Project description
|
|
177
|
+
"""
|
|
178
|
+
pyproject_path = self.get_dependency_file_path()
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
# Create dependencies section for Poetry
|
|
182
|
+
deps_section = ""
|
|
183
|
+
for dep in dependencies:
|
|
184
|
+
# Convert pip-style to poetry-style
|
|
185
|
+
if "==" in dep:
|
|
186
|
+
name, version = dep.split("==", 1)
|
|
187
|
+
deps_section += f'{name} = "{version}"\n'
|
|
188
|
+
else:
|
|
189
|
+
deps_section += f'{dep} = "*"\n'
|
|
190
|
+
|
|
191
|
+
# Create basic pyproject.toml content for Poetry
|
|
192
|
+
pyproject_content = f"""[tool.poetry]
|
|
193
|
+
name = "{project_name or 'fastapi-project'}"
|
|
194
|
+
version = "0.1.0"
|
|
195
|
+
description = "{description or 'A FastAPI project'}"
|
|
196
|
+
authors = ["{author or 'Author'} <{author_email or 'author@example.com'}>"]
|
|
197
|
+
readme = "README.md"
|
|
198
|
+
license = "MIT"
|
|
199
|
+
packages = [{{include = "src"}}]
|
|
200
|
+
|
|
201
|
+
[tool.poetry.dependencies]
|
|
202
|
+
python = "^3.8"
|
|
203
|
+
{deps_section}
|
|
204
|
+
|
|
205
|
+
[tool.poetry.group.dev.dependencies]
|
|
206
|
+
|
|
207
|
+
[build-system]
|
|
208
|
+
requires = ["poetry-core"]
|
|
209
|
+
build-backend = "poetry.core.masonry.api"
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
with open(pyproject_path, "w", encoding="utf-8") as f:
|
|
213
|
+
f.write(pyproject_content)
|
|
214
|
+
|
|
215
|
+
debug_log(
|
|
216
|
+
f"Generated {pyproject_path} with {len(dependencies)} dependencies",
|
|
217
|
+
"info",
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
except (OSError, UnicodeEncodeError) as e:
|
|
221
|
+
debug_log(f"Error generating pyproject.toml: {e}", "error")
|
|
222
|
+
raise BackendExceptions(f"Failed to generate pyproject.toml: {str(e)}")
|
|
223
|
+
|
|
224
|
+
def add_dependency(self, dependency: str, dev: bool = False) -> None:
|
|
225
|
+
"""
|
|
226
|
+
Add a new dependency using Poetry.
|
|
227
|
+
|
|
228
|
+
:param dependency: Dependency specification
|
|
229
|
+
:param dev: Whether this is a development dependency
|
|
230
|
+
"""
|
|
231
|
+
try:
|
|
232
|
+
# Use Poetry's add command to add dependency
|
|
233
|
+
cmd = ["poetry", "add"]
|
|
234
|
+
if dev:
|
|
235
|
+
cmd.append("--group=dev")
|
|
236
|
+
cmd.append(dependency)
|
|
237
|
+
|
|
238
|
+
subprocess.run(
|
|
239
|
+
cmd,
|
|
240
|
+
cwd=str(self.project_dir),
|
|
241
|
+
check=True,
|
|
242
|
+
capture_output=True,
|
|
243
|
+
text=True,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
debug_log(
|
|
247
|
+
f"Added {'dev ' if dev else ''}dependency '{dependency}' with Poetry",
|
|
248
|
+
"info",
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
except subprocess.CalledProcessError as e:
|
|
252
|
+
debug_log(f"Error adding dependency with Poetry: {e}", "error")
|
|
253
|
+
raise BackendExceptions(f"Failed to add dependency with Poetry: {str(e)}")
|
|
254
|
+
except OSError as e:
|
|
255
|
+
debug_log(f"System error adding dependency with Poetry: {e}", "error")
|
|
256
|
+
raise BackendExceptions(f"Failed to add dependency with Poetry: {str(e)}")
|
|
257
|
+
|
|
258
|
+
def initialize_project(
|
|
259
|
+
self, project_name: str, author: str, author_email: str, description: str
|
|
260
|
+
) -> None:
|
|
261
|
+
"""
|
|
262
|
+
Initialize a new Poetry project with metadata.
|
|
263
|
+
|
|
264
|
+
:param project_name: Name of the project
|
|
265
|
+
:param author: Author name
|
|
266
|
+
:param author_email: Author email
|
|
267
|
+
:param description: Project description
|
|
268
|
+
"""
|
|
269
|
+
try:
|
|
270
|
+
# Use Poetry init command with non-interactive mode
|
|
271
|
+
subprocess.run(
|
|
272
|
+
[
|
|
273
|
+
"poetry",
|
|
274
|
+
"init",
|
|
275
|
+
"--name",
|
|
276
|
+
project_name,
|
|
277
|
+
"--description",
|
|
278
|
+
description,
|
|
279
|
+
"--author",
|
|
280
|
+
f"{author} <{author_email}>",
|
|
281
|
+
"--license",
|
|
282
|
+
"MIT",
|
|
283
|
+
"--python",
|
|
284
|
+
"^3.8",
|
|
285
|
+
"--no-interaction",
|
|
286
|
+
],
|
|
287
|
+
cwd=str(self.project_dir),
|
|
288
|
+
check=True,
|
|
289
|
+
capture_output=True,
|
|
290
|
+
text=True,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
debug_log(f"Initialized Poetry project: {project_name}", "info")
|
|
294
|
+
|
|
295
|
+
except subprocess.CalledProcessError as e:
|
|
296
|
+
debug_log(f"Error initializing Poetry project: {e}", "error")
|
|
297
|
+
raise BackendExceptions(f"Failed to initialize Poetry project: {str(e)}")
|
|
298
|
+
except OSError as e:
|
|
299
|
+
debug_log(f"System error initializing Poetry project: {e}", "error")
|
|
300
|
+
raise BackendExceptions(f"Failed to initialize Poetry project: {str(e)}")
|
|
301
|
+
|
|
302
|
+
def lock_dependencies(self) -> None:
|
|
303
|
+
"""
|
|
304
|
+
Generate Poetry lock file.
|
|
305
|
+
|
|
306
|
+
:raises: BackendExceptions if lock generation fails
|
|
307
|
+
"""
|
|
308
|
+
try:
|
|
309
|
+
with console.status("[bold green]Generating Poetry lock file..."):
|
|
310
|
+
subprocess.run(
|
|
311
|
+
["poetry", "lock"],
|
|
312
|
+
cwd=str(self.project_dir),
|
|
313
|
+
check=True,
|
|
314
|
+
capture_output=True,
|
|
315
|
+
text=True,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
debug_log("Poetry lock file generated successfully", "info")
|
|
319
|
+
print_success("Poetry lock file generated successfully")
|
|
320
|
+
|
|
321
|
+
except subprocess.CalledProcessError as e:
|
|
322
|
+
debug_log(f"Error generating Poetry lock file: {e.stderr}", "error")
|
|
323
|
+
handle_exception(e, f"Error generating Poetry lock file: {str(e)}")
|
|
324
|
+
raise BackendExceptions("Failed to generate Poetry lock file")
|
|
325
|
+
except OSError as e:
|
|
326
|
+
debug_log(f"System error generating Poetry lock file: {e}", "error")
|
|
327
|
+
handle_exception(e, f"Error generating Poetry lock file: {str(e)}")
|
|
328
|
+
raise BackendExceptions(f"Failed to generate Poetry lock file: {str(e)}")
|
|
329
|
+
|
|
330
|
+
def run_script(self, script_command: str) -> None:
|
|
331
|
+
"""
|
|
332
|
+
Run a script using Poetry.
|
|
333
|
+
|
|
334
|
+
:param script_command: Command to run
|
|
335
|
+
:raises: BackendExceptions if script execution fails
|
|
336
|
+
"""
|
|
337
|
+
try:
|
|
338
|
+
subprocess.run(
|
|
339
|
+
["poetry", "run"] + script_command.split(),
|
|
340
|
+
cwd=str(self.project_dir),
|
|
341
|
+
check=True,
|
|
342
|
+
capture_output=True,
|
|
343
|
+
text=True,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
debug_log(f"Poetry script executed successfully: {script_command}", "info")
|
|
347
|
+
|
|
348
|
+
except subprocess.CalledProcessError as e:
|
|
349
|
+
debug_log(f"Error running Poetry script: {e.stderr}", "error")
|
|
350
|
+
raise BackendExceptions(f"Failed to run Poetry script: {str(e)}")
|
|
351
|
+
except OSError as e:
|
|
352
|
+
debug_log(f"System error running Poetry script: {e}", "error")
|
|
353
|
+
raise BackendExceptions(f"Failed to run Poetry script: {str(e)}")
|
|
354
|
+
|
|
355
|
+
def show_dependencies(self) -> str:
|
|
356
|
+
"""
|
|
357
|
+
Show project dependencies using Poetry.
|
|
358
|
+
|
|
359
|
+
:return: Dependencies information
|
|
360
|
+
:raises: BackendExceptions if showing dependencies fails
|
|
361
|
+
"""
|
|
362
|
+
try:
|
|
363
|
+
result = subprocess.run(
|
|
364
|
+
["poetry", "show"],
|
|
365
|
+
cwd=str(self.project_dir),
|
|
366
|
+
check=True,
|
|
367
|
+
capture_output=True,
|
|
368
|
+
text=True,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
return result.stdout
|
|
372
|
+
|
|
373
|
+
except subprocess.CalledProcessError as e:
|
|
374
|
+
debug_log(f"Error showing Poetry dependencies: {e.stderr}", "error")
|
|
375
|
+
raise BackendExceptions(f"Failed to show Poetry dependencies: {str(e)}")
|
|
376
|
+
except OSError as e:
|
|
377
|
+
debug_log(f"System error showing Poetry dependencies: {e}", "error")
|
|
378
|
+
raise BackendExceptions(f"Failed to show Poetry dependencies: {str(e)}")
|