A standard Python project typically follows a conventional directory layout to separate concerns. A common root structure looks like this:
project_root
├── docs/ # Project documentation
├── src/ # Source code or package name
├── tests/ # Unit and integration tests
├── README.md # Project overview
└── requirements.txt # Dependency list
The focus here is on the internal structure of the src directory (or the package directory) when building a Sanic application.
Minimal Structure for Simple Scripts
For quick prototypes or scripts that do not require maintenance, a single file approach is sufficient. You can place all logic, routes, and configuration inside one file, such as main.py.
Example: Single-file RSS Reader
First, install the necessarry libraries:
pip install feedparser sanic
Then, create a simple endpoint that fetches and returns RSS data as JSON:
from sanic import Sanic
from sanic.response import json
from feedparser import parse
app = Sanic("RSSReader")
@app.get("/")
async def fetch_feed(request):
rss_url = "http://blog.howie6879.cn/atom.xml"
parsed = parse(rss_url)
output = [
{"heading": item.title_detail["value"], "url": item.link}
for item in parsed.entries
]
return json(output)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
Adding Template Rendering
If the requirement evolves to display data in an HTML page rather than JSON, you can integrate Jinja2. Instead of defining the HTML string in the code, it is better to manage templates in a dedicated directory later, but for a minimal example:
from sanic import Sanic
from sanic.response import html, json
from feedparser import parse
from jinja2 import Template
app = Sanic("RSSReader")
html_template = Template("""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>RSS Feed Viewer</title>
</head>
<body>
<div>
{% for item in items %}
<h3><a href="{{ item.url }}">{{ item.heading }}</a></h3>
<em>{{ item.date }}</em>
<hr>
{% endfor %}
</div>
</body>
</html>
""")
@app.get("/json")
async def get_json(request):
parsed = parse("http://blog.howie6879.cn/atom.xml")
return json([{"heading": e.title_detail["value"], "url": e.link} for e in parsed.entries])
@app.get("/view")
async def get_html(request):
parsed = parse("http://blog.howie6879.cn/atom.xml")
records = [
{"heading": e.title_detail["value"], "url": e.link, "date": e.published}
for e in parsed.entries
]
return html(html_template.render(items=records))
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
Scalable Project Layout
For larger applications, organize the code into packages. A structured layout improves maintainability:
project_root
├── src/
│ ├── config/ # Configuration classes
│ ├── static/ # CSS, JavaScript, images
│ ├── templates/ # Jinja2 HTML files
│ ├── handlers/ # Route definitions and logic
│ │ ├── __init__.py
│ │ └── rss_routes.py
│ └── server.py # Entry point
├── tests/
└── requirements.txt
Configuration Module
Define a base configuration and environment-specific overrides.
src/config/base.py
import os
class BaseConfig:
TIMEZONE = 'Asia/Shanghai'
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
src/config/development.py
from .base import BaseConfig
class DevelopmentConfig(BaseConfig):
DEBUG = True
src/config/production.py
from .base import BaseConfig
class ProductionConfig(BaseConfig):
DEBUG = False
src/config/__init__.py
import os
from .base import BaseConfig
def get_config():
env = os.getenv('APP_ENV', 'DEV')
if env == 'PROD':
from .production import ProductionConfig
return ProductionConfig()
else:
from .development import DevelopmentConfig
return DevelopmentConfig()
SETTINGS = get_config()
Handlers (Views)
Separate the route logic into a dedicated package.
src/handlers/rss_routes.py
from sanic import Blueprint
from sanic.response import html, json
from feedparser import parse
from jinja2 import Environment, PackageLoader, select_autoescape
import sys
rss_blueprint = Blueprint("rss_bp", url_prefix="/")
async_mode = sys.version_info >= (3, 6)
jinja_env = Environment(
loader=PackageLoader('handlers', '../templates'),
autoescape=select_autoescape(['html', 'xml']),
enable_async=async_mode
)
async def render_template(tpl_name, **ctx):
tpl = jinja_env.get_template(tpl_name)
rendered = await tpl.render_async(**ctx)
return html(rendered)
@rss_blueprint.get("/api/feed")
async def feed_json(request):
content = parse("http://blog.howie6879.cn/atom.xml")
results = [
{"heading": entry.title_detail["value"], "url": entry.link}
for entry in content.entries
]
return json(results)
@rss_blueprint.get("/feed")
async def feed_html(request):
content = parse("http://blog.howie6879.cn/atom.xml")
results = [
{"heading": e.title_detail["value"], "url": e.link, "date": e.published}
for e in content.entries
]
return await render_template('feed_display.html', items=results)
src/handlers/__init__.py
from .rss_routes import rss_blueprint
Entry Point
src/server.py
from sanic import Sanic
from src.config import SETTINGS
from src.handlers import rss_blueprint
app = Sanic("StructuredApp")
app.blueprint(rss_blueprint)
app.config.from_object(SETTINGS)
app.static('/static', SETTINGS.BASE_DIR + '/static')
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000, debug=app.config.DEBUG)
Templates
src/templates/feed_display.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Feed Display</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="content">
{% for item in items %}
<h3><a href="{{ item.url }}">{{ item.heading }}</a></h3>
<span>{{ item.date }}</span>
<hr>
{% endfor %}
</div>
</body>
</html>
Static Files
src/static/style.css
.content {
max-width: 800px;
margin: 20px auto;
font-family: sans-serif;
}