Skip to content

Commit 08cda6f

Browse files
Migrate from FastAPI to Starlette
- Replace FastAPI with Starlette as the core framework - Add compatibility layer (compat.py) for FastAPI-like API - Update all imports from fastapi to raystack.compat or starlette - Simplify dependencies: remove python-jose, apscheduler, itsdangerous, pydantic-settings - Relax version constraints to avoid dependency conflicts - Update project templates to use Starlette - Fix pyproject.toml configuration issues
1 parent 7380776 commit 08cda6f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+218
-106
lines changed

pyproject.toml

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,34 +7,34 @@ name = "raystack"
77
version = "0.0.0"
88
requires-python = ">= 3.6"
99
dependencies = [
10-
"uvicorn<0.20.0",
11-
"fastapi<0.100.0",
12-
"asgiref<4.0.0",
13-
"jinja2<3.2.0",
14-
"bcrypt<4.1.0",
15-
# "python-jose<3.4.0", # Replaced with PyJWT
16-
"pyjwt<2.8.0",
17-
"itsdangerous<2.2.0",
18-
"python-multipart<0.1.0",
19-
"sqlalchemy<2.0.0",
20-
"alembic<1.12.0",
21-
"click<8.2.0",
10+
"uvicorn>=0.15.0",
11+
"starlette>=0.27.0",
12+
"asgiref>=3.0.0",
13+
"jinja2>=2.10.0",
14+
"bcrypt>=3.0.0",
15+
"pyjwt>=2.0.0",
16+
"python-multipart>=0.0.5",
17+
"sqlalchemy>=1.4.0,<2.0.0",
18+
"alembic>=1.8.0",
19+
"click>=7.0.0",
20+
"passlib[bcrypt]",
21+
"sqlmodel>=0.0.8",
22+
"pydantic>=1.10.13,<2.0.0",
2223
]
2324

2425
authors = [
2526
{name = "Vladimir Penzin", email = "[email protected]"},
2627
]
2728

28-
description = "FastAPI sizzles, Django dazzles. The best of both worlds in one framework."
29+
description = "Starlette sizzles, Django dazzles. The best of both worlds in one framework."
2930

