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.
- nextpy/__init__.py +50 -0
- nextpy/auth.py +94 -0
- nextpy/builder.py +123 -0
- nextpy/cli.py +490 -0
- nextpy/components/__init__.py +45 -0
- nextpy/components/feedback.py +210 -0
- nextpy/components/form.py +346 -0
- nextpy/components/head.py +167 -0
- nextpy/components/hooks_provider.py +64 -0
- nextpy/components/image.py +180 -0
- nextpy/components/layout.py +206 -0
- nextpy/components/link.py +132 -0
- nextpy/components/loader.py +65 -0
- nextpy/components/toast.py +101 -0
- nextpy/components/visual.py +185 -0
- nextpy/config.py +75 -0
- nextpy/core/__init__.py +21 -0
- nextpy/core/builder.py +237 -0
- nextpy/core/data_fetching.py +221 -0
- nextpy/core/renderer.py +252 -0
- nextpy/core/router.py +233 -0
- nextpy/core/sync.py +34 -0
- nextpy/db.py +121 -0
- nextpy/dev_server.py +69 -0
- nextpy/dev_tools.py +157 -0
- nextpy/errors.py +70 -0
- nextpy/hooks.py +348 -0
- nextpy/performance.py +78 -0
- nextpy/plugins.py +61 -0
- nextpy/py.typed +0 -0
- nextpy/server/__init__.py +6 -0
- nextpy/server/app.py +325 -0
- nextpy/server/debug.py +93 -0
- nextpy/server/middleware.py +88 -0
- nextpy/utils/__init__.py +0 -0
- nextpy/utils/cache.py +89 -0
- nextpy/utils/email.py +59 -0
- nextpy/utils/file_upload.py +65 -0
- nextpy/utils/logging.py +52 -0
- nextpy/utils/search.py +59 -0
- nextpy/utils/seo.py +85 -0
- nextpy/utils/validators.py +58 -0
- nextpy/websocket.py +76 -0
- nextpy_framework-1.0.0.dist-info/METADATA +343 -0
- nextpy_framework-1.0.0.dist-info/RECORD +49 -0
- nextpy_framework-1.0.0.dist-info/WHEEL +5 -0
- nextpy_framework-1.0.0.dist-info/entry_points.txt +2 -0
- nextpy_framework-1.0.0.dist-info/licenses/LICENSE +21 -0
- nextpy_framework-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NextPy File Upload Utilities
|
|
3
|
+
Handle file uploads with validation and storage
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
from fastapi import UploadFile
|
|
10
|
+
import shutil
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
UPLOAD_DIR = Path("uploads")
|
|
14
|
+
ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".pdf", ".txt", ".doc", ".docx"}
|
|
15
|
+
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def upload_file(
|
|
19
|
+
file: UploadFile,
|
|
20
|
+
directory: str = "general",
|
|
21
|
+
max_size: int = MAX_FILE_SIZE,
|
|
22
|
+
) -> Optional[str]:
|
|
23
|
+
"""Upload file with validation"""
|
|
24
|
+
|
|
25
|
+
# Validate extension
|
|
26
|
+
ext = Path(file.filename).suffix.lower()
|
|
27
|
+
if ext not in ALLOWED_EXTENSIONS:
|
|
28
|
+
raise ValueError(f"File type {ext} not allowed")
|
|
29
|
+
|
|
30
|
+
# Create upload directory
|
|
31
|
+
upload_path = UPLOAD_DIR / directory
|
|
32
|
+
upload_path.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
|
|
34
|
+
# Save file
|
|
35
|
+
file_path = upload_path / file.filename
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
with open(file_path, "wb") as f:
|
|
39
|
+
content = await file.read()
|
|
40
|
+
if len(content) > max_size:
|
|
41
|
+
raise ValueError(f"File too large (max {max_size} bytes)")
|
|
42
|
+
f.write(content)
|
|
43
|
+
|
|
44
|
+
return str(file_path)
|
|
45
|
+
except Exception as e:
|
|
46
|
+
if file_path.exists():
|
|
47
|
+
file_path.unlink()
|
|
48
|
+
raise e
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def delete_file(file_path: str) -> bool:
|
|
52
|
+
"""Delete uploaded file"""
|
|
53
|
+
try:
|
|
54
|
+
path = Path(file_path)
|
|
55
|
+
if path.exists():
|
|
56
|
+
path.unlink()
|
|
57
|
+
return True
|
|
58
|
+
return False
|
|
59
|
+
except Exception:
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_upload_url(file_path: str) -> str:
|
|
64
|
+
"""Get public URL for uploaded file"""
|
|
65
|
+
return f"/uploads/{file_path}"
|
nextpy/utils/logging.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NextPy Logging Utilities
|
|
3
|
+
Structured logging for debugging and monitoring
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Logger:
|
|
12
|
+
"""Simple logging wrapper"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, name: str):
|
|
15
|
+
self.logger = logging.getLogger(name)
|
|
16
|
+
|
|
17
|
+
if not self.logger.handlers:
|
|
18
|
+
handler = logging.StreamHandler()
|
|
19
|
+
formatter = logging.Formatter(
|
|
20
|
+
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
21
|
+
)
|
|
22
|
+
handler.setFormatter(formatter)
|
|
23
|
+
self.logger.addHandler(handler)
|
|
24
|
+
self.logger.setLevel(logging.INFO)
|
|
25
|
+
|
|
26
|
+
def info(self, message: str, **kwargs):
|
|
27
|
+
"""Log info message"""
|
|
28
|
+
self.logger.info(f"{message} {self._format_kwargs(kwargs)}")
|
|
29
|
+
|
|
30
|
+
def error(self, message: str, **kwargs):
|
|
31
|
+
"""Log error message"""
|
|
32
|
+
self.logger.error(f"{message} {self._format_kwargs(kwargs)}")
|
|
33
|
+
|
|
34
|
+
def debug(self, message: str, **kwargs):
|
|
35
|
+
"""Log debug message"""
|
|
36
|
+
self.logger.debug(f"{message} {self._format_kwargs(kwargs)}")
|
|
37
|
+
|
|
38
|
+
def warning(self, message: str, **kwargs):
|
|
39
|
+
"""Log warning message"""
|
|
40
|
+
self.logger.warning(f"{message} {self._format_kwargs(kwargs)}")
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def _format_kwargs(kwargs: dict) -> str:
|
|
44
|
+
"""Format kwargs for logging"""
|
|
45
|
+
if not kwargs:
|
|
46
|
+
return ""
|
|
47
|
+
return " | " + " ".join(f"{k}={v}" for k, v in kwargs.items())
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_logger(name: str) -> Logger:
|
|
51
|
+
"""Get logger instance"""
|
|
52
|
+
return Logger(name)
|
nextpy/utils/search.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NextPy Search Utilities
|
|
3
|
+
Simple full-text search functionality
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import List, Dict, Any
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def simple_search(query: str, items: List[Dict[str, Any]], fields: List[str]) -> List[Dict[str, Any]]:
|
|
11
|
+
"""Simple full-text search across multiple fields"""
|
|
12
|
+
query_lower = query.lower()
|
|
13
|
+
results = []
|
|
14
|
+
|
|
15
|
+
for item in items:
|
|
16
|
+
for field in fields:
|
|
17
|
+
if field in item:
|
|
18
|
+
value = str(item.get(field, "")).lower()
|
|
19
|
+
if query_lower in value:
|
|
20
|
+
results.append(item)
|
|
21
|
+
break
|
|
22
|
+
|
|
23
|
+
return results
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def fuzzy_search(query: str, items: List[Dict[str, Any]], field: str) -> List[Dict[str, Any]]:
|
|
27
|
+
"""Fuzzy search with scoring"""
|
|
28
|
+
def fuzzy_match(pattern: str, text: str) -> int:
|
|
29
|
+
"""Calculate fuzzy match score"""
|
|
30
|
+
pattern = pattern.lower()
|
|
31
|
+
text = text.lower()
|
|
32
|
+
score = 0
|
|
33
|
+
j = 0
|
|
34
|
+
|
|
35
|
+
for i, char in enumerate(pattern):
|
|
36
|
+
pos = text.find(char, j)
|
|
37
|
+
if pos == -1:
|
|
38
|
+
return 0
|
|
39
|
+
score += 1 / (pos - j + 1)
|
|
40
|
+
j = pos + 1
|
|
41
|
+
|
|
42
|
+
return score
|
|
43
|
+
|
|
44
|
+
results = []
|
|
45
|
+
for item in items:
|
|
46
|
+
text = str(item.get(field, ""))
|
|
47
|
+
score = fuzzy_match(query, text)
|
|
48
|
+
if score > 0:
|
|
49
|
+
results.append((item, score))
|
|
50
|
+
|
|
51
|
+
# Sort by score descending
|
|
52
|
+
results.sort(key=lambda x: x[1], reverse=True)
|
|
53
|
+
return [item for item, score in results]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def search_highlight(query: str, text: str) -> str:
|
|
57
|
+
"""Highlight search terms in text"""
|
|
58
|
+
pattern = re.compile(re.escape(query), re.IGNORECASE)
|
|
59
|
+
return pattern.sub(f'<mark>{query}</mark>', text)
|
nextpy/utils/seo.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NextPy SEO Utilities
|
|
3
|
+
Helpers for SEO optimization, structured data, sitemaps
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Dict, List, Any, Optional
|
|
7
|
+
import json
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def create_article_schema(
|
|
12
|
+
title: str,
|
|
13
|
+
description: str,
|
|
14
|
+
image: str,
|
|
15
|
+
author: str,
|
|
16
|
+
date_published: datetime,
|
|
17
|
+
date_modified: Optional[datetime] = None,
|
|
18
|
+
url: str = ""
|
|
19
|
+
) -> Dict[str, Any]:
|
|
20
|
+
"""Create structured data for article"""
|
|
21
|
+
return {
|
|
22
|
+
"@context": "https://schema.org",
|
|
23
|
+
"@type": "Article",
|
|
24
|
+
"headline": title,
|
|
25
|
+
"description": description,
|
|
26
|
+
"image": image,
|
|
27
|
+
"author": {
|
|
28
|
+
"@type": "Person",
|
|
29
|
+
"name": author
|
|
30
|
+
},
|
|
31
|
+
"datePublished": date_published.isoformat(),
|
|
32
|
+
"dateModified": (date_modified or date_published).isoformat(),
|
|
33
|
+
"url": url
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def create_organization_schema(
|
|
38
|
+
name: str,
|
|
39
|
+
url: str,
|
|
40
|
+
logo: str,
|
|
41
|
+
contact_url: Optional[str] = None
|
|
42
|
+
) -> Dict[str, Any]:
|
|
43
|
+
"""Create structured data for organization"""
|
|
44
|
+
return {
|
|
45
|
+
"@context": "https://schema.org",
|
|
46
|
+
"@type": "Organization",
|
|
47
|
+
"name": name,
|
|
48
|
+
"url": url,
|
|
49
|
+
"logo": logo,
|
|
50
|
+
"contactPoint": {
|
|
51
|
+
"@type": "ContactPoint",
|
|
52
|
+
"contactType": "Customer Service",
|
|
53
|
+
"url": contact_url
|
|
54
|
+
} if contact_url else None
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def generate_sitemap(routes: List[Dict[str, Any]]) -> str:
|
|
59
|
+
"""Generate XML sitemap from routes"""
|
|
60
|
+
sitemap = '<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
61
|
+
sitemap += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
|
|
62
|
+
|
|
63
|
+
for route in routes:
|
|
64
|
+
sitemap += " <url>\n"
|
|
65
|
+
sitemap += f" <loc>{route.get('url', '')}</loc>\n"
|
|
66
|
+
if route.get('lastmod'):
|
|
67
|
+
sitemap += f" <lastmod>{route['lastmod']}</lastmod>\n"
|
|
68
|
+
if route.get('priority'):
|
|
69
|
+
sitemap += f" <priority>{route['priority']}</priority>\n"
|
|
70
|
+
sitemap += " </url>\n"
|
|
71
|
+
|
|
72
|
+
sitemap += "</urlset>"
|
|
73
|
+
return sitemap
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def generate_robots_txt(sitemap_url: str) -> str:
|
|
77
|
+
"""Generate robots.txt content"""
|
|
78
|
+
return f"""User-agent: *
|
|
79
|
+
Allow: /
|
|
80
|
+
Disallow: /admin/
|
|
81
|
+
Disallow: /api/
|
|
82
|
+
Disallow: /_nextpy/
|
|
83
|
+
|
|
84
|
+
Sitemap: {sitemap_url}
|
|
85
|
+
"""
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NextPy Validation Utilities
|
|
3
|
+
Type-safe validation helpers using Pydantic
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, EmailStr, HttpUrl
|
|
7
|
+
from typing import Optional, List, Dict, Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ContactForm(BaseModel):
|
|
11
|
+
"""Contact form validation"""
|
|
12
|
+
name: str
|
|
13
|
+
email: EmailStr
|
|
14
|
+
message: str
|
|
15
|
+
subject: Optional[str] = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BlogPost(BaseModel):
|
|
19
|
+
"""Blog post validation"""
|
|
20
|
+
title: str
|
|
21
|
+
slug: str
|
|
22
|
+
content: str
|
|
23
|
+
excerpt: Optional[str] = None
|
|
24
|
+
featured_image: Optional[HttpUrl] = None
|
|
25
|
+
author: str
|
|
26
|
+
tags: List[str] = []
|
|
27
|
+
published: bool = False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class User(BaseModel):
|
|
31
|
+
"""User profile validation"""
|
|
32
|
+
email: EmailStr
|
|
33
|
+
username: str
|
|
34
|
+
full_name: str
|
|
35
|
+
bio: Optional[str] = None
|
|
36
|
+
avatar_url: Optional[HttpUrl] = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class LoginForm(BaseModel):
|
|
40
|
+
"""Login form validation"""
|
|
41
|
+
email: EmailStr
|
|
42
|
+
password: str
|
|
43
|
+
remember_me: bool = False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SignupForm(BaseModel):
|
|
47
|
+
"""Signup form validation"""
|
|
48
|
+
email: EmailStr
|
|
49
|
+
username: str
|
|
50
|
+
password: str
|
|
51
|
+
confirm_password: str
|
|
52
|
+
agree_to_terms: bool = False
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def validate_slug(slug: str) -> bool:
|
|
56
|
+
"""Validate URL-safe slug format"""
|
|
57
|
+
import re
|
|
58
|
+
return bool(re.match(r"^[a-z0-9]+(?:-[a-z0-9]+)*$", slug))
|
nextpy/websocket.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NextPy WebSocket Support
|
|
3
|
+
Real-time communication with clients
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from fastapi import WebSocket
|
|
7
|
+
from typing import Set, Dict, Callable, Any
|
|
8
|
+
import json
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ConnectionManager:
|
|
12
|
+
"""Manage WebSocket connections"""
|
|
13
|
+
|
|
14
|
+
def __init__(self):
|
|
15
|
+
self.active_connections: Set[WebSocket] = set()
|
|
16
|
+
self.subscribers: Dict[str, Set[WebSocket]] = {}
|
|
17
|
+
|
|
18
|
+
async def connect(self, websocket: WebSocket):
|
|
19
|
+
"""Accept new connection"""
|
|
20
|
+
await websocket.accept()
|
|
21
|
+
self.active_connections.add(websocket)
|
|
22
|
+
|
|
23
|
+
async def disconnect(self, websocket: WebSocket):
|
|
24
|
+
"""Remove connection"""
|
|
25
|
+
self.active_connections.discard(websocket)
|
|
26
|
+
for subs in self.subscribers.values():
|
|
27
|
+
subs.discard(websocket)
|
|
28
|
+
|
|
29
|
+
async def broadcast(self, message: Dict[str, Any]):
|
|
30
|
+
"""Send message to all connections"""
|
|
31
|
+
for connection in self.active_connections:
|
|
32
|
+
try:
|
|
33
|
+
await connection.send_json(message)
|
|
34
|
+
except:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
async def subscribe(self, websocket: WebSocket, channel: str):
|
|
38
|
+
"""Subscribe connection to channel"""
|
|
39
|
+
if channel not in self.subscribers:
|
|
40
|
+
self.subscribers[channel] = set()
|
|
41
|
+
self.subscribers[channel].add(websocket)
|
|
42
|
+
|
|
43
|
+
async def publish(self, channel: str, message: Dict[str, Any]):
|
|
44
|
+
"""Publish message to channel subscribers"""
|
|
45
|
+
if channel in self.subscribers:
|
|
46
|
+
for connection in self.subscribers[channel]:
|
|
47
|
+
try:
|
|
48
|
+
await connection.send_json(message)
|
|
49
|
+
except:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# Global manager instance
|
|
54
|
+
manager = ConnectionManager()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def handle_websocket(websocket: WebSocket):
|
|
58
|
+
"""Handle WebSocket connection"""
|
|
59
|
+
await manager.connect(websocket)
|
|
60
|
+
try:
|
|
61
|
+
while True:
|
|
62
|
+
data = await websocket.receive_text()
|
|
63
|
+
message = json.loads(data)
|
|
64
|
+
|
|
65
|
+
# Route message to appropriate handler
|
|
66
|
+
msg_type = message.get("type")
|
|
67
|
+
if msg_type == "subscribe":
|
|
68
|
+
await manager.subscribe(websocket, message.get("channel"))
|
|
69
|
+
elif msg_type == "publish":
|
|
70
|
+
await manager.publish(message.get("channel"), message.get("payload"))
|
|
71
|
+
elif msg_type == "broadcast":
|
|
72
|
+
await manager.broadcast(message.get("payload"))
|
|
73
|
+
except:
|
|
74
|
+
pass
|
|
75
|
+
finally:
|
|
76
|
+
await manager.disconnect(websocket)
|