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/core/router.py ADDED
@@ -0,0 +1,233 @@
1
+ """
2
+ NextPy Router - File-based routing system inspired by Next.js
3
+ Supports:
4
+ - File-based routing (pages/index.py -> /)
5
+ - Dynamic routes (pages/[slug].py -> /:slug)
6
+ - Nested routes (pages/blog/[id].py -> /blog/:id)
7
+ - Catch-all routes (pages/[...path].py -> /*)
8
+ - API routes (pages/api/*.py)
9
+ """
10
+
11
+ import os
12
+ import re
13
+ import importlib.util
14
+ from pathlib import Path
15
+ from typing import Dict, List, Optional, Callable, Any, Tuple
16
+ from dataclasses import dataclass, field
17
+ from pydantic import BaseModel
18
+
19
+
20
+ class RouteParams(BaseModel):
21
+ """Type-safe route parameters"""
22
+ params: Dict[str, str] = {}
23
+ query: Dict[str, str] = {}
24
+
25
+
26
+ @dataclass
27
+ class Route:
28
+ """Represents a single route in the application"""
29
+ path: str
30
+ file_path: Path
31
+ handler: Optional[Callable] = None
32
+ is_dynamic: bool = False
33
+ is_api: bool = False
34
+ is_catch_all: bool = False
35
+ param_names: List[str] = field(default_factory=list)
36
+ pattern: Optional[re.Pattern] = None
37
+
38
+ def matches(self, url_path: str) -> Optional[Dict[str, str]]:
39
+ """Check if this route matches the given URL path"""
40
+ if self.pattern:
41
+ match = self.pattern.match(url_path)
42
+ if match:
43
+ return match.groupdict()
44
+ elif self.path == url_path:
45
+ return {}
46
+ return None
47
+
48
+
49
+ @dataclass
50
+ class DynamicRoute(Route):
51
+ """A route with dynamic segments like [slug] or [...path]"""
52
+ is_dynamic: bool = True
53
+
54
+
55
+ class Router:
56
+ """
57
+ File-based router that scans the pages directory
58
+ and creates routes similar to Next.js
59
+ """
60
+
61
+ def __init__(self, pages_dir: str = "pages", templates_dir: str = "templates"):
62
+ self.pages_dir = Path(pages_dir)
63
+ self.templates_dir = Path(templates_dir)
64
+ self.routes: List[Route] = []
65
+ self.api_routes: List[Route] = []
66
+ self._route_cache: Dict[str, Route] = {}
67
+
68
+ def scan_pages(self) -> None:
69
+ """Scan the pages directory and register all routes"""
70
+ if not self.pages_dir.exists():
71
+ return
72
+
73
+ for file_path in self.pages_dir.rglob("*.py"):
74
+ if file_path.name.startswith("_"):
75
+ continue
76
+
77
+ route = self._create_route_from_file(file_path)
78
+ if route:
79
+ if route.is_api:
80
+ self.api_routes.append(route)
81
+ else:
82
+ self.routes.append(route)
83
+
84
+ self._sort_routes()
85
+
86
+ def _create_route_from_file(self, file_path: Path) -> Optional[Route]:
87
+ """Create a Route object from a Python file"""
88
+ relative_path = file_path.relative_to(self.pages_dir)
89
+ parts = list(relative_path.parts)
90
+
91
+ is_api = parts[0] == "api" if parts else False
92
+
93
+ route_parts = []
94
+ param_names = []
95
+ is_dynamic = False
96
+ is_catch_all = False
97
+
98
+ for part in parts:
99
+ if part.endswith(".py"):
100
+ part = part[:-3]
101
+
102
+ if part == "index":
103
+ continue
104
+
105
+ catch_all_match = re.match(r"\[\.\.\.(\w+)\]", part)
106
+ dynamic_match = re.match(r"\[(\w+)\]", part)
107
+
108
+ if catch_all_match:
109
+ param_name = catch_all_match.group(1)
110
+ param_names.append(param_name)
111
+ route_parts.append(f"(?P<{param_name}>.+)")
112
+ is_dynamic = True
113
+ is_catch_all = True
114
+ elif dynamic_match:
115
+ param_name = dynamic_match.group(1)
116
+ param_names.append(param_name)
117
+ route_parts.append(f"(?P<{param_name}>[^/]+)")
118
+ is_dynamic = True
119
+ else:
120
+ route_parts.append(re.escape(part))
121
+
122
+ if is_api and route_parts and route_parts[0] == "api":
123
+ route_parts = route_parts[1:]
124
+
125
+ path = "/" + "/".join(route_parts) if route_parts else "/"
126
+
127
+ if is_api:
128
+ path = "/api" + path if path != "/" else "/api"
129
+
130
+ pattern = None
131
+ if is_dynamic:
132
+ pattern_str = "^" + path.replace("/", r"\/") + "$"
133
+ pattern_str = re.sub(r"\\\(\?P", "(?P", pattern_str)
134
+ pattern_str = pattern_str.replace(r"\[", "[").replace(r"\]", "]")
135
+ pattern_str = pattern_str.replace(r"\+", "+")
136
+ try:
137
+ pattern = re.compile(pattern_str)
138
+ except re.error:
139
+ pattern = re.compile("^" + path + "$")
140
+
141
+ handler = self._load_handler(file_path)
142
+
143
+ route_class = DynamicRoute if is_dynamic else Route
144
+ return route_class(
145
+ path=path,
146
+ file_path=file_path,
147
+ handler=handler,
148
+ is_dynamic=is_dynamic,
149
+ is_api=is_api,
150
+ is_catch_all=is_catch_all,
151
+ param_names=param_names,
152
+ pattern=pattern,
153
+ )
154
+
155
+ def _load_handler(self, file_path: Path) -> Optional[Callable]:
156
+ """Dynamically load the handler function from a page file"""
157
+ try:
158
+ spec = importlib.util.spec_from_file_location(
159
+ file_path.stem,
160
+ file_path
161
+ )
162
+ if spec and spec.loader:
163
+ module = importlib.util.module_from_spec(spec)
164
+ spec.loader.exec_module(module)
165
+
166
+ for func_name in ["handler", "page", "get", "post", "default"]:
167
+ if hasattr(module, func_name):
168
+ return getattr(module, func_name)
169
+
170
+ if hasattr(module, "Page"):
171
+ return module.Page
172
+
173
+ except Exception as e:
174
+ print(f"Error loading handler from {file_path}: {e}")
175
+
176
+ return None
177
+
178
+ def _sort_routes(self) -> None:
179
+ """Sort routes so static routes come before dynamic ones"""
180
+ def route_priority(route: Route) -> Tuple[int, int, str]:
181
+ if route.is_catch_all:
182
+ return (2, len(route.param_names), route.path)
183
+ elif route.is_dynamic:
184
+ return (1, len(route.param_names), route.path)
185
+ else:
186
+ return (0, 0, route.path)
187
+
188
+ self.routes.sort(key=route_priority)
189
+ self.api_routes.sort(key=route_priority)
190
+
191
+ def match(self, url_path: str) -> Optional[Tuple[Route, Dict[str, str]]]:
192
+ """Find a route that matches the given URL path"""
193
+ url_path = url_path.rstrip("/") or "/"
194
+
195
+ if url_path in self._route_cache:
196
+ route = self._route_cache[url_path]
197
+ params = route.matches(url_path) or {}
198
+ return (route, params)
199
+
200
+ routes = self.api_routes if url_path.startswith("/api") else self.routes
201
+
202
+ for route in routes:
203
+ params = route.matches(url_path)
204
+ if params is not None:
205
+ if not route.is_dynamic:
206
+ self._route_cache[url_path] = route
207
+ return (route, params)
208
+
209
+ return None
210
+
211
+ def get_all_routes(self) -> List[Route]:
212
+ """Get all registered routes"""
213
+ return self.routes + self.api_routes
214
+
215
+ def get_static_routes(self) -> List[Route]:
216
+ """Get only static (non-dynamic) routes for SSG"""
217
+ return [r for r in self.routes if not r.is_dynamic]
218
+
219
+ def reload_route(self, file_path: Path) -> None:
220
+ """Reload a specific route (for hot reload)"""
221
+ self.routes = [r for r in self.routes if r.file_path != file_path]
222
+ self.api_routes = [r for r in self.api_routes if r.file_path != file_path]
223
+ self._route_cache.clear()
224
+
225
+ if file_path.exists():
226
+ route = self._create_route_from_file(file_path)
227
+ if route:
228
+ if route.is_api:
229
+ self.api_routes.append(route)
230
+ else:
231
+ self.routes.append(route)
232
+
233
+ self._sort_routes()
nextpy/core/sync.py ADDED
@@ -0,0 +1,34 @@
1
+ """
2
+ NextPy Sync Support
3
+ Allow pages to be written with sync functions instead of async
4
+ """
5
+
6
+ import asyncio
7
+ import inspect
8
+ from typing import Callable, Any, Dict
9
+
10
+
11
+ def sync_to_async(func: Callable) -> Callable:
12
+ """Convert sync function to async"""
13
+ if inspect.iscoroutinefunction(func):
14
+ return func
15
+
16
+ async def wrapper(*args, **kwargs):
17
+ loop = asyncio.get_event_loop()
18
+ return await loop.run_in_executor(None, func, *args, **kwargs)
19
+
20
+ return wrapper
21
+
22
+
23
+ async def run_sync(func: Callable, *args, **kwargs) -> Any:
24
+ """Run sync function in async context"""
25
+ if inspect.iscoroutinefunction(func):
26
+ return await func(*args, **kwargs)
27
+
28
+ loop = asyncio.get_event_loop()
29
+ return await loop.run_in_executor(None, func, *args, **kwargs)
30
+
31
+
32
+ def supports_sync(func: Callable) -> bool:
33
+ """Check if function supports sync"""
34
+ return not inspect.iscoroutinefunction(func)
nextpy/db.py ADDED
@@ -0,0 +1,121 @@
1
+ """
2
+ NextPy Database Layer
3
+ Support for SQLite, PostgreSQL, MySQL with SQLAlchemy ORM
4
+ """
5
+
6
+ import os
7
+ from typing import Optional, Dict, Any
8
+ from sqlalchemy import create_engine
9
+ from sqlalchemy.orm import sessionmaker, Session
10
+ from sqlalchemy.ext.declarative import declarative_base
11
+
12
+ Base = declarative_base()
13
+
14
+
15
+ class DatabaseConfig:
16
+ """Database configuration manager"""
17
+
18
+ def __init__(self, database_url: Optional[str] = None):
19
+ self.database_url = database_url or os.getenv("DATABASE_URL", "sqlite:///./nextpy.db")
20
+ self.echo = os.getenv("DB_ECHO", "false").lower() == "true"
21
+ self.pool_size = int(os.getenv("DB_POOL_SIZE", "5"))
22
+ self.max_overflow = int(os.getenv("DB_MAX_OVERFLOW", "10"))
23
+
24
+ def get_engine(self):
25
+ """Get SQLAlchemy engine"""
26
+ if self.database_url.startswith("sqlite"):
27
+ return create_engine(
28
+ self.database_url,
29
+ echo=self.echo,
30
+ connect_args={"check_same_thread": False}
31
+ )
32
+ else:
33
+ return create_engine(
34
+ self.database_url,
35
+ echo=self.echo,
36
+ pool_size=self.pool_size,
37
+ max_overflow=self.max_overflow,
38
+ pool_pre_ping=True
39
+ )
40
+
41
+
42
+ class Database:
43
+ """Database manager with connection pooling"""
44
+
45
+ def __init__(self, database_url: Optional[str] = None):
46
+ self.config = DatabaseConfig(database_url)
47
+ self.engine = self.config.get_engine()
48
+ self.SessionLocal = sessionmaker(bind=self.engine, expire_on_commit=False)
49
+
50
+ def get_session(self) -> Session:
51
+ """Get database session"""
52
+ return self.SessionLocal()
53
+
54
+ def create_tables(self):
55
+ """Create all tables"""
56
+ Base.metadata.create_all(self.engine)
57
+
58
+ def drop_tables(self):
59
+ """Drop all tables (development only)"""
60
+ Base.metadata.drop_all(self.engine)
61
+
62
+ def close(self):
63
+ """Close connection pool"""
64
+ self.engine.dispose()
65
+
66
+
67
+ # Global database instance
68
+ _db: Optional[Database] = None
69
+
70
+
71
+ def init_db(database_url: Optional[str] = None) -> Database:
72
+ """Initialize global database instance"""
73
+ global _db
74
+ _db = Database(database_url)
75
+ _db.create_tables()
76
+ return _db
77
+
78
+
79
+ def get_db() -> Database:
80
+ """Get global database instance"""
81
+ if _db is None:
82
+ raise RuntimeError("Database not initialized. Call init_db() first.")
83
+ return _db
84
+
85
+
86
+ def get_session() -> Session:
87
+ """Get database session"""
88
+ return get_db().get_session()
89
+
90
+
91
+ # Models example
92
+ from sqlalchemy import Column, Integer, String, DateTime, Text, Boolean
93
+ from datetime import datetime
94
+
95
+
96
+ class User(Base):
97
+ """User model"""
98
+ __tablename__ = "users"
99
+
100
+ id = Column(Integer, primary_key=True)
101
+ email = Column(String(255), unique=True, index=True)
102
+ username = Column(String(255), unique=True, index=True)
103
+ full_name = Column(String(255))
104
+ hashed_password = Column(String(255))
105
+ is_active = Column(Boolean, default=True)
106
+ created_at = Column(DateTime, default=datetime.utcnow)
107
+
108
+
109
+ class Post(Base):
110
+ """Blog post model"""
111
+ __tablename__ = "posts"
112
+
113
+ id = Column(Integer, primary_key=True)
114
+ title = Column(String(255), index=True)
115
+ slug = Column(String(255), unique=True, index=True)
116
+ content = Column(Text)
117
+ excerpt = Column(String(500))
118
+ author_id = Column(Integer, index=True)
119
+ published = Column(Boolean, default=False)
120
+ created_at = Column(DateTime, default=datetime.utcnow)
121
+ updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
nextpy/dev_server.py ADDED
@@ -0,0 +1,69 @@
1
+ """
2
+ NextPy Enhanced Development Server with hot reload and optimization
3
+ """
4
+
5
+ import asyncio
6
+ from watchdog.observers import Observer
7
+ from watchdog.events import FileSystemEventHandler
8
+ from pathlib import Path
9
+ from typing import Callable, List, Optional, Dict, Any
10
+
11
+
12
+ class OptimizedDevServer(FileSystemEventHandler):
13
+ """Development server with file watching and hot reload"""
14
+
15
+ def __init__(self, on_change: Callable[[str], None]):
16
+ self.on_change = on_change
17
+ self.debounce_timer: Optional[asyncio.Timer] = None
18
+ self.debounce_delay: float = 0.5 # seconds
19
+
20
+ def on_modified(self, event: Any) -> None:
21
+ """Handle file modification"""
22
+ if event.is_directory:
23
+ return
24
+
25
+ # Debounce rapid changes
26
+ if self.debounce_timer:
27
+ self.debounce_timer.cancel()
28
+
29
+ self.debounce_timer = asyncio.Timer(
30
+ self.debounce_delay,
31
+ lambda: self.on_change(event.src_path)
32
+ )
33
+ self.debounce_timer.start()
34
+
35
+ @staticmethod
36
+ def watch_directories(paths: List[Path], on_change: Callable[[str], None]) -> Observer:
37
+ """Watch multiple directories for changes"""
38
+ observer = Observer()
39
+ handler = OptimizedDevServer(on_change)
40
+
41
+ for path in paths:
42
+ observer.schedule(handler, str(path), recursive=True)
43
+
44
+ observer.start()
45
+ return observer
46
+
47
+
48
+ class DevServerStats:
49
+ """Track development server statistics"""
50
+
51
+ def __init__(self) -> None:
52
+ self.hot_reloads: int = 0
53
+ self.build_time: float = 0
54
+ self.files_changed: List[str] = []
55
+
56
+ def record_reload(self, file_path: str, build_time: float) -> None:
57
+ """Record a hot reload event"""
58
+ self.hot_reloads += 1
59
+ self.build_time = build_time
60
+ self.files_changed.append(file_path)
61
+
62
+ def get_stats(self) -> Dict[str, Any]:
63
+ """Get server statistics"""
64
+ return {
65
+ "hot_reloads": self.hot_reloads,
66
+ "last_build_time_ms": round(self.build_time * 1000, 2),
67
+ "files_changed": len(self.files_changed),
68
+ "recent_changes": self.files_changed[-5:]
69
+ }
nextpy/dev_tools.py ADDED
@@ -0,0 +1,157 @@
1
+ """
2
+ NextPy Development Tools - Generators and scaffolding
3
+ """
4
+
5
+ from pathlib import Path
6
+ from typing import Optional, List, Tuple
7
+
8
+
9
+ class PageGenerator:
10
+ """Generate page files from templates"""
11
+
12
+ @staticmethod
13
+ def create_page(name: str, pages_dir: Path = Path("pages")) -> Tuple[Path, Path]:
14
+ """Generate a new page file"""
15
+ name_clean = name.replace("-", "_").lower()
16
+ page_file = pages_dir / f"{name_clean}.py"
17
+
18
+ content = f'''"""
19
+ {name} page
20
+ """
21
+
22
+ def get_template():
23
+ return "{name_clean}.html"
24
+
25
+
26
+ async def get_server_side_props(context):
27
+ return {{
28
+ "props": {{
29
+ "title": "{name}",
30
+ "description": "Page description"
31
+ }}
32
+ }}
33
+ '''
34
+
35
+ page_file.write_text(content)
36
+
37
+ # Create template
38
+ template_file = Path("templates") / f"{name_clean}.html"
39
+ template_content = f'''{% extends "_base.html" %}
40
+
41
+ {% block content %}
42
+ <div class="max-w-6xl mx-auto py-12 px-4">
43
+ <h1 class="text-4xl font-bold mb-4">{{{{ title }}}}</h1>
44
+ <p class="text-gray-600 mb-8">{{{{ description }}}}</p>
45
+
46
+ <!-- Your content here -->
47
+ </div>
48
+ {% endblock %}
49
+ '''
50
+ template_file.write_text(template_content)
51
+
52
+ return page_file, template_file
53
+
54
+
55
+ class APIGenerator:
56
+ """Generate API route files"""
57
+
58
+ @staticmethod
59
+ def create_api(name: str, methods: Optional[List[str]] = None, api_dir: Path = Path("pages/api")) -> Path:
60
+ """Generate API endpoint"""
61
+ if methods is None:
62
+ methods = ["GET"]
63
+
64
+ name_clean = name.replace("-", "_").lower()
65
+ api_file = api_dir / f"{name_clean}.py"
66
+
67
+ method_stubs: List[str] = []
68
+ for method in methods:
69
+ method_lower = method.lower()
70
+ if method == "GET":
71
+ method_stubs.append(f'''async def {method_lower}(request):
72
+ """GET /{name_clean}"""
73
+ return {{"data": "hello"}}
74
+ ''')
75
+ elif method == "POST":
76
+ method_stubs.append(f'''async def {method_lower}(request):
77
+ """POST /{name_clean}"""
78
+ data = await request.json()
79
+ return {{"created": data}}
80
+ ''')
81
+ elif method in ["PUT", "DELETE"]:
82
+ method_stubs.append(f'''async def {method_lower}(request):
83
+ """{method} /{name_clean}"""
84
+ return {{"{method_lower.lower()}_id": 1}}
85
+ ''')
86
+
87
+ content = f'''"""
88
+ {name} API endpoint
89
+ """
90
+
91
+ {chr(10).join(method_stubs)}
92
+ '''
93
+
94
+ api_file.write_text(content)
95
+ return api_file
96
+
97
+
98
+ class ComponentGenerator:
99
+ """Generate component files"""
100
+
101
+ @staticmethod
102
+ def create_component(name: str, templates_dir: Path = Path("templates/components")) -> Path:
103
+ """Generate a new component"""
104
+ name_clean = name.replace("-", "_").lower()
105
+ component_file = templates_dir / f"{name_clean}.html"
106
+
107
+ content = f'''{{%- macro {name_clean}(label, items=[], **kwargs) -}}
108
+ <!-- {name} component -->
109
+ <div class="component-{name_clean}">
110
+ <h3>{{{{ label }}}}</h3>
111
+ {{% for item in items %}}
112
+ <div class="item">{{{{ item }}}}</div>
113
+ {{% endfor %}}
114
+ </div>
115
+ {{%- endmacro %}}
116
+ '''
117
+
118
+ component_file.write_text(content)
119
+ return component_file
120
+
121
+
122
+ class ModelGenerator:
123
+ """Generate database models"""
124
+
125
+ @staticmethod
126
+ def create_model(name: str, fields: Optional[dict] = None, models_dir: Path = Path("models")) -> Path:
127
+ """Generate database model"""
128
+ if fields is None:
129
+ fields = {"id": "Integer, primary_key=True", "name": "String(255)"}
130
+
131
+ models_dir.mkdir(exist_ok=True)
132
+ model_file = models_dir / f"{name.lower()}.py"
133
+
134
+ field_stubs: List[str] = []
135
+ for field_name, field_type in fields.items():
136
+ field_stubs.append(f" {field_name} = Column({field_type})")
137
+
138
+ content = f'''"""
139
+ {name} model
140
+ """
141
+
142
+ from nextpy.db import Base
143
+ from sqlalchemy import Column, String, Integer, DateTime, Text
144
+ from datetime import datetime
145
+
146
+
147
+ class {name}(Base):
148
+ """{{docstring}}"""
149
+ __tablename__ = "{name.lower()}s"
150
+
151
+ {chr(10).join(field_stubs)}
152
+ created_at = Column(DateTime, default=datetime.utcnow)
153
+ updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
154
+ '''
155
+
156
+ model_file.write_text(content)
157
+ return model_file
nextpy/errors.py ADDED
@@ -0,0 +1,70 @@
1
+ """
2
+ NextPy Error Handling
3
+ Custom exceptions and error utilities
4
+ """
5
+
6
+ from typing import Optional, Dict, Any
7
+
8
+
9
+ class NextPyException(Exception):
10
+ """Base NextPy exception"""
11
+ pass
12
+
13
+
14
+ class RouteNotFound(NextPyException):
15
+ """Route not found"""
16
+ pass
17
+
18
+
19
+ class TemplateNotFound(NextPyException):
20
+ """Template not found"""
21
+ pass
22
+
23
+
24
+ class DatabaseError(NextPyException):
25
+ """Database connection error"""
26
+ pass
27
+
28
+
29
+ class AuthenticationError(NextPyException):
30
+ """Authentication failed"""
31
+ pass
32
+
33
+
34
+ class ValidationError(NextPyException):
35
+ """Validation failed"""
36
+
37
+ def __init__(self, message: str, errors: Optional[Dict[str, str]] = None):
38
+ self.message = message
39
+ self.errors = errors or {}
40
+ super().__init__(message)
41
+
42
+
43
+ class RateLimitError(NextPyException):
44
+ """Rate limit exceeded"""
45
+ pass
46
+
47
+
48
+ def handle_error(error: Exception) -> Dict[str, Any]:
49
+ """Convert error to response dict"""
50
+ if isinstance(error, ValidationError):
51
+ return {
52
+ "error": error.message,
53
+ "details": error.errors,
54
+ "status": 400
55
+ }
56
+ elif isinstance(error, AuthenticationError):
57
+ return {
58
+ "error": str(error),
59
+ "status": 401
60
+ }
61
+ elif isinstance(error, RateLimitError):
62
+ return {
63
+ "error": str(error),
64
+ "status": 429
65
+ }
66
+ else:
67
+ return {
68
+ "error": str(error),
69
+ "status": 500
70
+ }