nextpy-framework 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.
Files changed (49) hide show
  1. nextpy/__init__.py +50 -0
  2. nextpy/auth.py +94 -0
  3. nextpy/builder.py +123 -0
  4. nextpy/cli.py +490 -0
  5. nextpy/components/__init__.py +45 -0
  6. nextpy/components/feedback.py +210 -0
  7. nextpy/components/form.py +346 -0
  8. nextpy/components/head.py +167 -0
  9. nextpy/components/hooks_provider.py +64 -0
  10. nextpy/components/image.py +180 -0
  11. nextpy/components/layout.py +206 -0
  12. nextpy/components/link.py +132 -0
  13. nextpy/components/loader.py +65 -0
  14. nextpy/components/toast.py +101 -0
  15. nextpy/components/visual.py +185 -0
  16. nextpy/config.py +75 -0
  17. nextpy/core/__init__.py +21 -0
  18. nextpy/core/builder.py +237 -0
  19. nextpy/core/data_fetching.py +221 -0
  20. nextpy/core/renderer.py +252 -0
  21. nextpy/core/router.py +233 -0
  22. nextpy/core/sync.py +34 -0
  23. nextpy/db.py +121 -0
  24. nextpy/dev_server.py +69 -0
  25. nextpy/dev_tools.py +157 -0
  26. nextpy/errors.py +70 -0
  27. nextpy/hooks.py +348 -0
  28. nextpy/performance.py +78 -0
  29. nextpy/plugins.py +61 -0
  30. nextpy/py.typed +0 -0
  31. nextpy/server/__init__.py +6 -0
  32. nextpy/server/app.py +325 -0
  33. nextpy/server/debug.py +93 -0
  34. nextpy/server/middleware.py +88 -0
  35. nextpy/utils/__init__.py +0 -0
  36. nextpy/utils/cache.py +89 -0
  37. nextpy/utils/email.py +59 -0
  38. nextpy/utils/file_upload.py +65 -0
  39. nextpy/utils/logging.py +52 -0
  40. nextpy/utils/search.py +59 -0
  41. nextpy/utils/seo.py +85 -0
  42. nextpy/utils/validators.py +58 -0
  43. nextpy/websocket.py +76 -0
  44. nextpy_framework-1.0.0.dist-info/METADATA +343 -0
  45. nextpy_framework-1.0.0.dist-info/RECORD +49 -0
  46. nextpy_framework-1.0.0.dist-info/WHEEL +5 -0
  47. nextpy_framework-1.0.0.dist-info/entry_points.txt +2 -0
  48. nextpy_framework-1.0.0.dist-info/licenses/LICENSE +21 -0
  49. nextpy_framework-1.0.0.dist-info/top_level.txt +1 -0
