pentagon-framework 0.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.
- pentagon_framework-0.1.0/LICENSE +21 -0
- pentagon_framework-0.1.0/PKG-INFO +46 -0
- pentagon_framework-0.1.0/README.md +32 -0
- pentagon_framework-0.1.0/pentagon/__init__.py +5 -0
- pentagon_framework-0.1.0/pentagon/cli/__init__.py +0 -0
- pentagon_framework-0.1.0/pentagon/cli/main.py +55 -0
- pentagon_framework-0.1.0/pentagon/cli/templates.py +127 -0
- pentagon_framework-0.1.0/pentagon/docs.py +260 -0
- pentagon_framework-0.1.0/pentagon/pentagon.py +73 -0
- pentagon_framework-0.1.0/pentagon/request.py +24 -0
- pentagon_framework-0.1.0/pentagon/response.py +16 -0
- pentagon_framework-0.1.0/pentagon/utils.py +143 -0
- pentagon_framework-0.1.0/pentagon/worker.py +11 -0
- pentagon_framework-0.1.0/pentagon_framework.egg-info/PKG-INFO +46 -0
- pentagon_framework-0.1.0/pentagon_framework.egg-info/SOURCES.txt +19 -0
- pentagon_framework-0.1.0/pentagon_framework.egg-info/dependency_links.txt +1 -0
- pentagon_framework-0.1.0/pentagon_framework.egg-info/entry_points.txt +2 -0
- pentagon_framework-0.1.0/pentagon_framework.egg-info/requires.txt +1 -0
- pentagon_framework-0.1.0/pentagon_framework.egg-info/top_level.txt +1 -0
- pentagon_framework-0.1.0/pyproject.toml +28 -0
- pentagon_framework-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ahmed Abdelkhaliq
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pentagon_framework
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A web framework for Python.
|
|
5
|
+
Author-email: Ahmed Abdelkhaliq <abdelkhaliqqq@gmail.com>
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.8
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: watchdog
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+

