Skip to main content

A Spring Boot-inspired Python web framework built on Starlette, combining the elegance of Spring's dependency injection with Python's simplicity

Project description

StarSpring

A Spring Boot-inspired Python web framework built on Starlette, combining the elegance of Spring's dependency injection with Python's simplicity.

Table of Contents


What is StarSpring?

StarSpring is a modern Python web framework that brings Spring Boot's proven architectural patterns to the Python ecosystem. It provides:

  • Dependency Injection: Automatic component scanning and dependency resolution
  • Declarative Programming: Annotations-based routing, services, and repositories
  • Built-in ORM: SQLAlchemy integration with Spring Data-style repositories
  • Convention over Configuration: Auto-configuration with sensible defaults
  • Production-Ready: Built on Starlette for high performance and ASGI support

StarSpring is ideal for developers who appreciate Spring Boot's structure but prefer Python's ecosystem, or Python developers looking for a more opinionated, enterprise-ready framework.


What's New in v0.2.0 🎉

Automatic Form Data Parsing

HTML forms are now automatically parsed into Pydantic models - no more manual await request.form()!

@PostMapping("/users")
async def create_user(self, form: UserCreateForm):  # Automatic parsing!
    user = await self.user_service.create(form.name, form.email, form.age)
    return ModelAndView("success.html", {"user": user}, status_code=201)

Custom HTTP Status Codes

Set custom status codes for template responses:

return ModelAndView("users/created.html", {"user": user}, status_code=201)
return ModelAndView("errors/404.html", {"message": "Not found"}, status_code=404)

HTTP Method Override

Use PUT, PATCH, and DELETE methods in HTML forms with the _method field:

<form action="/users/{{ user.id }}" method="POST">
    <input type="hidden" name="_method" value="PUT">
    <!-- form fields -->
</form>
@PutMapping("/users/{id}")  # Works with HTML forms!
async def update_user(self, id: int, form: UserForm):
    # Handles both API requests and form submissions

Key Features

Dependency Injection & IoC Container

Automatic component scanning, registration, and dependency resolution with constructor injection.

Declarative Routing

Define REST endpoints using familiar decorators like @GetMapping, @PostMapping, @PutMapping, and @DeleteMapping.

Spring Data-Style Repositories

Auto-implemented repository methods based on naming conventions:

async def find_by_username(self, username: str) -> User | None:
    pass  # Automatically implemented!

Built-in ORM

SQLAlchemy integration with automatic table creation, transaction management, and entity mapping.

Auto-Configuration

Automatically loads application.yaml and scans common module names (controller, service, repository, entity).

Template Support

Jinja2 integration for server-side rendering with the @TemplateMapping decorator.

Middleware Stack

Built-in exception handling, logging, CORS support, and custom middleware capabilities.

Type Safety

Full type hint support with automatic parameter injection and validation.


Framework Comparison

Feature StarSpring FastAPI Flask Django
Layered Architecture Yes No No Yes
Dependency Injection Built-in IoC Function params Manual/Extensions Manual
Built-in ORM Yes (SQLAlchemy) No Extension Yes (Django ORM)
Auto-Configuration Yes No No Partial
Auto-Repositories Yes No No No
Async Support Native (ASGI) Native (ASGI) Limited Partial
Template Engine Jinja2 Manual Jinja2 Django Templates
Admin Interface No No Extensions Yes

When to choose StarSpring:

  • You're familiar with Spring Boot and want similar patterns in Python
  • Building API-first or microservice architectures
  • Need strong separation of concerns (Controller/Service/Repository)
  • Want automatic dependency injection and component scanning
  • Prefer convention over configuration

Core Concepts

Application Context

The IoC (Inversion of Control) container that manages component lifecycle and dependencies.

Components

Classes decorated with @Controller, @Service, or @Repository are automatically discovered and registered.

Dependency Injection

Components are injected via constructor parameters:

@Controller("/api/users")
class UserController:
    def __init__(self, user_service: UserService):
        self.user_service = user_service

Entities

Domain models decorated with @Entity that map to database tables.

Repositories

Data access layer with auto-implemented query methods based on method names.

Services

Business logic layer, typically transactional, that orchestrates repositories.

Controllers

HTTP request handlers that define REST endpoints and return responses.


Installation

Requirements

  • Python 3.10 or higher
  • pip or uv package manager

Install from PyPI

pip install starspring

Install for Development

git clone https://github.com/DasunNethsara-04/starspring.git
cd starspring
pip install -e .

Optional Dependencies

For PostgreSQL support:

pip install psycopg2-binary
# or for async
pip install asyncpg

For MySQL support:

pip install pymysql

Quick Start

1. Create Project Structure

my_app/
├── entity.py
├── repository.py
├── service.py
├── controller.py
├── main.py
└── application.yaml

