pyreactor-forge 0.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.
@@ -0,0 +1,3 @@
1
+ """PyReactor Forge - Full-stack Python + React application generator."""
2
+ __version__ = "0.1.0"
3
+ __author__ = "Panagiotis Adamopoulos"
pyreactor_forge/cli.py ADDED
@@ -0,0 +1,234 @@
1
+ """PyReactor Forge CLI - JHipster-inspired full-stack generator for Python + React."""
2
+
3
+ import click
4
+ import sys
5
+ from pathlib import Path
6
+ from rich.console import Console
7
+ from rich.panel import Panel
8
+ from rich.text import Text
9
+
10
+ from pyreactor_forge import __version__
11
+ from pyreactor_forge.generators.app import AppGenerator
12
+ from pyreactor_forge.generators.entity import EntityGenerator
13
+ from pyreactor_forge.utils.display import print_banner, print_success, print_error
14
+
15
+
16
+ def _force_utf8_stdio():
17
+ """Windows defaults stdio to the legacy ANSI code page (cp1252 on most
18
+ installs), which cannot encode the Unicode we print. It only bites when
19
+ output is redirected -- a bare terminal goes through WriteConsoleW and
20
+ works fine -- so piping to a file or running under CI would otherwise
21
+ crash with UnicodeEncodeError.
22
+ """
23
+ for stream in (sys.stdout, sys.stderr):
24
+ reconfigure = getattr(stream, "reconfigure", None)
25
+ if reconfigure is not None:
26
+ reconfigure(encoding="utf-8", errors="replace")
27
+
28
+
29
+ _force_utf8_stdio()
30
+
31
+ console = Console()
32
+
33
+
34
+ @click.group()
35
+ @click.version_option(version=__version__, prog_name="PyReactor Forge")
36
+ def cli():
37
+ """
38
+ \b
39
+ ██████╗ ██╗ ██╗██████╗ ███████╗ █████╗ ██████╗████████╗ ██████╗ ██████╗
40
+ ██╔══██╗╚██╗ ██╔╝██╔══██╗██╔════╝██╔══██╗██╔════╝╚══██╔══╝██╔═══██╗██╔══██╗
41
+ ██████╔╝ ╚████╔╝ ██████╔╝█████╗ ███████║██║ ██║ ██║ ██║██████╔╝
42
+ ██╔═══╝ ╚██╔╝ ██╔══██╗██╔══╝ ██╔══██║██║ ██║ ██║ ██║██╔══██╗
43
+ ██║ ██║ ██║ ██║███████╗██║ ██║╚██████╗ ██║ ╚██████╔╝██║ ██║
44
+ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝
45
+
46
+ Full-stack Python + React application generator.
47
+ Inspired by JHipster. Built for the Python ecosystem.
48
+ """
49
+ pass
50
+
51
+
52
+ @cli.command()
53
+ @click.option("--name", "-n", prompt="Application name", help="Name of the application")
54
+ @click.option(
55
+ "--backend",
56
+ "-b",
57
+ type=click.Choice(["fastapi", "django", "flask"], case_sensitive=False),
58
+ default="fastapi",
59
+ prompt="Backend framework",
60
+ show_default=True,
61
+ help="Python backend framework",
62
+ )
63
+ @click.option(
64
+ "--frontend",
65
+ "-f",
66
+ type=click.Choice(["react", "react-ts"], case_sensitive=False),
67
+ default="react-ts",
68
+ prompt="Frontend framework",
69
+ show_default=True,
70
+ help="Frontend framework",
71
+ )
72
+ @click.option(
73
+ "--database",
74
+ "-d",
75
+ type=click.Choice(["postgresql", "mysql", "sqlite"], case_sensitive=False),
76
+ default="postgresql",
77
+ prompt="Database",
78
+ show_default=True,
79
+ help="Database engine",
80
+ )
81
+ @click.option(
82
+ "--auth",
83
+ "-a",
84
+ type=click.Choice(["jwt", "session", "oauth2"], case_sensitive=False),
85
+ default="jwt",
86
+ prompt="Authentication type",
87
+ show_default=True,
88
+ help="Authentication strategy",
89
+ )
90
+ @click.option(
91
+ "--output-dir",
92
+ "-o",
93
+ default=".",
94
+ help="Output directory (default: current directory)",
95
+ )
96
+ @click.option(
97
+ "--docker/--no-docker",
98
+ default=True,
99
+ prompt="Include Docker configuration?",
100
+ help="Generate Docker and docker-compose files",
101
+ )
102
+ @click.option(
103
+ "--ci",
104
+ type=click.Choice(["github-actions", "gitlab-ci", "none"], case_sensitive=False),
105
+ default="github-actions",
106
+ prompt="CI/CD pipeline",
107
+ show_default=True,
108
+ help="CI/CD pipeline configuration",
109
+ )
110
+ def new(name, backend, frontend, database, auth, output_dir, docker, ci):
111
+ """Generate a new full-stack Python + React application."""
112
+ print_banner()
113
+
114
+ config = {
115
+ "name": name,
116
+ "backend": backend,
117
+ "frontend": frontend,
118
+ "database": database,
119
+ "auth": auth,
120
+ "docker": docker,
121
+ "ci": ci,
122
+ "output_dir": output_dir,
123
+ }
124
+
125
+ console.print(
126
+ Panel.fit(
127
+ f"[bold cyan]Generating application:[/bold cyan] [yellow]{name}[/yellow]\n"
128
+ f" Backend: [green]{backend}[/green]\n"
129
+ f" Frontend: [green]{frontend}[/green]\n"
130
+ f" Database: [green]{database}[/green]\n"
131
+ f" Auth: [green]{auth}[/green]\n"
132
+ f" Docker: [green]{'yes' if docker else 'no'}[/green]\n"
133
+ f" CI/CD: [green]{ci}[/green]",
134
+ title="⚡ PyReactor Forge",
135
+ border_style="cyan",
136
+ )
137
+ )
138
+
139
+ generator = AppGenerator(config)
140
+ try:
141
+ generator.generate()
142
+ print_success(name, config)
143
+ except Exception as e:
144
+ print_error(str(e))
145
+ sys.exit(1)
146
+
147
+
148
+ @cli.command()
149
+ @click.option("--name", "-n", prompt="Entity name (singular, PascalCase)", help="Entity name, e.g. Product")
150
+ @click.option(
151
+ "--app-dir",
152
+ "-a",
153
+ default=".",
154
+ help="Path to the generated app directory",
155
+ )
156
+ def entity(name, app_dir):
157
+ """Add a new entity (model + API + UI) to an existing app."""
158
+ app_path = Path(app_dir)
159
+ config_file = app_path / ".pyforge.json"
160
+
161
+ if not config_file.exists():
162
+ console.print(
163
+ "[red]Error:[/red] No .pyforge.json found. "
164
+ "Run this command inside a PyReactor Forge-generated project."
165
+ )
166
+ sys.exit(1)
167
+
168
+ import json
169
+ with open(config_file, encoding="utf-8") as f:
170
+ app_config = json.load(f)
171
+
172
+ console.print(f"\n[bold cyan]⚡ Adding entity:[/bold cyan] [yellow]{name}[/yellow]\n")
173
+
174
+ # Interactively collect fields
175
+ fields = []
176
+ console.print("[dim]Define fields (press Enter with no name to finish):[/dim]\n")
177
+
178
+ while True:
179
+ field_name = click.prompt(" Field name", default="", show_default=False)
180
+ if not field_name:
181
+ break
182
+
183
+ field_type = click.prompt(
184
+ " Field type",
185
+ type=click.Choice(["string", "integer", "float", "boolean", "date", "datetime", "text"]),
186
+ default="string",
187
+ )
188
+ required = click.confirm(" Required?", default=True)
189
+ fields.append({"name": field_name, "type": field_type, "required": required})
190
+ console.print()
191
+
192
+ if not fields:
193
+ console.print("[yellow]No fields defined. Aborting.[/yellow]")
194
+ sys.exit(0)
195
+
196
+ gen = EntityGenerator(name, fields, app_config, app_path)
197
+ gen.generate()
198
+
199
+ console.print(f"\n[bold green]✓ Entity [yellow]{name}[/yellow] added successfully![/bold green]")
200
+ console.print("\nFiles created/updated:")
201
+ for f in gen.created_files:
202
+ console.print(f" [cyan]→[/cyan] {f}")
203
+
204
+
205
+ @cli.command()
206
+ def info():
207
+ """Display information about the PyReactor Forge generator."""
208
+ print_banner()
209
+ console.print(
210
+ Panel(
211
+ "[bold]PyReactor Forge[/bold] is a full-stack code generator inspired by JHipster.\n\n"
212
+ "[bold cyan]Supported backends:[/bold cyan]\n"
213
+ " • [green]FastAPI[/green] — Modern, fast async Python API framework\n"
214
+ " • [green]Django[/green] — Batteries-included Python web framework\n"
215
+ " • [green]Flask[/green] — Lightweight Python micro-framework\n\n"
216
+ "[bold cyan]Supported frontends:[/bold cyan]\n"
217
+ " • [green]React[/green] — JavaScript (Vite + React)\n"
218
+ " • [green]React-TS[/green] — TypeScript (Vite + React + TypeScript)\n\n"
219
+ "[bold cyan]Supported databases:[/bold cyan]\n"
220
+ " • [green]PostgreSQL[/green] • [green]MySQL[/green] • [green]SQLite[/green]\n\n"
221
+ "[bold cyan]Supported authentication:[/bold cyan]\n"
222
+ " • [green]JWT[/green] • [green]Session[/green] • [green]OAuth2[/green]\n\n"
223
+ "[bold cyan]Commands:[/bold cyan]\n"
224
+ " • [yellow]pyforge new[/yellow] — Scaffold a new application\n"
225
+ " • [yellow]pyforge entity[/yellow] — Add a new entity to an existing app\n"
226
+ " • [yellow]pyforge info[/yellow] — Show this information",
227
+ title="ℹ️ About PyReactor Forge",
228
+ border_style="blue",
229
+ )
230
+ )
231
+
232
+
233
+ if __name__ == "__main__":
234
+ cli()
File without changes
@@ -0,0 +1,281 @@
1
+ """App Generator - scaffolds the full project structure."""
2
+
3
+ import json
4
+ import os
5
+ import shutil
6
+ from pathlib import Path
7
+ from rich.console import Console
8
+ from rich.progress import Progress, SpinnerColumn, TextColumn
9
+
10
+ from pyreactor_forge.generators.backend.fastapi import FastAPIGenerator
11
+ from pyreactor_forge.generators.backend.django import DjangoGenerator
12
+ from pyreactor_forge.generators.backend.flask import FlaskGenerator
13
+ from pyreactor_forge.generators.frontend import FrontendGenerator
14
+ from pyreactor_forge.generators.devops import DevOpsGenerator
15
+
16
+ console = Console()
17
+
18
+
19
+ class AppGenerator:
20
+ def __init__(self, config: dict):
21
+ self.config = config
22
+ self.name = config["name"]
23
+ self.slug = self.name.lower().replace(" ", "-").replace("_", "-")
24
+ self.output_dir = Path(config.get("output_dir", ".")) / self.slug
25
+
26
+ def generate(self):
27
+ if self.output_dir.exists():
28
+ raise FileExistsError(
29
+ f"Directory '{self.output_dir}' already exists. "
30
+ "Please remove it or choose a different name."
31
+ )
32
+
33
+ self.output_dir.mkdir(parents=True)
34
+
35
+ with Progress(
36
+ SpinnerColumn(),
37
+ TextColumn("[progress.description]{task.description}"),
38
+ console=console,
39
+ ) as progress:
40
+ # Backend
41
+ task = progress.add_task("[cyan]Generating backend...", total=None)
42
+ self._generate_backend()
43
+ progress.update(task, description="[green]✓ Backend generated")
44
+
45
+ # Frontend
46
+ task2 = progress.add_task("[cyan]Generating frontend...", total=None)
47
+ self._generate_frontend()
48
+ progress.update(task2, description="[green]✓ Frontend generated")
49
+
50
+ # DevOps
51
+ if self.config.get("docker") or self.config.get("ci") != "none":
52
+ task3 = progress.add_task("[cyan]Generating DevOps config...", total=None)
53
+ self._generate_devops()
54
+ progress.update(task3, description="[green]✓ DevOps config generated")
55
+
56
+ # Root files
57
+ task4 = progress.add_task("[cyan]Writing project files...", total=None)
58
+ self._write_root_files()
59
+ progress.update(task4, description="[green]✓ Project files written")
60
+
61
+ def _generate_backend(self):
62
+ backend = self.config["backend"]
63
+ backend_dir = self.output_dir / "backend"
64
+ backend_dir.mkdir()
65
+
66
+ if backend == "fastapi":
67
+ gen = FastAPIGenerator(self.config, backend_dir)
68
+ elif backend == "django":
69
+ gen = DjangoGenerator(self.config, backend_dir)
70
+ else:
71
+ gen = FlaskGenerator(self.config, backend_dir)
72
+
73
+ gen.generate()
74
+
75
+ def _generate_frontend(self):
76
+ frontend_dir = self.output_dir / "frontend"
77
+ frontend_dir.mkdir()
78
+ gen = FrontendGenerator(self.config, frontend_dir)
79
+ gen.generate()
80
+
81
+ def _generate_devops(self):
82
+ gen = DevOpsGenerator(self.config, self.output_dir)
83
+ gen.generate()
84
+
85
+ def _write_root_files(self):
86
+ # .pyforge.json - project metadata
87
+ meta = {
88
+ "name": self.name,
89
+ "slug": self.slug,
90
+ "backend": self.config["backend"],
91
+ "frontend": self.config["frontend"],
92
+ "database": self.config["database"],
93
+ "auth": self.config["auth"],
94
+ "version": "0.1.0",
95
+ }
96
+ with open(self.output_dir / ".pyforge.json", "w", encoding="utf-8") as f:
97
+ json.dump(meta, f, indent=2)
98
+
99
+ # Root README
100
+ readme = self._render_readme()
101
+ (self.output_dir / "README.md").write_text(readme, encoding="utf-8")
102
+
103
+ # Root .gitignore
104
+ (self.output_dir / ".gitignore").write_text(ROOT_GITIGNORE, encoding="utf-8")
105
+
106
+ # Makefile
107
+ (self.output_dir / "Makefile").write_text(self._render_makefile(), encoding="utf-8")
108
+
109
+ def _render_readme(self):
110
+ name = self.name
111
+ backend = self.config["backend"]
112
+ frontend = self.config["frontend"]
113
+ db = self.config["database"]
114
+ auth = self.config["auth"]
115
+
116
+ return f"""# {name}
117
+
118
+ > Generated by [PyReactor Forge](https://github.com/panosadamop/pyreactor-forge) — Full-stack Python + React generator.
119
+
120
+ ## Stack
121
+
122
+ | Layer | Technology |
123
+ |------------|-------------------------------------|
124
+ | Backend | {backend.capitalize()} |
125
+ | Frontend | {frontend.upper()} |
126
+ | Database | {db.capitalize()} |
127
+ | Auth | {auth.upper()} |
128
+
129
+ ## Quick Start
130
+
131
+ ### Prerequisites
132
+
133
+ - Python 3.11+
134
+ - Node.js 18+
135
+ - {db.capitalize()} running locally (or use Docker)
136
+
137
+ ### Development
138
+
139
+ ```bash
140
+ # Install all dependencies and start both servers
141
+ make dev
142
+ ```
143
+
144
+ Or manually:
145
+
146
+ ```bash
147
+ # Backend
148
+ cd backend
149
+ python -m venv .venv && source .venv/bin/activate
150
+ pip install -r requirements.txt
151
+ {"python manage.py migrate && python manage.py runserver" if backend == "django" else "uvicorn app.main:app --reload"}
152
+
153
+ # Frontend (in another terminal)
154
+ cd frontend
155
+ npm install
156
+ npm run dev
157
+ ```
158
+
159
+ ### Docker
160
+
161
+ ```bash
162
+ docker-compose up --build
163
+ ```
164
+
165
+ ## Project Structure
166
+
167
+ ```
168
+ {name.lower()}/
169
+ ├── backend/ # {backend.capitalize()} application
170
+ │ ├── app/ # Application code
171
+ │ │ ├── models/ # Database models (SQLAlchemy / Django ORM)
172
+ │ │ ├── routers/ # API route handlers
173
+ │ │ ├── schemas/ # Pydantic schemas / serializers
174
+ │ │ └── core/ # Config, auth, database setup
175
+ │ └── tests/ # Backend tests
176
+ ├── frontend/ # React application
177
+ │ ├── src/
178
+ │ │ ├── components/ # Reusable UI components
179
+ │ │ ├── pages/ # Route-level page components
180
+ │ │ ├── services/ # API client layer
181
+ │ │ ├── hooks/ # Custom React hooks
182
+ │ │ └── store/ # State management
183
+ │ └── public/
184
+ ├── docker-compose.yml
185
+ ├── Makefile
186
+ └── .pyforge.json # PyReactor Forge project metadata
187
+ ```
188
+
189
+ ## API Documentation
190
+
191
+ When the backend is running, visit:
192
+ - Swagger UI: http://localhost:8000/docs
193
+ - ReDoc: http://localhost:8000/redoc
194
+
195
+ ## License
196
+
197
+ MIT
198
+ """
199
+
200
+ def _render_makefile(self):
201
+ backend = self.config["backend"]
202
+ run_cmd = (
203
+ "python manage.py runserver"
204
+ if backend == "django"
205
+ else "uvicorn app.main:app --reload --host 0.0.0.0 --port 8000"
206
+ )
207
+ migrate_cmd = (
208
+ "python manage.py migrate"
209
+ if backend == "django"
210
+ else "alembic upgrade head"
211
+ )
212
+ return f"""# Makefile for {self.name}
213
+
214
+ .PHONY: dev backend frontend install migrate test lint docker-up docker-down
215
+
216
+ dev:
217
+ \t$(MAKE) -j2 backend frontend
218
+
219
+ backend:
220
+ \tcd backend && source .venv/bin/activate && {run_cmd}
221
+
222
+ frontend:
223
+ \tcd frontend && npm run dev
224
+
225
+ install:
226
+ \tcd backend && python -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt
227
+ \tcd frontend && npm install
228
+
229
+ migrate:
230
+ \tcd backend && source .venv/bin/activate && {migrate_cmd}
231
+
232
+ test:
233
+ \tcd backend && source .venv/bin/activate && pytest
234
+ \tcd frontend && npm run test
235
+
236
+ lint:
237
+ \tcd backend && source .venv/bin/activate && ruff check . && mypy .
238
+ \tcd frontend && npm run lint
239
+
240
+ docker-up:
241
+ \tdocker-compose up --build -d
242
+
243
+ docker-down:
244
+ \tdocker-compose down
245
+ """
246
+
247
+
248
+ ROOT_GITIGNORE = """# Python
249
+ __pycache__/
250
+ *.py[cod]
251
+ *.egg-info/
252
+ .venv/
253
+ .env
254
+ dist/
255
+ build/
256
+ .mypy_cache/
257
+ .ruff_cache/
258
+ .pytest_cache/
259
+
260
+ # Node
261
+ node_modules/
262
+ dist/
263
+ .env.local
264
+ .env.*.local
265
+
266
+ # Database
267
+ *.db
268
+ *.sqlite3
269
+
270
+ # IDE
271
+ .vscode/
272
+ .idea/
273
+ *.swp
274
+
275
+ # OS
276
+ .DS_Store
277
+ Thumbs.db
278
+
279
+ # Docker
280
+ .docker/
281
+ """
File without changes