appgenerator-cli 1.0.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.
- appgenerator_cli-1.0.0.dist-info/METADATA +213 -0
- appgenerator_cli-1.0.0.dist-info/RECORD +48 -0
- appgenerator_cli-1.0.0.dist-info/WHEEL +4 -0
- appgenerator_cli-1.0.0.dist-info/entry_points.txt +4 -0
- pyforge/__init__.py +10 -0
- pyforge/commands/__init__.py +1 -0
- pyforge/commands/create.py +60 -0
- pyforge/generator.py +248 -0
- pyforge/main.py +32 -0
- pyforge/templates/ai/.env.example +29 -0
- pyforge/templates/ai/.gitignore +47 -0
- pyforge/templates/ai/Dockerfile +22 -0
- pyforge/templates/ai/README.md +97 -0
- pyforge/templates/ai/app/__init__.py +1 -0
- pyforge/templates/ai/app/agents/__init__.py +1 -0
- pyforge/templates/ai/app/agents/assistant.py +100 -0
- pyforge/templates/ai/app/chains/__init__.py +1 -0
- pyforge/templates/ai/app/chains/rag.py +50 -0
- pyforge/templates/ai/app/config.py +47 -0
- pyforge/templates/ai/app/tools/__init__.py +1 -0
- pyforge/templates/ai/app/tools/registry.py +19 -0
- pyforge/templates/ai/app/tools/search.py +34 -0
- pyforge/templates/ai/docker-compose.yml +39 -0
- pyforge/templates/ai/main.py +40 -0
- pyforge/templates/ai/pyproject.toml +28 -0
- pyforge/templates/ai/tests/__init__.py +1 -0
- pyforge/templates/ai/tests/conftest.py +21 -0
- pyforge/templates/ai/tests/test_agent.py +53 -0
- pyforge/templates/fastapi/.env.example +17 -0
- pyforge/templates/fastapi/.gitignore +42 -0
- pyforge/templates/fastapi/Dockerfile +27 -0
- pyforge/templates/fastapi/README.md +68 -0
- pyforge/templates/fastapi/app/__init__.py +1 -0
- pyforge/templates/fastapi/app/api/__init__.py +1 -0
- pyforge/templates/fastapi/app/api/v1/__init__.py +1 -0
- pyforge/templates/fastapi/app/api/v1/health.py +25 -0
- pyforge/templates/fastapi/app/config.py +45 -0
- pyforge/templates/fastapi/app/db/__init__.py +1 -0
- pyforge/templates/fastapi/app/db/session.py +35 -0
- pyforge/templates/fastapi/app/dependencies.py +12 -0
- pyforge/templates/fastapi/app/main.py +58 -0
- pyforge/templates/fastapi/app/models/__init__.py +4 -0
- pyforge/templates/fastapi/app/models/base.py +23 -0
- pyforge/templates/fastapi/docker-compose.yml +39 -0
- pyforge/templates/fastapi/pyproject.toml +29 -0
- pyforge/templates/fastapi/tests/__init__.py +1 -0
- pyforge/templates/fastapi/tests/conftest.py +46 -0
- pyforge/templates/fastapi/tests/test_health.py +13 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: appgenerator-cli
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A developer-friendly scaffolding CLI for FastAPI and LangChain/LangGraph projects
|
|
5
|
+
Keywords: cli,scaffolding,fastapi,langchain,langgraph,uv
|
|
6
|
+
Author: Rajendra Kumar Yadav
|
|
7
|
+
Author-email: Rajendra Kumar Yadav <yadavrajendrakumar@outlook.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
18
|
+
Requires-Dist: typer>=0.12.0
|
|
19
|
+
Requires-Dist: rich>=13.0.0
|
|
20
|
+
Requires-Dist: jinja2>=3.1.0
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Project-URL: Homepage, https://github.com/rajendrakumaryadav/pyforge
|
|
23
|
+
Project-URL: Repository, https://github.com/rajendrakumaryadav/pyforge
|
|
24
|
+
Project-URL: Issues, https://github.com/rajendrakumaryadav/pyforge/issues
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# ⚒ AppGenerator CLI
|
|
28
|
+
|
|
29
|
+
> Scaffold production-ready Python projects in seconds — powered by `uv`.
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
$ appgenerator create fastapi my_api
|
|
33
|
+
$ appgenerator create ai my_ai_app --docker --postgres
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install appgenerator-cli
|
|
42
|
+
# or, with uv (recommended):
|
|
43
|
+
uv tool install appgenerator-cli
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Verify:
|
|
47
|
+
```bash
|
|
48
|
+
appgenerator --version
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Commands
|
|
54
|
+
|
|
55
|
+
### `appgenerator create fastapi <name>`
|
|
56
|
+
|
|
57
|
+
Scaffold a **FastAPI** backend.
|
|
58
|
+
|
|
59
|
+
| Flag | Description |
|
|
60
|
+
|------|-------------|
|
|
61
|
+
| `--docker` | Add `Dockerfile` + `docker-compose.yml` |
|
|
62
|
+
| `--postgres` | Use PostgreSQL (`asyncpg`) instead of SQLite |
|
|
63
|
+
| `--redis` | Add Redis client (`redis`) |
|
|
64
|
+
| `--output PATH` | Create the project in a custom directory |
|
|
65
|
+
|
|
66
|
+
**Example:**
|
|
67
|
+
```bash
|
|
68
|
+
appgenerator create fastapi my_api --docker --postgres --redis
|
|
69
|
+
cd my_api
|
|
70
|
+
cp .env.example .env
|
|
71
|
+
uv run uvicorn app.main:app --reload
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### `appgenerator create ai <name>`
|
|
75
|
+
|
|
76
|
+
Scaffold a **LangChain / LangGraph** AI application.
|
|
77
|
+
|
|
78
|
+
| Flag | Description |
|
|
79
|
+
|------|-------------|
|
|
80
|
+
| `--docker` | Add `Dockerfile` + `docker-compose.yml` |
|
|
81
|
+
| `--postgres` | Add pgvector PostgreSQL support |
|
|
82
|
+
| `--redis` | Add Redis semantic cache |
|
|
83
|
+
| `--output PATH` | Create the project in a custom directory |
|
|
84
|
+
|
|
85
|
+
**Example:**
|
|
86
|
+
```bash
|
|
87
|
+
appgenerator create ai my_assistant --docker
|
|
88
|
+
cd my_assistant
|
|
89
|
+
cp .env.example .env # add your OPENAI_API_KEY
|
|
90
|
+
uv run python main.py
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Generated Project Structure
|
|
96
|
+
|
|
97
|
+
### FastAPI
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
my_api/
|
|
101
|
+
├── app/
|
|
102
|
+
│ ├── main.py # Application factory
|
|
103
|
+
│ ├── config.py # Pydantic Settings
|
|
104
|
+
│ ├── dependencies.py # FastAPI deps (DB session, etc.)
|
|
105
|
+
│ ├── api/v1/
|
|
106
|
+
│ │ └── health.py # Health-check endpoint
|
|
107
|
+
│ ├── models/
|
|
108
|
+
│ │ └── base.py # SQLModel base with timestamps
|
|
109
|
+
│ └── db/
|
|
110
|
+
│ └── session.py # Async session factory
|
|
111
|
+
├── tests/
|
|
112
|
+
│ ├── conftest.py # Async test client + DB fixtures
|
|
113
|
+
│ └── test_health.py
|
|
114
|
+
├── .env.example
|
|
115
|
+
├── .gitignore
|
|
116
|
+
├── pyproject.toml
|
|
117
|
+
├── Dockerfile # (--docker)
|
|
118
|
+
└── docker-compose.yml # (--docker)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### AI (LangChain / LangGraph)
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
my_assistant/
|
|
125
|
+
├── main.py # Interactive REPL
|
|
126
|
+
├── app/
|
|
127
|
+
│ ├── config.py # Pydantic Settings
|
|
128
|
+
│ ├── agents/
|
|
129
|
+
│ │ └── assistant.py # LangGraph ReAct agent
|
|
130
|
+
│ ├── chains/
|
|
131
|
+
│ │ └── rag.py # RAG chain example
|
|
132
|
+
│ └── tools/
|
|
133
|
+
│ ├── registry.py # Tool registry
|
|
134
|
+
│ └── search.py # Web search tool stub
|
|
135
|
+
├── tests/
|
|
136
|
+
│ └── test_agent.py
|
|
137
|
+
├── .env.example
|
|
138
|
+
├── .gitignore
|
|
139
|
+
├── pyproject.toml
|
|
140
|
+
├── Dockerfile # (--docker)
|
|
141
|
+
└── docker-compose.yml # (--docker)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Packaging & Publishing to PyPI
|
|
147
|
+
|
|
148
|
+
### 1. Build
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
# Install build tools
|
|
152
|
+
pip install build twine
|
|
153
|
+
|
|
154
|
+
# Build wheel + sdist
|
|
155
|
+
python -m build
|
|
156
|
+
# Outputs: dist/appgenerator_cli-0.1.0-py3-none-any.whl
|
|
157
|
+
# dist/appgenerator_cli-0.1.0.tar.gz
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### 2. Test on TestPyPI
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
twine upload --repository testpypi dist/*
|
|
164
|
+
pip install --index-url https://test.pypi.org/simple/ appgenerator-cli
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### 3. Publish to PyPI
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
twine upload dist/*
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Or with uv:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
uv build
|
|
177
|
+
uv publish
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Development Setup
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
git clone https://github.com/yourname/pyforge-cli
|
|
186
|
+
cd pyforge-cli
|
|
187
|
+
|
|
188
|
+
uv venv
|
|
189
|
+
uv sync --dev
|
|
190
|
+
|
|
191
|
+
# Run locally
|
|
192
|
+
uv run appgenerator --help
|
|
193
|
+
|
|
194
|
+
# Tests
|
|
195
|
+
uv run pytest
|
|
196
|
+
|
|
197
|
+
# Lint
|
|
198
|
+
uv run ruff check .
|
|
199
|
+
uv run ruff format .
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Requirements
|
|
205
|
+
|
|
206
|
+
- Python ≥ 3.10
|
|
207
|
+
- [`uv`](https://docs.astral.sh/uv/) installed on the system (for generated project env management)
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## License
|
|
212
|
+
|
|
213
|
+
MIT
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
pyforge/__init__.py,sha256=8izMloJRT3IJTWLHt4DPnRnF_HBWNa4kCeptw8ixZ4g,229
|
|
2
|
+
pyforge/commands/__init__.py,sha256=gQ6tnU0Rvm0-ESWFUBU-KDl5dpNOpUTG509hXOQQjwY,27
|
|
3
|
+
pyforge/commands/create.py,sha256=0uLonhz2kJ5f33_kC9Tf8ogsVMCn41gopRxWOS4SSFc,2233
|
|
4
|
+
pyforge/generator.py,sha256=vXlZqNfU6ar-9t_C0ip-9NODP39Y0FDbVl34ilFh0UM,8620
|
|
5
|
+
pyforge/main.py,sha256=6pU8tYJayiVnMBTE8yvwoln2AxhIN5R3glq2YyG1AhY,875
|
|
6
|
+
pyforge/templates/ai/.env.example,sha256=dhiwWIbwCpXRetrAHBswJZchhlW0c6Lm8FBPv-5iEpk,1332
|
|
7
|
+
pyforge/templates/ai/.gitignore,sha256=d37AN_cpkr6IYCoQhvWkP1g5-352Z5FxT0qsQVn_sro,399
|
|
8
|
+
pyforge/templates/ai/Dockerfile,sha256=X5_IlCm9oP9XrqCOD69WkXdx6LiDu-Itls17Im6uDP0,786
|
|
9
|
+
pyforge/templates/ai/README.md,sha256=6c39K6kt_2hbCBespsRLHqkUVSrn9kPC_j1u4wSBYVQ,2748
|
|
10
|
+
pyforge/templates/ai/app/__init__.py,sha256=NhaPqUY6LTjOiB6li75lrY01m3Maf77e-TrB610URA4,49
|
|
11
|
+
pyforge/templates/ai/app/agents/__init__.py,sha256=K3TLCPOIwxTcWpYR-rmP-QPJWdb5_wnMW5B1Wr68trs,25
|
|
12
|
+
pyforge/templates/ai/app/agents/assistant.py,sha256=Wi_X_CifyGtgJaAZ8ZqgOqEbDt7fQKUwcu0hDcdKHtQ,3655
|
|
13
|
+
pyforge/templates/ai/app/chains/__init__.py,sha256=gPLZRZ2MLXBiafF_riw6Wgo07yn_rjugUK1Bq8eZyqQ,44
|
|
14
|
+
pyforge/templates/ai/app/chains/rag.py,sha256=RrLT2DTNfSuP5tqnOSBlC3-33oZY6sdb8aKLprPrDxU,1383
|
|
15
|
+
pyforge/templates/ai/app/config.py,sha256=Co6_eaaWpaUq4siqm6LoETX5Np4tGrI2FAYuHHvbDzk,1088
|
|
16
|
+
pyforge/templates/ai/app/tools/__init__.py,sha256=ffZt2FB3NFrHjbvROCGyorUYdJHu-yzmi17E0sacXtQ,30
|
|
17
|
+
pyforge/templates/ai/app/tools/registry.py,sha256=iMYxWm2DNk7NnhV0Ng0ACVs4pyf6iIiLvxdQmorqMww,455
|
|
18
|
+
pyforge/templates/ai/app/tools/search.py,sha256=q4nGmVIR3dlVGxnASHx6YHJZZ2WduyCAm5aNjt6gvyE,1327
|
|
19
|
+
pyforge/templates/ai/docker-compose.yml,sha256=zAp94_ssKr21Ckm5Ai6-JcOhWZBg0Zi2vF-IES403Do,776
|
|
20
|
+
pyforge/templates/ai/main.py,sha256=3JtvWH3p8ZjfduMf5WED2L5jR7416bch0z8h3-sRG9s,948
|
|
21
|
+
pyforge/templates/ai/pyproject.toml,sha256=cGIXwjkq4W-ov22DzbM6LldP6KC-lg2nLo2OkGd05xI,543
|
|
22
|
+
pyforge/templates/ai/tests/__init__.py,sha256=j-x4uousrt8rA7mrJ8pNKAc_38kKUMrU8I7gvI1ylG0,41
|
|
23
|
+
pyforge/templates/ai/tests/conftest.py,sha256=YNMpVpX3-kolFtxcpW-1wep2ehtj6FXp5DS1hjSIS2k,625
|
|
24
|
+
pyforge/templates/ai/tests/test_agent.py,sha256=INDRb-XwMWSZOzK0ej3i2KwkDGIr5yZzD1ty565tMec,1762
|
|
25
|
+
pyforge/templates/fastapi/.env.example,sha256=6Iv7Opys57RpWdeYuZQOnleXG3n-MaY79gAON7o8Vjs,1092
|
|
26
|
+
pyforge/templates/fastapi/.gitignore,sha256=WWUEaHQnwHKCefTYooPgdDGJScKWIxlqFvBOVrvPdbE,307
|
|
27
|
+
pyforge/templates/fastapi/Dockerfile,sha256=4gk2LYeRHaU9LUL7RbOpbIYv2RiHPih7xYjuUZB-70c,923
|
|
28
|
+
pyforge/templates/fastapi/README.md,sha256=24i60XTJQ86edN6Ey_QfHueWiaW2FHjmOEjuqsyzVeg,1931
|
|
29
|
+
pyforge/templates/fastapi/app/__init__.py,sha256=WwDGHlbBHaUL99XtoQhbvGhV_O7EaCE6nCikhkk9cvs,46
|
|
30
|
+
pyforge/templates/fastapi/app/api/__init__.py,sha256=d7XMEse6wwtLORqrQmnhaKMI8KM3lJSzRVE7HVskq1w,19
|
|
31
|
+
pyforge/templates/fastapi/app/api/v1/__init__.py,sha256=TSD_55lofQDrklwHqqm_zlDAL695k0RTThXsgIDDqDY,22
|
|
32
|
+
pyforge/templates/fastapi/app/api/v1/health.py,sha256=WFkYMNXEs-RL76gF1j7tT8PQyzDRCLQn7b8tOTdCxsg,575
|
|
33
|
+
pyforge/templates/fastapi/app/config.py,sha256=nAGQZG5C9pKl8OO1UJ054oE1MwHNgJCd1_fkQxluUNc,1120
|
|
34
|
+
pyforge/templates/fastapi/app/db/__init__.py,sha256=-izZUgEShtb52VcDe78h2-xPG0Im1WDkKVHy8dMfg78,24
|
|
35
|
+
pyforge/templates/fastapi/app/db/session.py,sha256=XK8ekpRmF_S46Dgj2SK2gkYSyt9ibNu_8Nkee_CRoEU,968
|
|
36
|
+
pyforge/templates/fastapi/app/dependencies.py,sha256=U_wGGNnyYqbdBUevC9CHATHxMtJ6oIqxyRZyk54KlNg,362
|
|
37
|
+
pyforge/templates/fastapi/app/main.py,sha256=dfLQSZriNVao5w1L__VtlsWlznnj5Mts7tH1VnuAsBs,1378
|
|
38
|
+
pyforge/templates/fastapi/app/models/__init__.py,sha256=YNPfDtvd2_5O_4RfPf67FGHivfhpASepWeC9W-jut3M,127
|
|
39
|
+
pyforge/templates/fastapi/app/models/base.py,sha256=R6_YyZXhSMKCpum5Ago2pHduIJB6vQhiHLtRH8TU8AQ,637
|
|
40
|
+
pyforge/templates/fastapi/docker-compose.yml,sha256=fujLVY_QxzEV_ZHEpu5GAurO9SyIl0UfDORDtdn5rMQ,713
|
|
41
|
+
pyforge/templates/fastapi/pyproject.toml,sha256=sNce628pZlgSTuTCRHDcUAFqwCMG6T_xcDKgpNSLlC4,531
|
|
42
|
+
pyforge/templates/fastapi/tests/__init__.py,sha256=TiS6C1IzwAXmNa8u36Y2xzL1CTTZm2PwzAtmZgoqepE,18
|
|
43
|
+
pyforge/templates/fastapi/tests/conftest.py,sha256=8I69eLCzAtA5tlZ4m94ktxcUOnCs7uehBnd2odWXe98,1312
|
|
44
|
+
pyforge/templates/fastapi/tests/test_health.py,sha256=EEw9GEVlCoHv7BVTx2dZMOfot_73g_5orD8cHUjJL54,375
|
|
45
|
+
appgenerator_cli-1.0.0.dist-info/WHEEL,sha256=M4DeIjVCA49okfALADZoWX5JOGwnmHb-JOpQHtI-1c0,80
|
|
46
|
+
appgenerator_cli-1.0.0.dist-info/entry_points.txt,sha256=5bQ5Wcg5NqyyvTzJ0fZy8zO41FCKvABQ72ObGrSIW-o,78
|
|
47
|
+
appgenerator_cli-1.0.0.dist-info/METADATA,sha256=mZx_QnK8ZWjGnjRSzfmHPfEJhXWV57aP7R9dlHsyBwc,4882
|
|
48
|
+
appgenerator_cli-1.0.0.dist-info/RECORD,,
|
pyforge/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI command modules."""
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
`appgenerator create` subcommand group.
|
|
3
|
+
Delegates to template-specific generators.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
|
|
13
|
+
from pyforge.generator import ProjectGenerator
|
|
14
|
+
|
|
15
|
+
create_app = typer.Typer(no_args_is_help=True, rich_markup_mode="rich")
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _run_generator(
|
|
20
|
+
template: str,
|
|
21
|
+
project_name: str,
|
|
22
|
+
output_dir: Optional[Path],
|
|
23
|
+
docker: bool,
|
|
24
|
+
postgres: bool,
|
|
25
|
+
redis: bool,
|
|
26
|
+
) -> None:
|
|
27
|
+
target = (output_dir or Path.cwd()) / project_name
|
|
28
|
+
if target.exists():
|
|
29
|
+
console.print(f"[bold red]✗[/] Directory [bold]{target}[/] already exists.")
|
|
30
|
+
raise typer.Exit(code=1)
|
|
31
|
+
|
|
32
|
+
generator = ProjectGenerator(
|
|
33
|
+
template=template,
|
|
34
|
+
project_name=project_name,
|
|
35
|
+
target_dir=target,
|
|
36
|
+
options={"docker": docker, "postgres": postgres, "redis": redis},
|
|
37
|
+
)
|
|
38
|
+
generator.run()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@create_app.command("fastapi", help="Scaffold a [bold]FastAPI[/] backend project.")
|
|
42
|
+
def create_fastapi(
|
|
43
|
+
project_name: str = typer.Argument(..., help="Name of the new project."),
|
|
44
|
+
output_dir: Optional[Path] = typer.Option(None, "--output", "-o", help="Parent directory."),
|
|
45
|
+
docker: bool = typer.Option(False, "--docker", help="Add Dockerfile & docker-compose.yml."),
|
|
46
|
+
postgres: bool = typer.Option(False, "--postgres", help="Add PostgreSQL support."),
|
|
47
|
+
redis: bool = typer.Option(False, "--redis", help="Add Redis support."),
|
|
48
|
+
) -> None:
|
|
49
|
+
_run_generator("fastapi", project_name, output_dir, docker, postgres, redis)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@create_app.command("ai", help="Scaffold a [bold]LangChain / LangGraph[/] AI project.")
|
|
53
|
+
def create_ai(
|
|
54
|
+
project_name: str = typer.Argument(..., help="Name of the new project."),
|
|
55
|
+
output_dir: Optional[Path] = typer.Option(None, "--output", "-o", help="Parent directory."),
|
|
56
|
+
docker: bool = typer.Option(False, "--docker", help="Add Dockerfile & docker-compose.yml."),
|
|
57
|
+
postgres: bool = typer.Option(False, "--postgres", help="Add PostgreSQL vector DB support."),
|
|
58
|
+
redis: bool = typer.Option(False, "--redis", help="Add Redis cache support."),
|
|
59
|
+
) -> None:
|
|
60
|
+
_run_generator("ai", project_name, output_dir, docker, postgres, redis)
|
pyforge/generator.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core project generation engine.
|
|
3
|
+
Handles file rendering, uv init, and dependency installation.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from jinja2 import Environment, FileSystemLoader
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.panel import Panel
|
|
16
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
17
|
+
from rich.table import Table
|
|
18
|
+
|
|
19
|
+
console = Console()
|
|
20
|
+
|
|
21
|
+
# Package root — templates live here
|
|
22
|
+
TEMPLATES_DIR = Path(__file__).parent / "templates"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ProjectGenerator:
|
|
26
|
+
"""Renders a project template and initialises it with uv."""
|
|
27
|
+
|
|
28
|
+
FASTAPI_DEPS = [
|
|
29
|
+
"fastapi",
|
|
30
|
+
"uvicorn[standard]",
|
|
31
|
+
"sqlmodel",
|
|
32
|
+
"aiosqlite",
|
|
33
|
+
"pydantic",
|
|
34
|
+
"pydantic-settings",
|
|
35
|
+
"python-dotenv",
|
|
36
|
+
"alembic",
|
|
37
|
+
"httpx",
|
|
38
|
+
]
|
|
39
|
+
FASTAPI_DEV_DEPS = ["pytest", "pytest-asyncio", "httpx", "ruff", "mypy"]
|
|
40
|
+
|
|
41
|
+
AI_DEPS = [
|
|
42
|
+
"langchain",
|
|
43
|
+
"langgraph",
|
|
44
|
+
"langchain-community",
|
|
45
|
+
"langchain-openai",
|
|
46
|
+
"langchain-ollama",
|
|
47
|
+
"langchain-chroma",
|
|
48
|
+
"chromadb",
|
|
49
|
+
"openai",
|
|
50
|
+
"tiktoken",
|
|
51
|
+
"python-dotenv",
|
|
52
|
+
"pydantic",
|
|
53
|
+
"pydantic-settings",
|
|
54
|
+
"httpx",
|
|
55
|
+
]
|
|
56
|
+
AI_DEV_DEPS = ["pytest", "pytest-asyncio", "ruff", "mypy"]
|
|
57
|
+
|
|
58
|
+
OPTIONAL_DEPS: dict[str, list[str]] = {
|
|
59
|
+
"postgres": ["asyncpg", "psycopg2-binary"],
|
|
60
|
+
"redis": ["redis", "hiredis"],
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
template: str,
|
|
66
|
+
project_name: str,
|
|
67
|
+
target_dir: Path,
|
|
68
|
+
options: dict[str, Any],
|
|
69
|
+
) -> None:
|
|
70
|
+
self.template = template # "fastapi" | "ai"
|
|
71
|
+
self.project_name = project_name
|
|
72
|
+
self.target_dir = target_dir
|
|
73
|
+
self.options = options
|
|
74
|
+
self.template_dir = TEMPLATES_DIR / template
|
|
75
|
+
self.env = Environment(
|
|
76
|
+
loader=FileSystemLoader(str(self.template_dir)),
|
|
77
|
+
keep_trailing_newline=True,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# ------------------------------------------------------------------ #
|
|
81
|
+
# Public API
|
|
82
|
+
# ------------------------------------------------------------------ #
|
|
83
|
+
|
|
84
|
+
def run(self) -> None:
|
|
85
|
+
console.print()
|
|
86
|
+
console.print(
|
|
87
|
+
Panel.fit(
|
|
88
|
+
f"[bold cyan]⚒ AppGenerator[/] · Creating [bold]{self.project_name}[/] "
|
|
89
|
+
f"([italic]{self.template}[/] template)",
|
|
90
|
+
border_style="cyan",
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
console.print()
|
|
94
|
+
|
|
95
|
+
with Progress(
|
|
96
|
+
SpinnerColumn(),
|
|
97
|
+
TextColumn("[progress.description]{task.description}"),
|
|
98
|
+
console=console,
|
|
99
|
+
transient=True,
|
|
100
|
+
) as progress:
|
|
101
|
+
t = progress.add_task("Rendering template files …", total=None)
|
|
102
|
+
self._render_template()
|
|
103
|
+
progress.update(t, description="[green]✓[/] Template files written")
|
|
104
|
+
|
|
105
|
+
progress.update(t, description="Initialising uv project …", completed=None)
|
|
106
|
+
self._uv_init()
|
|
107
|
+
progress.update(t, description="[green]✓[/] uv project initialised")
|
|
108
|
+
|
|
109
|
+
progress.update(t, description="Installing dependencies …", completed=None)
|
|
110
|
+
self._install_deps()
|
|
111
|
+
progress.update(t, description="[green]✓[/] Dependencies installed")
|
|
112
|
+
|
|
113
|
+
self._print_success()
|
|
114
|
+
|
|
115
|
+
# ------------------------------------------------------------------ #
|
|
116
|
+
# Steps
|
|
117
|
+
# ------------------------------------------------------------------ #
|
|
118
|
+
|
|
119
|
+
def _render_template(self) -> None:
|
|
120
|
+
"""Walk the template directory, render Jinja files, copy static files."""
|
|
121
|
+
ctx = self._build_context()
|
|
122
|
+
|
|
123
|
+
for src in self.template_dir.rglob("*"):
|
|
124
|
+
if src.is_dir():
|
|
125
|
+
continue
|
|
126
|
+
|
|
127
|
+
rel = src.relative_to(self.template_dir)
|
|
128
|
+
dest = self.target_dir / rel
|
|
129
|
+
|
|
130
|
+
# Skip optional files based on flags
|
|
131
|
+
if self._should_skip(rel):
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
135
|
+
|
|
136
|
+
if src.suffix in {".py", ".toml", ".env", ".yml", ".yaml", ".md", ".txt", ".cfg", ".ini", ".dockerfile", ""} or src.name == ".env.example":
|
|
137
|
+
try:
|
|
138
|
+
template = self.env.get_template(str(rel).replace("\\", "/"))
|
|
139
|
+
dest.write_text(template.render(**ctx), encoding="utf-8")
|
|
140
|
+
except Exception:
|
|
141
|
+
# Binary or unparseable — just copy
|
|
142
|
+
shutil.copy2(src, dest)
|
|
143
|
+
else:
|
|
144
|
+
shutil.copy2(src, dest)
|
|
145
|
+
|
|
146
|
+
def _uv_init(self) -> None:
|
|
147
|
+
"""Run `uv init` inside the target directory (no-op if uv not found)."""
|
|
148
|
+
if not shutil.which("uv"):
|
|
149
|
+
console.print(
|
|
150
|
+
"[yellow]⚠[/] [bold]uv[/] not found — skipping venv initialisation. "
|
|
151
|
+
"Install uv: [link=https://docs.astral.sh/uv/]https://docs.astral.sh/uv/[/link]"
|
|
152
|
+
)
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
# uv init creates a pyproject.toml — we already created ours, so just create the venv
|
|
156
|
+
self._run(["uv", "venv"], cwd=self.target_dir)
|
|
157
|
+
|
|
158
|
+
def _install_deps(self) -> None:
|
|
159
|
+
"""Write dependencies into pyproject.toml and sync the venv.
|
|
160
|
+
|
|
161
|
+
Uses --no-sync on `uv add` so uv only updates pyproject.toml + uv.lock
|
|
162
|
+
without trying to build/install the generated project itself as an
|
|
163
|
+
editable package.
|
|
164
|
+
|
|
165
|
+
The actual installation is done by a follow-up
|
|
166
|
+
`uv sync --no-install-project`.
|
|
167
|
+
"""
|
|
168
|
+
if not shutil.which("uv"):
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
deps = self.FASTAPI_DEPS if self.template == "fastapi" else self.AI_DEPS
|
|
172
|
+
dev_deps = self.FASTAPI_DEV_DEPS if self.template == "fastapi" else self.AI_DEV_DEPS
|
|
173
|
+
|
|
174
|
+
for flag, pkgs in self.OPTIONAL_DEPS.items():
|
|
175
|
+
if self.options.get(flag):
|
|
176
|
+
deps = deps + pkgs
|
|
177
|
+
|
|
178
|
+
# --no-sync: only pin versions into pyproject.toml / uv.lock, don't build project
|
|
179
|
+
self._run(["uv", "add", "--no-sync"] + deps, cwd=self.target_dir)
|
|
180
|
+
self._run(["uv", "add", "--no-sync", "--dev"] + dev_deps, cwd=self.target_dir)
|
|
181
|
+
|
|
182
|
+
# Install all pinned deps into .venv, skipping the project root package
|
|
183
|
+
self._run(["uv", "sync", "--no-install-project"], cwd=self.target_dir)
|
|
184
|
+
|
|
185
|
+
# ------------------------------------------------------------------ #
|
|
186
|
+
# Helpers
|
|
187
|
+
# ------------------------------------------------------------------ #
|
|
188
|
+
|
|
189
|
+
def _build_context(self) -> dict[str, Any]:
|
|
190
|
+
pkg = self.project_name.lower().replace("-", "_").replace(" ", "_")
|
|
191
|
+
return {
|
|
192
|
+
"project_name": self.project_name,
|
|
193
|
+
"package_name": pkg,
|
|
194
|
+
"template": self.template,
|
|
195
|
+
"docker": self.options.get("docker", False),
|
|
196
|
+
"postgres": self.options.get("postgres", False),
|
|
197
|
+
"redis": self.options.get("redis", False),
|
|
198
|
+
"fastapi_deps": self.FASTAPI_DEPS,
|
|
199
|
+
"ai_deps": self.AI_DEPS,
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
def _should_skip(self, rel: Path) -> bool:
|
|
203
|
+
name = rel.name
|
|
204
|
+
parts = set(rel.parts)
|
|
205
|
+
if name in {"Dockerfile", "docker-compose.yml"} and not self.options.get("docker"):
|
|
206
|
+
return True
|
|
207
|
+
if "postgres" in parts and not self.options.get("postgres"):
|
|
208
|
+
return True
|
|
209
|
+
if "redis" in parts and not self.options.get("redis"):
|
|
210
|
+
return True
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
def _run(self, cmd: list[str], cwd: Path) -> None:
|
|
214
|
+
result = subprocess.run(
|
|
215
|
+
cmd,
|
|
216
|
+
cwd=cwd,
|
|
217
|
+
capture_output=True,
|
|
218
|
+
text=True,
|
|
219
|
+
)
|
|
220
|
+
if result.returncode != 0:
|
|
221
|
+
console.print(f"[red]Command failed:[/] {' '.join(cmd)}")
|
|
222
|
+
console.print(result.stderr)
|
|
223
|
+
sys.exit(result.returncode)
|
|
224
|
+
|
|
225
|
+
def _print_success(self) -> None:
|
|
226
|
+
table = Table.grid(padding=(0, 2))
|
|
227
|
+
table.add_column(style="bold green")
|
|
228
|
+
table.add_column()
|
|
229
|
+
|
|
230
|
+
rel = self.target_dir.resolve()
|
|
231
|
+
run_cmd = (
|
|
232
|
+
"uvicorn app.main:app --reload"
|
|
233
|
+
if self.template == "fastapi"
|
|
234
|
+
else "python main.py"
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
table.add_row("Project:", str(rel))
|
|
238
|
+
table.add_row("Template:", self.template)
|
|
239
|
+
table.add_row("venv:", str(rel / ".venv"))
|
|
240
|
+
table.add_row("", "")
|
|
241
|
+
table.add_row("Next steps:", f"cd {self.project_name}")
|
|
242
|
+
table.add_row("", "cp .env.example .env # fill in your secrets")
|
|
243
|
+
table.add_row("", f"uv run {run_cmd}")
|
|
244
|
+
|
|
245
|
+
console.print(
|
|
246
|
+
Panel(table, title="[bold green]✓ Project created[/]", border_style="green")
|
|
247
|
+
)
|
|
248
|
+
console.print()
|
pyforge/main.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AppGenerator CLI — A scaffolding tool for FastAPI and LangChain/LangGraph projects.
|
|
3
|
+
"""
|
|
4
|
+
import typer
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
|
|
7
|
+
from pyforge.commands.create import create_app
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(
|
|
10
|
+
name="appgenerator",
|
|
11
|
+
help="⚒️ AppGenerator — Scaffold production-ready Python projects instantly.",
|
|
12
|
+
no_args_is_help=True,
|
|
13
|
+
rich_markup_mode="rich",
|
|
14
|
+
)
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
app.add_typer(create_app, name="create", help="Create a new project from a template.")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.callback(invoke_without_command=True)
|
|
21
|
+
def main(
|
|
22
|
+
ctx: typer.Context,
|
|
23
|
+
version: bool = typer.Option(False, "--version", "-v", help="Show version and exit."),
|
|
24
|
+
) -> None:
|
|
25
|
+
if version:
|
|
26
|
+
from pyforge import __version__
|
|
27
|
+
console.print(f"[bold cyan]AppGenerator[/] version [bold]{__version__}[/]")
|
|
28
|
+
raise typer.Exit()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
if __name__ == "__main__":
|
|
32
|
+
app()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# ── LLM Providers ────────────────────────────────────────────
|
|
2
|
+
OPENAI_API_KEY=sk-...
|
|
3
|
+
OPENAI_MODEL=gpt-4o-mini
|
|
4
|
+
|
|
5
|
+
# Ollama (local models) — optional
|
|
6
|
+
OLLAMA_BASE_URL=http://localhost:11434
|
|
7
|
+
OLLAMA_MODEL=llama3.2
|
|
8
|
+
|
|
9
|
+
# ── Application ──────────────────────────────────────────────
|
|
10
|
+
APP_NAME="{{ project_name }}"
|
|
11
|
+
APP_ENV=development
|
|
12
|
+
DEBUG=true
|
|
13
|
+
|
|
14
|
+
# ── Vector Store ──────────────────────────────────────────────
|
|
15
|
+
CHROMA_PERSIST_DIR=./chroma_db
|
|
16
|
+
{% if postgres %}
|
|
17
|
+
# pgvector (PostgreSQL) — enable with --postgres
|
|
18
|
+
PGVECTOR_URL=postgresql://postgres:postgres@localhost:5432/{{ package_name }}_vectors
|
|
19
|
+
{% endif %}
|
|
20
|
+
|
|
21
|
+
{% if redis %}
|
|
22
|
+
# ── Redis Cache ───────────────────────────────────────────────
|
|
23
|
+
REDIS_URL=redis://localhost:6379/0
|
|
24
|
+
{% endif %}
|
|
25
|
+
|
|
26
|
+
# ── LangSmith (optional tracing) ─────────────────────────────
|
|
27
|
+
# LANGCHAIN_TRACING_V2=true
|
|
28
|
+
# LANGCHAIN_API_KEY=ls__...
|
|
29
|
+
# LANGCHAIN_PROJECT={{ project_name }}
|