2. Configure Database (application.yaml)

database:
  url: "sqlite:///app.db"
  ddl-auto: "create-if-not-exists"

server:
  port: 8000
  host: "0.0.0.0"

3. Define Entity (entity.py)

from starspring import BaseEntity, Column, Entity

@Entity(table_name="users")
class User(BaseEntity):
    # BaseEntity provides: id, created_at, updated_at
    
    username = Column(type=str, unique=True, nullable=False)
    email = Column(type=str, unique=True, nullable=False)
    is_active = Column(type=bool, default=True)
    role = Column(type=str, default="USER")

4. Create Repository (repository.py)

from starspring import Repository, StarRepository
from entity import User

@Repository
class UserRepository(StarRepository[User]):
    # Auto-implemented based on method name!
    async def find_by_username(self, username: str) -> User | None:
        pass
    
    async def find_by_email(self, email: str) -> User | None:
        pass

5. Implement Service (service.py)

from starspring import Service, Transactional
from entity import User
from repository import UserRepository

@Service
class UserService:
    def __init__(self, user_repo: UserRepository):
        self.user_repo = user_repo
    
    @Transactional
    async def create_user(self, username: str, email: str) -> User:
        # Check if user exists
        existing = await self.user_repo.find_by_email(email)
        if existing:
            raise ValueError(f"User with email {email} already exists")
        
        # Create and save
        user = User(username=username, email=email)
        return await self.user_repo.save(user)
    
    async def get_user(self, username: str) -> User | None:
        return await self.user_repo.find_by_username(username)

6. Build Controller (controller.py)

from starspring import Controller, GetMapping, PostMapping, ResponseEntity
from service import UserService

@Controller("/api/users")
class UserController:
    def __init__(self, user_service: UserService):
        self.user_service = user_service
    
    @PostMapping("/register")
    async def register(self, username: str, email: str):
        try:
            user = await self.user_service.create_user(username, email)
            return ResponseEntity.created(user)
        except ValueError as e:
            return ResponseEntity.bad_request(str(e))
    
    @GetMapping("/{username}")
    async def get_user(self, username: str):
        user = await self.user_service.get_user(username)
        if user:
            return ResponseEntity.ok(user)
        return ResponseEntity.not_found()
    
    @GetMapping("")
    async def list_users(self):
        users = await self.user_service.user_repo.find_all()
        return ResponseEntity.ok(users)

7. Bootstrap Application (main.py)

from starspring import StarSpringApplication

app = StarSpringApplication(title="My User API")

if __name__ == "__main__":
    app.run()

8. Run the Application

python main.py

Your API is now running at http://localhost:8000!

Test it:

# Register a user
curl -X POST http://localhost:8000/api/users/register \
  -H "Content-Type: application/json" \
  -d '{"username": "john", "email": "john@example.com"}'

# Get user
curl http://localhost:8000/api/users/john

# List all users
curl http://localhost:8000/api/users

Database & ORM

Configuration

StarSpring uses SQLAlchemy under the hood. Configure your database in application.yaml:

database:
  url: "postgresql://user:password@localhost/mydb"
  ddl-auto: "create-if-not-exists"  # Options: create, create-if-not-exists, update, none
  pool-size: 10
  max-overflow: 20

Supported databases:

  • SQLite: sqlite:///app.db
  • PostgreSQL: postgresql://user:pass@host/db
  • MySQL: mysql://user:pass@host/db

Defining Entities

Entities are Python classes decorated with @Entity:

from datetime import datetime
from starspring import BaseEntity, Column, Entity

@Entity(table_name="posts")
class Post(BaseEntity):
    title = Column(type=str, nullable=False, length=200)
    content = Column(type=str, nullable=False)
    published = Column(type=bool, default=False)
    author_id = Column(type=int, nullable=False)
    published_at = Column(type=datetime, nullable=True)

BaseEntity provides:

  • id: Auto-incrementing primary key
  • created_at: Timestamp of creation
  • updated_at: Timestamp of last update

Column Options

Column(
    type=str,           # Python type (str, int, bool, datetime)
    nullable=True,      # Allow NULL values
    unique=False,       # Unique constraint
    default=None,       # Default value
    length=None         # Max length for strings
)

Repository Patterns

StarSpring provides Spring Data-style repositories with auto-implemented methods:

from starspring.data.query_builder import QueryOperation

@Repository
class PostRepository(StarRepository[Post]):
    # Find by single field
    async def find_by_title(self, title: str) -> Post | None:
        pass
    
    # Find by multiple fields
    async def find_by_author_id_and_published(
        self, author_id: int, published: bool
    ) -> list[Post]:
        pass
    
    # Custom queries (manual implementation)
    async def find_recent_posts(self, limit: int = 10) -> list[Post]:
        # Implement custom logic here
        return await self._gateway.execute_query(
            "SELECT * FROM posts ORDER BY created_at DESC LIMIT :limit",
            {"limit": limit},
            Post,
            QueryOperation.FIND
        )

