Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ docker-compose.override.yml

# Keep directory structures but ignore their contents
!examples/
examples/*/
# examples/*/
!benchmarks/
benchmarks/*/
!haske-core/
Expand Down
1 change: 1 addition & 0 deletions examples/api_app/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Example API app
1 change: 1 addition & 0 deletions examples/api_app/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Example models
2 changes: 2 additions & 0 deletions examples/api_app/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
starlette
sqlalchemy
186 changes: 186 additions & 0 deletions examples/blog_app/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# examples/blog_app/app.py
from haske import Haske, Request, Response, RedirectResponse
from haske.auth import AuthManager
from haske.templates import render_template_async
from models import Post, User

app = Haske(__name__)
auth = AuthManager("your-secret-key-here", session_expiry=3600)

# Create a sample user for demonstration
def create_sample_user():
if not User.get_by_username("admin"):
User.create("admin", "admin@example.com", "hashed_password")

# Helper function to check authentication
def check_auth(request: Request):
session = auth.get_session(request)
if not session:
return False, RedirectResponse('/login')
return True, session

# Routes
@app.route("/")
async def homepage(request: Request):
posts = Post.all()
return await render_template_async("index.html", posts=posts)

@app.route("/login", methods=["GET", "POST"])
async def login(request: Request):
if request.method == "GET":
return await render_template_async("login.html")

form_data = await request.form()
username = form_data.get("username")
password = form_data.get("password")

# Simple authentication for demo
if username == "admin" and password == "password":
user = User.get_by_username(username)
if user:
response = RedirectResponse("/")
auth.create_session(response, user.id, {"username": user.username})
return response

return await render_template_async("login.html", error="Invalid credentials")

@app.route("/logout")
async def logout(request: Request):
response = RedirectResponse("/")
auth.clear_session(response)
return response

# MOVE SPECIFIC ROUTES BEFORE PARAMETERIZED ROUTES
@app.route("/posts/create", methods=["GET","POST"])
async def create_post_form(request: Request):
# Check authentication
is_authenticated, response_or_session = check_auth(request)
if not is_authenticated:
return response_or_session
return await render_template_async("create.html")

@app.route("/posts", methods=["POST"])
async def create_post(request: Request):
# Check authentication
is_authenticated, response_or_session = check_auth(request)
if not is_authenticated:
return response_or_session

form_data = await request.form()
title = form_data.get("title")
content = form_data.get("content")

if not title or not content:
return await render_template_async("create.html", error="Title and content are required")

# For demo, use author_id=1 (admin)
post = Post.create(title, content, 1)

# Use status code 303 (See Other) for POST redirects
return RedirectResponse(f"/posts/{post.id}", status_code=303)

# Parameterized routes should come after specific routes
@app.route("/posts/{post_id}", methods=["GET"])
async def get_post(request: Request):
post_id = int(request.path_params.get("post_id"))
post = Post.get(post_id)
if not post:
return Response("Post not found", status_code=404)
return await render_template_async("post.html", post=post)

@app.route("/posts/{post_id}/edit", methods=["GET"])
async def edit_post_form(request: Request):
# Check authentication
is_authenticated, response_or_session = check_auth(request)
if not is_authenticated:
return response_or_session

post_id = int(request.path_params.get("post_id"))
post = Post.get(post_id)
if not post:
return Response("Post not found", status_code=404)
return await render_template_async("edit.html", post=post)

@app.route("/posts/{post_id}/update", methods=["POST"])
async def update_post(request: Request):
# Check authentication
is_authenticated, response_or_session = check_auth(request)
if not is_authenticated:
return response_or_session

post_id = int(request.path_params.get("post_id"))
form_data = await request.form()
title = form_data.get("title")
content = form_data.get("content")

post = Post.update(post_id, title, content)
if not post:
return Response("Post not found", status_code=404)
return RedirectResponse(f"/posts/{post.id}")

@app.route("/posts/{post_id}/delete", methods=["POST"])
async def delete_post(request: Request):
# Check authentication
is_authenticated, response_or_session = check_auth(request)
if not is_authenticated:
return response_or_session

post_id = int(request.path_params.get("post_id"))
success = Post.delete(post_id)
if not success:
return Response("Post not found", status_code=404)
return RedirectResponse("/")

# API endpoints
@app.route("/api/posts", methods=["GET"])
async def api_get_posts(request: Request):
posts = Post.all()
return Response.json([{
"id": post.id,
"title": post.title,
"content": post.content,
"author_id": post.author_id,
"created_at": post.created_at.isoformat(),
"updated_at": post.updated_at.isoformat()
} for post in posts])

@app.route("/api/posts/{post_id}", methods=["GET"])
async def api_get_post(request: Request):
post_id = int(request.path_params.get("post_id"))
post = Post.get(post_id)
if not post:
return Response.json({"error": "Post not found"}, status_code=404)
return Response.json({
"id": post.id,
"title": post.title,
"content": post.content,
"author_id": post.author_id,
"created_at": post.created_at.isoformat(),
"updated_at": post.updated_at.isoformat()
})

@app.route("/api/posts", methods=["POST"])
async def api_create_post(request: Request):
# Check authentication for API
is_authenticated, response_or_session = check_auth(request)
if not is_authenticated:
return Response.json({"error": "Authentication required"}, status_code=401)

data = await request.json()
post = Post.create(data["title"], data["content"], 1) # author_id=1 for demo
return Response.json({
"id": post.id,
"title": post.title,
"content": post.content,
"author_id": post.author_id
}, status_code=201)

if __name__ == "__main__":
# Create sample user
create_sample_user()

app.run(
host="0.0.0.0",
port=8000,
debug=True
)
94 changes: 94 additions & 0 deletions examples/blog_app/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# examples/blog_app/models.py
from datetime import datetime
from typing import Optional, List

# In-memory database for demonstration
posts_db = []
users_db = []

class User:
def __init__(self, id: int, username: str, email: str, password_hash: str):
self.id = id
self.username = username
self.email = email
self.password_hash = password_hash
self.created_at = datetime.now()

@classmethod
def get(cls, user_id: int) -> Optional['User']:
return next((user for user in users_db if user.id == user_id), None)

@classmethod
def get_by_username(cls, username: str) -> Optional['User']:
return next((user for user in users_db if user.username == username), None)

@classmethod
def create(cls, username: str, email: str, password_hash: str) -> 'User':
user_id = len(users_db) + 1
user = User(user_id, username, email, password_hash)
users_db.append(user)
return user

class Post:
def __init__(self, id: int, title: str, content: str, author_id: int):
self.id = id
self.title = title
self.content = content
self.author_id = author_id
self.created_at = datetime.now()
self.updated_at = datetime.now()

@classmethod
def all(cls) -> List['Post']:
return posts_db

@classmethod
def get(cls, post_id: int) -> Optional['Post']:
return next((post for post in posts_db if post.id == post_id), None)

@classmethod
def create(cls, title: str, content: str, author_id: int) -> 'Post':
post_id = len(posts_db) + 1
post = Post(post_id, title, content, author_id)
posts_db.append(post)
return post

@classmethod
def update(cls, post_id: int, title: str, content: str) -> Optional['Post']:
post = cls.get(post_id)
if post:
post.title = title
post.content = content
post.updated_at = datetime.now()
return post

@classmethod
def delete(cls, post_id: int) -> bool:
global posts_db
post = cls.get(post_id)
if post:
posts_db = [p for p in posts_db if p.id != post_id]
return True
return False

# Create sample data synchronously
def init_sample_data():
# Create sample user
if not users_db:
user = User.create("admin", "admin@example.com", "hashed_password")
users_db.append(user)

# Create sample posts
if not posts_db:
posts = [
("Welcome to Haske Blog", "This is the first post on our amazing blog platform built with Haske!"),
("Getting Started with Haske", "Learn how to build web applications with the Haske framework."),
("Building REST APIs", "A guide to creating RESTful APIs with Haske and Python.")
]

for title, content in posts:
post = Post.create(title, content, 1)
posts_db.append(post)

# Initialize sample data
init_sample_data()
46 changes: 46 additions & 0 deletions examples/blog_app/static/css/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/* examples/blog_app/static/css/style.css */
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
background-color: #f8f9fa;
}

