A fast, lightweight, single-file Python WSGI framework with zero dependencies.
Project description
Lcore
A fast, lightweight, single-file Python WSGI framework with zero dependencies.
Inspired by the simplicity of Bottle.
Documentation • Live Playground • Getting Started • API Reference
Installation
pip install lcore
Note:
pip install lcorepulls the latest stable, tagged release and is the recommended way to install.Copying
lcore.pydirectly from themainbranch may include unreleased or experimental changes. If you prefer copying the file, always use a tagged release (e.g.v0.0.1) from the Releases page to ensure stability.
Quick Start
from lcore import Lcore
app = Lcore()
@app.route('/hello')
def hello():
return {'message': 'Hello, World!'}
app.run(host='0.0.0.0', port=8080)
Async note:
async defroute handlers are accepted but the worker thread is blocked for the duration -- there is no concurrency benefit. See the async caveat before using them.
Performance: In internal benchmarks, Lcore processes roughly 90,000 -- 120,000 requests/sec on simple JSON and plaintext routes (single process, no I/O), which is approximately 3 -- 4x higher throughput than Flask under the same conditions. Results vary by hardware and workload.
Features
- Single file, zero dependencies -- drop
lcore.pyinto any project - Full WSGI compliance -- works with Gunicorn, uWSGI, Waitress, Gevent, and 17 more server adapters
- Async/await --
async defroute handlers are accepted; note that the worker thread is blocked for the duration (WSGI constraint). See async caveat. - 7 built-in middleware -- CORS, CSRF, security headers, gzip compression, body limits, request ID, structured logging
- Security -- PBKDF2 password hashing, HMAC-SHA256 signed cookies, rate limiting, timing-safe comparison
- Dependency injection -- singleton, scoped, and transient lifetimes
- Plugin system -- JSON serialization, template rendering, and custom plugins with setup/apply lifecycle
- Request validation -- JSON body and query parameter validation with type checking
- Built-in test client --
TestClientfor unit testing routes without starting a server - Background tasks --
BackgroundTaskPoolfor fire-and-forget work (emails, cleanup, analytics) - 4 template engines -- built-in SimpleTemplate, plus Jinja2, Mako, and Cheetah adapters
- 12 lifecycle hooks -- request start, auth, handler enter/exit, response build/send, and more
- Module mounting -- compose sub-applications with isolated routes and middleware
- 21 server adapters -- use
server='auto'to auto-select the best available
Why Lcore Exists
Most Python web frameworks make one of two trade-offs:
- Simple and fast (Bottle) -- single file, zero deps, but no middleware stack, no DI, no security primitives, no lifecycle hooks.
- Full-featured (Flask, Django) -- lots of extensions, but each extension is a dependency, and the base framework is slow.
Lcore was built to close that gap. The goal is a framework that you can drop into any environment as a single file with no pip install, that includes a production-ready middleware stack, security primitives, dependency injection, and lifecycle hooks out of the box -- without reaching for extensions.
It is inspired by the minimalism of Bottle and uses the same single-file approach, but ships with the features that Bottle leaves to you.
Design Philosophy
- One file only. The entire framework is
lcore.py. No package structure, no sub-modules, no build step. Drop it in, import it, done. - Zero external dependencies. Every import is from Python's standard library (
hashlib,threading,json,gzip,logging,asyncio,http). Works in air-gapped environments, containers, and serverless functions wherepipmay not be available. - Production features built-in, not bolted on. CORS, CSRF, security headers, rate limiting, signed cookies, and password hashing are part of the framework. You should not need to install five extensions to secure a production API.
- Honest about limitations. Lcore is WSGI. It documents and warns loudly when a feature has constraints (async handlers, per-process rate limiting, thread-blocking timeouts). No surprises.
- Standard Python throughout. No metaclass magic, no descriptor abuse, no import-time side effects beyond what is declared. The source is meant to be read.
- 449 tests, zero external test dependencies. The test suite uses only
unittestfrom the standard library.
When NOT to use Lcore
Lcore is the right choice for many synchronous WSGI workloads, but it is the wrong choice in these situations:
| Situation | Better choice |
|---|---|
| You need WebSockets or real-time async I/O | FastAPI, Starlette, Quart |
| You need true async concurrency (hundreds of simultaneous outbound HTTP calls, async DB drivers) | FastAPI, Starlette |
| Your team is already on Flask and the migration cost outweighs any benefit | Stay on Flask |
| You need automatic OpenAPI / Swagger generation | FastAPI |
| You need ASGI and Uvicorn / Daphne | FastAPI, Starlette |
| Your workload is I/O-bound and you want event-loop concurrency | FastAPI, Starlette |
| You need a full MVC framework with ORM, admin panel, and migrations | Django |
If your workload is primarily synchronous -- REST APIs, internal services, background job APIs, microservices that talk to SQL databases via sync drivers -- Lcore is a strong fit.
Ideal Use Cases
- Internal REST APIs -- admin panels, dashboards, data pipelines that talk to a SQL database via psycopg2, sqlite3, or mysqlclient
- Microservices with no async I/O -- services that call other services synchronously, process files, hash passwords, or do CPU-bound work
- Rapid prototyping -- drop in a single file, no virtual environment ceremony, start handling requests in minutes
- Air-gapped or restricted environments -- zero external dependencies means no supply-chain risk and no
pipaccess required - Embedded or constrained deployments -- containers, serverless functions, or edge nodes where you want the smallest possible footprint
- Teams moving off Bottle -- familiar single-file philosophy with middleware, DI, and security built in
- Scripts that occasionally serve HTTP -- background workers, CLI tools, or data jobs that also expose a health check or webhook endpoint
Live Demo & Documentation
| Link | Description |
|---|---|
| play.lcore.lusansapkota.com.np | Interactive playground -- test routes, middleware, auth flows, and API endpoints live |
| lcore.lusansapkota.com.np | Full documentation with guides, API reference, and real-world example |
The playground runs the TaskFlow demo backend -- a complete project management API (like Trello/Jira) that demonstrates every Lcore feature: token auth, RBAC, CRUD, file uploads, SMTP email, async handlers, middleware stack, dependency injection, plugins, and a full SPA frontend.
Demo credentials:
| Username | Password | Role |
|---|---|---|
| admin | admin123 | admin |
| alice | alice123 | member |
| bob | bob123 | member |
Production Example
from lcore import (
Lcore, request, response, ctx, on_shutdown,
CORSMiddleware, SecurityHeadersMiddleware,
BodyLimitMiddleware, CompressionMiddleware,
hash_password, verify_password,
BackgroundTaskPool, TestClient,
rate_limit, validate_request,
)
app = Lcore()
# Middleware stack
app.use(BodyLimitMiddleware(max_size=10 * 1024 * 1024))
app.use(SecurityHeadersMiddleware(hsts=True))
app.use(CORSMiddleware(allow_origins=['https://myapp.com']))
app.use(CompressionMiddleware())
# Background tasks
tasks = BackgroundTaskPool(max_workers=4)
# Secure password hashing (PBKDF2-SHA256)
@app.post('/register')
@validate_request(body={'username': str, 'password': str})
def register():
data = request.json
hashed = hash_password(data['password'])
# store hashed in database...
return {'created': True}
@app.post('/login')
@rate_limit(5, per=300) # NOTE: per-process — see "Multi-worker" note below
def login():
data = request.json
user = db.find_user(data['username'])
if user and verify_password(data['password'], user['password_hash']):
return {'token': generate_token(user)}
return {'error': 'Invalid credentials'}
# Graceful shutdown
@on_shutdown
def cleanup():
tasks.shutdown(wait=True)
app.run(server='gunicorn', host='0.0.0.0', port=8080, workers=4)
Production Notes
Rate limiter is per-process
@rate_limit uses an in-process token bucket with 64-stripe locking. Under a
multi-worker server (e.g. gunicorn -w 4) each worker has its own independent
bucket store, so the effective per-client rate limit is N × limit.
Lcore ships a built-in RedisRateLimitBackend — no extra code required:
pip install redis
from lcore import RedisRateLimitBackend, rate_limit
_rl = RedisRateLimitBackend('redis://localhost:6379/0')
@app.post('/api/login')
@rate_limit(5, per=300, backend=_rl) # enforced across all workers
def login(): ...
If Redis is unavailable the backend fails open (allows the request) and logs a warning so a Redis outage does not take down your application.
Temporary workaround (no Redis): @rate_limit(5 // num_workers, per=300).
Reverse proxies — always use ProxyFixMiddleware
Without it, request.remote_addr returns the proxy's IP, not the real client, and
request.url may use the wrong scheme (http instead of https). Add it first:
app.use(ProxyFixMiddleware(trusted_proxies=['10.0.0.1'])) # your proxy's IP(s)
Request timeouts
TimeoutMiddleware uses a persistent thread pool (not per-request), so it is safe to use
under load. It returns 503 to the client when the deadline is exceeded, but the handler thread
continues running in the background — Python has no safe way to kill a thread. Avoid handlers
that block indefinitely.
app.use(TimeoutMiddleware(timeout=30))
Testing
from lcore import Lcore, TestClient
app = Lcore()
@app.route('/api/users/<id:int>')
def get_user(id):
return {'id': id, 'name': 'Alice'}
client = TestClient(app)
resp = client.get('/api/users/1')
assert resp.status_code == 200
assert resp.json['name'] == 'Alice'
resp = client.post('/api/items', json={'name': 'Widget'})
assert resp.status_code == 200
Project Structure
Lcore/
lcore.py # The framework (single file)
backend/ # TaskFlow demo -- real-world example using every feature
app.py # Main entry point
config.py # Multi-source config + validation
models.py # SQLite database layer
modules/ # Auth, users, projects, notifications, plugins
templates/ # Welcome page, frontend SPA, error page
tests/ # 449 tests (unittest)
docs/ # Documentation site (hosted at lcore.lusansapkota.com.np)
benchmarks/ # Performance benchmarks
Documentation
Full documentation at lcore.lusansapkota.com.np
- Getting Started -- installation, first app, core concepts
- Routing -- typed parameters, groups, async handlers
- Request/Response -- JSON, files, cookies, headers
- Middleware -- built-in and custom middleware
- Plugins -- plugin system and lifecycle
- Advanced -- DI, security, testing, background tasks, production deployment
- API Reference -- complete class and function reference
- Real-World Example -- TaskFlow backend walkthrough
License
MIT -- see LICENSE for details.
Built by Lusan Sapkota • Inspired by Bottle
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file lcore-0.0.2.tar.gz.
File metadata
- Download URL: lcore-0.0.2.tar.gz
- Upload date:
- Size: 63.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
11de10b50a1dc66511e0d366c28806f06f2c845da328fda291b06d880d31d14d
|
|
| MD5 |
dde33c17cddad77d2881f23263c3fa3c
|
|
| BLAKE2b-256 |
32ab327d8b9ce9f875e7963d470e128a66cde1baaf37fc919665d9d5c61f2bf0
|
File details
Details for the file lcore-0.0.2-py3-none-any.whl.
File metadata
- Download URL: lcore-0.0.2-py3-none-any.whl
- Upload date:
- Size: 62.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
22b5468aa9759b649a2c753846a38a41ab3d04e7dec8f99d74d483720d8b7b40
|
|
| MD5 |
99f0a875b8886dcde2ab7ad3dc1c19a6
|
|
| BLAKE2b-256 |
59c75dd411ea7525f58feac3edc07dbc01eeaa173e714b572506635011fd5a88
|