Auto-implemented method patterns:

  • find_by_<field> - Find by single field
  • find_by_<field>_and_<field> - Find by multiple fields
  • count_by_<field> - Count matching records
  • delete_by_<field> - Delete matching records
  • exists_by_<field> - Check if exists

Built-in methods:

  • save(entity) - Insert or update
  • find_by_id(id) - Find by primary key
  • find_all() - Get all records
  • delete(entity) - Delete entity
  • count() - Count all records

Transactions

Use the @Transactional decorator for automatic transaction management:

from starspring import Service, Transactional

@Service
class PostService:
    def __init__(self, post_repo: PostRepository):
        self.post_repo = post_repo
    
    @Transactional
    async def publish_post(self, post_id: int) -> Post:
        post = await self.post_repo.find_by_id(post_id)
        if not post:
            raise ValueError("Post not found")
        
        post.published = True
        post.published_at = datetime.now()
        return await self.post_repo.save(post)

Building REST APIs

Request Mapping Decorators

from starspring import (
    Controller, 
    GetMapping, 
    PostMapping, 
    PutMapping, 
    DeleteMapping,
    PatchMapping
)

@Controller("/api/posts")
class PostController:
    @GetMapping("")
    async def list_posts(self):
        # GET /api/posts
        pass
    
    @GetMapping("/{id}")
    async def get_post(self, id: int):
        # GET /api/posts/123
        pass
    
    @PostMapping("")
    async def create_post(self, title: str, content: str):
        # POST /api/posts
        pass
    
    @PutMapping("/{id}")
    async def update_post(self, id: int, title: str, content: str):
        # PUT /api/posts/123
        pass
    
    @DeleteMapping("/{id}")
    async def delete_post(self, id: int):
        # DELETE /api/posts/123
        pass

Parameter Injection

StarSpring automatically injects parameters from:

  • Path variables: /{id}id: int
  • Query parameters: ?page=1page: int
  • Request body: JSON payload → function parameters
@GetMapping("/search")
async def search_posts(
    self, 
    query: str,           # From ?query=...
    page: int = 1,        # From ?page=... (default: 1)
    limit: int = 10       # From ?limit=... (default: 10)
):
    # Implementation
    pass

Response Handling

Return entity directly:

@GetMapping("/{id}")
async def get_post(self, id: int):
    post = await self.post_service.get_post(id)
    return post  # Auto-serialized to JSON

Use ResponseEntity for control:

from starspring import ResponseEntity

@GetMapping("/{id}")
async def get_post(self, id: int):
    post = await self.post_service.get_post(id)
    if post:
        return ResponseEntity.ok(post)
    return ResponseEntity.not_found()

ResponseEntity methods:

  • ResponseEntity.ok(body) - 200 OK
  • ResponseEntity.created(body) - 201 Created
  • ResponseEntity.accepted(body) - 202 Accepted
  • ResponseEntity.no_content() - 204 No Content
  • ResponseEntity.bad_request(body) - 400 Bad Request
  • ResponseEntity.unauthorized(body) - 401 Unauthorized
  • ResponseEntity.forbidden(body) - 403 Forbidden
  • ResponseEntity.not_found(body) - 404 Not Found
  • ResponseEntity.status(code, body) - Custom status

Request Body Validation

from pydantic import BaseModel

class CreatePostRequest(BaseModel):
    title: str
    content: str
    published: bool = False

@PostMapping("")
async def create_post(self, request: CreatePostRequest):
    # Pydantic validates automatically
    post = await self.post_service.create_post(
        title=request.title,
        content=request.content,
        published=request.published
    )
    return ResponseEntity.created(post)

Working with Templates

StarSpring integrates Jinja2 for server-side rendering.

Setup Templates Directory

my_app/
├── templates/
│   ├── index.html
│   ├── post.html
│   └── layout.html
├── static/
│   ├── css/
│   └── js/
└── main.py

Configure Template Path

from starspring import StarSpringApplication

app = StarSpringApplication(title="My Blog")
app.add_template_directory("templates")
app.add_static_files("/static", "static")

Template Controller

from starspring import TemplateController, GetMapping, ModelAndView

@TemplateController("")
class WebController:
    def __init__(self, post_service: PostService):
        self.post_service = post_service
    
    @GetMapping("/")
    async def index(self) -> ModelAndView:
        posts = await self.post_service.get_recent_posts()
        return ModelAndView("index.html", {
            "posts": posts,
            "title": "My Blog"
        })
    
    @GetMapping("/post/{id}")
    async def view_post(self, id: int) -> ModelAndView:
        post = await self.post_service.get_post(id)
        return ModelAndView("post.html", {"post": post})

