nextpy-framework 2.0.0__tar.gz → 2.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.
- nextpy_framework-2.1.0/.nextpy_framework/nextpy/cli.py +1155 -0
- nextpy_framework-2.1.0/.nextpy_framework/nextpy/components/debug/AutoDebug.py +779 -0
- nextpy_framework-2.1.0/.nextpy_framework/nextpy/components/debug/DebugIcon.py +244 -0
- nextpy_framework-2.1.0/.nextpy_framework/nextpy/components/debug/DebugIconFixed.py +244 -0
- nextpy_framework-2.1.0/.nextpy_framework/nextpy/core/component_renderer.py +385 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/core/component_router.py +187 -27
- nextpy_framework-2.1.0/.nextpy_framework/nextpy/jsx_preprocessor.py +329 -0
- nextpy_framework-2.1.0/.nextpy_framework/nextpy/main.py +45 -0
- nextpy_framework-2.1.0/.nextpy_framework/nextpy/plugins/__init__.py +24 -0
- nextpy_framework-2.1.0/.nextpy_framework/nextpy/plugins/base.py +371 -0
- nextpy_framework-2.1.0/.nextpy_framework/nextpy/plugins/builtin.py +412 -0
- nextpy_framework-2.1.0/.nextpy_framework/nextpy/plugins/config.py +146 -0
- nextpy_framework-2.1.0/.nextpy_framework/nextpy/server/app.py +917 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy_framework.egg-info/PKG-INFO +1 -1
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy_framework.egg-info/SOURCES.txt +8 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/PKG-INFO +1 -1
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/pyproject.toml +1 -1
- nextpy_framework-2.0.0/.nextpy_framework/nextpy/cli.py +0 -1079
- nextpy_framework-2.0.0/.nextpy_framework/nextpy/core/component_renderer.py +0 -213
- nextpy_framework-2.0.0/.nextpy_framework/nextpy/jsx_preprocessor.py +0 -123
- nextpy_framework-2.0.0/.nextpy_framework/nextpy/server/app.py +0 -389
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/__init__.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/auth.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/builder.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/components/__init__.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/components/feedback.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/components/form.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/components/head.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/components/hooks_provider.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/components/image.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/components/layout.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/components/link.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/components/loader.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/components/navigation.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/components/toast.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/components/ui.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/components/visual.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/components.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/config.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/core/__init__.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/core/builder.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/core/data_fetching.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/core/demo_pages_simple.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/core/demo_router.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/core/renderer.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/core/router.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/core/sync.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/db.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/dev_server.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/dev_tools.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/errors.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/hooks.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/hooks_provider.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/hooks_provider_new.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/jsx.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/jsx_transformer.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/performance.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/plugins.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/py.typed +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/security.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/server/__init__.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/server/debug.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/server/middleware.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/true_jsx.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/utils/__init__.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/utils/cache.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/utils/email.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/utils/file_upload.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/utils/logging.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/utils/search.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/utils/seo.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/utils/validators.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy/websocket.py +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy_framework.egg-info/dependency_links.txt +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy_framework.egg-info/entry_points.txt +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy_framework.egg-info/requires.txt +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/.nextpy_framework/nextpy_framework.egg-info/top_level.txt +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/LICENSE +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/README.md +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/setup.cfg +0 -0
- {nextpy_framework-2.0.0 → nextpy_framework-2.1.0}/tests/test_routing.py +0 -0
|
@@ -0,0 +1,1155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NextPy CLI - Command-line interface for NextPy projects
|
|
3
|
+
Commands: dev, build, start
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
import asyncio
|
|
10
|
+
import signal
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
import uvicorn
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
from watchdog.observers import Observer
|
|
19
|
+
from watchdog.events import FileSystemEventHandler
|
|
20
|
+
WATCHDOG_AVAILABLE = True
|
|
21
|
+
except ImportError:
|
|
22
|
+
WATCHDOG_AVAILABLE = False
|
|
23
|
+
Observer = None
|
|
24
|
+
FileSystemEventHandler = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class HotReloadHandler:
|
|
28
|
+
"""Handles file system changes for hot reload with enhanced JSX support"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, reload_callback, debug: bool = False):
|
|
31
|
+
self.reload_callback = reload_callback
|
|
32
|
+
self._debounce_timer = None
|
|
33
|
+
self.debug = debug
|
|
34
|
+
self.last_reload_time = 0
|
|
35
|
+
self.reload_cooldown = 0.5 # 500ms cooldown between reloads
|
|
36
|
+
|
|
37
|
+
# Enhanced file patterns for better JSX detection
|
|
38
|
+
self.file_patterns = {
|
|
39
|
+
"python": [".py"],
|
|
40
|
+
"jsx": [".py.jsx", ".jsx"],
|
|
41
|
+
"templates": [".html", ".htm", ".jinja2", ".j2"],
|
|
42
|
+
"styles": [".css", ".scss", ".sass", ".less"],
|
|
43
|
+
"scripts": [".js", ".ts", ".mjs", ".cjs"],
|
|
44
|
+
"assets": [".json", ".yaml", ".yml", ".toml", ".ini"],
|
|
45
|
+
"config": [".env", ".env.example", "requirements.txt", "package.json", "tailwind.config.js", "postcss.config.js"]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Directories to watch
|
|
49
|
+
self.watch_dirs = {
|
|
50
|
+
"pages", "components", "templates", "public",
|
|
51
|
+
"static", "assets", "styles", "scripts", ".nextpy_framework"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Files that should always trigger reload
|
|
55
|
+
self.critical_files = {
|
|
56
|
+
"main.py", "app.py", "config.py", "settings.py",
|
|
57
|
+
"requirements.txt", "package.json", "pyproject.toml"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
def _should_reload_file(self, file_path: str) -> bool:
|
|
61
|
+
"""Determine if a file change should trigger reload"""
|
|
62
|
+
file_path = Path(file_path)
|
|
63
|
+
|
|
64
|
+
# Always reload critical files
|
|
65
|
+
if file_path.name in self.critical_files:
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
# Check if file is in a watched directory
|
|
69
|
+
parent_dirs = [part.name for part in file_path.parents]
|
|
70
|
+
if not any(dir_name in self.watch_dirs for dir_name in parent_dirs):
|
|
71
|
+
# If not in watched directories, check if it's in root
|
|
72
|
+
if len(parent_dirs) == 0 or parent_dirs[-1] == ".":
|
|
73
|
+
return any(file_path.name.endswith(pattern) for patterns in self.file_patterns.values() for pattern in patterns)
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
# Check file extension against all patterns
|
|
77
|
+
all_extensions = []
|
|
78
|
+
for patterns in self.file_patterns.values():
|
|
79
|
+
all_extensions.extend(patterns)
|
|
80
|
+
|
|
81
|
+
return any(file_path.name.endswith(ext) for ext in all_extensions)
|
|
82
|
+
|
|
83
|
+
def _get_file_type(self, file_path: str) -> str:
|
|
84
|
+
"""Categorize file type for logging"""
|
|
85
|
+
file_path = Path(file_path)
|
|
86
|
+
|
|
87
|
+
for file_type, extensions in self.file_patterns.items():
|
|
88
|
+
if any(file_path.name.endswith(ext) for ext in extensions):
|
|
89
|
+
return file_type
|
|
90
|
+
|
|
91
|
+
return "unknown"
|
|
92
|
+
|
|
93
|
+
def _debounce_reload(self, file_path: str = None):
|
|
94
|
+
"""Debounce reload calls to prevent excessive reloading"""
|
|
95
|
+
current_time = time.time()
|
|
96
|
+
|
|
97
|
+
if current_time - self.last_reload_time < self.reload_cooldown:
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
self.last_reload_time = current_time
|
|
101
|
+
|
|
102
|
+
if self.debug and file_path:
|
|
103
|
+
file_type = self._get_file_type(file_path)
|
|
104
|
+
click.echo(f" 🔄 Hot reload triggered by {file_type} file: {Path(file_path).name}", dim=True)
|
|
105
|
+
|
|
106
|
+
self._trigger_reload()
|
|
107
|
+
|
|
108
|
+
def on_modified(self, event):
|
|
109
|
+
"""Handle file modification events"""
|
|
110
|
+
if event.is_directory:
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
if self._should_reload_file(event.src_path):
|
|
114
|
+
self._debounce_reload(event.src_path)
|
|
115
|
+
|
|
116
|
+
def on_created(self, event):
|
|
117
|
+
"""Handle file creation events"""
|
|
118
|
+
if not event.is_directory and self._should_reload_file(event.src_path):
|
|
119
|
+
self._debounce_reload(event.src_path)
|
|
120
|
+
|
|
121
|
+
def on_deleted(self, event):
|
|
122
|
+
"""Handle file deletion events"""
|
|
123
|
+
if not event.is_directory and self._should_reload_file(event.src_path):
|
|
124
|
+
self._debounce_reload(event.src_path)
|
|
125
|
+
|
|
126
|
+
def on_moved(self, event):
|
|
127
|
+
"""Handle file move/rename events"""
|
|
128
|
+
if not event.is_directory:
|
|
129
|
+
# Handle both source and destination
|
|
130
|
+
if hasattr(event, 'dest_path') and event.dest_path:
|
|
131
|
+
if self._should_reload_file(event.src_path) or self._should_reload_file(event.dest_path):
|
|
132
|
+
self._debounce_reload(event.dest_path or event.src_path)
|
|
133
|
+
else:
|
|
134
|
+
if self._should_reload_file(event.src_path):
|
|
135
|
+
self._debounce_reload(event.src_path)
|
|
136
|
+
|
|
137
|
+
def _trigger_reload(self):
|
|
138
|
+
"""Trigger the reload callback"""
|
|
139
|
+
if self.reload_callback:
|
|
140
|
+
self.reload_callback()
|
|
141
|
+
|
|
142
|
+
def setup_file_watcher(self, project_dir: str = "."):
|
|
143
|
+
"""Setup enhanced file watcher with specific patterns"""
|
|
144
|
+
if not WATCHDOG_AVAILABLE:
|
|
145
|
+
click.echo(" ⚠️ Watchdog not installed. Hot reload disabled.", fg="yellow")
|
|
146
|
+
click.echo(" Install with: pip install watchdog", fg="yellow")
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
observer = Observer()
|
|
150
|
+
event_handler = WatchdogHotReloadHandler(self._trigger_reload, debug=self.debug)
|
|
151
|
+
|
|
152
|
+
# Watch specific directories with recursive monitoring
|
|
153
|
+
for watch_dir in self.watch_dirs:
|
|
154
|
+
dir_path = Path(project_dir) / watch_dir
|
|
155
|
+
if dir_path.exists():
|
|
156
|
+
observer.schedule(event_handler, str(dir_path), recursive=True)
|
|
157
|
+
if self.debug:
|
|
158
|
+
click.echo(f" 📁 Watching directory: {watch_dir}", dim=True)
|
|
159
|
+
|
|
160
|
+
# Also watch root directory for critical files
|
|
161
|
+
root_path = Path(project_dir)
|
|
162
|
+
if root_path.exists():
|
|
163
|
+
observer.schedule(event_handler, str(root_path), recursive=False)
|
|
164
|
+
|
|
165
|
+
return observer
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
if WATCHDOG_AVAILABLE:
|
|
169
|
+
class WatchdogHotReloadHandler(HotReloadHandler, FileSystemEventHandler):
|
|
170
|
+
"""Enhanced watchdog handler with better file filtering"""
|
|
171
|
+
pass
|
|
172
|
+
else:
|
|
173
|
+
class WatchdogHotReloadHandler(HotReloadHandler):
|
|
174
|
+
"""Fallback handler when watchdog is not available"""
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
def find_main_module():
|
|
178
|
+
# main.py is always expected at the project root for NextPy projects
|
|
179
|
+
# We ensure the current directory is in sys.path before calling uvicorn.run
|
|
180
|
+
return "main:app"
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _format_size(size_bytes: int) -> str:
|
|
184
|
+
"""Format file size in human-readable format"""
|
|
185
|
+
if size_bytes == 0:
|
|
186
|
+
return "0 B"
|
|
187
|
+
|
|
188
|
+
size_names = ["B", "KB", "MB", "GB", "TB"]
|
|
189
|
+
i = 0
|
|
190
|
+
size = float(size_bytes)
|
|
191
|
+
|
|
192
|
+
while size >= 1024.0 and i < len(size_names) - 1:
|
|
193
|
+
size /= 1024.0
|
|
194
|
+
i += 1
|
|
195
|
+
|
|
196
|
+
return f"{size:.1f} {size_names[i]}"
|
|
197
|
+
|
|
198
|
+
@click.group()
|
|
199
|
+
@click.version_option(version="1.0.0", prog_name="NextPy")
|
|
200
|
+
def cli():
|
|
201
|
+
"""NextPy - A Python web framework inspired by Next.js"""
|
|
202
|
+
pass
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@cli.command()
|
|
206
|
+
@click.option("--port", "-p", default=5000, help="Port to run the server on")
|
|
207
|
+
@click.option("--host", "-h", default="0.0.0.0", help="Host to bind to")
|
|
208
|
+
@click.option("--reload/--no-reload", default=True, help="Enable hot reload")
|
|
209
|
+
@click.option("--debug/--no-debug", default=True, help="Enable debug mode")
|
|
210
|
+
def dev(port: int, host: str, reload: bool, debug: bool):
|
|
211
|
+
"""Start the development server with enhanced hot reload"""
|
|
212
|
+
click.echo(click.style("\n NextPy Development Server", fg="cyan", bold=True))
|
|
213
|
+
click.echo(click.style(" ========================\n", fg="cyan"))
|
|
214
|
+
|
|
215
|
+
# Set debug environment variable
|
|
216
|
+
if debug:
|
|
217
|
+
os.environ["NEXTPY_DEBUG"] = "true"
|
|
218
|
+
os.environ["DEBUG"] = "true"
|
|
219
|
+
os.environ["DEVELOPMENT"] = "true"
|
|
220
|
+
else:
|
|
221
|
+
os.environ.pop("NEXTPY_DEBUG", None)
|
|
222
|
+
os.environ.pop("DEBUG", None)
|
|
223
|
+
os.environ.pop("DEVELOPMENT", None)
|
|
224
|
+
|
|
225
|
+
_ensure_project_structure()
|
|
226
|
+
|
|
227
|
+
click.echo(f" - Mode: {'Development' if debug else 'Production'}")
|
|
228
|
+
click.echo(f" - Host: {host} (accessible at http://localhost:{port})")
|
|
229
|
+
click.echo(f" - Port: {port}")
|
|
230
|
+
click.echo(f" - Reload: {'Enabled' if reload else 'Disabled'}")
|
|
231
|
+
click.echo(f" - Debug: {'Enabled' if debug else 'Disabled'}")
|
|
232
|
+
|
|
233
|
+
if reload and not WATCHDOG_AVAILABLE:
|
|
234
|
+
click.echo(f" - Watchdog: Not Available (install: pip install watchdog)", fg="yellow")
|
|
235
|
+
elif reload:
|
|
236
|
+
click.echo(f" - Watchdog: Available")
|
|
237
|
+
|
|
238
|
+
if debug:
|
|
239
|
+
click.echo(f" - Debug Icon: ✅ Auto-enabled")
|
|
240
|
+
click.echo(f" - Console Capture: ✅ Enabled")
|
|
241
|
+
click.echo(f" - Performance Monitoring: ✅ Enabled")
|
|
242
|
+
|
|
243
|
+
click.echo(f"\n ✨ Server ready at http://0.0.0.0:{port}")
|
|
244
|
+
click.echo(f" 🌐 Open http://localhost:{port} in your browser\n")
|
|
245
|
+
|
|
246
|
+
project_dir = Path('.')
|
|
247
|
+
os.chdir(project_dir)
|
|
248
|
+
|
|
249
|
+
# Ensure the current directory is in sys.path for module discovery
|
|
250
|
+
if str(project_dir.resolve()) not in sys.path:
|
|
251
|
+
sys.path.insert(0, str(project_dir.resolve()))
|
|
252
|
+
|
|
253
|
+
main_module = find_main_module()
|
|
254
|
+
|
|
255
|
+
if reload:
|
|
256
|
+
# Enhanced reload configuration with JSX support
|
|
257
|
+
reload_dirs = [
|
|
258
|
+
"pages",
|
|
259
|
+
"components",
|
|
260
|
+
"templates",
|
|
261
|
+
"public",
|
|
262
|
+
"static",
|
|
263
|
+
"styles",
|
|
264
|
+
"scripts",
|
|
265
|
+
".nextpy_framework"
|
|
266
|
+
]
|
|
267
|
+
|
|
268
|
+
# Filter to only existing directories
|
|
269
|
+
existing_reload_dirs = []
|
|
270
|
+
for reload_dir in reload_dirs:
|
|
271
|
+
dir_path = project_dir / reload_dir
|
|
272
|
+
if dir_path.exists():
|
|
273
|
+
existing_reload_dirs.append(reload_dir)
|
|
274
|
+
if debug:
|
|
275
|
+
click.echo(f" 📁 Watching: {reload_dir}/", dim=True)
|
|
276
|
+
|
|
277
|
+
# Enhanced reload patterns for JSX files
|
|
278
|
+
reload_includes = [
|
|
279
|
+
"*.py",
|
|
280
|
+
"*.py.jsx",
|
|
281
|
+
"*.jsx",
|
|
282
|
+
"*.html",
|
|
283
|
+
"*.htm",
|
|
284
|
+
"*.css",
|
|
285
|
+
"*.scss",
|
|
286
|
+
"*.sass",
|
|
287
|
+
"*.less",
|
|
288
|
+
"*.js",
|
|
289
|
+
"*.ts",
|
|
290
|
+
"*.json",
|
|
291
|
+
"*.yaml",
|
|
292
|
+
"*.yml",
|
|
293
|
+
"*.env",
|
|
294
|
+
"requirements.txt",
|
|
295
|
+
"package.json",
|
|
296
|
+
"tailwind.config.js",
|
|
297
|
+
"postcss.config.js"
|
|
298
|
+
]
|
|
299
|
+
|
|
300
|
+
uvicorn.run(
|
|
301
|
+
main_module,
|
|
302
|
+
host=host,
|
|
303
|
+
port=port,
|
|
304
|
+
reload=True,
|
|
305
|
+
reload_dirs=existing_reload_dirs,
|
|
306
|
+
reload_includes=reload_includes,
|
|
307
|
+
log_level="info",
|
|
308
|
+
)
|
|
309
|
+
else:
|
|
310
|
+
uvicorn.run(
|
|
311
|
+
main_module,
|
|
312
|
+
host=host,
|
|
313
|
+
port=port,
|
|
314
|
+
log_level="info",
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@cli.command()
|
|
319
|
+
@click.option("--out", "-o", default="out", help="Output directory for static files")
|
|
320
|
+
@click.option("--clean/--no-clean", default=True, help="Clean output directory first")
|
|
321
|
+
def build(out: str, clean: bool):
|
|
322
|
+
"""Build the project for production with enhanced feedback"""
|
|
323
|
+
click.echo(click.style("\n 🔨 NextPy Static Build", fg="green", bold=True))
|
|
324
|
+
click.echo(click.style(" ===================\n", fg="green"))
|
|
325
|
+
|
|
326
|
+
try:
|
|
327
|
+
from nextpy.core.builder import Builder
|
|
328
|
+
|
|
329
|
+
click.echo(f" 📂 Output directory: {out}/")
|
|
330
|
+
if clean:
|
|
331
|
+
click.echo(f" 🧹 Cleaning output directory...")
|
|
332
|
+
|
|
333
|
+
click.echo(f" ⚙️ Initializing builder...")
|
|
334
|
+
builder = Builder(out_dir=out)
|
|
335
|
+
|
|
336
|
+
click.echo(f" 🏗️ Building static files...")
|
|
337
|
+
|
|
338
|
+
async def run_build():
|
|
339
|
+
manifest = await builder.build(clean=clean)
|
|
340
|
+
return manifest
|
|
341
|
+
|
|
342
|
+
manifest = asyncio.run(run_build())
|
|
343
|
+
|
|
344
|
+
pages_count = len(manifest.get("pages", {}))
|
|
345
|
+
assets_count = len(manifest.get("assets", []))
|
|
346
|
+
total_size = manifest.get("total_size", 0)
|
|
347
|
+
|
|
348
|
+
click.echo()
|
|
349
|
+
click.echo(click.style(f" ✅ Build completed successfully!", fg="green", bold=True))
|
|
350
|
+
click.echo(f" 📄 Pages built: {pages_count}")
|
|
351
|
+
click.echo(f" 🎨 Assets processed: {assets_count}")
|
|
352
|
+
click.echo(f" 💾 Total size: {_format_size(total_size)}")
|
|
353
|
+
click.echo(f" 📍 Output: {out}/")
|
|
354
|
+
click.echo()
|
|
355
|
+
click.echo(click.style(f" 🚀 Ready for deployment!", fg="cyan", bold=True))
|
|
356
|
+
click.echo(f" 📖 Serve with: nextpy start --port 5000")
|
|
357
|
+
click.echo()
|
|
358
|
+
|
|
359
|
+
except Exception as e:
|
|
360
|
+
click.echo(click.style(f" ❌ Build failed: {str(e)}", fg="red"))
|
|
361
|
+
if "Builder" not in str(e):
|
|
362
|
+
click.echo(click.style(f" 💡 Make sure you're in a NextPy project directory", fg="yellow"))
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
@cli.command()
|
|
366
|
+
@click.option("--port", "-p", default=5000, help="Port to run the server on")
|
|
367
|
+
@click.option("--host", "-h", default="0.0.0.0", help="Host to bind to")
|
|
368
|
+
def start(port: int, host: str):
|
|
369
|
+
"""Start the production server with enhanced feedback"""
|
|
370
|
+
click.echo(click.style("\n 🚀 NextPy Production Server", fg="green", bold=True))
|
|
371
|
+
click.echo(click.style(" ========================\n", fg="green"))
|
|
372
|
+
|
|
373
|
+
click.echo(f" 🏭 Mode: Production")
|
|
374
|
+
click.echo(f" 🌐 Host: {host} (accessible at http://localhost:{port})")
|
|
375
|
+
click.echo(f" 🔌 Port: {port}")
|
|
376
|
+
click.echo(f" 👥 Workers: 4 (multi-process)")
|
|
377
|
+
click.echo(f" 📝 Logging: Warning level only")
|
|
378
|
+
|
|
379
|
+
click.echo(f"\n ✨ Production server ready at http://0.0.0.0:{port}")
|
|
380
|
+
click.echo(f" 🌐 Open http://localhost:{port} in your browser\n")
|
|
381
|
+
click.echo(click.style(f" 💡 Press Ctrl+C to stop the server", fg="yellow"))
|
|
382
|
+
click.echo()
|
|
383
|
+
|
|
384
|
+
try:
|
|
385
|
+
os.chdir(Path.cwd())
|
|
386
|
+
|
|
387
|
+
uvicorn.run(
|
|
388
|
+
"main:app",
|
|
389
|
+
host=host,
|
|
390
|
+
port=port,
|
|
391
|
+
workers=4,
|
|
392
|
+
log_level="warning",
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
except KeyboardInterrupt:
|
|
396
|
+
click.echo(click.style("\n 👋 Server stopped gracefully", fg="cyan"))
|
|
397
|
+
except Exception as e:
|
|
398
|
+
click.echo(click.style(f"\n ❌ Server error: {str(e)}", fg="red"))
|
|
399
|
+
click.echo(click.style(f" 💡 Make sure you have a main.py file with an app instance", fg="yellow"))
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
@cli.command()
|
|
403
|
+
@click.argument("name")
|
|
404
|
+
def create(name: str):
|
|
405
|
+
"""Create a new NextPy project with True JSX support"""
|
|
406
|
+
click.echo(click.style(f"\n 🚀 Creating NextPy project: {name}", fg="cyan", bold=True))
|
|
407
|
+
click.echo(click.style(" " + "=" * (25 + len(name)) + "\n", fg="cyan"))
|
|
408
|
+
|
|
409
|
+
project_dir = Path(name)
|
|
410
|
+
|
|
411
|
+
if project_dir.exists():
|
|
412
|
+
click.echo(click.style(f" ❌ Error: Directory '{name}' already exists", fg="red"))
|
|
413
|
+
click.echo(click.style(f" 💡 Try a different name or remove the existing directory", fg="yellow"))
|
|
414
|
+
return
|
|
415
|
+
|
|
416
|
+
click.echo(f" 📁 Creating project structure...")
|
|
417
|
+
|
|
418
|
+
try:
|
|
419
|
+
_create_project_structure(project_dir)
|
|
420
|
+
|
|
421
|
+
click.echo(click.style(f" ✅ Project successfully created!", fg="green", bold=True))
|
|
422
|
+
click.echo(f"\n 📍 Location: {project_dir.absolute()}")
|
|
423
|
+
click.echo(f"\n 🎯 Next steps:")
|
|
424
|
+
click.echo(f" 1️⃣ cd {name}")
|
|
425
|
+
click.echo(f" 2️⃣ pip install -r requirements.txt")
|
|
426
|
+
click.echo(f" 3️⃣ nextpy dev")
|
|
427
|
+
click.echo(f"\n 🌐 Your app will be available at: http://localhost:5000")
|
|
428
|
+
click.echo(f"\n 📚 Documentation: https://github.com/IBRAHIMFONYUY/nextpy-framework")
|
|
429
|
+
click.echo()
|
|
430
|
+
|
|
431
|
+
except Exception as e:
|
|
432
|
+
click.echo(click.style(f" ❌ Failed to create project: {str(e)}", fg="red"))
|
|
433
|
+
# Clean up partial creation
|
|
434
|
+
if project_dir.exists():
|
|
435
|
+
import shutil
|
|
436
|
+
shutil.rmtree(project_dir, ignore_errors=True)
|
|
437
|
+
click.echo(click.style(f" 🧹 Cleaned up partial files", fg="yellow"))
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
@cli.command()
|
|
441
|
+
def routes():
|
|
442
|
+
"""Display all registered routes with detailed information"""
|
|
443
|
+
click.echo(click.style("\n 🛣️ NextPy Routes Overview", fg="cyan", bold=True))
|
|
444
|
+
click.echo(click.style(" =====================\n", fg="cyan"))
|
|
445
|
+
|
|
446
|
+
try:
|
|
447
|
+
from nextpy.core.router import Router
|
|
448
|
+
|
|
449
|
+
router = Router()
|
|
450
|
+
router.scan_pages()
|
|
451
|
+
|
|
452
|
+
page_routes = [r for r in router.routes if not r.is_api]
|
|
453
|
+
api_routes = router.api_routes
|
|
454
|
+
|
|
455
|
+
click.echo(click.style(f" 📄 Page Routes ({len(page_routes)} total)", fg="blue", bold=True))
|
|
456
|
+
if page_routes:
|
|
457
|
+
for i, route in enumerate(page_routes, 1):
|
|
458
|
+
dynamic = " 🔀" if route.is_dynamic else " 📄"
|
|
459
|
+
file_info = f"({route.file_path})"
|
|
460
|
+
click.echo(f" {i:2d}. {dynamic} {route.path:<30} {file_info}")
|
|
461
|
+
else:
|
|
462
|
+
click.echo(f" ℹ️ No page routes found")
|
|
463
|
+
|
|
464
|
+
click.echo()
|
|
465
|
+
click.echo(click.style(f" 🔌 API Routes ({len(api_routes)} total)", fg="green", bold=True))
|
|
466
|
+
if api_routes:
|
|
467
|
+
for i, route in enumerate(api_routes, 1):
|
|
468
|
+
dynamic = " 🔀" if route.is_dynamic else " 🔌"
|
|
469
|
+
file_info = f"({route.file_path})"
|
|
470
|
+
methods = "[GET, POST, PUT, DELETE]" if hasattr(route, 'handler') else "[GET]"
|
|
471
|
+
click.echo(f" {i:2d}. {dynamic} {route.path:<30} {methods:<20} {file_info}")
|
|
472
|
+
else:
|
|
473
|
+
click.echo(f" ℹ️ No API routes found")
|
|
474
|
+
|
|
475
|
+
click.echo()
|
|
476
|
+
click.echo(click.style(f" 📊 Summary:", fg="yellow", bold=True))
|
|
477
|
+
click.echo(f" Total Routes: {len(page_routes + api_routes)}")
|
|
478
|
+
click.echo(f" Dynamic Routes: {len([r for r in page_routes + api_routes if r.is_dynamic])}")
|
|
479
|
+
click.echo(f" Static Routes: {len([r for r in page_routes + api_routes if not r.is_dynamic])}")
|
|
480
|
+
click.echo()
|
|
481
|
+
|
|
482
|
+
except Exception as e:
|
|
483
|
+
click.echo(click.style(f" ❌ Error scanning routes: {str(e)}", fg="red"))
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
@cli.command()
|
|
487
|
+
@click.option("--out", "-o", default="out", help="Output directory for static files")
|
|
488
|
+
def export(out: str):
|
|
489
|
+
"""Export static files with enhanced feedback"""
|
|
490
|
+
click.echo(click.style("\n 📦 NextPy Export", fg="green", bold=True))
|
|
491
|
+
click.echo(click.style(" =============\n", fg="green"))
|
|
492
|
+
|
|
493
|
+
try:
|
|
494
|
+
from nextpy.core.builder import Builder
|
|
495
|
+
|
|
496
|
+
click.echo(f" 📂 Output directory: {out}/")
|
|
497
|
+
click.echo(f" ⚙️ Initializing exporter...")
|
|
498
|
+
|
|
499
|
+
builder = Builder(out_dir=out)
|
|
500
|
+
|
|
501
|
+
click.echo(f" 📤 Exporting static files...")
|
|
502
|
+
|
|
503
|
+
async def run_export():
|
|
504
|
+
manifest = await builder.export_static()
|
|
505
|
+
return manifest
|
|
506
|
+
|
|
507
|
+
manifest = asyncio.run(run_export())
|
|
508
|
+
|
|
509
|
+
files_count = len(manifest.get("files", []))
|
|
510
|
+
total_size = manifest.get("total_size", 0)
|
|
511
|
+
|
|
512
|
+
click.echo()
|
|
513
|
+
click.echo(click.style(f" ✅ Export completed successfully!", fg="green", bold=True))
|
|
514
|
+
click.echo(f" 📁 Files exported: {files_count}")
|
|
515
|
+
click.echo(f" 💾 Total size: {_format_size(total_size)}")
|
|
516
|
+
click.echo(f" 📍 Output: {out}/")
|
|
517
|
+
click.echo()
|
|
518
|
+
click.echo(click.style(f" 🚀 Ready for static hosting!", fg="cyan", bold=True))
|
|
519
|
+
click.echo()
|
|
520
|
+
|
|
521
|
+
except Exception as e:
|
|
522
|
+
click.echo(click.style(f" ❌ Export failed: {str(e)}", fg="red"))
|
|
523
|
+
click.echo(click.style(f" 💡 Make sure you're in a NextPy project directory", fg="yellow"))
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
@cli.command()
|
|
527
|
+
def version():
|
|
528
|
+
"""Show version and system information"""
|
|
529
|
+
click.echo(click.style("\n 📋 NextPy Framework Info", fg="cyan", bold=True))
|
|
530
|
+
click.echo(click.style(" ===================\n", fg="cyan"))
|
|
531
|
+
|
|
532
|
+
click.echo(f" 🏷️ Version: 2.0.0")
|
|
533
|
+
click.echo(f" 🐍 Python: {sys.version.split()[0]}")
|
|
534
|
+
click.echo(f" ⚡ Framework: NextPy")
|
|
535
|
+
click.echo(f" 🎨 Architecture: True JSX")
|
|
536
|
+
click.echo(f" 🖥️ Development Server: uvicorn")
|
|
537
|
+
click.echo(f" 🔄 Hot Reload: Available")
|
|
538
|
+
click.echo(f" 📁 Static Files: Available")
|
|
539
|
+
click.echo(f" 🔌 API Routes: Available")
|
|
540
|
+
click.echo(f" 📄 Page Routes: Available")
|
|
541
|
+
click.echo(f" 🧩 Component Routes: Available")
|
|
542
|
+
click.echo(f" 📚 Component Library: Available")
|
|
543
|
+
click.echo(f" 👨💻 Developer: Ibrahim Fonyuy")
|
|
544
|
+
click.echo(f" 📜 License: MIT")
|
|
545
|
+
click.echo(f" 🐙 GitHub: https://github.com/IBRAHIMFONYUY/nextpy-framework")
|
|
546
|
+
click.echo(f" 📖 Documentation: https://nextpy.org/docs")
|
|
547
|
+
click.echo(f" 🆘 Support: https://github.com/IBRAHIMFONYUY/nextpy-framework/issues")
|
|
548
|
+
|
|
549
|
+
click.echo()
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
@cli.command()
|
|
553
|
+
def info():
|
|
554
|
+
"""Show comprehensive framework and system information"""
|
|
555
|
+
click.echo(click.style("\n 🖥️ NextPy System Information", fg="cyan", bold=True))
|
|
556
|
+
click.echo(click.style(" ==========================\n", fg="cyan"))
|
|
557
|
+
|
|
558
|
+
# Framework info
|
|
559
|
+
click.echo(click.style(" 📦 Framework Details:", fg="blue", bold=True))
|
|
560
|
+
click.echo(f" Version: 2.0.0")
|
|
561
|
+
click.echo(f" Architecture: True JSX")
|
|
562
|
+
click.echo(f" Python: {sys.version.split()[0]}")
|
|
563
|
+
|
|
564
|
+
# Feature status
|
|
565
|
+
click.echo(click.style("\n ⚡ Feature Status:", fg="green", bold=True))
|
|
566
|
+
watchdog_status = "✅ Available" if WATCHDOG_AVAILABLE else "❌ Not Available (pip install watchdog)"
|
|
567
|
+
click.echo(f" Hot Reload: {watchdog_status}")
|
|
568
|
+
click.echo(f" Static Files: ✅ Available")
|
|
569
|
+
click.echo(f" API Routes: ✅ Available")
|
|
570
|
+
click.echo(f" Page Routes: ✅ Available")
|
|
571
|
+
click.echo(f" Component Library: ✅ Available")
|
|
572
|
+
|
|
573
|
+
# Project structure check
|
|
574
|
+
click.echo(click.style("\n 📁 Project Structure:", fg="yellow", bold=True))
|
|
575
|
+
required_dirs = ["pages", "components", "templates", "public"]
|
|
576
|
+
for dir_name in required_dirs:
|
|
577
|
+
status = "✅" if Path(dir_name).exists() else "❌"
|
|
578
|
+
click.echo(f" {dir_name}/: {status}")
|
|
579
|
+
|
|
580
|
+
# Available commands
|
|
581
|
+
click.echo(click.style("\n 🛠️ Available Commands:", fg="purple", bold=True))
|
|
582
|
+
commands = [
|
|
583
|
+
("nextpy dev", "Start development server"),
|
|
584
|
+
("nextpy build", "Build for production"),
|
|
585
|
+
("nextpy start", "Start production server"),
|
|
586
|
+
("nextpy create <name>", "Create new project"),
|
|
587
|
+
("nextpy generate <type> <name>", "Generate components/pages/APIs"),
|
|
588
|
+
("nextpy routes", "Show all routes"),
|
|
589
|
+
("nextpy export", "Export static files"),
|
|
590
|
+
("nextpy version", "Show version info"),
|
|
591
|
+
("nextpy info", "Show this information")
|
|
592
|
+
]
|
|
593
|
+
for cmd, desc in commands:
|
|
594
|
+
click.echo(f" {cmd:<25} - {desc}")
|
|
595
|
+
|
|
596
|
+
click.echo()
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
@cli.command()
|
|
600
|
+
@click.argument("type", type=click.Choice(["page", "api", "component"]))
|
|
601
|
+
@click.argument("name")
|
|
602
|
+
def generate(type: str, name: str):
|
|
603
|
+
"""Generate new page, API endpoint, or component"""
|
|
604
|
+
click.echo(click.style(f"\n Generating {type}: {name}", fg="cyan", bold=True))
|
|
605
|
+
click.echo(click.style(" " + "=" * (20 + len(name) + len(type)) + "\n", fg="cyan"))
|
|
606
|
+
|
|
607
|
+
if type == "page":
|
|
608
|
+
_generate_page(name)
|
|
609
|
+
elif type == "api":
|
|
610
|
+
_generate_api(name)
|
|
611
|
+
elif type == "component":
|
|
612
|
+
_generate_component(name)
|
|
613
|
+
|
|
614
|
+
click.echo(click.style(f"\n {type.title()} '{name}' created successfully!\n", fg="green", bold=True))
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
@cli.group()
|
|
618
|
+
def plugin():
|
|
619
|
+
"""Plugin management commands"""
|
|
620
|
+
pass
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
@plugin.command()
|
|
624
|
+
def list():
|
|
625
|
+
"""List all available plugins"""
|
|
626
|
+
click.echo(click.style("\n 🔌 NextPy Plugins", fg="cyan", bold=True))
|
|
627
|
+
click.echo(click.style(" ================\n", fg="cyan"))
|
|
628
|
+
|
|
629
|
+
try:
|
|
630
|
+
from nextpy.plugins import plugin_manager
|
|
631
|
+
|
|
632
|
+
plugin_info = plugin_manager.get_plugin_info()
|
|
633
|
+
|
|
634
|
+
click.echo(click.style(f" 📊 Overview:", fg="blue", bold=True))
|
|
635
|
+
click.echo(f" Total plugins: {plugin_info['total_plugins']}")
|
|
636
|
+
click.echo(f" Enabled: {plugin_info['enabled_plugins']}")
|
|
637
|
+
click.echo(f" Disabled: {plugin_info['total_plugins'] - plugin_info['enabled_plugins']}")
|
|
638
|
+
|
|
639
|
+
click.echo()
|
|
640
|
+
click.echo(click.style(f" 📋 Plugin Details:", fg="green", bold=True))
|
|
641
|
+
|
|
642
|
+
for plugin in plugin_info['plugins']:
|
|
643
|
+
status = "✅" if plugin['enabled'] else "❌"
|
|
644
|
+
priority = plugin['priority']
|
|
645
|
+
click.echo(f" {status} {plugin['name']:<15} v{plugin['version']:<8} (Priority: {priority})")
|
|
646
|
+
|
|
647
|
+
if plugin['dependencies']:
|
|
648
|
+
click.echo(f" Dependencies: {', '.join(plugin['dependencies'])}")
|
|
649
|
+
|
|
650
|
+
click.echo()
|
|
651
|
+
|
|
652
|
+
except ImportError:
|
|
653
|
+
click.echo(click.style(" ❌ Plugin system not available", fg="red"))
|
|
654
|
+
click.echo(click.style(" 💡 Install with: pip install nextpy[plugins]", fg="yellow"))
|
|
655
|
+
except Exception as e:
|
|
656
|
+
click.echo(click.style(f" ❌ Error: {str(e)}", fg="red"))
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
@plugin.command()
|
|
660
|
+
@click.argument("name")
|
|
661
|
+
@click.option("--enable/--disable", default=True, help="Enable or disable the plugin")
|
|
662
|
+
def enable(name: str, enable: bool):
|
|
663
|
+
"""Enable or disable a plugin"""
|
|
664
|
+
action = "Enabling" if enable else "Disabling"
|
|
665
|
+
click.echo(click.style(f"\n {action} plugin: {name}", fg="cyan", bold=True))
|
|
666
|
+
click.echo(click.style(" " + "=" * (20 + len(name)) + "\n", fg="cyan"))
|
|
667
|
+
|
|
668
|
+
try:
|
|
669
|
+
from nextpy.plugins import plugin_manager
|
|
670
|
+
|
|
671
|
+
if enable:
|
|
672
|
+
plugin_manager.enable_plugin(name)
|
|
673
|
+
click.echo(click.style(f" ✅ Plugin '{name}' enabled successfully", fg="green"))
|
|
674
|
+
else:
|
|
675
|
+
plugin_manager.disable_plugin(name)
|
|
676
|
+
click.echo(click.style(f" ❌ Plugin '{name}' disabled", fg="yellow"))
|
|
677
|
+
|
|
678
|
+
click.echo()
|
|
679
|
+
|
|
680
|
+
except ImportError:
|
|
681
|
+
click.echo(click.style(" ❌ Plugin system not available", fg="red"))
|
|
682
|
+
except Exception as e:
|
|
683
|
+
click.echo(click.style(f" ❌ Error: {str(e)}", fg="red"))
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
@plugin.command()
|
|
687
|
+
@click.argument("name")
|
|
688
|
+
@click.option("--config", help="Plugin configuration as JSON string")
|
|
689
|
+
def configure(name: str, config: str):
|
|
690
|
+
"""Configure a plugin"""
|
|
691
|
+
click.echo(click.style(f"\n ⚙️ Configuring plugin: {name}", fg="cyan", bold=True))
|
|
692
|
+
click.echo(click.style(" " + "=" * (20 + len(name)) + "\n", fg="cyan"))
|
|
693
|
+
|
|
694
|
+
try:
|
|
695
|
+
from nextpy.plugins import plugin_manager
|
|
696
|
+
import json
|
|
697
|
+
|
|
698
|
+
if config:
|
|
699
|
+
try:
|
|
700
|
+
config_dict = json.loads(config)
|
|
701
|
+
except json.JSONDecodeError:
|
|
702
|
+
click.echo(click.style(" ❌ Invalid JSON configuration", fg="red"))
|
|
703
|
+
return
|
|
704
|
+
else:
|
|
705
|
+
config_dict = {}
|
|
706
|
+
|
|
707
|
+
plugin_manager.configure_plugin(name, config_dict)
|
|
708
|
+
click.echo(click.style(f" ✅ Plugin '{name}' configured successfully", fg="green"))
|
|
709
|
+
|
|
710
|
+
if config_dict:
|
|
711
|
+
click.echo(f" Configuration: {json.dumps(config_dict, indent=2)}")
|
|
712
|
+
|
|
713
|
+
click.echo()
|
|
714
|
+
|
|
715
|
+
except ImportError:
|
|
716
|
+
click.echo(click.style(" ❌ Plugin system not available", fg="red"))
|
|
717
|
+
except Exception as e:
|
|
718
|
+
click.echo(click.style(f" ❌ Error: {str(e)}", fg="red"))
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
@plugin.command()
|
|
722
|
+
@click.argument("file_path", type=click.Path(exists=True))
|
|
723
|
+
def load(file_path: str):
|
|
724
|
+
"""Load a plugin from file"""
|
|
725
|
+
click.echo(click.style(f"\n 📦 Loading plugin from: {file_path}", fg="cyan", bold=True))
|
|
726
|
+
click.echo(click.style(" " + "=" * (25 + len(file_path)) + "\n", fg="cyan"))
|
|
727
|
+
|
|
728
|
+
try:
|
|
729
|
+
from nextpy.plugins import plugin_manager
|
|
730
|
+
from pathlib import Path
|
|
731
|
+
|
|
732
|
+
plugin = plugin_manager.load_plugin_from_file(Path(file_path))
|
|
733
|
+
plugin_manager.register_plugin(plugin)
|
|
734
|
+
|
|
735
|
+
click.echo(click.style(f" ✅ Plugin '{plugin.name}' loaded successfully", fg="green"))
|
|
736
|
+
click.echo(f" Version: {plugin.version}")
|
|
737
|
+
click.echo(f" Priority: {plugin.priority.value}")
|
|
738
|
+
|
|
739
|
+
click.echo()
|
|
740
|
+
|
|
741
|
+
except ImportError:
|
|
742
|
+
click.echo(click.style(" ❌ Plugin system not available", fg="red"))
|
|
743
|
+
except Exception as e:
|
|
744
|
+
click.echo(click.style(f" ❌ Error: {str(e)}", fg="red"))
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
def _generate_page(name: str):
|
|
748
|
+
"""Generate a new page"""
|
|
749
|
+
page_path = Path(f"pages/{name}.py")
|
|
750
|
+
page_path.parent.mkdir(parents=True, exist_ok=True)
|
|
751
|
+
|
|
752
|
+
content = f'''"""Generated {name} page"""
|
|
753
|
+
|
|
754
|
+
def {name.title()}(props = None):
|
|
755
|
+
"""{name.title()} page component"""
|
|
756
|
+
props = props or {{}}
|
|
757
|
+
|
|
758
|
+
title = props.get("title", "{name.title()} Page")
|
|
759
|
+
|
|
760
|
+
return (
|
|
761
|
+
<div class="max-w-4xl mx-auto px-4 py-12">
|
|
762
|
+
<h1 class="mb-6 text-4xl font-bold text-gray-900">{{title}}</h1>
|
|
763
|
+
<p class="text-lg text-gray-600">
|
|
764
|
+
This is the {name} page generated by NextPy.
|
|
765
|
+
</p>
|
|
766
|
+
</div>
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
def getServerSideProps(context):
|
|
770
|
+
return {{
|
|
771
|
+
"props": {{
|
|
772
|
+
"title": "{name.title()} Page"
|
|
773
|
+
}}
|
|
774
|
+
}}
|
|
775
|
+
|
|
776
|
+
default = {name.title()}
|
|
777
|
+
'''
|
|
778
|
+
|
|
779
|
+
page_path.write_text(content)
|
|
780
|
+
click.echo(f" Created: {page_path}")
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def _generate_api(name: str):
|
|
784
|
+
"""Generate a new API endpoint"""
|
|
785
|
+
api_path = Path(f"pages/api/{name}.py")
|
|
786
|
+
api_path.parent.mkdir(parents=True, exist_ok=True)
|
|
787
|
+
|
|
788
|
+
content = f'''"""Generated {name} API endpoint"""
|
|
789
|
+
|
|
790
|
+
async def get(request):
|
|
791
|
+
"""GET /api/{name}"""
|
|
792
|
+
return {{
|
|
793
|
+
"message": "Hello from {name} API!",
|
|
794
|
+
"endpoint": "/api/{name}",
|
|
795
|
+
"method": "GET"
|
|
796
|
+
}}
|
|
797
|
+
|
|
798
|
+
async def post(request):
|
|
799
|
+
"""POST /api/{name}"""
|
|
800
|
+
body = await request.json()
|
|
801
|
+
return {{
|
|
802
|
+
"message": "POST request received",
|
|
803
|
+
"data": body,
|
|
804
|
+
"endpoint": "/api/{name}",
|
|
805
|
+
"method": "POST"
|
|
806
|
+
}}
|
|
807
|
+
'''
|
|
808
|
+
|
|
809
|
+
api_path.write_text(content)
|
|
810
|
+
click.echo(f" Created: {api_path}")
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
def _generate_component(name: str):
|
|
814
|
+
"""Generate a new component"""
|
|
815
|
+
component_path = Path(f"components/{name}.py")
|
|
816
|
+
component_path.parent.mkdir(parents=True, exist_ok=True)
|
|
817
|
+
|
|
818
|
+
content = f'''"""Generated {name} component"""
|
|
819
|
+
|
|
820
|
+
def {name.title()}(props = None):
|
|
821
|
+
"""{name.title()} component"""
|
|
822
|
+
props = props or {{}}
|
|
823
|
+
|
|
824
|
+
children = props.get("children", "")
|
|
825
|
+
className = props.get("className", "")
|
|
826
|
+
|
|
827
|
+
return (
|
|
828
|
+
<div class="{name.lower()}-component " + className>
|
|
829
|
+
{{children}}
|
|
830
|
+
</div>
|
|
831
|
+
)
|
|
832
|
+
|
|
833
|
+
default = {name.title()}
|
|
834
|
+
'''
|
|
835
|
+
|
|
836
|
+
component_path.write_text(content)
|
|
837
|
+
click.echo(f" Created: {component_path}")
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
def _ensure_project_structure():
|
|
841
|
+
"""Ensure the basic project structure exists"""
|
|
842
|
+
dirs = ["pages", "pages/api", "templates", "public", "public/css", "public/js"]
|
|
843
|
+
|
|
844
|
+
for dir_path in dirs:
|
|
845
|
+
Path(dir_path).mkdir(parents=True, exist_ok=True)
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
def _create_project_structure(project_dir: Path):
|
|
849
|
+
"""Create a new project structure with True JSX architecture"""
|
|
850
|
+
dirs = [
|
|
851
|
+
"pages",
|
|
852
|
+
"pages/api",
|
|
853
|
+
"components",
|
|
854
|
+
"components/ui",
|
|
855
|
+
"components/layout",
|
|
856
|
+
"pages/blog",
|
|
857
|
+
"pages/api/users",
|
|
858
|
+
"public",
|
|
859
|
+
"public/css",
|
|
860
|
+
"public/js",
|
|
861
|
+
"public/images",
|
|
862
|
+
"models",
|
|
863
|
+
]
|
|
864
|
+
|
|
865
|
+
for dir_path in dirs:
|
|
866
|
+
(project_dir / dir_path).mkdir(parents=True, exist_ok=True)
|
|
867
|
+
click.echo(f" Created: {dir_path}/")
|
|
868
|
+
|
|
869
|
+
# Create JSX-based homepage with .py extension
|
|
870
|
+
(project_dir / "pages" / "index.py").write_text('''"""Homepage with True JSX"""
|
|
871
|
+
|
|
872
|
+
def Home(props = None):
|
|
873
|
+
"""Homepage component"""
|
|
874
|
+
props = props or {}
|
|
875
|
+
|
|
876
|
+
title = props.get("title", "Welcome to NextPy")
|
|
877
|
+
message = props.get("message", "Your Python-powered web framework with True JSX")
|
|
878
|
+
|
|
879
|
+
return (
|
|
880
|
+
<div class="flex items-center justify-center min-h-screen bg-gradient-to-br from-blue-500 to-purple-600">
|
|
881
|
+
<div class="text-center text-white">
|
|
882
|
+
<h1 class="mb-4 text-5xl font-bold">{title}</h1>
|
|
883
|
+
<p class="text-xl">{message}</p>
|
|
884
|
+
<a href="https://github.com/nextpy/nextpy-framework" target="_blank"
|
|
885
|
+
class="inline-block px-6 py-3 mt-8 font-semibold text-blue-600 transition-all duration-300 transform bg-white rounded-lg shadow-lg hover:bg-gray-100 hover:text-blue-700 hover:scale-105">
|
|
886
|
+
Explore NextPy Framework
|
|
887
|
+
</a>
|
|
888
|
+
</div>
|
|
889
|
+
</div>
|
|
890
|
+
)
|
|
891
|
+
|
|
892
|
+
def getServerSideProps(context):
|
|
893
|
+
return {
|
|
894
|
+
"props": {
|
|
895
|
+
"title": "Welcome to NextPy",
|
|
896
|
+
"message": "Your Python-powered web framework with True JSX"
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
default = Home
|
|
901
|
+
''')
|
|
902
|
+
click.echo(" Created: pages/index.py")
|
|
903
|
+
|
|
904
|
+
# Create about page with True JSX
|
|
905
|
+
(project_dir / "pages" / "about.py").write_text('''"""About page with True JSX"""
|
|
906
|
+
|
|
907
|
+
def About(props = None):
|
|
908
|
+
"""About page component"""
|
|
909
|
+
props = props or {}
|
|
910
|
+
|
|
911
|
+
title = props.get("title", "About NextPy")
|
|
912
|
+
description = props.get("description", "Learn about the NextPy framework")
|
|
913
|
+
|
|
914
|
+
return (
|
|
915
|
+
<div class="max-w-4xl mx-auto px-4 py-12">
|
|
916
|
+
<h1 class="mb-6 text-4xl font-bold text-gray-900">{title}</h1>
|
|
917
|
+
<p class="text-lg text-gray-600 mb-4">{description}</p>
|
|
918
|
+
<p class="text-gray-700">
|
|
919
|
+
NextPy is a Python web framework inspired by Next.js, offering file-based routing,
|
|
920
|
+
server-side rendering, static site generation, and a modern True JSX component-based architecture.
|
|
921
|
+
</p>
|
|
922
|
+
</div>
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
def getServerSideProps(context):
|
|
926
|
+
return {
|
|
927
|
+
"props": {
|
|
928
|
+
"title": "About NextPy",
|
|
929
|
+
"description": "Learn about the NextPy framework"
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
default = About
|
|
934
|
+
''')
|
|
935
|
+
click.echo(" Created: pages/about.py")
|
|
936
|
+
|
|
937
|
+
# Create a reusable Button component
|
|
938
|
+
(project_dir / "components" / "ui" / "Button.py").write_text('''"""Button component"""
|
|
939
|
+
|
|
940
|
+
def Button(props = None):
|
|
941
|
+
"""Reusable Button component"""
|
|
942
|
+
props = props or {}
|
|
943
|
+
|
|
944
|
+
variant = props.get("variant", "default")
|
|
945
|
+
children = props.get("children", "Button")
|
|
946
|
+
className = props.get("className", "")
|
|
947
|
+
|
|
948
|
+
if variant == "primary":
|
|
949
|
+
variant_class = "bg-blue-600 text-white hover:bg-blue-700"
|
|
950
|
+
elif variant == "secondary":
|
|
951
|
+
variant_class = "bg-gray-200 text-gray-900 hover:bg-gray-300"
|
|
952
|
+
else:
|
|
953
|
+
variant_class = "bg-gray-600 text-white hover:bg-gray-700"
|
|
954
|
+
|
|
955
|
+
class_attr = f"px-4 py-2 rounded-lg font-medium transition-colors {variant_class} {className}"
|
|
956
|
+
|
|
957
|
+
return (
|
|
958
|
+
<button className={class_attr}
|
|
959
|
+
id={props.get("id")}
|
|
960
|
+
disabled={props.get("disabled", False)}>
|
|
961
|
+
{children}
|
|
962
|
+
</button>
|
|
963
|
+
)
|
|
964
|
+
|
|
965
|
+
default = Button
|
|
966
|
+
''')
|
|
967
|
+
click.echo(" Created: components/ui/Button.py")
|
|
968
|
+
|
|
969
|
+
# Create a Layout component
|
|
970
|
+
(project_dir / "components" / "layout" / "Layout.py").write_text('''"""Layout component"""
|
|
971
|
+
|
|
972
|
+
def Layout(props = None):
|
|
973
|
+
"""Layout component wrapper"""
|
|
974
|
+
props = props or {}
|
|
975
|
+
|
|
976
|
+
title = props.get("title", "NextPy App")
|
|
977
|
+
children = props.get("children", "")
|
|
978
|
+
|
|
979
|
+
return (
|
|
980
|
+
<div class="min-h-screen flex flex-col">
|
|
981
|
+
<header class="bg-white shadow-sm">
|
|
982
|
+
<div class="max-w-7xl mx-auto px-4 py-4">
|
|
983
|
+
<div class="flex items-center justify-between">
|
|
984
|
+
<h1 class="text-2xl font-bold text-gray-900">{title}</h1>
|
|
985
|
+
<nav class="flex space-x-4">
|
|
986
|
+
<a href="/" class="text-gray-600 hover:text-gray-900">Home</a>
|
|
987
|
+
<a href="/about" class="text-gray-600 hover:text-gray-900">About</a>
|
|
988
|
+
</nav>
|
|
989
|
+
</div>
|
|
990
|
+
</div>
|
|
991
|
+
</header>
|
|
992
|
+
<main class="flex-1">
|
|
993
|
+
{children}
|
|
994
|
+
</main>
|
|
995
|
+
<footer class="bg-gray-100 mt-auto">
|
|
996
|
+
<div class="max-w-7xl mx-auto px-4 py-6 text-center text-gray-600">
|
|
997
|
+
<p>© 2025 NextPy Framework. All rights reserved.</p>
|
|
998
|
+
</div>
|
|
999
|
+
</footer>
|
|
1000
|
+
</div>
|
|
1001
|
+
)
|
|
1002
|
+
|
|
1003
|
+
default = Layout
|
|
1004
|
+
''')
|
|
1005
|
+
click.echo(" Created: components/layout/Layout.py")
|
|
1006
|
+
|
|
1007
|
+
# Create VS Code configuration for JSX support
|
|
1008
|
+
(project_dir / ".vscode").mkdir(exist_ok=True)
|
|
1009
|
+
(project_dir / ".vscode" / "settings.json").write_text('''{
|
|
1010
|
+
"files.associations": {
|
|
1011
|
+
"*.py": "python",
|
|
1012
|
+
"*.py.jsx": "python",
|
|
1013
|
+
"*.jsx": "javascriptreact"
|
|
1014
|
+
},
|
|
1015
|
+
"emmet.includeLanguages": {
|
|
1016
|
+
"python": "html",
|
|
1017
|
+
"javascriptreact": "html",
|
|
1018
|
+
"typescriptreact": "html"
|
|
1019
|
+
},
|
|
1020
|
+
"emmet.triggerExpansionOnTab": true,
|
|
1021
|
+
"typescript.preferences.includePackageJsonAutoImports": "on",
|
|
1022
|
+
"editor.quickSuggestions": {
|
|
1023
|
+
"strings": true
|
|
1024
|
+
},
|
|
1025
|
+
"editor.suggestSelection": "first",
|
|
1026
|
+
"editor.wordBasedSuggestions": true,
|
|
1027
|
+
"editor.snippetSuggestions": "top",
|
|
1028
|
+
"editor.parameterHints": {
|
|
1029
|
+
"enabled": true
|
|
1030
|
+
},
|
|
1031
|
+
"editor.snippetSuggestions": "top",
|
|
1032
|
+
"html.autoClosingTags": true,
|
|
1033
|
+
"css.autoClosingTags": true,
|
|
1034
|
+
"javascript.autoClosingTags": true,
|
|
1035
|
+
"typescript.autoClosingTags": true,
|
|
1036
|
+
"editor.autoClosingBrackets": "always",
|
|
1037
|
+
"editor.autoClosingQuotes": "always",
|
|
1038
|
+
"editor.formatOnSave": true,
|
|
1039
|
+
"editor.codeActionsOnSave": {
|
|
1040
|
+
"source.fixAll.eslint": true,
|
|
1041
|
+
"source.organizeImports": true
|
|
1042
|
+
},
|
|
1043
|
+
"emmet.preferences": {
|
|
1044
|
+
"css.property.endWithSemicolon": true,
|
|
1045
|
+
"css.value.unit": "rem"
|
|
1046
|
+
},
|
|
1047
|
+
"files.exclude": {
|
|
1048
|
+
"**/__pycache__": true,
|
|
1049
|
+
"**/*.pyc": true,
|
|
1050
|
+
"**/node_modules": true,
|
|
1051
|
+
"**/out": true,
|
|
1052
|
+
"**/.next": true,
|
|
1053
|
+
"**/.pytest_cache": true,
|
|
1054
|
+
"**/.mypy_cache": true
|
|
1055
|
+
},
|
|
1056
|
+
"search.exclude": {
|
|
1057
|
+
"**/node_modules": true,
|
|
1058
|
+
"**/out": true,
|
|
1059
|
+
"**/.next": true,
|
|
1060
|
+
"**/__pycache__": true,
|
|
1061
|
+
"**/.pytest_cache": true,
|
|
1062
|
+
"**/.mypy_cache": true
|
|
1063
|
+
},
|
|
1064
|
+
"python.linting.enabled": true,
|
|
1065
|
+
"python.linting.pylintEnabled": false,
|
|
1066
|
+
"python.linting.flake8Enabled": false,
|
|
1067
|
+
"python.linting.pylintArgs": [
|
|
1068
|
+
"--disable=C0114,C0115,C0116,E1132,E1131,E1130"
|
|
1069
|
+
],
|
|
1070
|
+
"python.formatting.provider": "black",
|
|
1071
|
+
"[python]": {
|
|
1072
|
+
"editor.defaultFormatter": "ms-python.black-formatter",
|
|
1073
|
+
"editor.formatOnSave": true,
|
|
1074
|
+
"editor.rulers": [88],
|
|
1075
|
+
"editor.tabSize": 4,
|
|
1076
|
+
"editor.insertSpaces": true
|
|
1077
|
+
}
|
|
1078
|
+
}''')
|
|
1079
|
+
click.echo(" Created: .vscode/settings.json")
|
|
1080
|
+
|
|
1081
|
+
(project_dir / ".vscode" / "extensions.json").write_text('''{
|
|
1082
|
+
"recommendations": [
|
|
1083
|
+
"ms-python.python",
|
|
1084
|
+
"ms-python.vscode-pylance",
|
|
1085
|
+
"bradlc.vscode-tailwindcss",
|
|
1086
|
+
"esbenp.prettier-vscode",
|
|
1087
|
+
"ms-vscode.vscode-json",
|
|
1088
|
+
"formulahendry.auto-rename-tag",
|
|
1089
|
+
"christian-kohler.path-intellisense",
|
|
1090
|
+
"ms-vscode.vscode-html-css-class-completion",
|
|
1091
|
+
"ms-vscode.vscode-emmet",
|
|
1092
|
+
"ms-vscode.vscode-eslint",
|
|
1093
|
+
"dbaeumer.vscode-eslint",
|
|
1094
|
+
"ms-vscode.vscode-typescript-next",
|
|
1095
|
+
"ritwickdey.liveserver",
|
|
1096
|
+
"ms-vscode.vscode-jest",
|
|
1097
|
+
"esbenp.prettier-vscode",
|
|
1098
|
+
"streetsidesoftware.code-spell-checker",
|
|
1099
|
+
"gruntfuggly.todo-tree",
|
|
1100
|
+
"ms-vscode.vscode-git-graph",
|
|
1101
|
+
"eamodio.gitlens",
|
|
1102
|
+
"ms-vscode.vscode-docker",
|
|
1103
|
+
"ms-vscode.remote-explorer",
|
|
1104
|
+
"ms-vscode-remote.remote-containers",
|
|
1105
|
+
"ms-vscode.vscode-remote-wsl",
|
|
1106
|
+
"redhat.vscode-yaml",
|
|
1107
|
+
"ms-vscode.vscode-markdown",
|
|
1108
|
+
"yzhang.markdown-all-in-one",
|
|
1109
|
+
"shd101wyy.markdown-preview-enhanced",
|
|
1110
|
+
"ms-vscode.vscode-python",
|
|
1111
|
+
"kevinrose.vsc-python-indent",
|
|
1112
|
+
"ms-python.black-formatter",
|
|
1113
|
+
"ms-python.isort",
|
|
1114
|
+
"ms-python.flake8",
|
|
1115
|
+
"ms-python.mypy-type-checker"
|
|
1116
|
+
]
|
|
1117
|
+
}''')
|
|
1118
|
+
click.echo(" Created: .vscode/extensions.json")
|
|
1119
|
+
|
|
1120
|
+
# Create API example
|
|
1121
|
+
(project_dir / "pages" / "api" / "hello.py").write_text('''"""API example"""
|
|
1122
|
+
|
|
1123
|
+
def get(request):
|
|
1124
|
+
"""GET /api/hello"""
|
|
1125
|
+
return {"message": "Hello from NextPy API!", "status": "success"}
|
|
1126
|
+
|
|
1127
|
+
def post(request):
|
|
1128
|
+
"""POST /api/hello"""
|
|
1129
|
+
return {"message": "POST request received", "status": "success"}
|
|
1130
|
+
''')
|
|
1131
|
+
click.echo(" Created: pages/api/hello.py")
|
|
1132
|
+
|
|
1133
|
+
(project_dir / "requirements.txt").write_text('''fastapi>=0.100.0
|
|
1134
|
+
uvicorn>=0.23.0
|
|
1135
|
+
jinja2>=3.1.0
|
|
1136
|
+
pydantic>=2.0.0
|
|
1137
|
+
pydantic-settings>=2.0.0
|
|
1138
|
+
click>=8.1.0
|
|
1139
|
+
watchdog>=3.0.0
|
|
1140
|
+
python-multipart>=0.0.6
|
|
1141
|
+
pillow>=10.0.0
|
|
1142
|
+
aiofiles>=23.0.0
|
|
1143
|
+
httpx>=0.24.0
|
|
1144
|
+
sqlalchemy>=2.0.0
|
|
1145
|
+
python-dotenv>=1.0.0
|
|
1146
|
+
pyjwt>=2.8.0
|
|
1147
|
+
markdown>=3.0.0 # Added markdown for documentation rendering
|
|
1148
|
+
''')
|
|
1149
|
+
click.echo(" Created: requirements.txt")
|
|
1150
|
+
|
|
1151
|
+
|
|
1152
|
+
|
|
1153
|
+
|
|
1154
|
+
if __name__ == "__main__":
|
|
1155
|
+
cli()
|