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
@@ -0,0 +1,132 @@
1
+ """
2
+ NextPy Link Component - Client-side navigation with prefetching
3
+ Similar to Next.js's next/link with HTMX integration
4
+ """
5
+
6
+ from typing import Any, Optional
7
+ from markupsafe import Markup
8
+
9
+
10
+ class Link:
11
+ """
12
+ Link component for client-side navigation
13
+ Uses HTMX for SPA-like navigation without full page reloads
14
+
15
+ Usage in templates:
16
+ {{ Link("/about", "About Us", prefetch=True) }}
17
+ {{ Link("/blog/" + post.slug, post.title, class_="text-blue-500") }}
18
+
19
+ In Python:
20
+ link = Link("/about", "About")
21
+ print(link.render())
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ href: str,
27
+ text: Optional[str] = None,
28
+ prefetch: bool = True,
29
+ replace: bool = False,
30
+ scroll: bool = True,
31
+ class_: Optional[str] = None,
32
+ target: Optional[str] = None,
33
+ use_htmx: bool = True,
34
+ **kwargs: Any,
35
+ ):
36
+ self.href = href
37
+ self.text = text
38
+ self.prefetch = prefetch
39
+ self.replace = replace
40
+ self.scroll = scroll
41
+ self.class_ = class_
42
+ self.target = target
43
+ self.use_htmx = use_htmx
44
+ self.extra_attrs = kwargs
45
+
46
+ def render(self, children: Optional[str] = None) -> str:
47
+ """Render the link as an HTML anchor tag"""
48
+ content = children or self.text or self.href
49
+
50
+ attrs = [f'href="{self._escape(self.href)}"']
51
+
52
+ if self.class_:
53
+ attrs.append(f'class="{self._escape(self.class_)}"')
54
+
55
+ if self.target:
56
+ attrs.append(f'target="{self._escape(self.target)}"')
57
+
58
+ if self.use_htmx and not self.target:
59
+ attrs.append(f'hx-get="{self._escape(self.href)}"')
60
+ attrs.append('hx-target="#main-content"')
61
+ attrs.append('hx-swap="innerHTML"')
62
+ attrs.append('hx-push-url="true"')
63
+
64
+ if self.prefetch:
65
+ attrs.append('hx-trigger="mouseenter, click"')
66
+ attrs.append('preload="true"')
67
+
68
+ elif self.prefetch:
69
+ attrs.append('data-prefetch="true"')
70
+
71
+ if self.replace:
72
+ attrs.append('data-replace="true"')
73
+
74
+ if not self.scroll:
75
+ attrs.append('data-scroll="false"')
76
+
77
+ for key, value in self.extra_attrs.items():
78
+ attr_name = key.replace("_", "-")
79
+ if value is True:
80
+ attrs.append(attr_name)
81
+ elif value is not False and value is not None:
82
+ attrs.append(f'{attr_name}="{self._escape(str(value))}"')
83
+
84
+ attrs_str = " ".join(attrs)
85
+ return f"<a {attrs_str}>{content}</a>"
86
+
87
+ def __html__(self) -> str:
88
+ """Make the component work with Jinja2's autoescape"""
89
+ return self.render()
90
+
91
+ def __str__(self) -> str:
92
+ return self.render()
93
+
94
+ def __call__(
95
+ self,
96
+ href: Optional[str] = None,
97
+ text: Optional[str] = None,
98
+ **kwargs: Any
99
+ ) -> Markup:
100
+ """Allow calling Link() in templates with arguments"""
101
+ if href:
102
+ self.href = href
103
+ if text:
104
+ self.text = text
105
+ for key, value in kwargs.items():
106
+ if hasattr(self, key):
107
+ setattr(self, key, value)
108
+ else:
109
+ self.extra_attrs[key] = value
110
+ return Markup(self.render())
111
+
112
+ @staticmethod
113
+ def _escape(value: str) -> str:
114
+ """Escape HTML special characters"""
115
+ return (
116
+ str(value)
117
+ .replace("&", "&amp;")
118
+ .replace("<", "&lt;")
119
+ .replace(">", "&gt;")
120
+ .replace('"', "&quot;")
121
+ .replace("'", "&#x27;")
122
+ )
123
+
124
+
125
+ def link(href: str, text: Optional[str] = None, **kwargs: Any) -> Markup:
126
+ """
127
+ Functional helper to create a Link
128
+
129
+ Usage in templates:
130
+ {{ link("/about", "About Us") }}
131
+ """
132
+ return Markup(Link(href, text, **kwargs).render())
@@ -0,0 +1,65 @@
1
+ """
2
+ Loader/Spinner Components
3
+ Various loading indicators
4
+ """
5
+
6
+
7
+ def spinner(size: str = "md", color: str = "blue") -> str:
8
+ """Generate spinner HTML"""
9
+ sizes = {"sm": "w-4 h-4", "md": "w-8 h-8", "lg": "w-12 h-12"}
10
+ colors = {
11
+ "blue": "border-blue-500",
12
+ "red": "border-red-500",
13
+ "green": "border-green-500",
14
+ "purple": "border-purple-500"
15
+ }
16
+
17
+ return f'''
18
+ <div class="flex justify-center">
19
+ <div class="{sizes.get(size, sizes['md'])} border-4 border-gray-200 {colors.get(color, colors['blue'])} border-t-transparent rounded-full animate-spin"></div>
20
+ </div>
21
+ '''
22
+
23
+
24
+ def skeleton(lines: int = 3, width: str = "full") -> str:
25
+ """Generate skeleton loader (content placeholder)"""
26
+ skeleton_lines = "\n".join([
27
+ f'<div class="h-4 bg-gray-200 rounded mb-2 animate-pulse"></div>'
28
+ for _ in range(lines)
29
+ ])
30
+
31
+ return f'''
32
+ <div class="w-{width}">
33
+ {skeleton_lines}
34
+ </div>
35
+ '''
36
+
37
+
38
+ def progress_bar(value: int = 50, max_value: int = 100, color: str = "blue") -> str:
39
+ """Generate progress bar"""
40
+ percentage = (value / max_value) * 100
41
+ colors = {
42
+ "blue": "bg-blue-500",
43
+ "red": "bg-red-500",
44
+ "green": "bg-green-500",
45
+ "purple": "bg-purple-500"
46
+ }
47
+
48
+ return f'''
49
+ <div class="w-full bg-gray-200 rounded-full h-2">
50
+ <div class="{colors.get(color, colors['blue'])} h-2 rounded-full" style="width: {percentage}%"></div>
51
+ </div>
52
+ <p class="text-sm text-gray-600 mt-1">{value}/{max_value}</p>
53
+ '''
54
+
55
+
56
+ def loading_screen(message: str = "Loading...") -> str:
57
+ """Full screen loading overlay"""
58
+ return f'''
59
+ <div class="fixed inset-0 bg-white bg-opacity-90 flex items-center justify-center z-50">
60
+ <div class="text-center">
61
+ <div class="w-12 h-12 border-4 border-gray-200 border-t-blue-500 rounded-full animate-spin mx-auto mb-4"></div>
62
+ <p class="text-lg text-gray-700">{message}</p>
63
+ </div>
64
+ </div>
65
+ '''
@@ -0,0 +1,101 @@
1
+ """
2
+ Toast Notification Component
3
+ Display temporary alerts/notifications
4
+ """
5
+
6
+ from typing import Literal
7
+
8
+
9
+ class Toast:
10
+ """Toast notification system"""
11
+
12
+ def __init__(self):
13
+ self.toasts = []
14
+
15
+ def add(self, message: str, type: Literal["success", "error", "info", "warning"] = "info", duration: int = 3000):
16
+ """Add a toast notification"""
17
+ toast_id = len(self.toasts)
18
+ self.toasts.append({
19
+ "id": toast_id,
20
+ "message": message,
21
+ "type": type,
22
+ "duration": duration
23
+ })
24
+ return toast_id
25
+
26
+ def remove(self, toast_id: int):
27
+ """Remove a toast"""
28
+ self.toasts = [t for t in self.toasts if t["id"] != toast_id]
29
+
30
+ def clear_all(self):
31
+ """Clear all toasts"""
32
+ self.toasts = []
33
+
34
+
35
+ def toast_html() -> str:
36
+ """Generate toast container HTML"""
37
+ return '''
38
+ <div id="toast-container" class="fixed top-4 right-4 z-50 space-y-2" hx-ext="sse" sse-connect="/api/toasts">
39
+ <div hx-target="#toast-container" hx-swap="beforeend"></div>
40
+ </div>
41
+
42
+ <style>
43
+ .toast {
44
+ animation: slideInRight 0.3s ease-out, slideOutRight 0.3s ease-in 3s forwards;
45
+ padding: 16px;
46
+ border-radius: 8px;
47
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
48
+ font-weight: 500;
49
+ }
50
+
51
+ .toast.success {
52
+ background-color: #10b981;
53
+ color: white;
54
+ }
55
+
56
+ .toast.error {
57
+ background-color: #ef4444;
58
+ color: white;
59
+ }
60
+
61
+ .toast.info {
62
+ background-color: #3b82f6;
63
+ color: white;
64
+ }
65
+
66
+ .toast.warning {
67
+ background-color: #f59e0b;
68
+ color: white;
69
+ }
70
+
71
+ @keyframes slideInRight {
72
+ from {
73
+ transform: translateX(400px);
74
+ opacity: 0;
75
+ }
76
+ to {
77
+ transform: translateX(0);
78
+ opacity: 1;
79
+ }
80
+ }
81
+
82
+ @keyframes slideOutRight {
83
+ from {
84
+ transform: translateX(0);
85
+ opacity: 1;
86
+ }
87
+ to {
88
+ transform: translateX(400px);
89
+ opacity: 0;
90
+ }
91
+ }
92
+ </style>
93
+ '''
94
+
95
+
96
+ # Global toast instance
97
+ _toast = Toast()
98
+
99
+ def get_toast() -> Toast:
100
+ """Get global toast instance"""
101
+ return _toast
@@ -0,0 +1,185 @@
1
+ """
2
+ Visual Components for NextPy
3
+ Tabs, Accordion, Dropdown, Modal, Card variations
4
+ """
5
+
6
+
7
+ def Tabs(tabs: list, active_index: int = 0) -> str:
8
+ """
9
+ Tabs component
10
+ tabs: [{"label": "Tab 1", "content": "<p>Content 1</p>"}, ...]
11
+ """
12
+ tab_buttons_list = []
13
+ for i, tab in enumerate(tabs):
14
+ active_class = "border-blue-600 text-blue-600 font-bold" if i == active_index else "border-gray-300 text-gray-600"
15
+ tab_buttons_list.append(f'<button class="px-4 py-2 border-b-2 {active_class}" onclick="switchTab({i})">\n {tab["label"]}\n </button>')
16
+ tab_buttons = "\n".join(tab_buttons_list)
17
+
18
+ tab_contents_list = []
19
+ for i, tab in enumerate(tabs):
20
+ display_class = "block" if i == active_index else "hidden"
21
+ tab_contents_list.append(f'<div id="tab-content-{i}" class="{display_class} py-4">\n {tab["content"]}\n </div>')
22
+ tab_contents = "\n".join(tab_contents_list)
23
+
24
+ return f'''
25
+ <div class="space-y-4">
26
+ <div class="flex border-b border-gray-200">
27
+ {tab_buttons}
28
+ </div>
29
+ <div>
30
+ {tab_contents}
31
+ </div>
32
+ </div>
33
+ <script>
34
+ function switchTab(index) {{
35
+ document.querySelectorAll('[id^="tab-content-"]').forEach(el => el.classList.add('hidden'));
36
+ document.getElementById('tab-content-' + index).classList.remove('hidden');
37
+ document.querySelectorAll('button').forEach((btn, i) => {{
38
+ btn.classList.toggle('border-blue-600', i === index);
39
+ btn.classList.toggle('text-blue-600', i === index);
40
+ btn.classList.toggle('font-bold', i === index);
41
+ btn.classList.toggle('border-gray-300', i !== index);
42
+ btn.classList.toggle('text-gray-600', i !== index);
43
+ }});
44
+ }}
45
+ </script>
46
+ '''
47
+
48
+
49
+ def Accordion(items: list) -> str:
50
+ """
51
+ Accordion component
52
+ items: [{"title": "Section 1", "content": "<p>Content 1</p>"}, ...]
53
+ """
54
+ accordion_items = []
55
+ for i, item in enumerate(items):
56
+ accordion_items.append(f'''
57
+ <div class="border border-gray-300 mb-2 rounded-lg overflow-hidden">
58
+ <button onclick="toggleAccordion({i})" class="w-full text-left px-4 py-3 bg-gray-100 hover:bg-gray-200 font-semibold flex justify-between items-center">
59
+ {item["title"]}
60
+ <span id="accordion-icon-{i}">▼</span>
61
+ </button>
62
+ <div id="accordion-content-{i}" class="hidden px-4 py-3 bg-white text-gray-700">
63
+ {item["content"]}
64
+ </div>
65
+ </div>
66
+ ''')
67
+ accordion_html = "\n".join(accordion_items)
68
+
69
+ return f'''
70
+ <div class="space-y-2">
71
+ {accordion_html}
72
+ </div>
73
+ <script>
74
+ function toggleAccordion(index) {{
75
+ const content = document.getElementById('accordion-content-' + index);
76
+ const icon = document.getElementById('accordion-icon-' + index);
77
+ content.classList.toggle('hidden');
78
+ icon.textContent = content.classList.contains('hidden') ? '▼' : '▲';
79
+ }}
80
+ </script>
81
+ '''
82
+
83
+
84
+ def Dropdown(label: str, items: list, position: str = "left") -> str:
85
+ """
86
+ Dropdown component
87
+ items: [{"label": "Option 1", "href": "/path"}, ...]
88
+ """
89
+ position_class = "right-0" if position == "right" else "left-0"
90
+ items_html = "\n".join([
91
+ f'<a href="{item.get("href", "#")}" class="block px-4 py-2 text-gray-700 hover:bg-gray-100">{item["label"]}</a>'
92
+ for item in items
93
+ ])
94
+
95
+ return f'''
96
+ <div class="relative inline-block">
97
+ <button onclick="toggleDropdown()" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
98
+ {label}
99
+ </button>
100
+ <div id="dropdown-menu" class="hidden absolute {position_class} mt-2 bg-white border border-gray-300 rounded-lg shadow-lg z-10">
101
+ {items_html}
102
+ </div>
103
+ </div>
104
+ <script>
105
+ function toggleDropdown() {{
106
+ const menu = document.getElementById('dropdown-menu');
107
+ menu.classList.toggle('hidden');
108
+ }}
109
+ document.addEventListener('click', function(event) {{
110
+ const menu = document.getElementById('dropdown-menu');
111
+ if (!menu.parentElement.contains(event.target)) menu.classList.add('hidden');
112
+ }});
113
+ </script>
114
+ '''
115
+
116
+
117
+ def Modal(title: str, content: str, footer: str = "", show: bool = False) -> str:
118
+ """Modal component"""
119
+ display = "flex" if show else "hidden"
120
+ footer_html = f'<div class="px-6 py-4 border-t border-gray-200 flex justify-end gap-2">{footer}</div>' if footer else ''
121
+ return f'''
122
+ <div id="modal-overlay" class="{display} fixed inset-0 bg-black bg-opacity-50 justify-center items-center z-50">
123
+ <div class="bg-white rounded-lg shadow-xl max-w-md w-full mx-4">
124
+ <div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
125
+ <h2 class="text-xl font-bold">{title}</h2>
126
+ <button onclick="closeModal()" class="text-gray-500 hover:text-gray-700 text-2xl">&times;</button>
127
+ </div>
128
+ <div class="px-6 py-4">
129
+ {content}
130
+ </div>
131
+ {footer_html}
132
+ </div>
133
+ </div>
134
+ <script>
135
+ function openModal() {{
136
+ document.getElementById('modal-overlay').classList.remove('hidden');
137
+ document.getElementById('modal-overlay').classList.add('flex');
138
+ }}
139
+ function closeModal() {{
140
+ document.getElementById('modal-overlay').classList.add('hidden');
141
+ document.getElementById('modal-overlay').classList.remove('flex');
142
+ }}
143
+ </script>
144
+ '''
145
+
146
+
147
+ def Card(title: str, content: str, image: str = "", footer: str = "") -> str:
148
+ """Enhanced Card component"""
149
+ image_html = f'<img src="{image}" class="w-full h-48 object-cover rounded-t-lg"/>' if image else ""
150
+ footer_html = f'<div class="px-6 py-3 bg-gray-50 border-t border-gray-200 text-sm text-gray-600">{footer}</div>' if footer else ""
151
+
152
+ return f'''
153
+ <div class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition">
154
+ {image_html}
155
+ <div class="px-6 py-4">
156
+ <h3 class="font-bold text-lg mb-2">{title}</h3>
157
+ <p class="text-gray-600">{content}</p>
158
+ </div>
159
+ {footer_html}
160
+ </div>
161
+ '''
162
+
163
+
164
+ def Breadcrumb(items: list) -> str:
165
+ """Breadcrumb navigation"""
166
+ breadcrumb_parts = []
167
+ for item in items:
168
+ if item.get("href"):
169
+ breadcrumb_parts.append(f'<a href="{item.get("href")}" class="text-blue-600 hover:underline">{item["label"]}</a>')
170
+ else:
171
+ breadcrumb_parts.append(f'<span class="text-gray-700">{item["label"]}</span>')
172
+ breadcrumb_html = " / ".join(breadcrumb_parts)
173
+ return f'<nav class="text-sm text-gray-600 mb-4">{breadcrumb_html}</nav>'
174
+
175
+
176
+ def Pagination(current: int = 1, total: int = 10, base_url: str = "") -> str:
177
+ """Pagination component"""
178
+ pages = []
179
+ for i in range(1, total + 1):
180
+ if i == current:
181
+ pages.append(f'<span class="px-3 py-1 bg-blue-600 text-white rounded">{i}</span>')
182
+ else:
183
+ pages.append(f'<a href="{base_url}?page={i}" class="px-3 py-1 border border-gray-300 rounded hover:bg-gray-100">{i}</a>')
184
+
185
+ return f'<div class="flex gap-2">{" ".join(pages)}</div>'
nextpy/config.py ADDED
@@ -0,0 +1,75 @@
1
+ """
2
+ NextPy Configuration Manager
3
+ Environment variables, settings, and configuration
4
+ """
5
+
6
+ import os
7
+ from typing import Optional, Any
8
+ from dotenv import load_dotenv
9
+ from pydantic_settings import BaseSettings
10
+
11
+ # Load .env file
12
+ load_dotenv()
13
+
14
+
15
+ class Settings(BaseSettings):
16
+ """Application settings from environment variables"""
17
+
18
+ # App
19
+ app_name: str = "NextPy App"
20
+ debug: bool = os.getenv("DEBUG", "true").lower() == "true"
21
+ secret_key: str = os.getenv("SECRET_KEY", "change-me-in-production")
22
+ domain: str = os.getenv("DOMAIN", "localhost:5000")
23
+
24
+ # Database
25
+ database_url: str = os.getenv("DATABASE_URL", "sqlite:///./nextpy.db")
26
+ db_echo: bool = os.getenv("DB_ECHO", "false").lower() == "true"
27
+
28
+ # Auth
29
+ jwt_secret: str = os.getenv("JWT_SECRET", "change-me")
30
+ jwt_algorithm: str = "HS256"
31
+ jwt_expiration_hours: int = 24
32
+ session_secret: str = os.getenv("SESSION_SECRET", "change-me")
33
+
34
+ # Email
35
+ mail_server: str = os.getenv("MAIL_SERVER", "smtp.gmail.com")
36
+ mail_port: int = int(os.getenv("MAIL_PORT", "587"))
37
+ mail_username: str = os.getenv("MAIL_USERNAME", "")
38
+ mail_password: str = os.getenv("MAIL_PASSWORD", "")
39
+
40
+ # API Keys
41
+ api_key: Optional[str] = os.getenv("API_KEY")
42
+ stripe_key: Optional[str] = os.getenv("STRIPE_KEY")
43
+ openai_api_key: Optional[str] = os.getenv("OPENAI_API_KEY")
44
+
45
+ # URLs
46
+ frontend_url: str = os.getenv("FRONTEND_URL", "http://localhost:5000")
47
+ backend_url: str = os.getenv("BACKEND_URL", "http://localhost:5000")
48
+
49
+ class Config:
50
+ env_file = ".env"
51
+ case_sensitive = False
52
+
53
+
54
+ # Global settings instance
55
+ settings = Settings()
56
+
57
+
58
+ def get_setting(key: str, default: Any = None) -> Any:
59
+ """Get setting by key"""
60
+ return getattr(settings, key, default)
61
+
62
+
63
+ def get_env(key: str, default: Any = None) -> Any:
64
+ """Get environment variable"""
65
+ return os.getenv(key, default)
66
+
67
+
68
+ def is_production() -> bool:
69
+ """Check if running in production"""
70
+ return not settings.debug
71
+
72
+
73
+ def is_development() -> bool:
74
+ """Check if running in development"""
75
+ return settings.debug
@@ -0,0 +1,21 @@
1
+ """NextPy Core Module - Routing, Rendering, and Data Fetching"""
2
+
3
+ from nextpy.core.router import Router, Route, DynamicRoute
4
+ from nextpy.core.renderer import Renderer
5
+ from nextpy.core.data_fetching import (
6
+ get_server_side_props,
7
+ get_static_props,
8
+ get_static_paths,
9
+ )
10
+ from nextpy.core.builder import Builder
11
+
12
+ __all__ = [
13
+ "Router",
14
+ "Route",
15
+ "DynamicRoute",
16
+ "Renderer",
17
+ "get_server_side_props",
18
+ "get_static_props",
19
+ "get_static_paths",
20
+ "Builder",
21
+ ]