3031
readme = "README.md"
31-
license = {text = "MIT License"}
32+
license = "MIT"
3233
classifiers = [
3334
"Development Status :: 2 - Pre-Alpha",
3435
"Environment :: Web Environment",
35-
"Framework :: FastAPI",
36+
"Framework :: Starlette",
3637
"Intended Audience :: Developers",
37-
"License :: OSI Approved :: MIT License",
3838
"Operating System :: OS Independent",
3939
"Programming Language :: Python",
4040
"Programming Language :: Python :: 3",

requirements.txt

Lines changed: 0 additions & 13 deletions
This file was deleted.

setup.py

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ def read_readme():
1010
if os.path.exists(readme_path):
1111
with open(readme_path, "r", encoding="utf-8") as f:
1212
return f.read()
13-
return "FastAPI sizzles, Django dazzles. The best of both worlds in one framework."
13+
return "Starlette sizzles, Django dazzles. The best of both worlds in one framework."
1414

1515
setup(
1616
name="raystack",
1717
version="0.0.0",
18-
description="FastAPI sizzles, Django dazzles. The best of both worlds in one framework.",
18+
description="Starlette sizzles, Django dazzles. The best of both worlds in one framework.",
1919
long_description=read_readme(),
2020
long_description_content_type="text/markdown",
2121
author="Vladimir Penzin",
@@ -24,21 +24,8 @@ def read_readme():
2424
packages=find_packages(where="src"),
2525
package_dir={"": "src"},
2626
python_requires=">=3.6",
27-
# data_files=[("", ["suppress_warnings.pth"])], # Removed as no longer needed
28-
install_requires=[
29-
"uvicorn<0.20.0",
30-
"fastapi<0.100.0",
31-
"asgiref<4.0.0",
32-
"jinja2<3.2.0",
33-
"bcrypt<4.1.0",
34-
# "python-jose<3.4.0", # Replaced with PyJWT
35-
"pyjwt<2.8.0",
36-
"itsdangerous<2.2.0",
37-
"python-multipart<0.1.0",
38-
"sqlalchemy<2.0.0",
39-
"alembic<1.12.0",
40-
"click<8.2.0",
41-
],
27+
# Dependencies are defined in pyproject.toml
28+
# install_requires is automatically read from pyproject.toml
4229
entry_points={
4330
"console_scripts": [
4431
"raystack=raystack.core.management:execute_from_command_line",
@@ -47,9 +34,8 @@ def read_readme():
4734
classifiers=[
4835
"Development Status :: 2 - Pre-Alpha",
4936
"Environment :: Web Environment",
50-
"Framework :: FastAPI",
37+
"Framework :: Starlette",
5138
"Intended Audience :: Developers",
52-
"License :: OSI Approved :: MIT License",
5339
"Operating System :: OS Independent",
5440
"Programming Language :: Python",
5541
"Programming Language :: Python :: 3",
@@ -61,5 +47,5 @@ def read_readme():
6147
"Topic :: Software Development :: Libraries :: Application Frameworks",
6248
"Topic :: Software Development :: Libraries :: Python Modules",
6349
],
64-
license="MIT License",
50+
# License is defined in pyproject.toml
6551
)

src/raystack/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
# import importlib.util
66
import importlib
77

8-
from fastapi import FastAPI
9-
from fastapi.staticfiles import StaticFiles
8+
from starlette.applications import Starlette
9+
from starlette.staticfiles import StaticFiles
1010

1111
from raystack.conf import settings
1212
from raystack import shortcuts
@@ -23,7 +23,7 @@ def setup():
2323
logger = logging.getLogger("uvicorn")
2424

2525

26-
class Raystack(FastAPI):
26+
class Raystack(Starlette):
2727

2828
def __init__(self):
2929
super().__init__()
@@ -50,7 +50,7 @@ def include_routers(self):
5050

5151
# If module contains routers, include them
5252
if hasattr(module, "router"):
53-
self.include_router(module.router)
53+
self.mount("", module.router)
5454
logger.info(f"✅'{app_path}.router'")
5555
else:
5656
logger.warning(f"⚠️ '{app_path}.router'")

src/raystack/compat.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
"""
2+
Compatibility layer for FastAPI -> Starlette migration.
3+
Provides FastAPI-like API on top of Starlette.
4+
"""
5+
from starlette.routing import Router
6+
from starlette.requests import Request
7+
from starlette.responses import HTMLResponse, RedirectResponse, JSONResponse, Response
8+
from starlette.status import HTTP_200_OK, HTTP_201_CREATED, HTTP_400_BAD_REQUEST, HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN, HTTP_404_NOT_FOUND, HTTP_422_UNPROCESSABLE_ENTITY, HTTP_500_INTERNAL_SERVER_ERROR
9+
from typing import Any, Callable, Optional
10+
from functools import wraps
11+
import inspect
12+
13+
14+
# Export Starlette classes as FastAPI-compatible names
15+
__all__ = [
16+
'APIRouter',
17+
'Request',
18+
'HTMLResponse',
19+
'RedirectResponse',
20+
'JSONResponse',
21+
'Response',
22+
'HTTPException',
23+
'status',
24+
'Depends',
25+
'OAuth2PasswordBearer',
26+
]
27+
28+
29+
class APIRouter(Router):
30+
"""
31+
FastAPI-compatible router based on Starlette Router.
32+
"""
33+
def __init__(self, prefix: str = "", tags: Optional[list] = None, **kwargs):
34+
super().__init__(**kwargs)
35+
self.prefix = prefix
36+
self.tags = tags or []
37+
self._routes = []
38+
39+
def include_router(self, router: 'APIRouter', prefix: str = "", tags: Optional[list] = None, include_in_schema: bool = True):
40+
"""
41+
Include another router into this router.
42+
"""
43+
full_prefix = self.prefix + prefix
44+
if tags:
45+
combined_tags = self.tags + tags
46+
else:
47+
combined_tags = self.tags
48+
49+
# Mount the router with the combined prefix
50+
if full_prefix:
51+
# Create a new router with the prefix
52+
prefixed_router = Router()
53+
prefixed_router.mount(full_prefix, router)
54+
self.mount("", prefixed_router)
55+
else:
56+
self.mount("", router)
57+
58+
def get(self, path: str, **kwargs):
59+
"""Decorator for GET routes."""
60+
def decorator(func: Callable):
61+
return self.add_route(path, func, methods=["GET"], **kwargs)
62+
return decorator
63+
64+
def post(self, path: str, **kwargs):
65+
"""Decorator for POST routes."""
66+
def decorator(func: Callable):
67+
return self.add_route(path, func, methods=["POST"], **kwargs)
68+
return decorator
69+
70+
def put(self, path: str, **kwargs):
71+
"""Decorator for PUT routes."""
72+
def decorator(func: Callable):
73+
return self.add_route(path, func, methods=["PUT"], **kwargs)
74+
return decorator
75+
76+
def delete(self, path: str, **kwargs):
77+
"""Decorator for DELETE routes."""
78+
def decorator(func: Callable):
79+
return self.add_route(path, func, methods=["DELETE"], **kwargs)
80+
return decorator
81+
82+
def patch(self, path: str, **kwargs):
83+
"""Decorator for PATCH routes."""
84+
def decorator(func: Callable):
85+
return self.add_route(path, func, methods=["PATCH"], **kwargs)
86+
return decorator
87+
88+
89+
class HTTPException(Exception):
90+
"""HTTP exception for error responses."""
91+
def __init__(self, status_code: int, detail: Any = None, headers: Optional[dict] = None):
92+
self.status_code = status_code
93+
self.detail = detail
94+
self.headers = headers
95+
96+
97+
# Status codes module
98+
class status:
99+
HTTP_200_OK = HTTP_200_OK
100+
HTTP_201_CREATED = HTTP_201_CREATED
101+
HTTP_400_BAD_REQUEST = HTTP_400_BAD_REQUEST
102+
HTTP_401_UNAUTHORIZED = HTTP_401_UNAUTHORIZED
103+
HTTP_403_FORBIDDEN = HTTP_403_FORBIDDEN
104+
HTTP_404_NOT_FOUND = HTTP_404_NOT_FOUND
105+
HTTP_422_UNPROCESSABLE_ENTITY = HTTP_422_UNPROCESSABLE_ENTITY
106+
HTTP_500_INTERNAL_SERVER_ERROR = HTTP_500_INTERNAL_SERVER_ERROR
107+
108+
109+
# Dependency injection system
110+
class Depends:
111+
"""
112+
Dependency injection system compatible with FastAPI's Depends.
113+
"""
114+
def __init__(self, dependency: Callable):
115+
self.dependency = dependency
116+
117+
async def __call__(self, request: Request):
118+
"""Resolve the dependency."""
119+
if inspect.iscoroutinefunction(self.dependency):
120+
return await self.dependency(request)
121+
else:
122+
return self.dependency(request)
123+
124+
125+
# OAuth2PasswordBearer replacement
126+
class OAuth2PasswordBearer:
127+
"""
128+
OAuth2 password bearer token extractor.
129+
"""
130+
def __init__(self, tokenUrl: str, scheme_name: Optional[str] = None):
131+
self.tokenUrl = tokenUrl
132+
self.scheme_name = scheme_name or "OAuth2"
133+
134+
async def __call__(self, request: Request) -> str:
135+
"""Extract token from Authorization header."""
136+
authorization = request.headers.get("Authorization")
137+
if not authorization:
138+
raise HTTPException(
139+
status_code=status.HTTP_403_FORBIDDEN,
140+
detail="Not authenticated"
141+
)
142+
scheme, token = authorization.split(" ", 1)
143+
if scheme.lower() != "bearer":
144+
raise HTTPException(
145+
status_code=status.HTTP_403_FORBIDDEN,
146+
detail="Invalid authentication scheme"
147+
)
148+
return token

src/raystack/conf/app_template_async/__init__.py-tpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from fastapi import APIRouter
1+
from raystack.compat import APIRouter
22

33
from .api import router as api_router
44
from .urls import router as urls_router

src/raystack/conf/app_template_async/api.py-tpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from fastapi import APIRouter
1+
from raystack.compat import APIRouter
22

33

44
router = APIRouter()

src/raystack/conf/app_template_async/deps.py-tpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from fastapi import Depends
1+
from raystack.compat import Depends
22
from sqlalchemy.ext.asyncio import AsyncSession
33

44
# Use framework functionality for database operations

src/raystack/conf/app_template_async/urls.py-tpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from fastapi import APIRouter
1+
from raystack.compat import APIRouter
22
from . import views
33

44
router = APIRouter()

src/raystack/conf/app_template_async/views.py-tpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from raystack.shortcuts import render_template
2-
from fastapi import Request, Depends
2+
from raystack.compat import Request, Depends
33
from sqlalchemy.ext.asyncio import AsyncSession
44
from sqlmodel import select
55

0 commit comments

Comments
 (0)