.navbar-brand {
font-weight: bold;
font-size: 1.5rem;
}

.content {
font-size: 1.1rem;
line-height: 1.8;
}

.content p {
margin-bottom: 1rem;
}

.card {
border: none;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: box-shadow 0.3s ease;
}

.card:hover {
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}

.btn {
border-radius: 0.25rem;
}

.form-control {
border-radius: 0.25rem;
}

.alert {
border-radius: 0.25rem;
}

footer {
margin-top: auto;
}
37 changes: 37 additions & 0 deletions examples/blog_app/static/js/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// examples/blog_app/static/js/app.js
document.addEventListener('DOMContentLoaded', function() {
console.log('Haske Blog loaded successfully!');

// Add some interactive features
const deleteButtons = document.querySelectorAll('.btn-danger');
deleteButtons.forEach(button => {
button.addEventListener('click', function(e) {
if (!confirm('Are you sure you want to delete this post?')) {
e.preventDefault();
}
});
});

// Form validation
const forms = document.querySelectorAll('form');
forms.forEach(form => {
form.addEventListener('submit', function(e) {
const requiredFields = form.querySelectorAll('[required]');
let valid = true;

requiredFields.forEach(field => {
if (!field.value.trim()) {
valid = false;
field.classList.add('is-invalid');
} else {
field.classList.remove('is-invalid');
}
});

if (!valid) {
e.preventDefault();
alert('Please fill in all required fields.');
}
});
});
});
10 changes: 10 additions & 0 deletions examples/blog_app/templates/about.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<title>{{ app_name }}</title>
</head>
<body>
<h1>About {{ app_name }}</h1>
<p>This page is powered by Haske 🚀</p>
</body>
</html>
Loading