Template Files

templates/layout.html:

<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}My Blog{% endblock %}</title>
    <link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
    <nav>
        <a href="/">Home</a>
    </nav>
    <main>
        {% block content %}{% endblock %}
    </main>
</body>
</html>

templates/index.html:

{% extends "layout.html" %}

{% block title %}{{ title }}{% endblock %}

{% block content %}
<h1>Recent Posts</h1>
{% for post in posts %}
    <article>
        <h2><a href="/post/{{ post.id }}">{{ post.title }}</a></h2>
        <p>{{ post.content[:200] }}...</p>
        <small>{{ post.created_at }}</small>
    </article>
{% endfor %}
{% endblock %}

Middleware

Built-in Middleware

StarSpring includes:

  • ExceptionHandlerMiddleware: Catches and formats exceptions
  • LoggingMiddleware: Logs requests and responses
  • CORSMiddleware: Handles Cross-Origin Resource Sharing

Enable CORS

from starspring import StarSpringApplication
from starspring.middleware.cors import CORSConfig

app = StarSpringApplication(title="My API")

cors_config = CORSConfig(
    allow_origins=["http://localhost:3000"],
    allow_methods=["GET", "POST", "PUT", "DELETE"],
    allow_headers=["*"],
    allow_credentials=True
)

app.add_cors(cors_config)

Custom Middleware

from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request

class CustomMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        # Before request
        print(f"Request: {request.method} {request.url}")
        
        # Process request
        response = await call_next(request)
        
        # After request
        print(f"Response: {response.status_code}")
        
        return response

# Add to application
from starlette.middleware import Middleware

app = StarSpringApplication(title="My API")
app._middleware.append(Middleware(CustomMiddleware))

Advanced Features

Lifecycle Hooks

app = StarSpringApplication(title="My API")

@app.on_startup
async def startup():
    print("Application starting...")
    # Initialize resources

@app.on_shutdown
async def shutdown():
    print("Application shutting down...")
    # Cleanup resources

Environment-Specific Configuration

# application.yaml
spring:
  profiles:
    active: ${ENVIRONMENT:development}

---
# Development profile
spring:
  profiles: development
database:
  url: "sqlite:///dev.db"

---
# Production profile
spring:
  profiles: production
database:
  url: "postgresql://user:pass@prod-db/myapp"

Manual Component Registration

from starspring.core.context import get_application_context

# Register bean manually
context = get_application_context()
context.register_bean("my_service", MyService())

# Get bean
service = context.get_bean(MyService)

Custom Repository Methods

from starspring.data.query_builder import QueryOperation

@Repository
class PostRepository(StarRepository[Post]):
    async def find_published_posts(self) -> list[Post]:
        # Custom SQL query
        return await self._gateway.execute_query(
            "SELECT * FROM posts WHERE published = :published",
            {"published": True},
            Post,
            QueryOperation.FIND
        )

Examples

Complete examples are available in the examples/ directory:

  • examples/test1: Basic CRUD API with users
  • examples/blog: Blog application with templates
  • examples/microservice: Microservice architecture example

Contributing

Contributions are welcome! Please:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

StarSpring is released under the MIT License. See LICENSE file for details.


Support


Built with love for the Python community by developers who appreciate Spring Boot's elegance.

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

starspring-0.2.0.tar.gz (49.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

starspring-0.2.0-py3-none-any.whl (53.2 kB view details)

Uploaded Python 3

File details

Details for the file starspring-0.2.0.tar.gz.

File metadata

  • Download URL: starspring-0.2.0.tar.gz
  • Upload date:
  • Size: 49.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: pdm/2.26.6 CPython/3.14.2 Windows/11

File hashes

Hashes for starspring-0.2.0.tar.gz
Algorithm Hash digest
SHA256 043943c5bf60e980f2c86e9d3daf3fda2de4311cc90d99c5812c7ca5a93437cb
MD5 236c0beec52e4bc56d317c2057955e49
BLAKE2b-256 0796b1e955fa6d3644c8f97ec63203d60c6002300a511e63474a1ce787e0e314

See more details on using hashes here.

File details

Details for the file starspring-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: starspring-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 53.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: pdm/2.26.6 CPython/3.14.2 Windows/11

File hashes

Hashes for starspring-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2a62441242b60be1c1d0bf86135a07d61074ea708f992d45ce7da72de3a097d3
MD5 5763527cb3ac164dee935a6228c247f2
BLAKE2b-256 d548905e67c0af2d97cd2cf21433546c682c72db50c8f679506b99cca94959ce

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page