Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b481d17f2c | ||
|
|
9aafc547e6 | ||
|
|
033f97ca72 | ||
|
|
4954a3e613 | ||
|
|
00119ce463 | ||
|
|
58660ec8d4 |
@ -1,6 +1,6 @@
|
||||
FROM python:3.13-slim
|
||||
WORKDIR /app
|
||||
RUN pip install --no-cache-dir https://git.pyserve.org/Shifty/pyserveX/releases/download/v0.9.10/pyserve-0.9.10-py3-none-any.whl
|
||||
RUN pip install --no-cache-dir https://git.pyserve.org/aegis/pyserveX/releases/download/v0.9.10/pyserve-0.9.10-py3-none-any.whl
|
||||
COPY . /app/docs/
|
||||
COPY config.docs.yaml /app/config.docs.yaml
|
||||
EXPOSE 80
|
||||
|
||||
104
README.md
104
README.md
@ -1,3 +1,107 @@
|
||||
# docs.pyserve.org
|
||||
|
||||
This repository contains the source files for the documentation of the PyServe project, which can be found at [docs.pyserve.org](https://docs.pyserve.org).
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
docs/
|
||||
├── index.html # Main documentation page
|
||||
├── blog.html # Blog listing page
|
||||
├── blog-post.html # Individual blog post template
|
||||
├── style.css # Global styles with highlight.js integration
|
||||
├── getting-started/ # Getting started guides
|
||||
│ ├── index.html
|
||||
│ ├── installation.html
|
||||
│ └── quickstart.html
|
||||
├── guides/ # User guides
|
||||
│ ├── index.html
|
||||
│ ├── asgi-mount.html
|
||||
│ ├── configuration.html
|
||||
│ ├── process-orchestration.html
|
||||
│ ├── reverse-proxy.html
|
||||
│ └── routing.html
|
||||
├── reference/ # API reference
|
||||
│ ├── index.html
|
||||
│ ├── asgi-mount.html
|
||||
│ ├── cli.html
|
||||
│ └── extensions.html
|
||||
└── scripts/ # JavaScript utilities
|
||||
├── blog.js
|
||||
├── blog-post.js
|
||||
└── version-fetcher.js
|
||||
```
|
||||
|
||||
## Standard Page Template
|
||||
|
||||
All documentation pages follow this structure:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Page Title - pyserve</title>
|
||||
<link rel="stylesheet" href="../style.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
<script>hljs.highlightAll();</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
<div id="header">
|
||||
<h1>pyserve</h1>
|
||||
<div class="tagline">python application orchestrator</div>
|
||||
</div>
|
||||
|
||||
<div class="breadcrumb">
|
||||
<a href="../index.html">Home</a> / <a href="index.html">Section</a> / Page
|
||||
</div>
|
||||
|
||||
<div id="content">
|
||||
<!-- Page content here -->
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
## Breadcrumb Navigation Format
|
||||
|
||||
- Root pages: `Home / Section`
|
||||
- Subsection pages: `Home / Section / Page`
|
||||
- Always use `/` as separator
|
||||
- Link to parent section with `index.html`
|
||||
- Link to home with `../index.html` (or `index.html` from root)
|
||||
|
||||
## Code Highlighting
|
||||
|
||||
All pages use highlight.js for automatic syntax highlighting:
|
||||
|
||||
- **Theme**: `github-dark.min.css` (matches dark theme)
|
||||
- **Auto-initialization**: `hljs.highlightAll()` runs on page load
|
||||
- **Custom styles**: Enhanced color palette in `style.css`
|
||||
- **Languages detected**: YAML, Bash, Python, Plaintext
|
||||
|
||||
### Code Block Format
|
||||
|
||||
All code blocks use semantic language classes:
|
||||
|
||||
```html
|
||||
<pre><code class="language-yaml">
|
||||
server:
|
||||
host: 0.0.0.0
|
||||
port: 8080
|
||||
</code></pre>
|
||||
```
|
||||
|
||||
Supported languages:
|
||||
- `language-yaml` — YAML configuration files
|
||||
- `language-bash` — Shell commands and scripts
|
||||
- `language-python` — Python code examples
|
||||
- `language-plaintext` — Plain text output
|
||||
|
||||
## Deployment
|
||||
|
||||
Documentation is deployed via Gitea Actions (see `.gitea/workflows/deploy.yml`)
|
||||
|
||||
99
blog/blog-post.html
Normal file
99
blog/blog-post.html
Normal file
@ -0,0 +1,99 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title id="page-title">Post - pyserve</title>
|
||||
<link rel="stylesheet" href="../style.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/marked.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
<script src="scripts/blog-post.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
<div id="header">
|
||||
<h1>pyserve</h1>
|
||||
<div class="tagline">python application orchestrator</div>
|
||||
</div>
|
||||
|
||||
<div class="breadcrumb">
|
||||
<a href="../index.html">Home</a> / <a href="blog.html">Blog</a> / <span id="breadcrumb-title">Post</span>
|
||||
</div>
|
||||
|
||||
<div id="main-wrapper">
|
||||
<div id="content">
|
||||
<div id="post-loading" class="loading-state">
|
||||
<p>Loading post...</p>
|
||||
</div>
|
||||
|
||||
<article id="post-content" class="blog-post-full" style="display: none;">
|
||||
<div class="post-meta">
|
||||
<span class="post-date" id="post-date"></span>
|
||||
<span class="post-tag" id="post-tag"></span>
|
||||
</div>
|
||||
|
||||
<h1 id="post-title" class="post-title-full"></h1>
|
||||
|
||||
<div class="post-author-full" id="post-author-full" style="display: none;">
|
||||
<img src="" alt="" class="author-avatar-large" id="author-avatar">
|
||||
<div class="author-info-full">
|
||||
<span class="author-name-large" id="author-name"></span>
|
||||
<span class="author-login" id="author-login"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="post-body" class="post-body-full"></div>
|
||||
|
||||
<div class="post-footer">
|
||||
<a href="blog.html" class="back-link">← Back to Blog</a>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<div id="post-error" class="error-state" style="display: none;">
|
||||
<h2>Post Not Found</h2>
|
||||
<p>The requested blog post could not be found.</p>
|
||||
<a href="blog.html" class="back-link">← Back to Blog</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside id="sidebar">
|
||||
<div class="partner-block">
|
||||
<h3>Share</h3>
|
||||
<div style="margin-top: 15px;">
|
||||
<button onclick="copyLink()" class="share-button">📋 Copy Link</button>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 20px; padding-top: 15px; border-top: 1px solid #2a2a2a;">
|
||||
<h4 style="color: #3cb371; font-size: 13px; margin-bottom: 10px;">Navigation</h4>
|
||||
<ul class="plain" style="font-size: 12px;">
|
||||
<li><a href="blog.html">All Posts</a></li>
|
||||
<li><a href="index.html">Documentation</a></li>
|
||||
<li><a href="https://git.pyserve.org/Shifty/docs.pyserve.org" target="_blank">Repository</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<div id="footer">
|
||||
<p>pyserve © 2024-2025 | MIT License</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function copyLink() {
|
||||
navigator.clipboard.writeText(window.location.href).then(() => {
|
||||
const btn = event.target;
|
||||
const originalText = btn.textContent;
|
||||
btn.textContent = '✓ Copied!';
|
||||
btn.style.background = '#3cb371';
|
||||
setTimeout(() => {
|
||||
btn.textContent = originalText;
|
||||
btn.style.background = '';
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
65
blog/blog.html
Normal file
65
blog/blog.html
Normal file
@ -0,0 +1,65 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Blog - pyserve</title>
|
||||
<link rel="stylesheet" href="../style.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/marked.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
<script src="scripts/blog.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
<div id="header">
|
||||
<h1>pyserve</h1>
|
||||
<div class="tagline">python application orchestrator</div>
|
||||
</div>
|
||||
|
||||
<div class="breadcrumb">
|
||||
<a href="../index.html">Home</a> / Blog
|
||||
</div>
|
||||
|
||||
<div id="main-wrapper">
|
||||
<div id="content">
|
||||
<h2>Blog & Updates</h2>
|
||||
<p>Latest news, updates and technical articles about pyserve.</p>
|
||||
|
||||
<div id="blog-loading" class="loading-state">
|
||||
<p>Loading posts...</p>
|
||||
</div>
|
||||
|
||||
<div id="blog-posts" class="blog-posts"></div>
|
||||
|
||||
<div id="blog-error" class="error-state" style="display: none;">
|
||||
<p>Failed to load blog posts. Please try again later.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside id="sidebar">
|
||||
<div class="partner-block">
|
||||
<h3>About the Blog</h3>
|
||||
<p style="font-size: 12px; color: #999; line-height: 1.6;">
|
||||
Blog posts are powered by Git releases from
|
||||
<a href="https://git.pyserve.org/Shifty/docs.pyserve.org" target="_blank">docs.pyserve.org</a> repository.
|
||||
</p>
|
||||
|
||||
<div style="margin-top: 20px; padding-top: 15px; border-top: 1px solid #2a2a2a;">
|
||||
<h4 style="color: #3cb371; font-size: 13px; margin-bottom: 10px;">Categories</h4>
|
||||
<ul class="plain" style="font-size: 12px;">
|
||||
<li><a href="#releases">Releases</a></li>
|
||||
<li><a href="#tutorials">Tutorials</a></li>
|
||||
<li><a href="#updates">Updates</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<div id="footer">
|
||||
<p>pyserve © 2024-2025 | MIT License</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
179
blog/scripts/blog-post.js
Normal file
179
blog/scripts/blog-post.js
Normal file
@ -0,0 +1,179 @@
|
||||
// Blog Post Viewer
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
function getPostId() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return params.get('id');
|
||||
}
|
||||
|
||||
async function fetchPost(postId) {
|
||||
try {
|
||||
const response = await fetch(`/api/blog/post/${postId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn(`Post API request failed: ${response.status}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const post = await response.json();
|
||||
console.log(`✓ Fetched post: ${post.name}`);
|
||||
return post;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Post API fetch error:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date to readable string
|
||||
*/
|
||||
function formatDate(dateString) {
|
||||
const date = new Date(dateString);
|
||||
const options = { year: 'numeric', month: 'long', day: 'numeric' };
|
||||
return date.toLocaleDateString('en-US', options);
|
||||
}
|
||||
|
||||
function parseMarkdown(markdown) {
|
||||
if (!markdown) return '<p>No content available.</p>';
|
||||
|
||||
if (typeof marked !== 'undefined' && typeof hljs !== 'undefined') {
|
||||
marked.setOptions({
|
||||
highlight: function(code, lang) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
return hljs.highlight(code, { language: lang }).value;
|
||||
}
|
||||
return hljs.highlightAuto(code).value;
|
||||
},
|
||||
breaks: true,
|
||||
gfm: true
|
||||
});
|
||||
return marked.parse(markdown);
|
||||
}
|
||||
|
||||
let html = markdown
|
||||
.replace(/```(\w+)?\n([\s\S]*?)```/g, '<pre><code class="language-$1">$2</code></pre>')
|
||||
.replace(/^#### (.*$)/gim, '<h4>$1</h4>')
|
||||
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
|
||||
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
|
||||
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
|
||||
.replace(/^---$/gm, '<hr>')
|
||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
||||
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
||||
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>')
|
||||
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" style="max-width: 100%; height: auto;">')
|
||||
.replace(/^\* (.+)$/gm, '<li>$1</li>')
|
||||
.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>')
|
||||
.replace(/^\d+\. (.+)$/gm, '<li>$1</li>')
|
||||
.replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>')
|
||||
.split('\n\n')
|
||||
.map(para => {
|
||||
para = para.trim();
|
||||
if (para.match(/^<(h[1-6]|ul|ol|pre|blockquote|hr)/)) {
|
||||
return para;
|
||||
}
|
||||
return para ? `<p>${para}</p>` : '';
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render post to DOM
|
||||
*/
|
||||
function renderPost(post) {
|
||||
const loading = document.getElementById('post-loading');
|
||||
const content = document.getElementById('post-content');
|
||||
const error = document.getElementById('post-error');
|
||||
|
||||
if (!post) {
|
||||
loading.style.display = 'none';
|
||||
error.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
loading.style.display = 'none';
|
||||
content.style.display = 'block';
|
||||
|
||||
// Update page title
|
||||
document.getElementById('page-title').textContent = `${post.name} - pyserve`;
|
||||
document.getElementById('breadcrumb-title').textContent = post.name;
|
||||
|
||||
// Update meta
|
||||
const date = formatDate(post.published_at || post.created_at);
|
||||
document.getElementById('post-date').textContent = date;
|
||||
document.getElementById('post-tag').textContent = post.tag_name || 'post';
|
||||
|
||||
// Update title
|
||||
document.getElementById('post-title').textContent = post.name;
|
||||
|
||||
// Parse and render body
|
||||
const bodyHtml = parseMarkdown(post.body);
|
||||
document.getElementById('post-body').innerHTML = bodyHtml;
|
||||
|
||||
// Update author info
|
||||
if (post.author) {
|
||||
const authorSection = document.getElementById('post-author-full');
|
||||
const avatarImg = document.getElementById('author-avatar');
|
||||
const authorName = document.getElementById('author-name');
|
||||
const authorLogin = document.getElementById('author-login');
|
||||
|
||||
if (avatarImg) {
|
||||
avatarImg.src = post.author.avatar_url || 'https://via.placeholder.com/80';
|
||||
avatarImg.alt = post.author.full_name || post.author.login;
|
||||
}
|
||||
if (authorName) authorName.textContent = post.author.full_name || post.author.login;
|
||||
if (authorLogin) authorLogin.textContent = `@${post.author.login}`;
|
||||
|
||||
authorSection.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show error state
|
||||
*/
|
||||
function showError() {
|
||||
const loading = document.getElementById('post-loading');
|
||||
const error = document.getElementById('post-error');
|
||||
|
||||
loading.style.display = 'none';
|
||||
error.style.display = 'block';
|
||||
}
|
||||
|
||||
/**
|
||||
* Main initialization
|
||||
*/
|
||||
async function init() {
|
||||
const postId = getPostId();
|
||||
|
||||
if (!postId) {
|
||||
showError();
|
||||
return;
|
||||
}
|
||||
|
||||
const post = await fetchPost(postId);
|
||||
|
||||
if (post) {
|
||||
renderPost(post);
|
||||
} else {
|
||||
showError();
|
||||
}
|
||||
}
|
||||
|
||||
// Start when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
})();
|
||||
218
blog/scripts/blog.js
Normal file
218
blog/scripts/blog.js
Normal file
@ -0,0 +1,218 @@
|
||||
// Blog System - Powered by Gitea Releases
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const API_URL = '/api/blog/posts';
|
||||
const CACHE_KEY = 'pyserve_blog_cache';
|
||||
const CACHE_DURATION = 1800000;
|
||||
|
||||
async function fetchBlogPosts() {
|
||||
try {
|
||||
const response = await fetch(API_URL, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn(`Blog API request failed: ${response.status}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const posts = await response.json();
|
||||
console.log(`✓ Fetched ${posts.length} blog posts`);
|
||||
return posts;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Blog API fetch error:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getCachedPosts() {
|
||||
try {
|
||||
const cached = localStorage.getItem(CACHE_KEY);
|
||||
if (!cached) return null;
|
||||
|
||||
const data = JSON.parse(cached);
|
||||
const now = Date.now();
|
||||
|
||||
if (now - data.timestamp < CACHE_DURATION) {
|
||||
return data.posts;
|
||||
}
|
||||
|
||||
localStorage.removeItem(CACHE_KEY);
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Cache read error:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function cachePosts(posts) {
|
||||
try {
|
||||
const data = {
|
||||
posts: posts,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
localStorage.setItem(CACHE_KEY, JSON.stringify(data));
|
||||
} catch (error) {
|
||||
console.error('Cache write error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(dateString) {
|
||||
const date = new Date(dateString);
|
||||
const options = { year: 'numeric', month: 'long', day: 'numeric' };
|
||||
return date.toLocaleDateString('en-US', options);
|
||||
}
|
||||
|
||||
function parseMarkdown(markdown) {
|
||||
if (!markdown) return '';
|
||||
|
||||
if (typeof marked !== 'undefined' && typeof hljs !== 'undefined') {
|
||||
marked.setOptions({
|
||||
highlight: function(code, lang) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
return hljs.highlight(code, { language: lang }).value;
|
||||
}
|
||||
return hljs.highlightAuto(code).value;
|
||||
},
|
||||
breaks: true,
|
||||
gfm: true
|
||||
});
|
||||
return marked.parse(markdown);
|
||||
}
|
||||
|
||||
let html = markdown
|
||||
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
|
||||
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
|
||||
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
|
||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
||||
.replace(/```(\w+)?\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>')
|
||||
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
||||
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>')
|
||||
.replace(/\n\n/g, '</p><p>')
|
||||
.replace(/\n/g, '<br>');
|
||||
|
||||
return '<p>' + html + '</p>';
|
||||
}
|
||||
|
||||
function extractExcerpt(markdown, maxLength = 200) {
|
||||
if (!markdown) return 'No description available.';
|
||||
|
||||
// Remove markdown formatting and get first paragraph
|
||||
const plain = markdown
|
||||
.replace(/^#+\s+.*/gm, '') // Remove headers
|
||||
.replace(/\*\*(.+?)\*\*/g, '$1') // Remove bold
|
||||
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Remove links
|
||||
.replace(/```[\s\S]*?```/g, '') // Remove code blocks
|
||||
.replace(/`([^`]+)`/g, '$1') // Remove inline code
|
||||
.trim();
|
||||
|
||||
const firstParagraph = plain.split('\n\n')[0] || plain.split('\n')[0];
|
||||
|
||||
if (firstParagraph.length > maxLength) {
|
||||
return firstParagraph.substring(0, maxLength) + '...';
|
||||
}
|
||||
|
||||
return firstParagraph || 'No description available.';
|
||||
}
|
||||
|
||||
function createPostCard(post) {
|
||||
const excerpt = extractExcerpt(post.body);
|
||||
const date = formatDate(post.published_at || post.created_at);
|
||||
const tag = post.tag_name || 'post';
|
||||
const author = post.author || {};
|
||||
const authorName = author.full_name || author.login || 'Unknown';
|
||||
const authorLogin = author.login || '';
|
||||
const authorAvatar = author.avatar_url || '';
|
||||
|
||||
return `
|
||||
<article class="blog-post-card">
|
||||
<div class="post-meta">
|
||||
<span class="post-date">${date}</span>
|
||||
<span class="post-tag">${tag}</span>
|
||||
</div>
|
||||
<h3 class="post-title">
|
||||
<a href="blog-post.html?id=${tag}">${post.name || tag}</a>
|
||||
</h3>
|
||||
<div class="post-excerpt">${excerpt}</div>
|
||||
<div class="post-author">
|
||||
<img src="${authorAvatar}" alt="${authorName}" class="author-avatar" onerror="this.style.display='none'">
|
||||
<div class="author-info">
|
||||
<span class="author-name">${authorName}</span>
|
||||
${authorLogin ? `<span class="author-login">@${authorLogin}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<a href="blog-post.html?id=${tag}" class="read-more">Read more →</a>
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderPosts(posts) {
|
||||
const container = document.getElementById('blog-posts');
|
||||
const loading = document.getElementById('blog-loading');
|
||||
const error = document.getElementById('blog-error');
|
||||
|
||||
if (!container) return;
|
||||
|
||||
loading.style.display = 'none';
|
||||
|
||||
if (!posts || posts.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="note">
|
||||
<strong>No posts yet</strong>
|
||||
<p>Check back soon for updates and articles!</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort by date (newest first)
|
||||
const sortedPosts = posts.sort((a, b) => {
|
||||
const dateA = new Date(a.published_at || a.created_at);
|
||||
const dateB = new Date(b.published_at || b.created_at);
|
||||
return dateB - dateA;
|
||||
});
|
||||
|
||||
// Render posts
|
||||
container.innerHTML = sortedPosts.map(post => createPostCard(post)).join('');
|
||||
}
|
||||
|
||||
function showError() {
|
||||
const loading = document.getElementById('blog-loading');
|
||||
const error = document.getElementById('blog-error');
|
||||
|
||||
if (loading) loading.style.display = 'none';
|
||||
if (error) error.style.display = 'block';
|
||||
}
|
||||
|
||||
async function init() {
|
||||
// Try cached posts first
|
||||
const cachedPosts = getCachedPosts();
|
||||
if (cachedPosts) {
|
||||
renderPosts(cachedPosts);
|
||||
}
|
||||
|
||||
// Fetch fresh posts in background
|
||||
const posts = await fetchBlogPosts();
|
||||
if (posts) {
|
||||
cachePosts(posts);
|
||||
renderPosts(posts);
|
||||
} else if (!cachedPosts) {
|
||||
showError();
|
||||
}
|
||||
}
|
||||
|
||||
// Start when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
})();
|
||||
@ -22,7 +22,19 @@ extensions:
|
||||
config:
|
||||
regex_locations:
|
||||
"=/api/version":
|
||||
proxy_pass: "https://git.pyserve.org/api/v1/repos/Shifty/pyserveX/releases/latest"
|
||||
proxy_pass: "https://git.pyserve.org/api/v1/repos/aegis/pyserveX/releases/latest"
|
||||
headers:
|
||||
- "X-Forwarded-For: $remote_addr"
|
||||
- "X-Real-IP: $remote_addr"
|
||||
|
||||
"=/api/blog/posts":
|
||||
proxy_pass: "https://git.pyserve.org/api/v1/repos/aegis/docs.pyserve.org/releases"
|
||||
headers:
|
||||
- "X-Forwarded-For: $remote_addr"
|
||||
- "X-Real-IP: $remote_addr"
|
||||
|
||||
"~^/api/blog/post/(?P<tag>.+)$":
|
||||
proxy_pass: "https://git.pyserve.org/api/v1/repos/aegis/docs.pyserve.org/releases/tags/{tag}"
|
||||
headers:
|
||||
- "X-Forwarded-For: $remote_addr"
|
||||
- "X-Real-IP: $remote_addr"
|
||||
|
||||
@ -5,6 +5,9 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Getting Started - pyserve</title>
|
||||
<link rel="stylesheet" href="../style.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
<script>hljs.highlightAll();</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
@ -14,7 +17,7 @@
|
||||
</div>
|
||||
|
||||
<div class="breadcrumb">
|
||||
<a href="../index.html">pyserve</a> » Getting Started
|
||||
<a href="../index.html">Home</a> / Getting Started
|
||||
</div>
|
||||
|
||||
<div id="content">
|
||||
|
||||
@ -5,6 +5,9 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Installation - pyserve</title>
|
||||
<link rel="stylesheet" href="../style.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
<script>hljs.highlightAll();</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
@ -14,7 +17,7 @@
|
||||
</div>
|
||||
|
||||
<div class="breadcrumb">
|
||||
<a href="../index.html">pyserve</a> » <a href="/getting-started/">Getting Started</a> » Installation
|
||||
<a href="../index.html">Home</a> / <a href="index.html">Getting Started</a> / Installation
|
||||
</div>
|
||||
|
||||
<div id="content">
|
||||
@ -29,32 +32,32 @@
|
||||
<h3>Install from Release (Recommended)</h3>
|
||||
<p>Download the latest wheel file from <a href="https://git.pyserve.org/Shifty/pyserveX/releases">Git Releases</a> and install it:</p>
|
||||
|
||||
<pre><span class="comment"># Download the wheel file from releases</span>
|
||||
<span class="comment"># Example: pyserve-0.7.0-py3-none-any.whl</span>
|
||||
<pre><code class="language-bash"># Download the wheel file from releases
|
||||
# Example: pyserve-0.7.0-py3-none-any.whl
|
||||
|
||||
pip install pyserve-0.7.0-py3-none-any.whl</pre>
|
||||
pip install pyserve-0.7.0-py3-none-any.whl</code></pre>
|
||||
|
||||
<p>After installation, the <code>pyserve</code> command will be available in your terminal:</p>
|
||||
<pre>pyserve --version</pre>
|
||||
<pre><code class="language-bash">pyserve --version</code></pre>
|
||||
|
||||
<h3>Install from Source</h3>
|
||||
<p>For development or if you want the latest changes:</p>
|
||||
|
||||
<pre><span class="comment"># Clone the repository</span>
|
||||
<pre><code class="language-bash"># Clone the repository
|
||||
git clone https://github.com/ShiftyX1/PyServe.git
|
||||
cd PyServe
|
||||
|
||||
<span class="comment"># Install with Poetry (recommended for development)</span>
|
||||
# Install with Poetry (recommended for development)
|
||||
make init
|
||||
|
||||
<span class="comment"># Or build and install the package</span>
|
||||
# Or build and install the package
|
||||
make build
|
||||
pip install dist/pyserve-*.whl</pre>
|
||||
pip install dist/pyserve-*.whl</code></pre>
|
||||
|
||||
<h3>Verify Installation</h3>
|
||||
<p>Check that pyserve is installed correctly:</p>
|
||||
<pre>pyserve --version
|
||||
<span class="comment"># Output: pyserve 0.7.0</span></pre>
|
||||
<pre><code class="language-bash">pyserve --version
|
||||
# Output: pyserve 0.7.0</code></pre>
|
||||
|
||||
<h3>Dependencies</h3>
|
||||
<p>pyserve automatically installs the following dependencies:</p>
|
||||
|
||||
@ -5,6 +5,9 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Quick Start - pyserve</title>
|
||||
<link rel="stylesheet" href="../style.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
<script>hljs.highlightAll();</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
@ -14,7 +17,7 @@
|
||||
</div>
|
||||
|
||||
<div class="breadcrumb">
|
||||
<a href="../index.html">pyserve</a> » <a href="/getting-started/">Getting Started</a> » Quick Start
|
||||
<a href="../index.html">Home</a> / <a href="index.html">Getting Started</a> / Quick Start
|
||||
</div>
|
||||
|
||||
<div id="content">
|
||||
@ -25,37 +28,37 @@
|
||||
<h3>1. Create Configuration File</h3>
|
||||
<p>Create a file named <code>config.yaml</code> in your project directory:</p>
|
||||
|
||||
<pre><span class="directive">http:</span>
|
||||
<span class="directive">static_dir:</span> <span class="value">./static</span>
|
||||
<span class="directive">templates_dir:</span> <span class="value">./templates</span>
|
||||
<pre><code class="language-yaml">http:
|
||||
static_dir: ./static
|
||||
templates_dir: ./templates
|
||||
|
||||
<span class="directive">server:</span>
|
||||
<span class="directive">host:</span> <span class="value">0.0.0.0</span>
|
||||
<span class="directive">port:</span> <span class="value">8080</span>
|
||||
server:
|
||||
host: 0.0.0.0
|
||||
port: 8080
|
||||
|
||||
<span class="directive">logging:</span>
|
||||
<span class="directive">level:</span> <span class="value">INFO</span>
|
||||
<span class="directive">console_output:</span> <span class="value">true</span>
|
||||
logging:
|
||||
level: INFO
|
||||
console_output: true
|
||||
|
||||
<span class="directive">extensions:</span>
|
||||
- <span class="directive">type:</span> <span class="value">routing</span>
|
||||
<span class="directive">config:</span>
|
||||
<span class="directive">regex_locations:</span>
|
||||
<span class="value">"__default__"</span>:
|
||||
<span class="directive">root:</span> <span class="value">"./static"</span>
|
||||
<span class="directive">index_file:</span> <span class="value">"index.html"</span></pre>
|
||||
extensions:
|
||||
- type: routing
|
||||
config:
|
||||
regex_locations:
|
||||
"__default__":
|
||||
root: "./static"
|
||||
index_file: "index.html"</code></pre>
|
||||
|
||||
<h3>2. Create Static Directory</h3>
|
||||
<p>Create a <code>static</code> folder and add an <code>index.html</code>:</p>
|
||||
|
||||
<pre>mkdir -p static
|
||||
echo '<h1>Hello from pyserve!</h1>' > static/index.html</pre>
|
||||
<pre><code class="language-bash">mkdir -p static
|
||||
echo '<h1>Hello from pyserve!</h1>' > static/index.html</code></pre>
|
||||
|
||||
<h3>3. Start the Server</h3>
|
||||
<pre>pyserve</pre>
|
||||
<pre><code class="language-bash">pyserve</code></pre>
|
||||
|
||||
<p>You should see output like:</p>
|
||||
<pre>Starting PyServe server on 0.0.0.0:8080</pre>
|
||||
<pre><code class="language-plaintext">Starting PyServe server on 0.0.0.0:8080</code></pre>
|
||||
|
||||
<h3>4. Open in Browser</h3>
|
||||
<p>Navigate to <a href="http://localhost:8080">http://localhost:8080</a> — you should see your page!</p>
|
||||
@ -63,44 +66,44 @@ echo '<h1>Hello from pyserve!</h1>' > static/index.html</pre>
|
||||
<h3>Using CLI Options</h3>
|
||||
<p>Override configuration via command line:</p>
|
||||
|
||||
<pre><span class="comment"># Use a different config file</span>
|
||||
<pre><code class="language-bash"># Use a different config file
|
||||
pyserve -c /path/to/config.yaml
|
||||
|
||||
<span class="comment"># Override host and port</span>
|
||||
# Override host and port
|
||||
pyserve --host 127.0.0.1 --port 9000
|
||||
|
||||
<span class="comment"># Enable debug mode (verbose logging)</span>
|
||||
pyserve --debug</pre>
|
||||
# Enable debug mode (verbose logging)
|
||||
pyserve --debug</code></pre>
|
||||
|
||||
<h3>Example: Serve Documentation</h3>
|
||||
<p>Serve a documentation directory with proper caching:</p>
|
||||
|
||||
<pre><span class="directive">http:</span>
|
||||
<span class="directive">static_dir:</span> <span class="value">./docs</span>
|
||||
<pre><code class="language-yaml">http:
|
||||
static_dir: ./docs
|
||||
|
||||
<span class="directive">server:</span>
|
||||
<span class="directive">host:</span> <span class="value">0.0.0.0</span>
|
||||
<span class="directive">port:</span> <span class="value">8000</span>
|
||||
server:
|
||||
host: 0.0.0.0
|
||||
port: 8000
|
||||
|
||||
<span class="directive">extensions:</span>
|
||||
- <span class="directive">type:</span> <span class="value">routing</span>
|
||||
<span class="directive">config:</span>
|
||||
<span class="directive">regex_locations:</span>
|
||||
<span class="value">"=/"</span>:
|
||||
<span class="directive">root:</span> <span class="value">"./docs"</span>
|
||||
<span class="directive">index_file:</span> <span class="value">"index.html"</span>
|
||||
extensions:
|
||||
- type: routing
|
||||
config:
|
||||
regex_locations:
|
||||
"=/":
|
||||
root: "./docs"
|
||||
index_file: "index.html"
|
||||
|
||||
<span class="value">"~*\\.(css|js)$"</span>:
|
||||
<span class="directive">root:</span> <span class="value">"./docs"</span>
|
||||
<span class="directive">cache_control:</span> <span class="value">"public, max-age=3600"</span>
|
||||
"~*\\.(css|js)$":
|
||||
root: "./docs"
|
||||
cache_control: "public, max-age=3600"
|
||||
|
||||
<span class="value">"~*\\.html$"</span>:
|
||||
<span class="directive">root:</span> <span class="value">"./docs"</span>
|
||||
<span class="directive">cache_control:</span> <span class="value">"no-cache"</span>
|
||||
"~*\\.html$":
|
||||
root: "./docs"
|
||||
cache_control: "no-cache"
|
||||
|
||||
<span class="value">"__default__"</span>:
|
||||
<span class="directive">root:</span> <span class="value">"./docs"</span>
|
||||
<span class="directive">index_file:</span> <span class="value">"index.html"</span></pre>
|
||||
"__default__":
|
||||
root: "./docs"
|
||||
index_file: "index.html"</code></pre>
|
||||
|
||||
<div class="note">
|
||||
<strong>Next Steps:</strong>
|
||||
|
||||
1
google5fb264e861dd30d7.html
Normal file
1
google5fb264e861dd30d7.html
Normal file
@ -0,0 +1 @@
|
||||
google-site-verification: google5fb264e861dd30d7.html
|
||||
@ -5,6 +5,9 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ASGI Mounting - pyserve</title>
|
||||
<link rel="stylesheet" href="../style.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
<script>hljs.highlightAll();</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
@ -14,7 +17,7 @@
|
||||
</div>
|
||||
|
||||
<div class="breadcrumb">
|
||||
<a href="../index.html">pyserve</a> » <a href="index.html">Guides</a> » ASGI Mounting
|
||||
<a href="../index.html">Home</a> / <a href="index.html">Guides</a> / ASGI Mounting
|
||||
</div>
|
||||
|
||||
<div id="content">
|
||||
@ -42,15 +45,15 @@
|
||||
<h3>Configuration</h3>
|
||||
<p>ASGI applications are mounted via the <code>asgi</code> extension:</p>
|
||||
|
||||
<pre><span class="directive">extensions:</span>
|
||||
- <span class="directive">type:</span> <span class="value">asgi</span>
|
||||
<span class="directive">config:</span>
|
||||
<span class="directive">mounts:</span>
|
||||
- <span class="directive">path:</span> <span class="value">"/api"</span>
|
||||
<span class="directive">app_path:</span> <span class="value">"myapp.api:app"</span>
|
||||
<span class="directive">app_type:</span> <span class="value">asgi</span>
|
||||
<span class="directive">name:</span> <span class="value">"api-app"</span>
|
||||
<span class="directive">strip_path:</span> <span class="value">true</span></pre>
|
||||
<pre><code class="language-yaml">extensions:
|
||||
- type: asgi
|
||||
config:
|
||||
mounts:
|
||||
- path: "/api"
|
||||
app_path: "myapp.api:app"
|
||||
app_type: asgi
|
||||
name: "api-app"
|
||||
strip_path: true</code></pre>
|
||||
|
||||
<h3>Mount Configuration Options</h3>
|
||||
<dl>
|
||||
@ -82,23 +85,23 @@
|
||||
<h3>Mounting FastAPI</h3>
|
||||
<p>FastAPI applications are native ASGI:</p>
|
||||
|
||||
<pre><span class="comment"># myapp/api.py</span>
|
||||
<pre><code class="language-python"># myapp/api.py
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/users")
|
||||
async def get_users():
|
||||
return [{"id": 1, "name": "Alice"}]</pre>
|
||||
return [{"id": 1, "name": "Alice"}]</code></pre>
|
||||
|
||||
<pre><span class="directive">extensions:</span>
|
||||
- <span class="directive">type:</span> <span class="value">asgi</span>
|
||||
<span class="directive">config:</span>
|
||||
<span class="directive">mounts:</span>
|
||||
- <span class="directive">path:</span> <span class="value">"/api"</span>
|
||||
<span class="directive">app_path:</span> <span class="value">"myapp.api:app"</span>
|
||||
<span class="directive">app_type:</span> <span class="value">asgi</span>
|
||||
<span class="directive">name:</span> <span class="value">"fastapi-app"</span></pre>
|
||||
<pre><code class="language-yaml">extensions:
|
||||
- type: asgi
|
||||
config:
|
||||
mounts:
|
||||
- path: "/api"
|
||||
app_path: "myapp.api:app"
|
||||
app_type: asgi
|
||||
name: "fastapi-app"</code></pre>
|
||||
|
||||
<p>With this configuration:</p>
|
||||
<ul class="indent">
|
||||
@ -109,23 +112,23 @@ async def get_users():
|
||||
<h3>Mounting Flask</h3>
|
||||
<p>Flask applications are WSGI and will be automatically wrapped:</p>
|
||||
|
||||
<pre><span class="comment"># myapp/flask_api.py</span>
|
||||
<pre><code class="language-python"># myapp/flask_api.py
|
||||
from flask import Flask
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route("/hello")
|
||||
def hello():
|
||||
return {"message": "Hello from Flask!"}</pre>
|
||||
return {"message": "Hello from Flask!"}</code></pre>
|
||||
|
||||
<pre><span class="directive">extensions:</span>
|
||||
- <span class="directive">type:</span> <span class="value">asgi</span>
|
||||
<span class="directive">config:</span>
|
||||
<span class="directive">mounts:</span>
|
||||
- <span class="directive">path:</span> <span class="value">"/flask"</span>
|
||||
<span class="directive">app_path:</span> <span class="value">"myapp.flask_api:app"</span>
|
||||
<span class="directive">app_type:</span> <span class="value">wsgi</span>
|
||||
<span class="directive">name:</span> <span class="value">"flask-app"</span></pre>
|
||||
<pre><code class="language-yaml">extensions:
|
||||
- type: asgi
|
||||
config:
|
||||
mounts:
|
||||
- path: "/flask"
|
||||
app_path: "myapp.flask_api:app"
|
||||
app_type: wsgi
|
||||
name: "flask-app"</code></pre>
|
||||
|
||||
<div class="note">
|
||||
<strong>Note:</strong> WSGI wrapping requires either <code>a2wsgi</code> or <code>asgiref</code>
|
||||
@ -135,19 +138,19 @@ def hello():
|
||||
<h3>Mounting Django</h3>
|
||||
<p>Django can be mounted using its ASGI application:</p>
|
||||
|
||||
<pre><span class="directive">extensions:</span>
|
||||
- <span class="directive">type:</span> <span class="value">asgi</span>
|
||||
<span class="directive">config:</span>
|
||||
<span class="directive">mounts:</span>
|
||||
- <span class="directive">path:</span> <span class="value">"/django"</span>
|
||||
<span class="directive">django_settings:</span> <span class="value">"myproject.settings"</span>
|
||||
<span class="directive">module_path:</span> <span class="value">"/path/to/django/project"</span>
|
||||
<span class="directive">name:</span> <span class="value">"django-app"</span></pre>
|
||||
<pre><code class="language-yaml">extensions:
|
||||
- type: asgi
|
||||
config:
|
||||
mounts:
|
||||
- path: "/django"
|
||||
django_settings: "myproject.settings"
|
||||
module_path: "/path/to/django/project"
|
||||
name: "django-app"</code></pre>
|
||||
|
||||
<h3>Factory Pattern</h3>
|
||||
<p>Use factory functions to create apps with custom configuration:</p>
|
||||
|
||||
<pre><span class="comment"># myapp/api.py</span>
|
||||
<pre><code class="language-python"># myapp/api.py
|
||||
from fastapi import FastAPI
|
||||
|
||||
def create_app(debug: bool = False, prefix: str = "/v1") -> FastAPI:
|
||||
@ -157,19 +160,19 @@ def create_app(debug: bool = False, prefix: str = "/v1") -> FastAPI:
|
||||
async def status():
|
||||
return {"debug": debug}
|
||||
|
||||
return app</pre>
|
||||
return app</code></pre>
|
||||
|
||||
<pre><span class="directive">extensions:</span>
|
||||
- <span class="directive">type:</span> <span class="value">asgi</span>
|
||||
<span class="directive">config:</span>
|
||||
<span class="directive">mounts:</span>
|
||||
- <span class="directive">path:</span> <span class="value">"/api"</span>
|
||||
<span class="directive">app_path:</span> <span class="value">"myapp.api:create_app"</span>
|
||||
<span class="directive">app_type:</span> <span class="value">asgi</span>
|
||||
<span class="directive">factory:</span> <span class="value">true</span>
|
||||
<span class="directive">factory_args:</span>
|
||||
<span class="directive">debug:</span> <span class="value">true</span>
|
||||
<span class="directive">prefix:</span> <span class="value">"/v2"</span></pre>
|
||||
<pre><code class="language-yaml">extensions:
|
||||
- type: asgi
|
||||
config:
|
||||
mounts:
|
||||
- path: "/api"
|
||||
app_path: "myapp.api:create_app"
|
||||
app_type: asgi
|
||||
factory: true
|
||||
factory_args:
|
||||
debug: true
|
||||
prefix: "/v2"</code></pre>
|
||||
|
||||
<h3>Path Stripping</h3>
|
||||
<p>By default, <code>strip_path: true</code> removes the mount prefix from requests:</p>
|
||||
@ -195,31 +198,31 @@ def create_app(debug: bool = False, prefix: str = "/v1") -> FastAPI:
|
||||
<h3>Multiple Mounts</h3>
|
||||
<p>Mount multiple applications at different paths:</p>
|
||||
|
||||
<pre><span class="directive">extensions:</span>
|
||||
- <span class="directive">type:</span> <span class="value">asgi</span>
|
||||
<span class="directive">config:</span>
|
||||
<span class="directive">mounts:</span>
|
||||
<span class="comment"># FastAPI for REST API</span>
|
||||
- <span class="directive">path:</span> <span class="value">"/api"</span>
|
||||
<span class="directive">app_path:</span> <span class="value">"apps.api:app"</span>
|
||||
<span class="directive">app_type:</span> <span class="value">asgi</span>
|
||||
<pre><code class="language-yaml">extensions:
|
||||
- type: asgi
|
||||
config:
|
||||
mounts:
|
||||
# FastAPI for REST API
|
||||
- path: "/api"
|
||||
app_path: "apps.api:app"
|
||||
app_type: asgi
|
||||
|
||||
<span class="comment"># Flask admin panel</span>
|
||||
- <span class="directive">path:</span> <span class="value">"/admin"</span>
|
||||
<span class="directive">app_path:</span> <span class="value">"apps.admin:app"</span>
|
||||
<span class="directive">app_type:</span> <span class="value">wsgi</span>
|
||||
# Flask admin panel
|
||||
- path: "/admin"
|
||||
app_path: "apps.admin:app"
|
||||
app_type: wsgi
|
||||
|
||||
<span class="comment"># Starlette websocket handler</span>
|
||||
- <span class="directive">path:</span> <span class="value">"/ws"</span>
|
||||
<span class="directive">app_path:</span> <span class="value">"apps.websocket:app"</span>
|
||||
<span class="directive">app_type:</span> <span class="value">asgi</span>
|
||||
# Starlette websocket handler
|
||||
- path: "/ws"
|
||||
app_path: "apps.websocket:app"
|
||||
app_type: asgi
|
||||
|
||||
<span class="comment"># Standard routing for static files</span>
|
||||
- <span class="directive">type:</span> <span class="value">routing</span>
|
||||
<span class="directive">config:</span>
|
||||
<span class="directive">regex_locations:</span>
|
||||
<span class="value">"__default__"</span>:
|
||||
<span class="directive">root:</span> <span class="value">"./static"</span></pre>
|
||||
# Standard routing for static files
|
||||
- type: routing
|
||||
config:
|
||||
regex_locations:
|
||||
"__default__":
|
||||
root: "./static"</code></pre>
|
||||
|
||||
<h3>Mount Priority</h3>
|
||||
<p>Mounts are matched by path length (longest first). Given mounts at
|
||||
@ -233,24 +236,24 @@ def create_app(debug: bool = False, prefix: str = "/v1") -> FastAPI:
|
||||
<p>ASGI mounts work alongside the routing extension. The <code>asgi</code> extension
|
||||
should be listed before <code>routing</code> to handle mounted paths first:</p>
|
||||
|
||||
<pre><span class="directive">extensions:</span>
|
||||
<span class="comment"># ASGI apps handle /api/* and /admin/*</span>
|
||||
- <span class="directive">type:</span> <span class="value">asgi</span>
|
||||
<span class="directive">config:</span>
|
||||
<span class="directive">mounts:</span>
|
||||
- <span class="directive">path:</span> <span class="value">"/api"</span>
|
||||
<span class="directive">app_path:</span> <span class="value">"myapp:api"</span>
|
||||
<span class="directive">app_type:</span> <span class="value">asgi</span>
|
||||
<pre><code class="language-yaml">extensions:
|
||||
# ASGI apps handle /api/* and /admin/*
|
||||
- type: asgi
|
||||
config:
|
||||
mounts:
|
||||
- path: "/api"
|
||||
app_path: "myapp:api"
|
||||
app_type: asgi
|
||||
|
||||
<span class="comment"># Routing handles everything else</span>
|
||||
- <span class="directive">type:</span> <span class="value">routing</span>
|
||||
<span class="directive">config:</span>
|
||||
<span class="directive">regex_locations:</span>
|
||||
<span class="value">"=/health"</span>:
|
||||
<span class="directive">return:</span> <span class="value">"200 OK"</span>
|
||||
<span class="value">"__default__"</span>:
|
||||
<span class="directive">spa_fallback:</span> <span class="value">true</span>
|
||||
<span class="directive">root:</span> <span class="value">"./dist"</span></pre>
|
||||
# Routing handles everything else
|
||||
- type: routing
|
||||
config:
|
||||
regex_locations:
|
||||
"=/health":
|
||||
return: "200 OK"
|
||||
"__default__":
|
||||
spa_fallback: true
|
||||
root: "./dist"</code></pre>
|
||||
|
||||
<h3>Python API</h3>
|
||||
<p>For programmatic mounting, see <a href="../reference/asgi-mount.html">ASGI Mount API Reference</a>.</p>
|
||||
|
||||
@ -5,6 +5,9 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Configuration - pyserve</title>
|
||||
<link rel="stylesheet" href="../style.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
<script>hljs.highlightAll();</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
@ -14,7 +17,7 @@
|
||||
</div>
|
||||
|
||||
<div class="breadcrumb">
|
||||
<a href="../index.html">pyserve</a> » <a href="/guides/">Guides</a> » Configuration
|
||||
<a href="../index.html">Home</a> / <a href="index.html">Guides</a> / Configuration
|
||||
</div>
|
||||
|
||||
<div id="content">
|
||||
@ -128,42 +131,42 @@
|
||||
<p>List of extension modules to load. See <a href="../reference/extensions.html">Extensions Reference</a>.</p>
|
||||
|
||||
<h3>Complete Example</h3>
|
||||
<pre><span class="directive">http:</span>
|
||||
<span class="directive">static_dir:</span> <span class="value">./static</span>
|
||||
<span class="directive">templates_dir:</span> <span class="value">./templates</span>
|
||||
<pre><code class="language-yaml">http:
|
||||
static_dir: ./static
|
||||
templates_dir: ./templates
|
||||
|
||||
<span class="directive">server:</span>
|
||||
<span class="directive">host:</span> <span class="value">0.0.0.0</span>
|
||||
<span class="directive">port:</span> <span class="value">8080</span>
|
||||
<span class="directive">backlog:</span> <span class="value">5</span>
|
||||
<span class="directive">default_root:</span> <span class="value">false</span>
|
||||
<span class="directive">proxy_timeout:</span> <span class="value">30.0</span>
|
||||
server:
|
||||
host: 0.0.0.0
|
||||
port: 8080
|
||||
backlog: 5
|
||||
default_root: false
|
||||
proxy_timeout: 30.0
|
||||
|
||||
<span class="directive">ssl:</span>
|
||||
<span class="directive">enabled:</span> <span class="value">false</span>
|
||||
<span class="directive">cert_file:</span> <span class="value">./ssl/cert.pem</span>
|
||||
<span class="directive">key_file:</span> <span class="value">./ssl/key.pem</span>
|
||||
ssl:
|
||||
enabled: false
|
||||
cert_file: ./ssl/cert.pem
|
||||
key_file: ./ssl/key.pem
|
||||
|
||||
<span class="directive">logging:</span>
|
||||
<span class="directive">level:</span> <span class="value">INFO</span>
|
||||
<span class="directive">console_output:</span> <span class="value">true</span>
|
||||
<span class="directive">format:</span>
|
||||
<span class="directive">type:</span> <span class="value">standard</span>
|
||||
<span class="directive">use_colors:</span> <span class="value">true</span>
|
||||
<span class="directive">timestamp_format:</span> <span class="value">"%Y-%m-%d %H:%M:%S"</span>
|
||||
<span class="directive">files:</span>
|
||||
- <span class="directive">path:</span> <span class="value">./logs/pyserve.log</span>
|
||||
<span class="directive">level:</span> <span class="value">DEBUG</span>
|
||||
<span class="directive">max_bytes:</span> <span class="value">10485760</span>
|
||||
<span class="directive">backup_count:</span> <span class="value">5</span>
|
||||
logging:
|
||||
level: INFO
|
||||
console_output: true
|
||||
format:
|
||||
type: standard
|
||||
use_colors: true
|
||||
timestamp_format: "%Y-%m-%d %H:%M:%S"
|
||||
files:
|
||||
- path: ./logs/pyserve.log
|
||||
level: DEBUG
|
||||
max_bytes: 10485760
|
||||
backup_count: 5
|
||||
|
||||
<span class="directive">extensions:</span>
|
||||
- <span class="directive">type:</span> <span class="value">routing</span>
|
||||
<span class="directive">config:</span>
|
||||
<span class="directive">regex_locations:</span>
|
||||
<span class="value">"__default__"</span>:
|
||||
<span class="directive">root:</span> <span class="value">"./static"</span>
|
||||
<span class="directive">index_file:</span> <span class="value">"index.html"</span></pre>
|
||||
extensions:
|
||||
- type: routing
|
||||
config:
|
||||
regex_locations:
|
||||
"__default__":
|
||||
root: "./static"
|
||||
index_file: "index.html"</code></pre>
|
||||
|
||||
<div class="warning">
|
||||
<strong>Warning:</strong> When running in production, always use SSL
|
||||
|
||||
@ -5,6 +5,9 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Guides - pyserve</title>
|
||||
<link rel="stylesheet" href="../style.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
<script>hljs.highlightAll();</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
@ -14,7 +17,7 @@
|
||||
</div>
|
||||
|
||||
<div class="breadcrumb">
|
||||
<a href="../index.html">pyserve</a> » Guides
|
||||
<a href="../index.html">Home</a> / Guides
|
||||
</div>
|
||||
|
||||
<div id="content">
|
||||
|
||||
@ -5,6 +5,9 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Process Orchestration - pyserve</title>
|
||||
<link rel="stylesheet" href="../style.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
<script>hljs.highlightAll();</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
@ -14,7 +17,7 @@
|
||||
</div>
|
||||
|
||||
<div class="breadcrumb">
|
||||
<a href="../index.html">pyserve</a> » <a href="/guides/">Guides</a> » Process Orchestration
|
||||
<a href="../index.html">Home</a> / <a href="index.html">Guides</a> / Process Orchestration
|
||||
</div>
|
||||
|
||||
<div id="content">
|
||||
@ -38,7 +41,7 @@
|
||||
</ul>
|
||||
|
||||
<h3>Architecture</h3>
|
||||
<pre>
|
||||
<pre><code class="language-bash">
|
||||
PyServe Gateway (:8000)
|
||||
│
|
||||
┌────────────────┼────────────────┐
|
||||
@ -46,25 +49,25 @@
|
||||
FastAPI Flask Starlette
|
||||
:9001 :9002 :9003
|
||||
/api/* /admin/* /ws/*
|
||||
</pre>
|
||||
</code></pre>
|
||||
<p>PyServe acts as a gateway, routing requests to the appropriate subprocess based on URL path.</p>
|
||||
|
||||
<h3>Basic Configuration</h3>
|
||||
<pre><span class="directive">server:</span>
|
||||
<span class="directive">host:</span> <span class="value">0.0.0.0</span>
|
||||
<span class="directive">port:</span> <span class="value">8000</span>
|
||||
<pre><code class="language-yaml">server:
|
||||
host: 0.0.0.0
|
||||
port: 8000
|
||||
|
||||
<span class="directive">extensions:</span>
|
||||
- <span class="directive">type:</span> <span class="value">process_orchestration</span>
|
||||
<span class="directive">config:</span>
|
||||
<span class="directive">apps:</span>
|
||||
- <span class="directive">name:</span> <span class="value">api</span>
|
||||
<span class="directive">path:</span> <span class="value">/api</span>
|
||||
<span class="directive">app_path:</span> <span class="value">myapp.api:app</span>
|
||||
extensions:
|
||||
- type: process_orchestration
|
||||
config:
|
||||
apps:
|
||||
- name: api
|
||||
path: /api
|
||||
app_path: myapp.api:app
|
||||
|
||||
- <span class="directive">name:</span> <span class="value">admin</span>
|
||||
<span class="directive">path:</span> <span class="value">/admin</span>
|
||||
<span class="directive">app_path:</span> <span class="value">myapp.admin:app</span></pre>
|
||||
- name: admin
|
||||
path: /admin
|
||||
app_path: myapp.admin:app</code></pre>
|
||||
|
||||
<h3>App Configuration Options</h3>
|
||||
<dl>
|
||||
@ -127,18 +130,18 @@
|
||||
</dl>
|
||||
|
||||
<h3>Global Configuration</h3>
|
||||
<pre><span class="directive">extensions:</span>
|
||||
- <span class="directive">type:</span> <span class="value">process_orchestration</span>
|
||||
<span class="directive">config:</span>
|
||||
<span class="directive">port_range:</span> <span class="value">[9000, 9999]</span>
|
||||
<span class="directive">health_check_enabled:</span> <span class="value">true</span>
|
||||
<span class="directive">proxy_timeout:</span> <span class="value">60.0</span>
|
||||
<span class="directive">logging:</span>
|
||||
<span class="directive">httpx_level:</span> <span class="value">warning</span>
|
||||
<span class="directive">proxy_logs:</span> <span class="value">true</span>
|
||||
<span class="directive">health_check_logs:</span> <span class="value">false</span>
|
||||
<span class="directive">apps:</span>
|
||||
<span class="comment"># ...</span></pre>
|
||||
<pre><code class="language-yaml">extensions:
|
||||
- type: process_orchestration
|
||||
config:
|
||||
port_range: [9000, 9999]
|
||||
health_check_enabled: true
|
||||
proxy_timeout: 60.0
|
||||
logging:
|
||||
httpx_level: warning
|
||||
proxy_logs: true
|
||||
health_check_logs: false
|
||||
apps:
|
||||
# ...</code></pre>
|
||||
|
||||
<dl>
|
||||
<dt>port_range</dt>
|
||||
@ -158,7 +161,7 @@
|
||||
</dl>
|
||||
|
||||
<h3>FastAPI Example</h3>
|
||||
<pre><span class="comment"># myapp/api.py</span>
|
||||
<pre><code class="language-python"># myapp/api.py
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
@ -169,22 +172,22 @@ async def health():
|
||||
|
||||
@app.get("/users")
|
||||
async def get_users():
|
||||
return [{"id": 1, "name": "Alice"}]</pre>
|
||||
return [{"id": 1, "name": "Alice"}]</code></pre>
|
||||
|
||||
<pre><span class="directive">extensions:</span>
|
||||
- <span class="directive">type:</span> <span class="value">process_orchestration</span>
|
||||
<span class="directive">config:</span>
|
||||
<span class="directive">apps:</span>
|
||||
- <span class="directive">name:</span> <span class="value">api</span>
|
||||
<span class="directive">path:</span> <span class="value">/api</span>
|
||||
<span class="directive">app_path:</span> <span class="value">myapp.api:app</span>
|
||||
<span class="directive">workers:</span> <span class="value">4</span>
|
||||
<span class="directive">health_check_path:</span> <span class="value">/health</span></pre>
|
||||
<pre><code class="language-yaml">extensions:
|
||||
- type: process_orchestration
|
||||
config:
|
||||
apps:
|
||||
- name: api
|
||||
path: /api
|
||||
app_path: myapp.api:app
|
||||
workers: 4
|
||||
health_check_path: /health</code></pre>
|
||||
|
||||
<p>Requests to <code>/api/users</code> are proxied to the FastAPI process as <code>/users</code>.</p>
|
||||
|
||||
<h3>Flask Example (WSGI)</h3>
|
||||
<pre><span class="comment"># myapp/admin.py</span>
|
||||
<pre><code class="language-python"># myapp/admin.py
|
||||
from flask import Flask
|
||||
|
||||
app = Flask(__name__)
|
||||
@ -195,17 +198,17 @@ def health():
|
||||
|
||||
@app.route("/dashboard")
|
||||
def dashboard():
|
||||
return {"page": "dashboard"}</pre>
|
||||
return {"page": "dashboard"}</code></pre>
|
||||
|
||||
<pre><span class="directive">extensions:</span>
|
||||
- <span class="directive">type:</span> <span class="value">process_orchestration</span>
|
||||
<span class="directive">config:</span>
|
||||
<span class="directive">apps:</span>
|
||||
- <span class="directive">name:</span> <span class="value">admin</span>
|
||||
<span class="directive">path:</span> <span class="value">/admin</span>
|
||||
<span class="directive">app_path:</span> <span class="value">myapp.admin:app</span>
|
||||
<span class="directive">app_type:</span> <span class="value">wsgi</span>
|
||||
<span class="directive">workers:</span> <span class="value">2</span></pre>
|
||||
<pre><code class="language-yaml">extensions:
|
||||
- type: process_orchestration
|
||||
config:
|
||||
apps:
|
||||
- name: admin
|
||||
path: /admin
|
||||
app_path: myapp.admin:app
|
||||
app_type: wsgi
|
||||
workers: 2</code></pre>
|
||||
|
||||
<div class="note">
|
||||
<strong>Note:</strong> WSGI support requires <code>a2wsgi</code> package:
|
||||
@ -213,7 +216,7 @@ def dashboard():
|
||||
</div>
|
||||
|
||||
<h3>Factory Pattern</h3>
|
||||
<pre><span class="comment"># myapp/api.py</span>
|
||||
<pre><code class="language-python"># myapp/api.py
|
||||
from fastapi import FastAPI
|
||||
|
||||
def create_app(debug: bool = False) -> FastAPI:
|
||||
@ -223,49 +226,49 @@ def create_app(debug: bool = False) -> FastAPI:
|
||||
async def health():
|
||||
return {"status": "ok", "debug": debug}
|
||||
|
||||
return app</pre>
|
||||
return app</code></pre>
|
||||
|
||||
<pre><span class="directive">apps:</span>
|
||||
- <span class="directive">name:</span> <span class="value">api</span>
|
||||
<span class="directive">path:</span> <span class="value">/api</span>
|
||||
<span class="directive">app_path:</span> <span class="value">myapp.api:create_app</span>
|
||||
<span class="directive">factory:</span> <span class="value">true</span></pre>
|
||||
<pre><code class="language-bash">apps:
|
||||
- name: api
|
||||
path: /api
|
||||
app_path: myapp.api:create_app
|
||||
factory: true</code></pre>
|
||||
|
||||
<h3>Environment Variables</h3>
|
||||
<p>Pass environment variables to subprocesses:</p>
|
||||
<pre><span class="directive">apps:</span>
|
||||
- <span class="directive">name:</span> <span class="value">api</span>
|
||||
<span class="directive">path:</span> <span class="value">/api</span>
|
||||
<span class="directive">app_path:</span> <span class="value">myapp.api:app</span>
|
||||
<span class="directive">env:</span>
|
||||
<span class="directive">DATABASE_URL:</span> <span class="value">"postgresql://localhost/mydb"</span>
|
||||
<span class="directive">REDIS_URL:</span> <span class="value">"redis://localhost:6379"</span>
|
||||
<span class="directive">DEBUG:</span> <span class="value">"false"</span></pre>
|
||||
<pre><code class="language-bash">apps:
|
||||
- name: api
|
||||
path: /api
|
||||
app_path: myapp.api:app
|
||||
env:
|
||||
DATABASE_URL: "postgresql://localhost/mydb"
|
||||
REDIS_URL: "redis://localhost:6379"
|
||||
DEBUG: "false"</code></pre>
|
||||
|
||||
<h3>Multiple Applications</h3>
|
||||
<pre><span class="directive">extensions:</span>
|
||||
- <span class="directive">type:</span> <span class="value">process_orchestration</span>
|
||||
<span class="directive">config:</span>
|
||||
<span class="directive">port_range:</span> <span class="value">[9000, 9999]</span>
|
||||
<span class="directive">apps:</span>
|
||||
<span class="comment"># FastAPI REST API</span>
|
||||
- <span class="directive">name:</span> <span class="value">api</span>
|
||||
<span class="directive">path:</span> <span class="value">/api</span>
|
||||
<span class="directive">app_path:</span> <span class="value">apps.api:app</span>
|
||||
<span class="directive">workers:</span> <span class="value">4</span>
|
||||
<pre><code class="language-yaml">extensions:
|
||||
- type: process_orchestration
|
||||
config:
|
||||
port_range: [9000, 9999]
|
||||
apps:
|
||||
# FastAPI REST API
|
||||
- name: api
|
||||
path: /api
|
||||
app_path: apps.api:app
|
||||
workers: 4
|
||||
|
||||
<span class="comment"># Flask Admin Panel</span>
|
||||
- <span class="directive">name:</span> <span class="value">admin</span>
|
||||
<span class="directive">path:</span> <span class="value">/admin</span>
|
||||
<span class="directive">app_path:</span> <span class="value">apps.admin:app</span>
|
||||
<span class="directive">app_type:</span> <span class="value">wsgi</span>
|
||||
<span class="directive">workers:</span> <span class="value">2</span>
|
||||
# Flask Admin Panel
|
||||
- name: admin
|
||||
path: /admin
|
||||
app_path: apps.admin:app
|
||||
app_type: wsgi
|
||||
workers: 2
|
||||
|
||||
<span class="comment"># Starlette WebSocket Handler</span>
|
||||
- <span class="directive">name:</span> <span class="value">websocket</span>
|
||||
<span class="directive">path:</span> <span class="value">/ws</span>
|
||||
<span class="directive">app_path:</span> <span class="value">apps.websocket:app</span>
|
||||
<span class="directive">workers:</span> <span class="value">1</span></pre>
|
||||
# Starlette WebSocket Handler
|
||||
- name: websocket
|
||||
path: /ws
|
||||
app_path: apps.websocket:app
|
||||
workers: 1</code></pre>
|
||||
|
||||
<h3>Request Tracing</h3>
|
||||
<p>PyServe automatically generates and propagates <code>X-Request-ID</code> headers:</p>
|
||||
|
||||
@ -5,6 +5,9 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Reverse Proxy - pyserve</title>
|
||||
<link rel="stylesheet" href="../style.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
<script>hljs.highlightAll();</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
@ -14,7 +17,7 @@
|
||||
</div>
|
||||
|
||||
<div class="breadcrumb">
|
||||
<a href="../index.html">pyserve</a> » <a href="/guides/">Guides</a> » Reverse Proxy
|
||||
<a href="../index.html">Home</a> / <a href="index.html">Guides</a> / Reverse Proxy
|
||||
</div>
|
||||
|
||||
<div id="content">
|
||||
@ -25,12 +28,12 @@
|
||||
<h3>Basic Proxy Configuration</h3>
|
||||
<p>Use the <code>proxy_pass</code> directive in routing:</p>
|
||||
|
||||
<pre><span class="directive">extensions:</span>
|
||||
- <span class="directive">type:</span> <span class="value">routing</span>
|
||||
<span class="directive">config:</span>
|
||||
<span class="directive">regex_locations:</span>
|
||||
<span class="value">"~^/api/"</span>:
|
||||
<span class="directive">proxy_pass:</span> <span class="value">"http://localhost:9001"</span></pre>
|
||||
<pre><code class="language-yaml">extensions:
|
||||
- type: routing
|
||||
config:
|
||||
regex_locations:
|
||||
"~^/api/":
|
||||
proxy_pass: "http://localhost:9001"</code></pre>
|
||||
|
||||
<p>All requests to <code>/api/*</code> will be forwarded to <code>http://localhost:9001/api/*</code>.</p>
|
||||
|
||||
@ -59,21 +62,21 @@
|
||||
<h3>Custom Headers</h3>
|
||||
<p>Add custom headers to proxied requests:</p>
|
||||
|
||||
<pre><span class="value">"~^/api/"</span>:
|
||||
<span class="directive">proxy_pass:</span> <span class="value">"http://localhost:9001"</span>
|
||||
<span class="directive">headers:</span>
|
||||
- <span class="value">"X-Custom-Header: my-value"</span>
|
||||
- <span class="value">"Authorization: Bearer token123"</span></pre>
|
||||
<pre><code class="language-bash">"~^/api/":
|
||||
proxy_pass: "http://localhost:9001"
|
||||
headers:
|
||||
- "X-Custom-Header: my-value"
|
||||
- "Authorization: Bearer token123"</code></pre>
|
||||
|
||||
<h3>Dynamic Headers with Captures</h3>
|
||||
<p>Use regex capture groups to build dynamic headers:</p>
|
||||
|
||||
<pre><span class="value">"~^/api/v(?P<version>\\d+)/(?P<service>\\w+)"</span>:
|
||||
<span class="directive">proxy_pass:</span> <span class="value">"http://localhost:9001"</span>
|
||||
<span class="directive">headers:</span>
|
||||
- <span class="value">"X-API-Version: {version}"</span>
|
||||
- <span class="value">"X-Service: {service}"</span>
|
||||
- <span class="value">"X-Client-IP: $remote_addr"</span></pre>
|
||||
<pre><code class="language-bash">"~^/api/v(?P<version>\\d+)/(?P<service>\\w+)":
|
||||
proxy_pass: "http://localhost:9001"
|
||||
headers:
|
||||
- "X-API-Version: {version}"
|
||||
- "X-Service: {service}"
|
||||
- "X-Client-IP: $remote_addr"</code></pre>
|
||||
|
||||
<p>Special variables:</p>
|
||||
<ul class="indent">
|
||||
@ -84,49 +87,49 @@
|
||||
<h3>Proxy Timeout</h3>
|
||||
<p>Configure timeout for proxy requests:</p>
|
||||
|
||||
<pre><span class="comment"># Global default timeout</span>
|
||||
<span class="directive">server:</span>
|
||||
<span class="directive">proxy_timeout:</span> <span class="value">30.0</span>
|
||||
<pre><code class="language-yaml"># Global default timeout
|
||||
server:
|
||||
proxy_timeout: 30.0
|
||||
|
||||
<span class="comment"># Per-route timeout</span>
|
||||
<span class="directive">extensions:</span>
|
||||
- <span class="directive">type:</span> <span class="value">routing</span>
|
||||
<span class="directive">config:</span>
|
||||
<span class="directive">regex_locations:</span>
|
||||
<span class="value">"~^/api/slow"</span>:
|
||||
<span class="directive">proxy_pass:</span> <span class="value">"http://localhost:9001"</span>
|
||||
<span class="directive">timeout:</span> <span class="value">120</span> <span class="comment"># 2 minutes for slow endpoints</span></pre>
|
||||
# Per-route timeout
|
||||
extensions:
|
||||
- type: routing
|
||||
config:
|
||||
regex_locations:
|
||||
"~^/api/slow":
|
||||
proxy_pass: "http://localhost:9001"
|
||||
timeout: 120 # 2 minutes for slow endpoints</code></pre>
|
||||
|
||||
<h3>URL Rewriting</h3>
|
||||
<p>The proxy preserves the original request path by default:</p>
|
||||
|
||||
<pre><span class="comment"># Request: GET /api/users/123</span>
|
||||
<span class="comment"># Proxied: GET http://backend:9001/api/users/123</span>
|
||||
<span class="value">"~^/api/"</span>:
|
||||
<span class="directive">proxy_pass:</span> <span class="value">"http://backend:9001"</span></pre>
|
||||
<pre><code class="language-bash"># Request: GET /api/users/123
|
||||
# Proxied: GET http://backend:9001/api/users/123
|
||||
"~^/api/":
|
||||
proxy_pass: "http://backend:9001"</code></pre>
|
||||
|
||||
<p>To proxy to a specific path:</p>
|
||||
|
||||
<pre><span class="comment"># Request: GET /api/users/123</span>
|
||||
<span class="comment"># Proxied: GET http://backend:9001/v2/users/123 (path preserved)</span>
|
||||
<span class="value">"~^/api/"</span>:
|
||||
<span class="directive">proxy_pass:</span> <span class="value">"http://backend:9001/v2"</span></pre>
|
||||
<pre><code class="language-bash"># Request: GET /api/users/123
|
||||
# Proxied: GET http://backend:9001/v2/users/123 (path preserved)
|
||||
"~^/api/":
|
||||
proxy_pass: "http://backend:9001/v2"</code></pre>
|
||||
|
||||
<h3>Load Balancing Example</h3>
|
||||
<p>Route different services to different backends:</p>
|
||||
|
||||
<pre><span class="directive">extensions:</span>
|
||||
- <span class="directive">type:</span> <span class="value">routing</span>
|
||||
<span class="directive">config:</span>
|
||||
<span class="directive">regex_locations:</span>
|
||||
<span class="value">"~^/api/users"</span>:
|
||||
<span class="directive">proxy_pass:</span> <span class="value">"http://user-service:8001"</span>
|
||||
<pre><code class="language-yaml">extensions:
|
||||
- type: routing
|
||||
config:
|
||||
regex_locations:
|
||||
"~^/api/users":
|
||||
proxy_pass: "http://user-service:8001"
|
||||
|
||||
<span class="value">"~^/api/orders"</span>:
|
||||
<span class="directive">proxy_pass:</span> <span class="value">"http://order-service:8002"</span>
|
||||
"~^/api/orders":
|
||||
proxy_pass: "http://order-service:8002"
|
||||
|
||||
<span class="value">"~^/api/products"</span>:
|
||||
<span class="directive">proxy_pass:</span> <span class="value">"http://product-service:8003"</span></pre>
|
||||
"~^/api/products":
|
||||
proxy_pass: "http://product-service:8003"</code></pre>
|
||||
|
||||
<h3>Error Handling</h3>
|
||||
<p>pyserve returns appropriate error codes for proxy failures:</p>
|
||||
|
||||
@ -5,6 +5,9 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Routing - pyserve</title>
|
||||
<link rel="stylesheet" href="../style.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
<script>hljs.highlightAll();</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
@ -14,7 +17,7 @@
|
||||
</div>
|
||||
|
||||
<div class="breadcrumb">
|
||||
<a href="../index.html">pyserve</a> » <a href="/guides/">Guides</a> » Routing
|
||||
<a href="../index.html">Home</a> / <a href="index.html">Guides</a> / Routing
|
||||
</div>
|
||||
|
||||
<div id="content">
|
||||
@ -59,29 +62,29 @@
|
||||
<h3>Routing Configuration</h3>
|
||||
<p>Routing is configured via the <code>routing</code> extension:</p>
|
||||
|
||||
<pre><span class="directive">extensions:</span>
|
||||
- <span class="directive">type:</span> <span class="value">routing</span>
|
||||
<span class="directive">config:</span>
|
||||
<span class="directive">regex_locations:</span>
|
||||
<span class="comment"># Exact match for health check</span>
|
||||
<span class="value">"=/health"</span>:
|
||||
<span class="directive">return:</span> <span class="value">"200 OK"</span>
|
||||
<span class="directive">content_type:</span> <span class="value">"text/plain"</span>
|
||||
<pre><code class="language-yaml">extensions:
|
||||
- type: routing
|
||||
config:
|
||||
regex_locations:
|
||||
# Exact match for health check
|
||||
"=/health":
|
||||
return: "200 OK"
|
||||
content_type: "text/plain"
|
||||
|
||||
<span class="comment"># Static files with caching</span>
|
||||
<span class="value">"~*\\.(js|css|png|jpg|gif|ico)$"</span>:
|
||||
<span class="directive">root:</span> <span class="value">"./static"</span>
|
||||
<span class="directive">cache_control:</span> <span class="value">"public, max-age=31536000"</span>
|
||||
# Static files with caching
|
||||
"~*\\.(js|css|png|jpg|gif|ico)$":
|
||||
root: "./static"
|
||||
cache_control: "public, max-age=31536000"
|
||||
|
||||
<span class="comment"># HTML files without caching</span>
|
||||
<span class="value">"~*\\.html$"</span>:
|
||||
<span class="directive">root:</span> <span class="value">"./static"</span>
|
||||
<span class="directive">cache_control:</span> <span class="value">"no-cache"</span>
|
||||
# HTML files without caching
|
||||
"~*\\.html$":
|
||||
root: "./static"
|
||||
cache_control: "no-cache"
|
||||
|
||||
<span class="comment"># Default fallback</span>
|
||||
<span class="value">"__default__"</span>:
|
||||
<span class="directive">root:</span> <span class="value">"./static"</span>
|
||||
<span class="directive">index_file:</span> <span class="value">"index.html"</span></pre>
|
||||
# Default fallback
|
||||
"__default__":
|
||||
root: "./static"
|
||||
index_file: "index.html"</code></pre>
|
||||
|
||||
<h3>Location Directives</h3>
|
||||
|
||||
@ -117,11 +120,11 @@
|
||||
<h3>Named Capture Groups</h3>
|
||||
<p>Regex locations support named capture groups that can be used in headers and proxy URLs:</p>
|
||||
|
||||
<pre><span class="value">"~^/api/v(?P<version>\\d+)/(?P<resource>\\w+)"</span>:
|
||||
<span class="directive">proxy_pass:</span> <span class="value">"http://backend:9001"</span>
|
||||
<span class="directive">headers:</span>
|
||||
- <span class="value">"X-API-Version: {version}"</span>
|
||||
- <span class="value">"X-Resource: {resource}"</span></pre>
|
||||
<pre><code class="language-bash">"~^/api/v(?P<version>\\d+)/(?P<resource>\\w+)":
|
||||
proxy_pass: "http://backend:9001"
|
||||
headers:
|
||||
- "X-API-Version: {version}"
|
||||
- "X-Resource: {resource}"</code></pre>
|
||||
|
||||
<p>Request to <code>/api/v2/users</code> will have headers:</p>
|
||||
<ul class="indent">
|
||||
@ -132,14 +135,14 @@
|
||||
<h3>SPA Configuration</h3>
|
||||
<p>For Single Page Applications, use <code>spa_fallback</code> with <code>exclude_patterns</code>:</p>
|
||||
|
||||
<pre><span class="value">"__default__"</span>:
|
||||
<span class="directive">spa_fallback:</span> <span class="value">true</span>
|
||||
<span class="directive">root:</span> <span class="value">"./dist"</span>
|
||||
<span class="directive">index_file:</span> <span class="value">"index.html"</span>
|
||||
<span class="directive">exclude_patterns:</span>
|
||||
- <span class="value">"/api/"</span>
|
||||
- <span class="value">"/assets/"</span>
|
||||
- <span class="value">"/static/"</span></pre>
|
||||
<pre><code class="language-python">"__default__":
|
||||
spa_fallback: true
|
||||
root: "./dist"
|
||||
index_file: "index.html"
|
||||
exclude_patterns:
|
||||
- "/api/"
|
||||
- "/assets/"
|
||||
- "/static/"</code></pre>
|
||||
|
||||
<p>This will:</p>
|
||||
<ul class="indent">
|
||||
@ -150,11 +153,11 @@
|
||||
<h3>Static File Serving</h3>
|
||||
<p>Basic static file configuration:</p>
|
||||
|
||||
<pre><span class="value">"~*\\.(css|js|png|jpg|gif|svg|woff2?)$"</span>:
|
||||
<span class="directive">root:</span> <span class="value">"./static"</span>
|
||||
<span class="directive">cache_control:</span> <span class="value">"public, max-age=86400"</span>
|
||||
<span class="directive">headers:</span>
|
||||
- <span class="value">"X-Content-Type-Options: nosniff"</span></pre>
|
||||
<pre><code class="language-bash">"~*\\.(css|js|png|jpg|gif|svg|woff2?)$":
|
||||
root: "./static"
|
||||
cache_control: "public, max-age=86400"
|
||||
headers:
|
||||
- "X-Content-Type-Options: nosniff"</code></pre>
|
||||
|
||||
<div class="note">
|
||||
<strong>Note:</strong> pyserve automatically detects MIME types based on file extensions.
|
||||
|
||||
@ -5,7 +5,11 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>pyserve - Documentation</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
<script>hljs.highlightAll();</script>
|
||||
<script src="scripts/version-fetcher.js" defer></script>
|
||||
<meta name="yandex-verification" content="df79e6b90bcf4305" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
@ -99,6 +103,7 @@
|
||||
|
||||
<h2>Resources</h2>
|
||||
<ul class="plain">
|
||||
<li><a href="blog/blog.html">📝 Blog & Updates</a></li>
|
||||
<li><a href="https://git.pyserve.org/Shifty/pyserveX">Git Repository (currently read-only for non-members)</a></li>
|
||||
<li><a href="https://git.pyserve.org/Shifty/pyserveX/releases">Releases & Downloads</a></li>
|
||||
</ul>
|
||||
|
||||
@ -5,6 +5,9 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ASGI Mount API - pyserve</title>
|
||||
<link rel="stylesheet" href="../style.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
<script>hljs.highlightAll();</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
@ -14,7 +17,7 @@
|
||||
</div>
|
||||
|
||||
<div class="breadcrumb">
|
||||
<a href="../index.html">pyserve</a> » <a href="/reference/">Reference</a> » ASGI Mount API
|
||||
<a href="../index.html">Home</a> / <a href="index.html">Reference</a> / ASGI Mount API
|
||||
</div>
|
||||
|
||||
<div id="content">
|
||||
@ -28,18 +31,18 @@
|
||||
<h4>ASGIAppLoader</h4>
|
||||
<p>Loads and manages ASGI/WSGI applications from Python import paths.</p>
|
||||
|
||||
<pre><span class="keyword">from</span> pyserve <span class="keyword">import</span> ASGIAppLoader
|
||||
<pre><code class="language-python">from pyserve import ASGIAppLoader
|
||||
|
||||
loader = ASGIAppLoader()
|
||||
|
||||
<span class="comment"># Load an ASGI app</span>
|
||||
# Load an ASGI app
|
||||
app = loader.load_app(
|
||||
app_path=<span class="value">"mymodule:app"</span>,
|
||||
app_type=<span class="value">"asgi"</span>,
|
||||
module_path=<span class="value">"/path/to/project"</span>,
|
||||
factory=<span class="value">False</span>,
|
||||
factory_args=<span class="value">None</span>
|
||||
)</pre>
|
||||
app_path="mymodule:app",
|
||||
app_type="asgi",
|
||||
module_path="/path/to/project",
|
||||
factory=False,
|
||||
factory_args=None
|
||||
)</code></pre>
|
||||
|
||||
<h5>Methods</h5>
|
||||
<dl>
|
||||
@ -66,14 +69,14 @@ app = loader.load_app(
|
||||
<h4>MountedApp</h4>
|
||||
<p>Represents an application mounted at a specific path.</p>
|
||||
|
||||
<pre><span class="keyword">from</span> pyserve <span class="keyword">import</span> MountedApp
|
||||
<pre><code class="language-python">from pyserve import MountedApp
|
||||
|
||||
mount = MountedApp(
|
||||
path=<span class="value">"/api"</span>,
|
||||
path="/api",
|
||||
app=my_asgi_app,
|
||||
name=<span class="value">"my-api"</span>,
|
||||
strip_path=<span class="value">True</span>
|
||||
)</pre>
|
||||
name="my-api",
|
||||
strip_path=True
|
||||
)</code></pre>
|
||||
|
||||
<h5>Attributes</h5>
|
||||
<dl>
|
||||
@ -102,19 +105,19 @@ mount = MountedApp(
|
||||
<h4>ASGIMountManager</h4>
|
||||
<p>Manages multiple mounted applications and routes requests.</p>
|
||||
|
||||
<pre><span class="keyword">from</span> pyserve <span class="keyword">import</span> ASGIMountManager
|
||||
<pre><code class="language-python">from pyserve import ASGIMountManager
|
||||
|
||||
manager = ASGIMountManager()
|
||||
|
||||
<span class="comment"># Mount using app instance</span>
|
||||
manager.mount(path=<span class="value">"/api"</span>, app=my_app)
|
||||
# Mount using app instance
|
||||
manager.mount(path="/api", app=my_app)
|
||||
|
||||
<span class="comment"># Mount using import path</span>
|
||||
# Mount using import path
|
||||
manager.mount(
|
||||
path=<span class="value">"/flask"</span>,
|
||||
app_path=<span class="value">"myapp:flask_app"</span>,
|
||||
app_type=<span class="value">"wsgi"</span>
|
||||
)</pre>
|
||||
path="/flask",
|
||||
app_path="myapp:flask_app",
|
||||
app_type="wsgi"
|
||||
)</code></pre>
|
||||
|
||||
<h5>Methods</h5>
|
||||
<dl>
|
||||
@ -147,80 +150,80 @@ manager.mount(
|
||||
<p>Convenience functions for loading specific framework applications:</p>
|
||||
|
||||
<h4>create_fastapi_app()</h4>
|
||||
<pre><span class="keyword">from</span> pyserve <span class="keyword">import</span> create_fastapi_app
|
||||
<pre><code class="language-python">from pyserve import create_fastapi_app
|
||||
|
||||
app = create_fastapi_app(
|
||||
app_path=<span class="value">"myapp.api:app"</span>,
|
||||
module_path=<span class="value">None</span>,
|
||||
factory=<span class="value">False</span>,
|
||||
factory_args=<span class="value">None</span>
|
||||
)</pre>
|
||||
app_path="myapp.api:app",
|
||||
module_path=None,
|
||||
factory=False,
|
||||
factory_args=None
|
||||
)</code></pre>
|
||||
|
||||
<h4>create_flask_app()</h4>
|
||||
<pre><span class="keyword">from</span> pyserve <span class="keyword">import</span> create_flask_app
|
||||
<pre><code class="language-python">from pyserve import create_flask_app
|
||||
|
||||
app = create_flask_app(
|
||||
app_path=<span class="value">"myapp.web:app"</span>,
|
||||
module_path=<span class="value">None</span>,
|
||||
factory=<span class="value">False</span>,
|
||||
factory_args=<span class="value">None</span>
|
||||
)</pre>
|
||||
app_path="myapp.web:app",
|
||||
module_path=None,
|
||||
factory=False,
|
||||
factory_args=None
|
||||
)</code></pre>
|
||||
<p>Automatically wraps the WSGI app for ASGI compatibility.</p>
|
||||
|
||||
<h4>create_django_app()</h4>
|
||||
<pre><span class="keyword">from</span> pyserve <span class="keyword">import</span> create_django_app
|
||||
<pre><code class="language-python">from pyserve import create_django_app
|
||||
|
||||
app = create_django_app(
|
||||
settings_module=<span class="value">"myproject.settings"</span>,
|
||||
module_path=<span class="value">"/path/to/project"</span>
|
||||
)</pre>
|
||||
settings_module="myproject.settings",
|
||||
module_path="/path/to/project"
|
||||
)</code></pre>
|
||||
<p>Sets <code>DJANGO_SETTINGS_MODULE</code> and returns Django's ASGI application.</p>
|
||||
|
||||
<h4>create_starlette_app()</h4>
|
||||
<pre><span class="keyword">from</span> pyserve <span class="keyword">import</span> create_starlette_app
|
||||
<pre><code class="language-python">from pyserve import create_starlette_app
|
||||
|
||||
app = create_starlette_app(
|
||||
app_path=<span class="value">"myapp:starlette_app"</span>,
|
||||
module_path=<span class="value">None</span>,
|
||||
factory=<span class="value">False</span>,
|
||||
factory_args=<span class="value">None</span>
|
||||
)</pre>
|
||||
app_path="myapp:starlette_app",
|
||||
module_path=None,
|
||||
factory=False,
|
||||
factory_args=None
|
||||
)</code></pre>
|
||||
|
||||
<h3>Usage Example</h3>
|
||||
<p>Complete example mounting multiple applications:</p>
|
||||
|
||||
<pre><span class="keyword">from</span> pyserve <span class="keyword">import</span> (
|
||||
<pre><code class="language-python">from pyserve import (
|
||||
PyServeServer,
|
||||
ASGIMountManager,
|
||||
create_fastapi_app,
|
||||
create_flask_app
|
||||
)
|
||||
|
||||
<span class="comment"># Create mount manager</span>
|
||||
# Create mount manager
|
||||
mounts = ASGIMountManager()
|
||||
|
||||
<span class="comment"># Mount FastAPI</span>
|
||||
api_app = create_fastapi_app(<span class="value">"myapp.api:app"</span>)
|
||||
<span class="keyword">if</span> api_app:
|
||||
mounts.mount(<span class="value">"/api"</span>, app=api_app, name=<span class="value">"api"</span>)
|
||||
# Mount FastAPI
|
||||
api_app = create_fastapi_app("myapp.api:app")
|
||||
if api_app:
|
||||
mounts.mount("/api", app=api_app, name="api")
|
||||
|
||||
<span class="comment"># Mount Flask</span>
|
||||
admin_app = create_flask_app(<span class="value">"myapp.admin:app"</span>)
|
||||
<span class="keyword">if</span> admin_app:
|
||||
mounts.mount(<span class="value">"/admin"</span>, app=admin_app, name=<span class="value">"admin"</span>)
|
||||
# Mount Flask
|
||||
admin_app = create_flask_app("myapp.admin:app")
|
||||
if admin_app:
|
||||
mounts.mount("/admin", app=admin_app, name="admin")
|
||||
|
||||
<span class="comment"># List mounts</span>
|
||||
<span class="keyword">for</span> mount <span class="keyword">in</span> mounts.list_mounts():
|
||||
print(f<span class="value">"Mounted {mount['name']} at {mount['path']}"</span>)</pre>
|
||||
# List mounts
|
||||
for mount in mounts.list_mounts():
|
||||
print(f"Mounted {mount['name']} at {mount['path']}")</code></pre>
|
||||
|
||||
<h3>Error Handling</h3>
|
||||
<p>All loader functions return <code>None</code> on failure and log errors.
|
||||
Check the return value before using:</p>
|
||||
|
||||
<pre>app = create_fastapi_app(<span class="value">"nonexistent:app"</span>)
|
||||
<span class="keyword">if</span> app <span class="keyword">is None</span>:
|
||||
<span class="comment"># Handle error - check logs for details</span>
|
||||
print(<span class="value">"Failed to load application"</span>)</pre>
|
||||
<pre><code class="language-bash">app = create_fastapi_app("nonexistent:app")
|
||||
if app is None:
|
||||
# Handle error - check logs for details
|
||||
print("Failed to load application")</code></pre>
|
||||
|
||||
<h3>WSGI Compatibility</h3>
|
||||
<p>For WSGI applications, pyserve uses adapters in this priority:</p>
|
||||
@ -230,9 +233,9 @@ admin_app = create_flask_app(<span class="value">"myapp.admin:app"</span>)
|
||||
</ol>
|
||||
|
||||
<p>Install an adapter:</p>
|
||||
<pre>pip install a2wsgi <span class="comment"># recommended</span>
|
||||
<span class="comment"># or</span>
|
||||
pip install asgiref</pre>
|
||||
<pre><code class="language-bash">pip install a2wsgi # recommended
|
||||
# or
|
||||
pip install asgiref</code></pre>
|
||||
|
||||
<div class="note">
|
||||
<strong>See Also:</strong>
|
||||
|
||||
@ -5,6 +5,9 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CLI Reference - pyserve</title>
|
||||
<link rel="stylesheet" href="../style.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
<script>hljs.highlightAll();</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
@ -14,7 +17,7 @@
|
||||
</div>
|
||||
|
||||
<div class="breadcrumb">
|
||||
<a href="../index.html">pyserve</a> » <a href="/reference/">Reference</a> » CLI
|
||||
<a href="../index.html">Home</a> / <a href="index.html">Reference</a> / CLI
|
||||
</div>
|
||||
|
||||
<div id="content">
|
||||
@ -23,7 +26,7 @@
|
||||
<p>pyserve provides a command-line interface for server management.</p>
|
||||
|
||||
<h3>Synopsis</h3>
|
||||
<pre>pyserve [OPTIONS]</pre>
|
||||
<pre><code class="language-bash">pyserve [OPTIONS]</code></pre>
|
||||
|
||||
<h3>Options</h3>
|
||||
|
||||
@ -63,20 +66,20 @@
|
||||
<h3>Examples</h3>
|
||||
|
||||
<p><strong>Start with default configuration:</strong></p>
|
||||
<pre>pyserve</pre>
|
||||
<pre><code class="language-bash">pyserve</code></pre>
|
||||
|
||||
<p><strong>Start with custom config file:</strong></p>
|
||||
<pre>pyserve -c /path/to/config.yaml</pre>
|
||||
<pre><code class="language-bash">pyserve -c /path/to/config.yaml</code></pre>
|
||||
|
||||
<p><strong>Override host and port:</strong></p>
|
||||
<pre>pyserve --host 127.0.0.1 --port 9000</pre>
|
||||
<pre><code class="language-bash">pyserve --host 127.0.0.1 --port 9000</code></pre>
|
||||
|
||||
<p><strong>Enable debug mode:</strong></p>
|
||||
<pre>pyserve --debug</pre>
|
||||
<pre><code class="language-bash">pyserve --debug</code></pre>
|
||||
|
||||
<p><strong>Show version:</strong></p>
|
||||
<pre>pyserve --version
|
||||
<span class="comment"># Output: pyserve 0.7.0</span></pre>
|
||||
<pre><code class="language-bash">pyserve --version
|
||||
# Output: pyserve 0.7.0</code></pre>
|
||||
|
||||
<h3>Configuration Priority</h3>
|
||||
<p>Settings are applied in the following order (later overrides earlier):</p>
|
||||
@ -122,15 +125,15 @@
|
||||
<h3>Development Commands (Makefile)</h3>
|
||||
<p>When working with the source repository, use make commands:</p>
|
||||
|
||||
<pre>make run <span class="comment"># Start in development mode</span>
|
||||
make run-prod <span class="comment"># Start in production mode</span>
|
||||
make test <span class="comment"># Run tests</span>
|
||||
make test-cov <span class="comment"># Tests with coverage</span>
|
||||
make lint <span class="comment"># Check code with linters</span>
|
||||
make format <span class="comment"># Format code</span>
|
||||
make build <span class="comment"># Build wheel package</span>
|
||||
make clean <span class="comment"># Clean temporary files</span>
|
||||
make help <span class="comment"># Show all commands</span></pre>
|
||||
<pre><code class="language-bash">make run # Start in development mode
|
||||
make run-prod # Start in production mode
|
||||
make test # Run tests
|
||||
make test-cov # Tests with coverage
|
||||
make lint # Check code with linters
|
||||
make format # Format code
|
||||
make build # Build wheel package
|
||||
make clean # Clean temporary files
|
||||
make help # Show all commands</code></pre>
|
||||
</div>
|
||||
|
||||
<div id="footer">
|
||||
|
||||
@ -5,6 +5,9 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Extensions - pyserve</title>
|
||||
<link rel="stylesheet" href="../style.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
<script>hljs.highlightAll();</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
@ -14,7 +17,7 @@
|
||||
</div>
|
||||
|
||||
<div class="breadcrumb">
|
||||
<a href="../index.html">pyserve</a> » <a href="/reference/">Reference</a> » Extensions
|
||||
<a href="../index.html">Home</a> / <a href="index.html">Reference</a> / Extensions
|
||||
</div>
|
||||
|
||||
<div id="content">
|
||||
@ -55,27 +58,27 @@
|
||||
<h3>Extension Configuration</h3>
|
||||
<p>Extensions are configured in the <code>extensions</code> section:</p>
|
||||
|
||||
<pre><span class="directive">extensions:</span>
|
||||
- <span class="directive">type:</span> <span class="value">routing</span>
|
||||
<span class="directive">config:</span>
|
||||
<span class="comment"># extension-specific configuration</span>
|
||||
<pre><code class="language-yaml">extensions:
|
||||
- type: routing
|
||||
config:
|
||||
# extension-specific configuration
|
||||
|
||||
- <span class="directive">type:</span> <span class="value">security</span>
|
||||
<span class="directive">config:</span>
|
||||
<span class="comment"># ...</span></pre>
|
||||
- type: security
|
||||
config:
|
||||
# ...</code></pre>
|
||||
|
||||
<h3>Routing Extension</h3>
|
||||
<p>The primary extension for URL routing. See <a href="../guides/routing.html">Routing Guide</a> for full documentation.</p>
|
||||
|
||||
<pre><span class="directive">- type:</span> <span class="value">routing</span>
|
||||
<span class="directive">config:</span>
|
||||
<span class="directive">regex_locations:</span>
|
||||
<span class="value">"=/health"</span>:
|
||||
<span class="directive">return:</span> <span class="value">"200 OK"</span>
|
||||
<span class="value">"~^/api/"</span>:
|
||||
<span class="directive">proxy_pass:</span> <span class="value">"http://backend:9001"</span>
|
||||
<span class="value">"__default__"</span>:
|
||||
<span class="directive">root:</span> <span class="value">"./static"</span></pre>
|
||||
<pre><code class="language-python">- type: routing
|
||||
config:
|
||||
regex_locations:
|
||||
"=/health":
|
||||
return: "200 OK"
|
||||
"~^/api/":
|
||||
proxy_pass: "http://backend:9001"
|
||||
"__default__":
|
||||
root: "./static"</code></pre>
|
||||
|
||||
<h3>Security Extension</h3>
|
||||
<p>Adds security headers and IP-based access control.</p>
|
||||
@ -92,16 +95,16 @@
|
||||
<dd>List of blocked IP addresses (blacklist mode)</dd>
|
||||
</dl>
|
||||
|
||||
<pre><span class="directive">- type:</span> <span class="value">security</span>
|
||||
<span class="directive">config:</span>
|
||||
<span class="directive">security_headers:</span>
|
||||
<span class="directive">X-Frame-Options:</span> <span class="value">DENY</span>
|
||||
<span class="directive">X-Content-Type-Options:</span> <span class="value">nosniff</span>
|
||||
<span class="directive">X-XSS-Protection:</span> <span class="value">"1; mode=block"</span>
|
||||
<span class="directive">Strict-Transport-Security:</span> <span class="value">"max-age=31536000"</span>
|
||||
<span class="directive">blocked_ips:</span>
|
||||
- <span class="value">"192.168.1.100"</span>
|
||||
- <span class="value">"10.0.0.50"</span></pre>
|
||||
<pre><code class="language-bash">- type: security
|
||||
config:
|
||||
security_headers:
|
||||
X-Frame-Options: DENY
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: "1; mode=block"
|
||||
Strict-Transport-Security: "max-age=31536000"
|
||||
blocked_ips:
|
||||
- "192.168.1.100"
|
||||
- "10.0.0.50"</code></pre>
|
||||
|
||||
<p>Default security headers if not specified:</p>
|
||||
<ul class="indent">
|
||||
@ -122,11 +125,11 @@
|
||||
<dd>Default cache TTL in seconds. Default: <code>3600</code></dd>
|
||||
</dl>
|
||||
|
||||
<pre><span class="directive">- type:</span> <span class="value">caching</span>
|
||||
<span class="directive">config:</span>
|
||||
<span class="directive">cache_ttl:</span> <span class="value">3600</span>
|
||||
<span class="directive">cache_patterns:</span>
|
||||
- <span class="value">"/api/public/*"</span></pre>
|
||||
<pre><code class="language-bash">- type: caching
|
||||
config:
|
||||
cache_ttl: 3600
|
||||
cache_patterns:
|
||||
- "/api/public/*"</code></pre>
|
||||
|
||||
<h3>Monitoring Extension</h3>
|
||||
<p>Collects request metrics and provides statistics.</p>
|
||||
@ -137,9 +140,9 @@
|
||||
<dd>Enable metrics collection. Default: <code>true</code></dd>
|
||||
</dl>
|
||||
|
||||
<pre><span class="directive">- type:</span> <span class="value">monitoring</span>
|
||||
<span class="directive">config:</span>
|
||||
<span class="directive">enable_metrics:</span> <span class="value">true</span></pre>
|
||||
<pre><code class="language-bash">- type: monitoring
|
||||
config:
|
||||
enable_metrics: true</code></pre>
|
||||
|
||||
<p>Collected metrics (available at <code>/metrics</code>):</p>
|
||||
<ul class="indent">
|
||||
@ -217,28 +220,28 @@
|
||||
<dd>Django settings module (for Django apps only)</dd>
|
||||
</dl>
|
||||
|
||||
<pre><span class="directive">- type:</span> <span class="value">asgi</span>
|
||||
<span class="directive">config:</span>
|
||||
<span class="directive">mounts:</span>
|
||||
<span class="comment"># FastAPI application</span>
|
||||
- <span class="directive">path:</span> <span class="value">"/api"</span>
|
||||
<span class="directive">app_path:</span> <span class="value">"myapp.api:app"</span>
|
||||
<span class="directive">app_type:</span> <span class="value">asgi</span>
|
||||
<span class="directive">name:</span> <span class="value">"api"</span>
|
||||
<pre><code class="language-bash">- type: asgi
|
||||
config:
|
||||
mounts:
|
||||
# FastAPI application
|
||||
- path: "/api"
|
||||
app_path: "myapp.api:app"
|
||||
app_type: asgi
|
||||
name: "api"
|
||||
|
||||
<span class="comment"># Flask application (WSGI)</span>
|
||||
- <span class="directive">path:</span> <span class="value">"/admin"</span>
|
||||
<span class="directive">app_path:</span> <span class="value">"myapp.admin:app"</span>
|
||||
<span class="directive">app_type:</span> <span class="value">wsgi</span>
|
||||
<span class="directive">name:</span> <span class="value">"admin"</span>
|
||||
# Flask application (WSGI)
|
||||
- path: "/admin"
|
||||
app_path: "myapp.admin:app"
|
||||
app_type: wsgi
|
||||
name: "admin"
|
||||
|
||||
<span class="comment"># Factory pattern with arguments</span>
|
||||
- <span class="directive">path:</span> <span class="value">"/api/v2"</span>
|
||||
<span class="directive">app_path:</span> <span class="value">"myapp.api:create_app"</span>
|
||||
<span class="directive">factory:</span> <span class="value">true</span>
|
||||
<span class="directive">factory_args:</span>
|
||||
<span class="directive">debug:</span> <span class="value">true</span>
|
||||
<span class="directive">version:</span> <span class="value">"2.0"</span></pre>
|
||||
# Factory pattern with arguments
|
||||
- path: "/api/v2"
|
||||
app_path: "myapp.api:create_app"
|
||||
factory: true
|
||||
factory_args:
|
||||
debug: true
|
||||
version: "2.0"</code></pre>
|
||||
|
||||
<p>Supported frameworks:</p>
|
||||
<ul class="indent">
|
||||
@ -268,27 +271,27 @@
|
||||
<li>Request tracing with X-Request-ID</li>
|
||||
</ul>
|
||||
|
||||
<pre><span class="directive">- type:</span> <span class="value">process_orchestration</span>
|
||||
<span class="directive">config:</span>
|
||||
<span class="directive">port_range:</span> <span class="value">[9000, 9999]</span>
|
||||
<span class="directive">health_check_enabled:</span> <span class="value">true</span>
|
||||
<span class="directive">proxy_timeout:</span> <span class="value">60.0</span>
|
||||
<span class="directive">logging:</span>
|
||||
<span class="directive">httpx_level:</span> <span class="value">warning</span>
|
||||
<span class="directive">proxy_logs:</span> <span class="value">true</span>
|
||||
<span class="directive">health_check_logs:</span> <span class="value">false</span>
|
||||
<span class="directive">apps:</span>
|
||||
- <span class="directive">name:</span> <span class="value">api</span>
|
||||
<span class="directive">path:</span> <span class="value">/api</span>
|
||||
<span class="directive">app_path:</span> <span class="value">myapp.api:app</span>
|
||||
<span class="directive">workers:</span> <span class="value">4</span>
|
||||
<span class="directive">health_check_path:</span> <span class="value">/health</span>
|
||||
<pre><code class="language-yaml">- type: process_orchestration
|
||||
config:
|
||||
port_range: [9000, 9999]
|
||||
health_check_enabled: true
|
||||
proxy_timeout: 60.0
|
||||
logging:
|
||||
httpx_level: warning
|
||||
proxy_logs: true
|
||||
health_check_logs: false
|
||||
apps:
|
||||
- name: api
|
||||
path: /api
|
||||
app_path: myapp.api:app
|
||||
workers: 4
|
||||
health_check_path: /health
|
||||
|
||||
- <span class="directive">name:</span> <span class="value">admin</span>
|
||||
<span class="directive">path:</span> <span class="value">/admin</span>
|
||||
<span class="directive">app_path:</span> <span class="value">myapp.admin:app</span>
|
||||
<span class="directive">app_type:</span> <span class="value">wsgi</span>
|
||||
<span class="directive">workers:</span> <span class="value">2</span></pre>
|
||||
- name: admin
|
||||
path: /admin
|
||||
app_path: myapp.admin:app
|
||||
app_type: wsgi
|
||||
workers: 2</code></pre>
|
||||
|
||||
<h4>App Configuration</h4>
|
||||
<dl>
|
||||
|
||||
@ -5,6 +5,9 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Reference - pyserve</title>
|
||||
<link rel="stylesheet" href="../style.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
<script>hljs.highlightAll();</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
@ -14,7 +17,7 @@
|
||||
</div>
|
||||
|
||||
<div class="breadcrumb">
|
||||
<a href="../index.html">pyserve</a> » Reference
|
||||
<a href="../index.html">Home</a> / Reference
|
||||
</div>
|
||||
|
||||
<div id="content">
|
||||
|
||||
@ -1,7 +1,4 @@
|
||||
/**
|
||||
* Version Fetcher - Automatically fetches and displays latest pyserveX version
|
||||
* from Gitea releases API.
|
||||
*/
|
||||
// Version Fetcher - Displays latest pyserveX version from Gitea API
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
391
style.css
391
style.css
@ -221,21 +221,6 @@ dd {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Syntax highlighting for config examples */
|
||||
.directive {
|
||||
color: #5fba7d;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #87ceeb;
|
||||
}
|
||||
|
||||
.comment {
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Main wrapper for two-column layout */
|
||||
#main-wrapper {
|
||||
display: flex;
|
||||
@ -440,3 +425,379 @@ dd {
|
||||
letter-spacing: 0.5px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Blog styles */
|
||||
.blog-posts {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.blog-post-card {
|
||||
background: #0d0d0d;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.blog-post-card:hover {
|
||||
border-color: #3cb371;
|
||||
box-shadow: 0 4px 12px rgba(60, 179, 113, 0.1);
|
||||
}
|
||||
|
||||
.post-meta {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.post-date {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.post-tag {
|
||||
background: #1a2a1a;
|
||||
color: #3cb371;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.post-author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #2a2a2a;
|
||||
}
|
||||
|
||||
.author-avatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: #2a2a2a;
|
||||
}
|
||||
|
||||
.author-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.author-name {
|
||||
color: #e0e0e0;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.author-login {
|
||||
color: #888;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.post-title {
|
||||
margin: 10px 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.post-title a {
|
||||
color: #e0e0e0;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.post-title a:hover {
|
||||
color: #3cb371;
|
||||
}
|
||||
|
||||
.post-excerpt {
|
||||
color: #999;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
margin: 10px 0 15px 0;
|
||||
}
|
||||
|
||||
.read-more {
|
||||
color: #5fba7d;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.read-more:hover {
|
||||
color: #7ccd9a;
|
||||
}
|
||||
|
||||
.loading-state,
|
||||
.error-state {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.error-state {
|
||||
color: #b8860b;
|
||||
}
|
||||
|
||||
/* Blog post full view */
|
||||
.blog-post-full {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.post-title-full {
|
||||
font-size: 28px;
|
||||
color: #e0e0e0;
|
||||
margin: 15px 0 20px 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.post-body-full {
|
||||
color: #c9c9c9;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.post-body-full h1 {
|
||||
font-size: 24px;
|
||||
margin: 30px 0 15px 0;
|
||||
color: #e0e0e0;
|
||||
border-bottom: 2px solid #2e8b57;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.post-body-full h2 {
|
||||
font-size: 20px;
|
||||
margin: 25px 0 12px 0;
|
||||
color: #e0e0e0;
|
||||
border-bottom: 1px solid #333;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.post-body-full h3 {
|
||||
font-size: 17px;
|
||||
margin: 20px 0 10px 0;
|
||||
color: #d0d0d0;
|
||||
}
|
||||
|
||||
.post-body-full h4 {
|
||||
font-size: 15px;
|
||||
margin: 15px 0 8px 0;
|
||||
color: #d0d0d0;
|
||||
}
|
||||
|
||||
.post-body-full p {
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.post-body-full ul,
|
||||
.post-body-full ol {
|
||||
margin: 12px 0;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
.post-body-full li {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.post-body-full pre {
|
||||
background: #0d0d0d;
|
||||
border: 1px solid #333;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
overflow-x: auto;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.post-body-full code {
|
||||
background: #0d0d0d;
|
||||
border: 1px solid #333;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.post-body-full pre code {
|
||||
border: none;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.post-body-full blockquote {
|
||||
border-left: 3px solid #3cb371;
|
||||
margin: 15px 0;
|
||||
padding: 10px 20px;
|
||||
background: #1a2a1a;
|
||||
font-style: italic;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.post-body-full hr {
|
||||
border: none;
|
||||
border-top: 1px solid #333;
|
||||
margin: 25px 0;
|
||||
}
|
||||
|
||||
.post-body-full img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.post-footer {
|
||||
margin-top: 40px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #333;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
color: #5fba7d;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
color: #7ccd9a;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.share-button {
|
||||
background: #0d0d0d;
|
||||
border: 1px solid #3cb371;
|
||||
color: #3cb371;
|
||||
padding: 8px 16px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
transition: all 0.2s ease;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.share-button:hover {
|
||||
background: #3cb371;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.post-author-full {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
padding: 15px;
|
||||
background: #0d0d0d;
|
||||
border: 1px solid #333;
|
||||
border-radius: 6px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.author-avatar-large {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: #2a2a2a;
|
||||
}
|
||||
|
||||
.author-info-full {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.author-name-large {
|
||||
color: #e0e0e0;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.post-body-full pre {
|
||||
background: #0d0d0d;
|
||||
border: 1px solid #333;
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
overflow-x: auto;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.post-body-full code {
|
||||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.post-body-full pre code {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Highlight.js integration */
|
||||
pre code.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
code.hljs {
|
||||
padding: 3px 5px;
|
||||
}
|
||||
|
||||
/* Ensure highlight.js styles work with our dark theme */
|
||||
.hljs {
|
||||
background: #0d0d0d !important;
|
||||
color: #b0b0b0;
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: #6a9955;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag,
|
||||
.hljs-literal,
|
||||
.hljs-type {
|
||||
color: #569cd6;
|
||||
}
|
||||
|
||||
.hljs-string,
|
||||
.hljs-title,
|
||||
.hljs-section {
|
||||
color: #ce9178;
|
||||
}
|
||||
|
||||
.hljs-name,
|
||||
.hljs-attribute {
|
||||
color: #9cdcfe;
|
||||
}
|
||||
|
||||
.hljs-variable,
|
||||
.hljs-template-variable {
|
||||
color: #4ec9b0;
|
||||
}
|
||||
|
||||
.hljs-number {
|
||||
color: #b5cea8;
|
||||
}
|
||||
|
||||
.hljs-built_in,
|
||||
.hljs-builtin-name {
|
||||
color: #4ec9b0;
|
||||
}
|
||||
|
||||
.hljs-meta {
|
||||
color: #808080;
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user