nextpy/cli.py ADDED
@@ -0,0 +1,490 @@
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 asyncio
9
+ import signal
10
+ from pathlib import Path
11
+ from typing import Optional
12
+
13
+ import click
14
+ import uvicorn
15
+ from watchdog.observers import Observer
16
+ from watchdog.events import FileSystemEventHandler
17
+
18
+
19
+ class HotReloadHandler(FileSystemEventHandler):
20
+ """Handles file system changes for hot reload"""
21
+
22
+ def __init__(self, reload_callback):
23
+ self.reload_callback = reload_callback
24
+ self._debounce_timer = None
25
+
26
+ def on_modified(self, event):
27
+ if event.is_directory:
28
+ return
29
+
30
+ extensions = (".py", ".html", ".jinja2", ".css", ".js")
31
+ if isinstance(event.src_path, str) and any(event.src_path.endswith(ext) for ext in extensions):
32
+ self._trigger_reload()
33
+
34
+ def on_created(self, event):
35
+ if not event.is_directory:
36
+ self._trigger_reload()
37
+
38
+ def on_deleted(self, event):
39
+ if not event.is_directory:
40
+ self._trigger_reload()
41
+
42
+ def _trigger_reload(self):
43
+ if self.reload_callback:
44
+ self.reload_callback()
45
+
46
+
47
+ @click.group()
48
+ @click.version_option(version="1.0.0", prog_name="NextPy")
49
+ def cli():
50
+ """NextPy - A Python web framework inspired by Next.js"""
51
+ pass
52
+
53
+
54
+ @cli.command()
55
+ @click.option("--port", "-p", default=5000, help="Port to run the server on")
56
+ @click.option("--host", "-h", default="0.0.0.0", help="Host to bind to")
57
+ @click.option("--reload/--no-reload", default=True, help="Enable hot reload")
58
+ @click.option("--debug/--no-debug", default=True, help="Enable debug mode")
59
+ def dev(port: int, host: str, reload: bool, debug: bool):
60
+ """Start the development server with hot reload"""
61
+ click.echo(click.style("\n NextPy Development Server", fg="cyan", bold=True))
62
+ click.echo(click.style(" ========================\n", fg="cyan"))
63
+
64
+ _ensure_project_structure()
65
+
66
+ click.echo(f" - Mode: {'Development' if debug else 'Production'}")
67
+ click.echo(f" - Host: {host} (accessible at http://localhost:{port})")
68
+ click.echo(f" - Port: {port}")
69
+ click.echo(f" - Reload: {'Enabled' if reload else 'Disabled'}")
70
+ click.echo(f"\n ✨ Server ready at http://0.0.0.0:{port}")
71
+ click.echo(f" 🌐 Open http://localhost:{port} in your browser\n")
72
+
73
+ os.chdir(Path.cwd())
74
+
75
+ if reload:
76
+ uvicorn.run(
77
+ "main:app",
78
+ host=host,
79
+ port=port,
80
+ reload=True,
81
+ reload_dirs=["pages", "templates", "nextpy"],
82
+ log_level="info",
83
+ )
84
+ else:
85
+ uvicorn.run(
86
+ "main:app",
87
+ host=host,
88
+ port=port,
89
+ log_level="info",
90
+ )
91
+
92
+
93
+ @cli.command()
94
+ @click.option("--out", "-o", default="out", help="Output directory for static files")
95
+ @click.option("--clean/--no-clean", default=True, help="Clean output directory first")
96
+ def build(out: str, clean: bool):
97
+ """Build the project for production (SSG)"""
98
+ click.echo(click.style("\n NextPy Static Build", fg="green", bold=True))
99
+ click.echo(click.style(" ===================\n", fg="green"))
100
+
101
+ from nextpy.core.builder import Builder
102
+
103
+ builder = Builder(out_dir=out)
104
+
105
+ async def run_build():
106
+ manifest = await builder.build(clean=clean)
107
+ return manifest
108
+
109
+ manifest = asyncio.run(run_build())
110
+
111
+ pages_count = len(manifest.get("pages", {}))
112
+ click.echo(f"\n Built {pages_count} pages")
113
+ click.echo(f" Output: {out}/")
114
+ click.echo(click.style("\n Build complete!\n", fg="green", bold=True))
115
+
116
+
117
+ @cli.command()
118
+ @click.option("--port", "-p", default=5000, help="Port to run the server on")
119
+ @click.option("--host", "-h", default="0.0.0.0", help="Host to bind to")
120
+ def start(port: int, host: str):
121
+ """Start the production server"""
122
+ click.echo(click.style("\n NextPy Production Server", fg="green", bold=True))
123
+ click.echo(click.style(" ========================\n", fg="green"))
124
+
125
+ click.echo(f" - Mode: Production")
126
+ click.echo(f" - Host: {host} (accessible at http://localhost:{port})")
127
+ click.echo(f" - Port: {port}")
128
+ click.echo(f"\n ✨ Server ready at http://0.0.0.0:{port}")
129
+ click.echo(f" 🌐 Open http://localhost:{port} in your browser\n")
130
+
131
+ os.chdir(Path.cwd())
132
+
133
+ uvicorn.run(
134
+ "main:app",
135
+ host=host,
136
+ port=port,
137
+ workers=4,
138
+ log_level="warning",
139
+ )
140
+
141
+
142
+ @cli.command()
143
+ @click.argument("name")
144
+ def create(name: str):
145
+ """Create a new NextPy project"""
146
+ click.echo(click.style(f"\n Creating NextPy project: {name}", fg="cyan", bold=True))
147
+ click.echo(click.style(" " + "=" * (25 + len(name)) + "\n", fg="cyan"))
148
+
149
+ project_dir = Path(name)
150
+
151
+ if project_dir.exists():
152
+ click.echo(click.style(f" Error: Directory '{name}' already exists", fg="red"))
153
+ return
154
+
155
+ _create_project_structure(project_dir)
156
+
157
+ click.echo(f"\n Project created at: {project_dir.absolute()}")
158
+ click.echo(f"\n Next steps:")
159
+ click.echo(f" cd {name}")
160
+ click.echo(f" pip install -r requirements.txt")
161
+ click.echo(f" nextpy dev")
162
+ click.echo()
163
+
164
+
165
+ @cli.command()
166
+ def routes():
167
+ """Display all registered routes"""
168
+ click.echo(click.style("\n NextPy Routes", fg="cyan", bold=True))
169
+ click.echo(click.style(" =============\n", fg="cyan"))
170
+
171
+ from nextpy.core.router import Router
172
+
173
+ router = Router()
174
+ router.scan_pages()
175
+
176
+ click.echo(" Page Routes:")
177
+ for route in router.routes:
178
+ dynamic = " (dynamic)" if route.is_dynamic else ""
179
+ click.echo(f" {route.path}{dynamic} -> {route.file_path}")
180
+
181
+ click.echo("\n API Routes:")
182
+ for route in router.api_routes:
183
+ dynamic = " (dynamic)" if route.is_dynamic else ""
184
+ click.echo(f" {route.path}{dynamic} -> {route.file_path}")
185
+
186
+ click.echo()
187
+
188
+
189
+ def _ensure_project_structure():
190
+ """Ensure the basic project structure exists"""
191
+ dirs = ["pages", "pages/api", "templates", "public", "public/css", "public/js"]
192
+
193
+ for dir_path in dirs:
194
+ Path(dir_path).mkdir(parents=True, exist_ok=True)
195
+
196
+
197
+ def _create_project_structure(project_dir: Path):
198
+ """Create a new project structure"""
199
+ dirs = [
200
+ "pages",
201
+ "pages/api",
202
+ "pages/blog",
203
+ "pages/api/users",
204
+ "templates",
205
+ "templates/components",
206
+ "public",
207
+ "public/css",
208
+ "public/js",
209
+ "public/images",
210
+ "models",
211
+ ]
212
+
213
+ for dir_path in dirs:
214
+ (project_dir / dir_path).mkdir(parents=True, exist_ok=True)
215
+ click.echo(f" Created: {dir_path}/")
216
+
217
+ (project_dir / "pages" / "index.py").write_text('''"""Homepage"""
218
+
219
+ def get_template():
220
+ return "index.html"
221
+
222
+ async def get_server_side_props(context):
223
+ return {
224
+ "props": {
225
+ "title": "Welcome to NextPy",
226
+ "message": "Your Python-powered web framework"
227
+ }
228
+ }
229
+ ''')
230
+ click.echo(" Created: pages/index.py")
231
+
232
+ (project_dir / "templates" / "_base.html").write_text('''<!DOCTYPE html>
233
+ <html lang="en">
234
+ <head>
235
+ <meta charset="UTF-8">
236
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
237
+ <title>{% block title %}NextPy App{% endblock %}</title>
238
+ <script src="https://unpkg.com/htmx.org@1.9.10"></script>
239
+ <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
240
+ {% block head %}{% endblock %}
241
+ </head>
242
+ <body>
243
+ <div id="main-content">
244
+ {% block content %}{{ content | safe }}{% endblock %}
245
+ </div>
246
+ {% block scripts %}{% endblock %}
247
+ </body>
248
+ </html>
249
+ ''')
250
+ click.echo(" Created: templates/_base.html")
251
+
252
+ (project_dir / "templates" / "index.html").write_text('''{% extends "_base.html" %}
253
+
254
+ {% block title %}{{ title }}{% endblock %}
255
+
256
+ {% block content %}
257
+ <div class="min-h-screen bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center">
258
+ <div class="text-center text-white">
259
+ <h1 class="text-5xl font-bold mb-4">{{ title }}</h1>
260
+ <p class="text-xl">{{ message }}</p>
261
+ </div>
262
+ </div>
263
+ {% endblock %}
264
+ ''')
265
+ click.echo(" Created: templates/index.html")
266
+
267
+ (project_dir / "main.py").write_text('''"""NextPy Application Entry Point"""
268
+
269
+ from nextpy import create_app
270
+
271
+ app = create_app(debug=True)
272
+
273
+ if __name__ == "__main__":
274
+ import uvicorn
275
+ uvicorn.run(app, host="0.0.0.0", port=5000)
276
+ ''')
277
+ click.echo(" Created: main.py")
278
+
279
+ (project_dir / "requirements.txt").write_text('''fastapi>=0.100.0
280
+ uvicorn>=0.23.0
281
+ jinja2>=3.1.0
282
+ pydantic>=2.0.0
283
+ pydantic-settings>=2.0.0
284
+ click>=8.1.0
285
+ watchdog>=3.0.0
286
+ python-multipart>=0.0.6
287
+ pillow>=10.0.0
288
+ aiofiles>=23.0.0
289
+ httpx>=0.24.0
290
+ sqlalchemy>=2.0.0
291
+ python-dotenv>=1.0.0
292
+ pyjwt>=2.8.0
293
+ ''')
294
+ click.echo(" Created: requirements.txt")
295
+
296
+ (project_dir / "pages" / "about.py").write_text('''"""About page"""
297
+
298
+ def get_template():
299
+ return "about.html"
300
+
301
+ async def get_server_side_props(context):
302
+ return {
303
+ "props": {
304
+ "title": "About NextPy",
305
+ "description": "Learn about the NextPy framework"
306
+ }
307
+ }
308
+ ''')
309
+ click.echo(" Created: pages/about.py")
310
+
311
+ (project_dir / "pages" / "blog" / "index.py").write_text('''"""Blog listing"""
312
+
313
+ def get_template():
314
+ return "blog/index.html"
315
+
316
+ async def get_server_side_props(context):
317
+ posts = [
318
+ {"slug": "hello", "title": "Hello World"},
319
+ {"slug": "welcome", "title": "Welcome to NextPy"}
320
+ ]
321
+ return {"props": {"posts": posts}}
322
+ ''')
323
+ click.echo(" Created: pages/blog/index.py")
324
+
325
+ (project_dir / "pages" / "blog" / "[slug].py").write_text('''"""Dynamic blog post"""
326
+
327
+ def get_template():
328
+ return "blog/post.html"
329
+
330
+ async def get_server_side_props(context):
331
+ slug = context["params"]["slug"]
332
+ return {
333
+ "props": {
334
+ "slug": slug,
335
+ "title": f"Post: {slug}",
336
+ "content": "Blog post content here"
337
+ }
338
+ }
339
+ ''')
340
+ click.echo(" Created: pages/blog/[slug].py")
341
+
342
+ (project_dir / "pages" / "api" / "posts.py").write_text('''"""API route for posts"""
343
+
344
+ async def get(request):
345
+ """GET /api/posts"""
346
+ return {"posts": [{"id": 1, "title": "Post 1"}]}
347
+
348
+ async def post(request):
349
+ """POST /api/posts"""
350
+ body = await request.json()
351
+ return {"id": 2, "title": body.get("title")}, 201
352
+ ''')
353
+ click.echo(" Created: pages/api/posts.py")
354
+
355
+ (project_dir / "pages" / "api" / "users" / "[id].py").write_text('''"""Dynamic API route"""
356
+
357
+ async def get(request):
358
+ """GET /api/users/:id"""
359
+ user_id = request.path_params["id"]
360
+ return {"id": user_id, "name": f"User {user_id}"}
361
+
362
+ async def put(request):
363
+ """PUT /api/users/:id"""
364
+ user_id = request.path_params["id"]
365
+ body = await request.json()
366
+ return {"id": user_id, "updated": True}
367
+
368
+ async def delete(request):
369
+ """DELETE /api/users/:id"""
370
+ user_id = request.path_params["id"]
371
+ return {"deleted": True}, 204
372
+ ''')
373
+ click.echo(" Created: pages/api/users/[id].py")
374
+
375
+ (project_dir / "templates" / "components" / "button.html").write_text('''{% macro button(text, href="", onclick="", variant="primary", disabled=false) %}
376
+ <a href="{{ href }}" class="px-4 py-2 rounded-lg font-semibold transition" style="background: {% if variant == 'primary' %}#3b82f6{% else %}#6b7280{% endif %}; color: white;">
377
+ {{ text }}
378
+ </a>
379
+ {% endmacro %}
380
+ ''')
381
+ click.echo(" Created: templates/components/button.html")
382
+
383
+ (project_dir / "templates" / "components" / "card.html").write_text('''{% macro card(title="", content="", footer="") %}
384
+ <div class="bg-white rounded-lg shadow-md p-6 mb-4">
385
+ {% if title %}<h3 class="font-bold text-lg mb-2">{{ title }}</h3>{% endif %}
386
+ {% if content %}<p class="text-gray-600">{{ content }}</p>{% endif %}
387
+ {% if footer %}<div class="mt-4 text-sm text-gray-500">{{ footer }}</div>{% endif %}
388
+ </div>
389
+ {% endmacro %}
390
+ ''')
391
+ click.echo(" Created: templates/components/card.html")
392
+
393
+ (project_dir / "templates" / "components" / "modal.html").write_text('''{% macro modal(id="", title="", content="") %}
394
+ <div id="{{ id }}" style="display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 1000;">
395
+ <div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 24px; border-radius: 8px; min-width: 400px;">
396
+ {% if title %}<h2 class="text-2xl font-bold mb-4">{{ title }}</h2>{% endif %}
397
+ {% if content %}<p class="text-gray-600 mb-6">{{ content }}</p>{% endif %}
398
+ <button onclick="document.getElementById('{{ id }}').style.display = 'none';" class="px-4 py-2 bg-blue-600 text-white rounded">Close</button>
399
+ </div>
400
+ </div>
401
+ {% endmacro %}
402
+ ''')
403
+ click.echo(" Created: templates/components/modal.html")
404
+
405
+ (project_dir / "models" / "user.py").write_text('''"""User database model"""
406
+
407
+ from nextpy.db import Base
408
+ from sqlalchemy import Column, Integer, String, DateTime
409
+ from datetime import datetime
410
+
411
+ class User(Base):
412
+ __tablename__ = "users"
413
+
414
+ id = Column(Integer, primary_key=True)
415
+ name = Column(String(255))
416
+ email = Column(String(255), unique=True)
417
+ created_at = Column(DateTime, default=datetime.utcnow)
418
+
419
+ def __repr__(self):
420
+ return f"<User {self.name}>"
421
+ ''')
422
+ click.echo(" Created: models/user.py")
423
+
424
+ (project_dir / "nextpy.config.py").write_text('''"""NextPy Configuration"""
425
+
426
+ from pydantic_settings import BaseSettings
427
+
428
+ class Settings(BaseSettings):
429
+ app_name: str = "MyNextPyApp"
430
+ debug: bool = True
431
+ database_url: str = "sqlite:///app.db"
432
+ secret_key: str = "your-secret-key-change-in-production"
433
+
434
+ class Config:
435
+ env_file = ".env"
436
+
437
+ settings = Settings()
438
+ ''')
439
+ click.echo(" Created: nextpy.config.py")
440
+
441
+ (project_dir / ".env").write_text('''DATABASE_URL=sqlite:///app.db
442
+ DEBUG=True
443
+ SECRET_KEY=your-secret-key-change-in-production
444
+ SMTP_HOST=smtp.gmail.com
445
+ SMTP_PORT=587
446
+ SMTP_USER=your-email@gmail.com
447
+ SMTP_PASSWORD=your-password
448
+ ''')
449
+ click.echo(" Created: .env")
450
+
451
+ (project_dir / ".gitignore").write_text('''# Python
452
+ __pycache__/
453
+ *.py[cod]
454
+ *$py.class
455
+ *.so
456
+ .Python
457
+ env/
458
+ venv/
459
+ *.egg-info/
460
+ dist/
461
+ build/
462
+
463
+ # Database
464
+ *.db
465
+ *.sqlite3
466
+
467
+ # Environment
468
+ .env
469
+ .env.local
470
+
471
+ # IDE
472
+ .vscode/
473
+ .idea/
474
+ *.swp
475
+ *.swo
476
+ *~
477
+
478
+ # OS
479
+ .DS_Store
480
+ Thumbs.db
481
+
482
+ # Build output
483
+ out/
484
+ .nextpy/
485
+ ''')
486
+ click.echo(" Created: .gitignore")
487
+
488
+
489
+ if __name__ == "__main__":
490
+ cli()
@@ -0,0 +1,45 @@
1
+ """NextPy Components - Reusable UI components"""
2
+
3
+ from nextpy.components.head import Head
4
+ from nextpy.components.link import Link
5
+ from nextpy.components.image import Image
6
+ from nextpy.components import form, feedback, layout, loader, toast, visual, hooks_provider
7
+
8
+ # Form components
9
+ from nextpy.components.form import (
10
+ Input, TextArea, Select, Checkbox, Radio, RadioGroup, Form, FormGroup, FileInput,
11
+ NumberInput, DateInput, TimeInput, PasswordInput, RangeInput, ColorInput
12
+ )
13
+
14
+ # Feedback components
15
+ from nextpy.components.feedback import Alert, Badge, Progress
16
+
17
+ # Layout components
18
+ from nextpy.components.layout import Grid, Flex, Stack, Container, Sidebar
19
+
20
+ # Loader components
21
+ from nextpy.components.loader import spinner, skeleton, progress_bar, loading_screen
22
+
23
+ # Toast components
24
+ from nextpy.components.toast import Toast, get_toast, toast_html
25
+
26
+ # Visual components
27
+ from nextpy.components.visual import (
28
+ Tabs, Accordion, Dropdown, Modal, Card, Breadcrumb, Pagination
29
+ )
30
+
31
+ # Hooks provider
32
+ from nextpy.components.hooks_provider import HooksProvider, HooksContext, with_hooks
33
+
34
+ __all__ = [
35
+ "Head", "Link", "Image",
36
+ "Input", "TextArea", "Select", "Checkbox", "Radio", "RadioGroup", "Form", "FormGroup", "FileInput",
37
+ "NumberInput", "DateInput", "TimeInput", "PasswordInput", "RangeInput", "ColorInput",
38
+ "Alert", "Badge", "Progress",
39
+ "Grid", "Flex", "Stack", "Container", "Sidebar",
40
+ "spinner", "skeleton", "progress_bar", "loading_screen",
41
+ "Toast", "get_toast", "toast_html",
42
+ "Tabs", "Accordion", "Dropdown", "Modal", "Card", "Breadcrumb", "Pagination",
43
+ "HooksProvider", "HooksContext", "with_hooks",
44
+ "form", "feedback", "layout", "loader", "toast", "visual", "hooks_provider"
45
+ ]