pyapp-cli 0.1.0__tar.gz

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.
@@ -0,0 +1,42 @@
1
+ Metadata-Version: 2.3
2
+ Name: pyapp-cli
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Requires-Dist: colorama>=0.4.6
6
+ Requires-Dist: fire>=0.7.1
7
+ Requires-Dist: inquirerpy>=0.3.4
8
+ Requires-Dist: pydantic>=2.12.5
9
+ Requires-Python: >=3.12, <4.0
10
+ Description-Content-Type: text/markdown
11
+
12
+ # PyApp CLI
13
+
14
+ ## About
15
+
16
+ PyApp CLI is a powerful tool that makes python project setup painless. With this CLI you can easily scaffold FastAPI, Flask or Django project with all necessary dependencies.
17
+
18
+ ## Supported platforms
19
+
20
+ The CLI can be run on Ubuntu Linux, but it's not guaranteed the CLI will work on other platforms.
21
+
22
+ ## Get started
23
+
24
+ Before setting up new project, install Python and appropriate package manager you'll use for your project. PyAPP CLI supports following package managers:
25
+
26
+ - pip
27
+ - poetry
28
+ - uv
29
+
30
+ Use `main.py init` command to run the CLI:
31
+
32
+ ```bash
33
+ python main.py init # python interpreter
34
+ poetry run python main.py init # poetry
35
+ uv run main.py init # uv
36
+ ```
37
+
38
+ Alertnatively, you can use python3 prefix when running command.
39
+
40
+ ## Contributing
41
+
42
+ Feel free to open an issue if you have found a bug, have a feature request or you want to expand list of available libraries/frameworks/package managers. You also can open PR if you wish.
@@ -0,0 +1,31 @@
1
+ # PyApp CLI
2
+
3
+ ## About
4
+
5
+ PyApp CLI is a powerful tool that makes python project setup painless. With this CLI you can easily scaffold FastAPI, Flask or Django project with all necessary dependencies.
6
+
7
+ ## Supported platforms
8
+
9
+ The CLI can be run on Ubuntu Linux, but it's not guaranteed the CLI will work on other platforms.
10
+
11
+ ## Get started
12
+
13
+ Before setting up new project, install Python and appropriate package manager you'll use for your project. PyAPP CLI supports following package managers:
14
+
15
+ - pip
16
+ - poetry
17
+ - uv
18
+
19
+ Use `main.py init` command to run the CLI:
20
+
21
+ ```bash
22
+ python main.py init # python interpreter
23
+ poetry run python main.py init # poetry
24
+ uv run main.py init # uv
25
+ ```
26
+
27
+ Alertnatively, you can use python3 prefix when running command.
28
+
29
+ ## Contributing
30
+
31
+ Feel free to open an issue if you have found a bug, have a feature request or you want to expand list of available libraries/frameworks/package managers. You also can open PR if you wish.
@@ -0,0 +1,19 @@
1
+ [project]
2
+ name = "pyapp-cli"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12,<4.0"
7
+ dependencies = [
8
+ "colorama>=0.4.6",
9
+ "fire>=0.7.1",
10
+ "inquirerpy>=0.3.4",
11
+ "pydantic>=2.12.5",
12
+ ]
13
+
14
+ [project.scripts]
15
+ pyapp = "pyapp_cli:main"
16
+
17
+ [build-system]
18
+ requires = ["uv_build>=0.10.12,<0.11.0"]
19
+ build-backend = "uv_build"
@@ -0,0 +1,8 @@
1
+ import fire
2
+
3
+ from .generator import ProjectGenerator
4
+ from .logger import Logger
5
+
6
+
7
+ def main() -> None:
8
+ fire.Fire(ProjectGenerator(logger=Logger()))
@@ -0,0 +1,209 @@
1
+ import os
2
+ from pathlib import Path
3
+ import shutil
4
+ import subprocess
5
+ import sys
6
+ from typing import Literal
7
+
8
+ from InquirerPy import prompt
9
+
10
+ from .logger import Logger
11
+ from .questions import questions
12
+ from .schemas import Answers
13
+
14
+ venv_dir = ".venv"
15
+
16
+
17
+ class ProjectGenerator:
18
+ # frameworks without built-in CLI for scaffolding the project
19
+ _framework_packages = set(["FastAPI", "Flask"])
20
+ _templates_dir = os.path.abspath("templates")
21
+
22
+ def __init__(self, logger: Logger) -> None:
23
+ self._logger = logger
24
+
25
+ def init(self):
26
+ raw_answers = prompt(questions)
27
+ answers = Answers.model_validate(raw_answers)
28
+
29
+ self._create_project_folder(answers.project_path, answers.source_folder)
30
+
31
+ if answers.package_manager == "pip":
32
+ self._ensure_pip_installation()
33
+ elif answers.package_manager == "poetry":
34
+ self._ensure_poetry_installation()
35
+ elif answers.package_manager == "uv":
36
+ self._ensure_uv_installation()
37
+
38
+ if answers.framework == "None" or answers.framework in self._framework_packages:
39
+ self._create_main_file(answers.source_folder, answers.framework)
40
+
41
+ dependencies = answers.libraries
42
+ if answers.framework != "None":
43
+ dependencies.append(self._framework_id(answers.framework))
44
+
45
+ if answers.package_manager == "pip":
46
+ self._pip_setup_project(dependencies)
47
+ elif answers.package_manager == "poetry":
48
+ self._poetry_setup_project(dependencies)
49
+ elif answers.package_manager == "uv":
50
+ self._uv_setup_project(dependencies, answers.python_version)
51
+
52
+ if answers.framework == "Django":
53
+ self._django_setup_project(answers.package_manager, answers.source_folder)
54
+
55
+ self._logger.success("Finished! Enjoy the project :)")
56
+
57
+ def _framework_id(self, framework: str) -> str:
58
+ return framework.lower()
59
+
60
+ def _missing_dependency_message(self, dependency: str) -> str:
61
+ return (
62
+ f"Can't detect installed {dependency}.\n"
63
+ f"Ensure {dependency} is installed and then run the script again."
64
+ )
65
+
66
+ def _create_project_folder(self, project_path: str, source_folder: str) -> None:
67
+ Path(project_path).mkdir(exist_ok=True)
68
+ if source_folder != "root":
69
+ Path(project_path, source_folder).mkdir(exist_ok=True)
70
+ os.chdir(project_path)
71
+ self._logger.log(f"Created folder '{project_path}'")
72
+
73
+ def _ensure_pip_installation(self) -> None:
74
+ pip_exists = shutil.which("pip") is not None
75
+ if pip_exists:
76
+ self._logger.success("Detected pip")
77
+ else:
78
+ self._logger.error(self._missing_dependency_message("pip"))
79
+ exit(127)
80
+
81
+ def _ensure_poetry_installation(self) -> None:
82
+ poetry_exists = shutil.which("poetry") is not None
83
+ if poetry_exists:
84
+ self._logger.success("Detected Poetry")
85
+ else:
86
+ self._logger.error(self._missing_dependency_message("Poetry"))
87
+ exit(127)
88
+
89
+ def _ensure_uv_installation(self) -> None:
90
+ uv_exists = shutil.which("uv") is not None
91
+ if uv_exists:
92
+ self._logger.success("Detected uv")
93
+ else:
94
+ self._logger.error(self._missing_dependency_message("uv"))
95
+ exit(127)
96
+
97
+ def _create_main_file(self, source_folder: str, framework: str) -> None:
98
+ python_file = None
99
+ if source_folder == "root":
100
+ python_file = Path("main.py")
101
+ else:
102
+ python_file = Path(source_folder, "main.py")
103
+ python_file.parent.mkdir(parents=True, exist_ok=True)
104
+ self._logger.log("Added main.py")
105
+
106
+ project_template = None
107
+ try:
108
+ template_path = os.path.join(
109
+ self._templates_dir, f"{self._framework_id(framework)}.py"
110
+ )
111
+ with open(template_path, "r") as template_file:
112
+ project_template = template_file.read()
113
+ self._logger.log(f"Applied {self._framework_id(framework)} template")
114
+ except FileNotFoundError:
115
+ pass
116
+
117
+ with open(python_file, "w") as f:
118
+ if project_template is not None:
119
+ f.write(project_template)
120
+
121
+ def _pip_setup_project(self, dependencies: list[str]) -> None:
122
+ self._logger.log("Creating virtual environment...")
123
+ subprocess.check_call(
124
+ [
125
+ sys.executable,
126
+ "-m",
127
+ "venv",
128
+ os.path.join(venv_dir),
129
+ ],
130
+ stdout=subprocess.DEVNULL,
131
+ stderr=subprocess.DEVNULL,
132
+ )
133
+
134
+ if len(dependencies) > 0:
135
+ self._logger.log("Installing dependencies...")
136
+ python_executable = os.path.abspath(
137
+ os.path.join(venv_dir, "bin", "python3")
138
+ )
139
+ subprocess.check_call(
140
+ [python_executable, "-m", "pip", "install", *dependencies],
141
+ )
142
+ result = subprocess.run(
143
+ [python_executable, "-m", "pip", "freeze"],
144
+ check=True,
145
+ capture_output=True,
146
+ text=True,
147
+ )
148
+ with open("requirements.txt", "w") as f:
149
+ f.write(result.stdout)
150
+ self._logger.success("Saved dependencies to requirements.txt")
151
+
152
+ def _poetry_setup_project(self, dependencies: list[str]) -> None:
153
+ subprocess.check_call(
154
+ ["poetry", "init", "-n"],
155
+ stdout=subprocess.DEVNULL,
156
+ stderr=subprocess.DEVNULL,
157
+ )
158
+ self._logger.log("Initialized poetry project")
159
+
160
+ if len(dependencies) > 0:
161
+ self._logger.log("Installing dependencies...")
162
+ custom_env = os.environ.copy()
163
+ custom_env["POETRY_VIRTUALENVS_IN_PROJECT"] = "true"
164
+ custom_env["VIRTUAL_ENV"] = os.path.abspath(venv_dir)
165
+ subprocess.check_call(
166
+ [
167
+ "poetry",
168
+ "add",
169
+ *dependencies,
170
+ ],
171
+ env=custom_env,
172
+ )
173
+
174
+ def _uv_setup_project(self, dependencies: list[str], python_version: str) -> None:
175
+ subprocess.check_call(
176
+ ["uv", "init", "--bare", "--no-workspace"],
177
+ stdout=subprocess.DEVNULL,
178
+ stderr=subprocess.DEVNULL,
179
+ )
180
+ self._logger.log("Initialized uv project")
181
+
182
+ with open(".python-version", "w") as f:
183
+ f.write(python_version)
184
+
185
+ if len(dependencies) > 0:
186
+ self._logger.log("Installing dependencies...")
187
+ subprocess.check_call(
188
+ [
189
+ "uv",
190
+ "add",
191
+ *dependencies,
192
+ ],
193
+ )
194
+
195
+ def _django_setup_project(
196
+ self, package_manager: Literal["pip", "poetry", "uv"], source_folder: str
197
+ ) -> None:
198
+ python_executable = os.path.abspath(os.path.join(venv_dir, "bin", "python3"))
199
+ django_util = [python_executable, "-m", "django"]
200
+ if package_manager == "uv":
201
+ django_util = ["uv", "run", "django-admin"]
202
+ self._logger.log("Initialized Django project")
203
+
204
+ if source_folder != "root":
205
+ os.chdir(source_folder)
206
+
207
+ subprocess.check_call(
208
+ [*django_util, "startproject", "mysite", "."],
209
+ )
@@ -0,0 +1,15 @@
1
+ from colorama import Fore, Style, init
2
+
3
+
4
+ class Logger:
5
+ def __init__(self) -> None:
6
+ init(autoreset=True)
7
+
8
+ def log(self, message: str) -> None:
9
+ print(Style.DIM + message)
10
+
11
+ def success(self, message: str) -> None:
12
+ print(Fore.GREEN + message)
13
+
14
+ def error(self, message: str) -> None:
15
+ print(Fore.RED + message)
@@ -0,0 +1,47 @@
1
+ questions = [
2
+ {
3
+ "type": "input",
4
+ "name": "project_path",
5
+ "message": "Enter the project path",
6
+ },
7
+ {
8
+ "type": "list",
9
+ "name": "package_manager",
10
+ "message": "Choose the package manager",
11
+ "choices": ["pip", "poetry", "uv"],
12
+ },
13
+ {
14
+ "type": "input",
15
+ "name": "python_version",
16
+ "message": "Enter Python version",
17
+ "default": "3.12",
18
+ },
19
+ {
20
+ "type": "list",
21
+ "name": "source_folder",
22
+ "message": "Choose the source code folder (root - project folder)",
23
+ "choices": ["root", "src", "app"],
24
+ },
25
+ {
26
+ "type": "list",
27
+ "name": "framework",
28
+ "message": "Choose the framework",
29
+ "choices": ["None", "FastAPI", "Flask", "Django"],
30
+ },
31
+ {
32
+ "type": "checkbox",
33
+ "name": "libraries",
34
+ "message": "Choose the libraries",
35
+ "choices": [
36
+ "gunicorn",
37
+ "uvicorn",
38
+ "sqlalchemy",
39
+ "firebase-admin",
40
+ "pydantic",
41
+ "pydantic-settings",
42
+ "pytest",
43
+ "pytest-asyncio",
44
+ "python-dotenv",
45
+ ],
46
+ },
47
+ ]
@@ -0,0 +1,12 @@
1
+ from typing import Literal
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class Answers(BaseModel):
7
+ project_path: str
8
+ package_manager: Literal["pip", "poetry", "uv"]
9
+ python_version: str
10
+ source_folder: Literal["root", "src", "app"]
11
+ framework: Literal["None", "FastAPI", "Flask", "Django"]
12
+ libraries: list[str]
@@ -0,0 +1,8 @@
1
+ from fastapi import FastAPI
2
+
3
+ app = FastAPI()
4
+
5
+
6
+ @app.get("/")
7
+ def read_root():
8
+ return "Hello, World!"
@@ -0,0 +1,8 @@
1
+ from flask import Flask
2
+
3
+ app = Flask(__name__)
4
+
5
+
6
+ @app.get("/")
7
+ def hello_world():
8
+ return "Hello, World!"