|
|
17
|
+
## Pentagon - A Lightweight Python Web Framework
|
|
18
|
+
|
|
19
|
+
Pentagon is a modern, high-performance, web framework for building APIs with Python.
|
|
20
|
+
## Features
|
|
21
|
+
|
|
22
|
+
- **Lighting Fast**: Very high performance.
|
|
23
|
+
- **Auto Documentation**: You register endpoints, Pentagon generates the documenations.
|
|
24
|
+
- **Easy**: Designed to be easy to use and learn, easy documentation.
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
Just run the pip command and you're all set!
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install pentagon
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
## Usage/Examples
|
|
37
|
+
|
|
38
|
+
Creating a project using pentagon
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
penta-admin init project --name "My Cool Project"
|
|
42
|
+
```
|
|
43
|
+
## Authors
|
|
44
|
+
|
|
45
|
+
- [@abde1khaliq](https://www.github.com/abde1khaliq)
|
|
46
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
|
|
2
|
+

|
|
3
|
+
## Pentagon - A Lightweight Python Web Framework
|
|
4
|
+
|
|
5
|
+
Pentagon is a modern, high-performance, web framework for building APIs with Python.
|
|
6
|
+
## Features
|
|
7
|
+
|
|
8
|
+
- **Lighting Fast**: Very high performance.
|
|
9
|
+
- **Auto Documentation**: You register endpoints, Pentagon generates the documenations.
|
|
10
|
+
- **Easy**: Designed to be easy to use and learn, easy documentation.
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
Just run the pip command and you're all set!
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install pentagon
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
## Usage/Examples
|
|
23
|
+
|
|
24
|
+
Creating a project using pentagon
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
penta-admin init project --name "My Cool Project"
|
|
28
|
+
```
|
|
29
|
+
## Authors
|
|
30
|
+
|
|
31
|
+
- [@abde1khaliq](https://www.github.com/abde1khaliq)
|
|
32
|
+
|
|
File without changes
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import argparse
|
|
3
|
+
from .templates import TEMPLATES
|
|
4
|
+
|
|
5
|
+
PROJECT_STRUCTURE = {
|
|
6
|
+
"": ["runserver.py", "README.md", "config.py"],
|
|
7
|
+
"app": ["__init__.py"],
|
|
8
|
+
"app/database": ["__init__.py", "connection.py"],
|
|
9
|
+
"app/models": ["__init__.py"],
|
|
10
|
+
"app/router": ["__init__.py", "index.py"],
|
|
11
|
+
"app/services": ["__init__.py"],
|
|
12
|
+
"app/schemas": ["__init__.py"],
|
|
13
|
+
"app/utils": ["__init__.py"]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
def init_project(name, location):
|
|
17
|
+
project_path = os.path.join(location, name)
|
|
18
|
+
os.makedirs(project_path, exist_ok=True)
|
|
19
|
+
|
|
20
|
+
for rel_path, files in PROJECT_STRUCTURE.items():
|
|
21
|
+
folder_path = os.path.join(project_path, rel_path)
|
|
22
|
+
os.makedirs(folder_path, exist_ok=True)
|
|
23
|
+
|
|
24
|
+
for file in files:
|
|
25
|
+
full_file_path = os.path.join(folder_path, file)
|
|
26
|
+
# Use the full relative path for the template key if it exists, otherwise just the filename
|
|
27
|
+
template_key = os.path.join(rel_path, file) if rel_path else file
|
|
28
|
+
|
|
29
|
+
# Fallback to filename if specific path key not found
|
|
30
|
+
if template_key not in TEMPLATES:
|
|
31
|
+
template_key = file
|
|
32
|
+
|
|
33
|
+
with open(full_file_path, "w") as f:
|
|
34
|
+
template = TEMPLATES.get(template_key, f"# {file} generated by Pentagon CLI\n")
|
|
35
|
+
f.write(template.strip())
|
|
36
|
+
|
|
37
|
+
print(f"Project '{name}' created at {project_path}")
|
|
38
|
+
|
|
39
|
+
def main():
|
|
40
|
+
parser = argparse.ArgumentParser(prog="penta-admin", description="Pentagon Framework CLI")
|
|
41
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
42
|
+
|
|
43
|
+
# Change 'create' back to 'init' to match user request and common practice
|
|
44
|
+
init_parser = subparsers.add_parser("init", help="Initialize a new project")
|
|
45
|
+
init_parser.add_argument("project", choices=["project"], help="Type of init (project)")
|
|
46
|
+
init_parser.add_argument("--name", required=True, help="Project name")
|
|
47
|
+
init_parser.add_argument("--location", default=".", help="Target directory")
|
|
48
|
+
|
|
49
|
+
args = parser.parse_args()
|
|
50
|
+
|
|
51
|
+
if args.command == "init" and args.project == "project":
|
|
52
|
+
init_project(args.name, args.location)
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
main()
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
TEMPLATES = {
|
|
2
|
+
'runserver.py': """
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
from pentagon import Pentagon
|
|
6
|
+
from config import setup_app
|
|
7
|
+
|
|
8
|
+
app = Pentagon(title="Pentagon Project", description="Project made using Pentagon for Python.")
|
|
9
|
+
|
|
10
|
+
if __name__ == "__main__":
|
|
11
|
+
# Fix for circular imports: ensures 'from runserver import app' works during discovery
|
|
12
|
+
sys.modules['runserver'] = sys.modules['__main__']
|
|
13
|
+
|
|
14
|
+
setup_app(app)
|
|
15
|
+
app.run(watch=True)
|
|
16
|
+
""",
|
|
17
|
+
|
|
18
|
+
'config.py': """
|
|
19
|
+
import importlib
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
|
|
23
|
+
def setup_app(app):
|
|
24
|
+
\"\"\"
|
|
25
|
+
Automatically links all routers and models to the Pentagon app.
|
|
26
|
+
\"\"\"
|
|
27
|
+
components = ['router', 'models']
|
|
28
|
+
|
|
29
|
+
# Ensure current directory is in path so 'app' package is found
|
|
30
|
+
if os.getcwd() not in sys.path:
|
|
31
|
+
sys.path.append(os.getcwd())
|
|
32
|
+
|
|
33
|
+
base_path = os.path.join(os.path.dirname(__file__), 'app')
|
|
34
|
+
|
|
35
|
+
for component in components:
|
|
36
|
+
comp_path = os.path.join(base_path, component)
|
|
37
|
+
if not os.path.exists(comp_path):
|
|
38
|
+
continue
|
|
39
|
+
|
|
40
|
+
for file in os.listdir(comp_path):
|
|
41
|
+
if file.endswith('.py') and file != '__init__.py':
|
|
42
|
+
module_name = f"app.{component}.{file[:-3]}"
|
|
43
|
+
try:
|
|
44
|
+
# This will trigger the decorators (e.g., @app.get) in the module
|
|
45
|
+
importlib.import_module(module_name)
|
|
46
|
+
print(f"Loaded {component}: {module_name}")
|
|
47
|
+
except Exception as e:
|
|
48
|
+
print(f"Failed to load {module_name}: {e}")
|
|
49
|
+
|
|
50
|
+
print(f"Pentagon: {len(app.router)} endpoints registered.")
|
|
51
|
+
""",
|
|
52
|
+
|
|
53
|
+
"README.md": """
|
|
54
|
+
# Pentagon Project
|
|
55
|
+
|
|
56
|
+
This is a project created using the Pentagon framework.
|
|
57
|
+
|
|
58
|
+
## Structure
|
|
59
|
+
- `/app`: Contains your application logic.
|
|
60
|
+
- `/database`: DB connection and session management.
|
|
61
|
+
- `/models`: SQLAlchemy models.
|
|
62
|
+
- `/router`: API endpoints.
|
|
63
|
+
- `/services`: Business logic.
|
|
64
|
+
- `/schemas`: Data validation schemas.
|
|
65
|
+
- `/utils`: Helper functions.
|
|
66
|
+
- `runserver.py`: Entry point for the application.
|
|
67
|
+
- `config.py`: Application configuration and component linker.
|
|
68
|
+
|
|
69
|
+
## How to use
|
|
70
|
+
1. Define your models in `app/models/`.
|
|
71
|
+
2. Define your routes in `app/router/`.
|
|
72
|
+
3. Run `python runserver.py` and everything will be auto-linked!
|
|
73
|
+
""",
|
|
74
|
+
|
|
75
|
+
"connection.py": """
|
|
76
|
+
from sqlalchemy import Column, Integer, String, create_engine
|
|
77
|
+
from sqlalchemy.ext.declarative import declarative_base
|
|
78
|
+
from sqlalchemy.orm import sessionmaker
|
|
79
|
+
import os
|
|
80
|
+
|
|
81
|
+
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./pentagon.db")
|
|
82
|
+
|
|
83
|
+
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False} if "sqlite" in DATABASE_URL else {})
|
|
84
|
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
|
85
|
+
|
|
86
|
+
Base = declarative_base()
|
|
87
|
+
|
|
88
|
+
def get_db():
|
|
89
|
+
db = SessionLocal()
|
|
90
|
+
try:
|
|
91
|
+
yield db
|
|
92
|
+
finally:
|
|
93
|
+
db.close()
|
|
94
|
+
|
|
95
|
+
def init_db():
|
|
96
|
+
Base.metadata.create_all(bind=engine)
|
|
97
|
+
""",
|
|
98
|
+
|
|
99
|
+
"app/__init__.py": """
|
|
100
|
+
# Application package root
|
|
101
|
+
""",
|
|
102
|
+
|
|
103
|
+
"database/__init__.py": """
|
|
104
|
+
from .connection import engine, Base, SessionLocal, get_db, init_db
|
|
105
|
+
""",
|
|
106
|
+
|
|
107
|
+
"models/__init__.py": """
|
|
108
|
+
# Import all models here or let config.py auto-load them from this directory
|
|
109
|
+
""",
|
|
110
|
+
|
|
111
|
+
"router/__init__.py": """
|
|
112
|
+
# Import all routers here or let config.py auto-load them from this directory
|
|
113
|
+
""",
|
|
114
|
+
|
|
115
|
+
"index.py": """
|
|
116
|
+
from runserver import app
|
|
117
|
+
from pentagon.utils import render_welcome
|
|
118
|
+
|
|
119
|
+
@app.get("/", group="General", description="Home page")
|
|
120
|
+
def index(request):
|
|
121
|
+
return render_welcome(app.title, app.description)
|
|
122
|
+
""",
|
|
123
|
+
|
|
124
|
+
"services/__init__.py": "# Business logic package",
|
|
125
|
+
"schemas/__init__.py": "# Validation schemas package",
|
|
126
|
+
"utils/__init__.py": "# Utility functions package"
|
|
127
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
from .response import Response
|
|
2
|
+
|
|
3
|
+
def render_docs(title, description, docs):
|
|
4
|
+
grouped_docs = {}
|
|
5
|
+
for path, meta in docs.items():
|
|
6
|
+
group = meta.get("group") or "General"
|
|
7
|
+
if group not in grouped_docs:
|
|
8
|
+
grouped_docs[group] = []
|
|
9
|
+
grouped_docs[group].append((path, meta))
|
|
10
|
+
|
|
11
|
+
sections = []
|
|
12
|
+
for group, items in grouped_docs.items():
|
|
13
|
+
rows = "".join(
|
|
14
|
+
f"""
|
|
15
|
+
<tr class="table-row">
|
|
16
|
+
<td class="code-cell"><code>{path}</code></td>
|
|
17
|
+
<td class="methods-cell">
|
|
18
|
+
<div class="methods-wrapper">
|
|
19
|
+
{', '.join(meta['methods'])}
|
|
20
|
+
</div>
|
|
21
|
+
</td>
|
|
22
|
+
<td class="desc-cell">{meta['description']}</td>
|
|
23
|
+
</tr>
|
|
24
|
+
"""
|
|
25
|
+
for path, meta in items
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
sections.append(f"""
|
|
29
|
+
<div class="group-section">
|
|
30
|
+
<h2 class="group-header">{group}</h2>
|
|
31
|
+
<div class="table-wrapper">
|
|
32
|
+
<table>
|
|
33
|
+
<thead>
|
|
34
|
+
<tr>
|
|
35
|
+
<th>Path</th>
|
|
36
|
+
<th>Methods</th>
|
|
37
|
+
<th>Description</th>
|
|
38
|
+
</tr>
|
|
39
|
+
</thead>
|
|
40
|
+
<tbody>
|
|
41
|
+
{rows}
|
|
42
|
+
</tbody>
|
|
43
|
+
</table>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
""")
|
|
47
|
+
|
|
48
|
+
all_sections = "\n".join(sections)
|
|
49
|
+
|
|
50
|
+
html = f"""
|
|
51
|
+
<!DOCTYPE html>
|
|
52
|
+
<html lang="en">
|
|
53
|
+
<head>
|
|
54
|
+
<meta charset="UTF-8">
|
|
55
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
56
|
+
<title>{title} Documentation</title>
|
|
57
|
+
|
|
58
|
+
<!-- Import Syne Font -->
|
|
59
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
60
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
61
|
+
|
|
62
|
+
<style>
|
|
63
|
+
/* Sleek Monochrome Theme Variables */
|
|
64
|
+
:root {{
|
|
65
|
+
--bg: #ffffff;
|
|
66
|
+
--text: #000000;
|
|
67
|
+
--border: rgba(0, 0, 0, 0.08);
|
|
68
|
+
--hover-bg: rgba(0, 0, 0, 0.02);
|
|
69
|
+
--code-bg: rgba(0, 0, 0, 0.04);
|
|
70
|
+
--invert-bg: #000000;
|
|
71
|
+
--invert-text: #ffffff;
|
|
72
|
+
}}
|
|
73
|
+
|
|
74
|
+
[data-theme="dark"] {{
|
|
75
|
+
--bg: #050505;
|
|
76
|
+
--text: #ffffff;
|
|
77
|
+
--border: rgba(255, 255, 255, 0.1);
|
|
78
|
+
--hover-bg: rgba(255, 255, 255, 0.03);
|
|
79
|
+
--code-bg: rgba(255, 255, 255, 0.06);
|
|
80
|
+
--invert-bg: #ffffff;
|
|
81
|
+
--invert-text: #000000;
|
|
82
|
+
}}
|
|
83
|
+
|
|
84
|
+
/* Reset & Typography */
|
|
85
|
+
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
|
|
86
|
+
body {{
|
|
87
|
+
font-family: sans-serif;
|
|
88
|
+
background-color: var(--bg);
|
|
89
|
+
color: var(--text);
|
|
90
|
+
transition: background-color 0.3s ease, color 0.3s ease;
|
|
91
|
+
line-height: 1.6;
|
|
92
|
+
-webkit-font-smoothing: antialiased;
|
|
93
|
+
-moz-osx-font-smoothing: grayscale;
|
|
94
|
+
}}
|
|
95
|
+
|
|
96
|
+
.container {{
|
|
97
|
+
max-width: 1000px;
|
|
98
|
+
margin: 0 auto;
|
|
99
|
+
padding: 80px 24px;
|
|
100
|
+
}}
|
|
101
|
+
|
|
102
|
+
/* Header */
|
|
103
|
+
header {{
|
|
104
|
+
display: flex;
|
|
105
|
+
justify-content: space-between;
|
|
106
|
+
align-items: center;
|
|
107
|
+
margin-bottom: 60px;
|
|
108
|
+
}}
|
|
109
|
+
.header-content h1 {{
|
|
110
|
+
font-size: 42px;
|
|
111
|
+
font-weight: 800;
|
|
112
|
+
letter-spacing: -0.02em;
|
|
113
|
+
margin-bottom: 8px;
|
|
114
|
+
}}
|
|
115
|
+
.header-content p {{
|
|
116
|
+
font-size: 18px;
|
|
117
|
+
font-weight: 500;
|
|
118
|
+
opacity: 0.6;
|
|
119
|
+
}}
|
|
120
|
+
|
|
121
|
+
/* Pill-shaped Theme Toggle */
|
|
122
|
+
button#theme-toggle {{
|
|
123
|
+
background: var(--invert-bg);
|
|
124
|
+
color: var(--invert-text);
|
|
125
|
+
border: none;
|
|
126
|
+
padding: 12px 24px;
|
|
127
|
+
font-family: 'Syne', sans-serif;
|
|
128
|
+
font-size: 14px;
|
|
129
|
+
font-weight: 700;
|
|
130
|
+
border-radius: 999px; /* Pill shape */
|
|
131
|
+
cursor: pointer;
|
|
132
|
+
transition: transform 0.2s ease, opacity 0.2s ease;
|
|
133
|
+
}}
|
|
134
|
+
|
|
135
|
+
.group-section {{
|
|
136
|
+
margin-bottom: 64px;
|
|
137
|
+
}}
|
|
138
|
+
|
|
139
|
+
/* Table Wrapper (Rounded Corners) */
|
|
140
|
+
h2.group-header {{
|
|
141
|
+
font-size: 24px;
|
|
142
|
+
font-weight: 700;
|
|
143
|
+
margin-bottom: 24px;
|
|
144
|
+
letter-spacing: -0.01em;
|
|
145
|
+
text-transform: capitalize;
|
|
146
|
+
}}
|
|
147
|
+
|
|
148
|
+
.table-wrapper {{
|
|
149
|
+
background-color: var(--bg);
|
|
150
|
+
border: 1px solid var(--border);
|
|
151
|
+
border-radius: 5px;
|
|
152
|
+
overflow: hidden;
|
|
153
|
+
}}
|
|
154
|
+
|
|
155
|
+
table {{
|
|
156
|
+
width: 100%;
|
|
157
|
+
border-collapse: collapse;
|
|
158
|
+
text-align: left;
|
|
159
|
+
}}
|
|
160
|
+
|
|
161
|
+
th, td {{
|
|
162
|
+
padding: 20px 24px;
|
|
163
|
+
border-bottom: 1px solid var(--border);
|
|
164
|
+
}}
|
|
165
|
+
|
|
166
|
+
/* Remove bottom border from last row */
|
|
167
|
+
tr:last-child td {{
|
|
168
|
+
border-bottom: none;
|
|
169
|
+
}}
|
|
170
|
+
|
|
171
|
+
th {{
|
|
172
|
+
font-weight: 600;
|
|
173
|
+
font-size: 14px;
|
|
174
|
+
opacity: 0.5;
|
|
175
|
+
text-transform: uppercase;
|
|
176
|
+
letter-spacing: 0.05em;
|
|
177
|
+
background-color: var(--hover-bg);
|
|
178
|
+
}}
|
|
179
|
+
|
|
180
|
+
.table-row {{
|
|
181
|
+
transition: background-color 0.2s ease;
|
|
182
|
+
}}
|
|
183
|
+
.table-row:hover {{
|
|
184
|
+
background-color: var(--hover-bg);
|
|
185
|
+
}}
|
|
186
|
+
|
|
187
|
+
/* Sleek Code Tags */
|
|
188
|
+
code {{
|
|
189
|
+
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
190
|
+
background-color: var(--code-bg);
|
|
191
|
+
padding: 6px 12px;
|
|
192
|
+
font-size: 14px;
|
|
193
|
+
border-radius: 8px; /* Rounded code blocks */
|
|
194
|
+
font-weight: 500;
|
|
195
|
+
}}
|
|
196
|
+
|
|
197
|
+
.methods-wrapper {{
|
|
198
|
+
display: inline-block;
|
|
199
|
+
border: 1px solid var(--border);
|
|
200
|
+
padding: 6px 16px;
|
|
201
|
+
border-radius: 999px; /* Pill-shaped methods */
|
|
202
|
+
font-size: 13px;
|
|
203
|
+
font-weight: 700;
|
|
204
|
+
}}
|
|
205
|
+
|
|
206
|
+
.code-cell {{ width: 35%; }}
|
|
207
|
+
.methods-cell {{ width: 25%; }}
|
|
208
|
+
.desc-cell {{ font-weight: 500; opacity: 0.8; }}
|
|
209
|
+
|
|
210
|
+
/* Mobile Responsiveness */
|
|
211
|
+
@media (max-width: 768px) {{
|
|
212
|
+
header {{
|
|
213
|
+
flex-direction: column;
|
|
214
|
+
align-items: flex-start;
|
|
215
|
+
gap: 24px;
|
|
216
|
+
}}
|
|
217
|
+
.container {{ padding: 40px 20px; }}
|
|
218
|
+
th, td {{ padding: 16px; }}
|
|
219
|
+
.code-cell, .methods-cell {{ width: auto; }}
|
|
220
|
+
}}
|
|
221
|
+
</style>
|
|
222
|
+
</head>
|
|
223
|
+
<body>
|
|
224
|
+
<div class="container">
|
|
225
|
+
<header>
|
|
226
|
+
<div class="header-content">
|
|
227
|
+
<h1>{title}</h1>
|
|
228
|
+
<p>{description}</p>
|
|
229
|
+
</div>
|
|
230
|
+
<button id="theme-toggle">Toggle Theme</button>
|
|
231
|
+
</header>
|
|
232
|
+
|
|
233
|
+
<main>
|
|
234
|
+
{all_sections}
|
|
235
|
+
</main>
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
<script>
|
|
239
|
+
const toggle = document.getElementById('theme-toggle');
|
|
240
|
+
|
|
241
|
+
// Check for user's saved preference or OS level preference
|
|
242
|
+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
243
|
+
const savedTheme = localStorage.getItem('pentagon-theme');
|
|
244
|
+
|
|
245
|
+
if (savedTheme === 'dark' || (!savedTheme && prefersDark)) {{
|
|
246
|
+
document.documentElement.setAttribute('data-theme', 'dark');
|
|
247
|
+
}}
|
|
248
|
+
|
|
249
|
+
// Handle toggle click
|
|
250
|
+
toggle.addEventListener('click', () => {{
|
|
251
|
+
const currentTheme = document.documentElement.getAttribute('data-theme');
|
|
252
|
+
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
|
253
|
+
document.documentElement.setAttribute('data-theme', newTheme);
|
|
254
|
+
localStorage.setItem('pentagon-theme', newTheme);
|
|
255
|
+
}});
|
|
256
|
+
</script>
|
|
257
|
+
</body>
|
|
258
|
+
</html>
|
|
259
|
+
"""
|
|
260
|
+
return Response(html, content_type="text/html")
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from wsgiref.simple_server import make_server
|
|
2
|
+
from .request import Request
|
|
3
|
+
from .response import Response
|
|
4
|
+
from .docs import render_docs
|
|
5
|
+
from .worker import ReloadHandler
|
|
6
|
+
from watchdog.observers import Observer
|
|
7
|
+
|
|
8
|
+
class Pentagon:
|
|
9
|
+
def __init__(self, title="Pentagon Framework", description="A web framework for Python."):
|
|
10
|
+
self.title = title
|
|
11
|
+
self.description = description
|
|
12
|
+
self.router = {}
|
|
13
|
+
self.docs = {}
|
|
14
|
+
|
|
15
|
+
self.router["/docs"] = self.docs_html
|
|
16
|
+
self.docs["/docs"] = {
|
|
17
|
+
"group": "General",
|
|
18
|
+
"methods": ["GET"],
|
|
19
|
+
"description": "Browsable API documentation"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
def docs_html(self, request: Request):
|
|
23
|
+
return render_docs(self.title, self.description, self.docs)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def __call__(self, environ, start_response):
|
|
27
|
+
request = Request(environ)
|
|
28
|
+
handler = self.router.get(request.path, self.not_found)
|
|
29
|
+
response = handler(request)
|
|
30
|
+
start_response(response.status, response.headers)
|
|
31
|
+
return response.body
|
|
32
|
+
|
|
33
|
+
def not_found(self, request: Request):
|
|
34
|
+
return Response("Not Found", status="404 Not Found")
|
|
35
|
+
|
|
36
|
+
def route(self, path: str, group: str=None, methods: list=["GET"], description: str=""):
|
|
37
|
+
def decorator(func):
|
|
38
|
+
self.router[path] = func
|
|
39
|
+
self.docs[path] = {
|
|
40
|
+
"group": group,
|
|
41
|
+
"methods": methods,
|
|
42
|
+
"description": description
|
|
43
|
+
}
|
|
44
|
+
return func
|
|
45
|
+
return decorator
|
|
46
|
+
|
|
47
|
+
def get(self, path: str, **kwargs):
|
|
48
|
+
return self.route(path, methods=["GET"], **kwargs)
|
|
49
|
+
|
|
50
|
+
def post(self, path: str, **kwargs):
|
|
51
|
+
return self.route(path, methods=["POST"], **kwargs)
|
|
52
|
+
|
|
53
|
+
def put(self, path: str, **kwargs):
|
|
54
|
+
return self.route(path, methods=["PUT"], **kwargs)
|
|
55
|
+
|
|
56
|
+
def delete(self, path: str, **kwargs):
|
|
57
|
+
return self.route(path, methods=["DELETE"], **kwargs)
|
|
58
|
+
|
|
59
|
+
def run(self, host="127.0.0.1", port=8000, watch=False):
|
|
60
|
+
if watch:
|
|
61
|
+
observer = Observer()
|
|
62
|
+
observer.schedule(ReloadHandler(), path=".", recursive=True)
|
|
63
|
+
observer.start()
|
|
64
|
+
print("File watcher active. Auto-reload on changes.")
|
|
65
|
+
|
|
66
|
+
with make_server(host, port, self) as httpd:
|
|
67
|
+
print(f"Serving on http://{host}:{port}")
|
|
68
|
+
try:
|
|
69
|
+
httpd.serve_forever()
|
|
70
|
+
except KeyboardInterrupt:
|
|
71
|
+
if watch:
|
|
72
|
+
observer.stop()
|
|
73
|
+
observer.join()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
class Request:
|
|
2
|
+
def __init__(self, environ):
|
|
3
|
+
self.method = environ['REQUEST_METHOD']
|
|
4
|
+
self.path = environ['PATH_INFO']
|
|
5
|
+
self.query_string = environ.get('QUERY_STRING', '')
|
|
6
|
+
self.headers = self.parse_headers(environ)
|
|
7
|
+
self.body = self.read_body(environ)
|
|
8
|
+
|
|
9
|
+
def parse_headers(self, environ):
|
|
10
|
+
headers = {}
|
|
11
|
+
for key, value in environ.items():
|
|
12
|
+
if key.startswith('HTTP_'):
|
|
13
|
+
header_name = key[5:].replace('_', '-').title()
|
|
14
|
+
headers[header_name] = value
|
|
15
|
+
return headers
|
|
16
|
+
|
|
17
|
+
def read_body(self, environ):
|
|
18
|
+
try:
|
|
19
|
+
content_length = int(environ.get('CONTENT_LENGTH', 0))
|
|
20
|
+
except (ValueError, TypeError):
|
|
21
|
+
content_length = 0
|
|
22
|
+
if content_length > 0:
|
|
23
|
+
return environ['wsgi.input'].read(content_length)
|
|
24
|
+
return b''
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
class Response:
|
|
4
|
+
def __init__(self, body, status="200 OK", content_type="text/plain"):
|
|
5
|
+
if isinstance(body, dict) or isinstance(body, list):
|
|
6
|
+
body = json.dumps(body)
|
|
7
|
+
content_type = content_type or "application/json"
|
|
8
|
+
elif isinstance(body, bytes):
|
|
9
|
+
content_type = content_type or "application/octet-stream"
|
|
10
|
+
elif isinstance(body, str):
|
|
11
|
+
content_type = content_type or "text/plain"
|
|
12
|
+
self.status = status
|
|
13
|
+
self.headers = [("Content-Type", content_type)]
|
|
14
|
+
if isinstance(body, str):
|
|
15
|
+
body = body.encode("utf-8")
|
|
16
|
+
self.body = [body]
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
from .response import Response
|
|
2
|
+
|
|
3
|
+
def render_welcome(title, description):
|
|
4
|
+
html = f"""
|
|
5
|
+
<!DOCTYPE html>
|
|
6
|
+
<html lang="en">
|
|
7
|
+
<head>
|
|
8
|
+
<meta charset="UTF-8">
|
|
9
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
10
|
+
<title>Welcome to {title}</title>
|
|
11
|
+
|
|
12
|
+
<!-- Import Syne Font -->
|
|
13
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
14
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
15
|
+
|
|
16
|
+
<style>
|
|
17
|
+
:root {{
|
|
18
|
+
--bg: #ffffff;
|
|
19
|
+
--text: #000000;
|
|
20
|
+
--border: rgba(0, 0, 0, 0.08);
|
|
21
|
+
--hover-bg: rgba(0, 0, 0, 0.02);
|
|
22
|
+
--invert-bg: #000000;
|
|
23
|
+
--invert-text: #ffffff;
|
|
24
|
+
}}
|
|
25
|
+
|
|
26
|
+
[data-theme="dark"] {{
|
|
27
|
+
--bg: #050505;
|
|
28
|
+
--text: #ffffff;
|
|
29
|
+
--border: rgba(255, 255, 255, 0.1);
|
|
30
|
+
--hover-bg: rgba(255, 255, 255, 0.03);
|
|
31
|
+
--invert-bg: #ffffff;
|
|
32
|
+
--invert-text: #000000;
|
|
33
|
+
}}
|
|
34
|
+
|
|
35
|
+
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
|
|
36
|
+
body {{
|
|
37
|
+
font-family: sans-serif;
|
|
38
|
+
background-color: var(--bg);
|
|
39
|
+
color: var(--text);
|
|
40
|
+
transition: background-color 0.3s ease, color 0.3s ease;
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
justify-content: center;
|
|
44
|
+
min-height: 100vh;
|
|
45
|
+
-webkit-font-smoothing: antialiased;
|
|
46
|
+
}}
|
|
47
|
+
|
|
48
|
+
.content {{
|
|
49
|
+
text-align: center;
|
|
50
|
+
max-width: 600px;
|
|
51
|
+
padding: 40px;
|
|
52
|
+
}}
|
|
53
|
+
|
|
54
|
+
h1 {{
|
|
55
|
+
font-size: 64px;
|
|
56
|
+
font-weight: 800;
|
|
57
|
+
letter-spacing: -0.04em;
|
|
58
|
+
margin-bottom: 16px;
|
|
59
|
+
}}
|
|
60
|
+
|
|
61
|
+
p {{
|
|
62
|
+
font-size: 20px;
|
|
63
|
+
opacity: 0.6;
|
|
64
|
+
margin-bottom: 48px;
|
|
65
|
+
line-height: 1.5;
|
|
66
|
+
}}
|
|
67
|
+
|
|
68
|
+
.actions {{
|
|
69
|
+
display: flex;
|
|
70
|
+
gap: 16px;
|
|
71
|
+
justify-content: center;
|
|
72
|
+
}}
|
|
73
|
+
|
|
74
|
+
.btn {{
|
|
75
|
+
text-decoration: none;
|
|
76
|
+
padding: 16px 32px;
|
|
77
|
+
border-radius: 999px;
|
|
78
|
+
font-weight: 700;
|
|
79
|
+
font-size: 16px;
|
|
80
|
+
transition: transform 0.2s ease, opacity 0.2s;
|
|
81
|
+
}}
|
|
82
|
+
|
|
83
|
+
.btn-primary {{
|
|
84
|
+
background-color: var(--invert-bg);
|
|
85
|
+
color: var(--invert-text);
|
|
86
|
+
}}
|
|
87
|
+
|
|
88
|
+
.btn-secondary {{
|
|
89
|
+
border: 1px solid var(--border);
|
|
90
|
+
color: var(--text);
|
|
91
|
+
}}
|
|
92
|
+
|
|
93
|
+
.btn:hover {{
|
|
94
|
+
transform: translateY(-2px);
|
|
95
|
+
opacity: 0.9;
|
|
96
|
+
}}
|
|
97
|
+
|
|
98
|
+
#theme-toggle {{
|
|
99
|
+
position: fixed;
|
|
100
|
+
top: 24px;
|
|
101
|
+
right: 24px;
|
|
102
|
+
background: none;
|
|
103
|
+
border: 1px solid var(--border);
|
|
104
|
+
color: var(--text);
|
|
105
|
+
padding: 8px 16px;
|
|
106
|
+
border-radius: 999px;
|
|
107
|
+
cursor: pointer;
|
|
108
|
+
font-weight: 600;
|
|
109
|
+
font-size: 12px;
|
|
110
|
+
}}
|
|
111
|
+
</style>
|
|
112
|
+
</head>
|
|
113
|
+
<body>
|
|
114
|
+
<button id="theme-toggle">Toggle Theme</button>
|
|
115
|
+
<div class="content">
|
|
116
|
+
<h1>{title}</h1>
|
|
117
|
+
<p>{description}</p>
|
|
118
|
+
<div class="actions">
|
|
119
|
+
<a href="/docs" class="btn btn-primary">View Documentation</a>
|
|
120
|
+
<a href="https://github.com" class="btn btn-secondary">GitHub</a>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<script>
|
|
125
|
+
const toggle = document.getElementById('theme-toggle');
|
|
126
|
+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
127
|
+
const savedTheme = localStorage.getItem('pentagon-theme');
|
|
128
|
+
|
|
129
|
+
if (savedTheme === 'dark' || (!savedTheme && prefersDark)) {{
|
|
130
|
+
document.documentElement.setAttribute('data-theme', 'dark');
|
|
131
|
+
}}
|
|
132
|
+
|
|
133
|
+
toggle.addEventListener('click', () => {{
|
|
134
|
+
const currentTheme = document.documentElement.getAttribute('data-theme');
|
|
135
|
+
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
|
136
|
+
document.documentElement.setAttribute('data-theme', newTheme);
|
|
137
|
+
localStorage.setItem('pentagon-theme', newTheme);
|
|
138
|
+
}});
|
|
139
|
+
</script>
|
|
140
|
+
</body>
|
|
141
|
+
</html>
|
|
142
|
+
"""
|
|
143
|
+
return Response(html, content_type="text/html")
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from watchdog.events import FileSystemEventHandler
|
|
2
|
+
import os, sys
|
|
3
|
+
|
|
4
|
+
class ReloadHandler(FileSystemEventHandler):
|
|
5
|
+
def on_modified(self, event):
|
|
6
|
+
if event.is_directory:
|
|
7
|
+
return
|
|
8
|
+
if event.src_path.endswith(".py"):
|
|
9
|
+
print(f"Reloading due to change in: {os.path.basename(event.src_path)}")
|
|
10
|
+
# Clear __pycache__ or other potential issues? Not really needed for execv
|
|
11
|
+
os.execv(sys.executable, [sys.executable] + sys.argv)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pentagon_framework
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A web framework for Python.
|
|
5
|
+
Author-email: Ahmed Abdelkhaliq <abdelkhaliqqq@gmail.com>
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.8
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: watchdog
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+

|
|
17
|
+
## Pentagon - A Lightweight Python Web Framework
|
|
18
|
+
|
|
19
|
+
Pentagon is a modern, high-performance, web framework for building APIs with Python.
|
|
20
|
+
## Features
|
|
21
|
+
|
|
22
|
+
- **Lighting Fast**: Very high performance.
|
|
23
|
+
- **Auto Documentation**: You register endpoints, Pentagon generates the documenations.
|
|
24
|
+
- **Easy**: Designed to be easy to use and learn, easy documentation.
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
Just run the pip command and you're all set!
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install pentagon
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
## Usage/Examples
|
|
37
|
+
|
|
38
|
+
Creating a project using pentagon
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
penta-admin init project --name "My Cool Project"
|
|
42
|
+
```
|
|
43
|
+
## Authors
|
|
44
|
+
|
|
45
|
+
- [@abde1khaliq](https://www.github.com/abde1khaliq)
|
|
46
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
pentagon/__init__.py
|
|
5
|
+
pentagon/docs.py
|
|
6
|
+
pentagon/pentagon.py
|
|
7
|
+
pentagon/request.py
|
|
8
|
+
pentagon/response.py
|
|
9
|
+
pentagon/utils.py
|
|
10
|
+
pentagon/worker.py
|
|
11
|
+
pentagon/cli/__init__.py
|
|
12
|
+
pentagon/cli/main.py
|
|
13
|
+
pentagon/cli/templates.py
|
|
14
|
+
pentagon_framework.egg-info/PKG-INFO
|
|
15
|
+
pentagon_framework.egg-info/SOURCES.txt
|
|
16
|
+
pentagon_framework.egg-info/dependency_links.txt
|
|
17
|
+
pentagon_framework.egg-info/entry_points.txt
|
|
18
|
+
pentagon_framework.egg-info/requires.txt
|
|
19
|
+
pentagon_framework.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
watchdog
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pentagon
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pentagon_framework"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Ahmed Abdelkhaliq", email="abdelkhaliqqq@gmail.com" },
|
|
10
|
+
]
|
|
11
|
+
description = "A web framework for Python."
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.8"
|
|
14
|
+
dependencies = [
|
|
15
|
+
"watchdog",
|
|
16
|
+
]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
penta-admin = "pentagon.cli.main:main"
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.packages.find]
|
|
27
|
+
where = ["."]
|
|
28
|
+
include = ["pentagon*"]
|