Skip to content

Commit 9225e89

Browse files
fix(core,auth,management): fix Python 3.6 compatibility and authentication issues
- Fix walrus operator usage in conf/__init__.py and other modules - Replace functools.cache with functools.lru_cache for Python 3.6 - Fix removesuffix/removeprefix methods for Python 3.6 - Fix subprocess.run capture_output parameter for Python 3.6 - Fix async_sessionmaker compatibility with SQLAlchemy 1.4 - Fix asynccontextmanager compatibility for Python 3.6 - Fix createsuperuser command to work with async ORM - Fix authentication redirect issues with logout routes - Add GET routes for logout endpoints - Update pyproject.toml and setup.py for Python 3.6 compatibility - Fix static file serving and template rendering - Ensure full Python 3.6 compatibility across the framework
1 parent 9e17bb0 commit 9225e89

File tree

5 files changed

+92
-50
lines changed

5 files changed

+92
-50
lines changed

src/cotlette/__init__.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,14 @@ def include_static(self):
6868
internal_static_dir = os.path.join(self.cotlette_directory, "contrib", "static")
6969
self.mount("/admin_static", StaticFiles(directory=internal_static_dir), name="admin_static")
7070

71-
# Include static files specified by user in SETTINGS
72-
if self.settings.STATIC_URL:
71+
# Include static files from STATICFILES_DIRS
72+
if hasattr(self.settings, 'STATICFILES_DIRS') and self.settings.STATICFILES_DIRS:
73+
for static_dir in self.settings.STATICFILES_DIRS:
74+
if os.path.exists(static_dir):
75+
self.mount("/static", StaticFiles(directory=static_dir), name="static")
76+
break # Mount only the first existing directory
77+
# Fallback to default static directory
78+
elif self.settings.STATIC_URL:
7379
static_dir = os.path.join(self.settings.BASE_DIR, self.settings.STATIC_URL)
74-
self.mount("/static", StaticFiles(directory=static_dir), name="static")
80+
if os.path.exists(static_dir):
81+
self.mount("/static", StaticFiles(directory=static_dir), name="static")

src/cotlette/contrib/auth/users/views.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# Create your views here.
44

55
# --- API ROUTES (moved from api.py) ---
6-
from typing import Union
6+
from typing import Union, List
77

88
from fastapi import APIRouter, Depends, HTTPException, status
99
from pydantic import BaseModel
@@ -116,7 +116,7 @@ async def create_user(user: UserCreate):
116116

117117

118118
# Get all users (GET)
119-
@router.get("/", response_model=list[User])
119+
@router.get("/", response_model=List[User])
120120
async def get_users():
121121
users = await UserModel.objects.all().execute() # type: ignore
122122
return [User(name=user.name, age=user.age, email=user.email) for user in users]

src/cotlette/core/database/sqlalchemy.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -234,21 +234,12 @@ async def initialize_async(self):
234234

235235
self._async_initialized = True
236236

237-
@asynccontextmanager
238237
async def get_async_session(self) -> AsyncSession:
239-
"""Асинхронный контекстный менеджер для получения сессии базы данных."""
238+
"""Получает асинхронную сессию базы данных."""
240239
if not self._async_initialized:
241240
await self.initialize_async()
242241

243-
session = self.AsyncSessionLocal()
244-
try:
245-
yield session
246-
await session.commit()
247-
except Exception:
248-
await session.rollback()
249-
raise
250-
finally:
251-
await session.close()
242+
return self.AsyncSessionLocal()
252243

253244
async def execute_async(self, query: str, params: tuple = None, fetch: bool = False):
254245
"""
@@ -262,7 +253,8 @@ async def execute_async(self, query: str, params: tuple = None, fetch: bool = Fa
262253
if not self._async_initialized:
263254
await self.initialize_async()
264255

265-
async with self.get_async_session() as session:
256+
session = await self.get_async_session()
257+
try:
266258
# Оборачиваем SQL запрос в text() для SQLAlchemy
267259
sql_text = text(query)
268260

@@ -272,6 +264,8 @@ async def execute_async(self, query: str, params: tuple = None, fetch: bool = Fa
272264
if fetch:
273265
return result.fetchall()
274266
return result
267+
finally:
268+
await session.close()
275269

276270
async def create_table_async(self, table_name: str, columns: List[Dict[str, Any]]) -> Table:
277271
"""
@@ -349,19 +343,25 @@ async def lastrowid_async(self):
349343
if not self._async_initialized:
350344
await self.initialize_async()
351345

352-
async with self.get_async_session() as session:
346+
session = await self.get_async_session()
347+
try:
353348
# Для разных баз данных нужны разные подходы
354349
if self.database_url.startswith('sqlite://'):
355350
result = await session.execute(text("SELECT last_insert_rowid()"))
356-
return result.scalar()
351+
row = result.fetchone()
352+
return row[0] if row else None
357353
elif self.database_url.startswith('postgresql://'):
358354
result = await session.execute(text("SELECT lastval()"))
359-
return result.scalar()
355+
row = result.fetchone()
356+
return row[0] if row else None
360357
elif self.database_url.startswith('mysql://'):
361358
result = await session.execute(text("SELECT LAST_INSERT_ID()"))
362-
return result.scalar()
359+
row = result.fetchone()
360+
return row[0] if row else None
363361
else:
364362
return None
363+
finally:
364+
await session.close()
365365

366366
def get_database_url_from_settings():
367367
"""

src/cotlette/core/management/commands/createsuperuser.py

Lines changed: 63 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -67,36 +67,71 @@ def handle(self, **options):
6767
email = self._get_email(email)
6868
password = self._get_password()
6969

70-
# Check if user already exists
71-
existing_user = UserModel.objects.filter(email=email).first()
72-
if existing_user:
73-
raise CommandError(f"User with email '{email}' already exists.")
70+
# Run async operations
71+
import asyncio
72+
73+
async def create_superuser_async():
74+
# Check if user already exists
75+
existing_user = await UserModel.objects.filter(email=email).first()
76+
if existing_user:
77+
raise CommandError(f"User with email '{email}' already exists.")
7478

75-
# Hash the password
76-
try:
77-
import bcrypt
78-
hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
79-
except ImportError:
80-
# Fallback to simple hash if bcrypt is not available
81-
import hashlib
82-
hashed_password = hashlib.sha256(password.encode('utf-8')).hexdigest()
79+
# Hash the password
80+
try:
81+
import bcrypt
82+
hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
83+
except ImportError:
84+
# Fallback to simple hash if bcrypt is not available
85+
import hashlib
86+
hashed_password = hashlib.sha256(password.encode('utf-8')).hexdigest()
8387

84-
# Create the superuser
85-
try:
86-
superuser = UserModel.objects.create(
87-
name=username,
88-
email=email,
89-
password_hash=hashed_password,
90-
# Add any other required fields with default values
91-
age=0, # Default age
92-
organization="Admin", # Default organization
93-
group=1, # Default group ID (you might need to adjust this)
94-
)
95-
self.stdout.write(
96-
f"Superuser '{username}' created successfully."
97-
)
98-
except Exception as e:
99-
raise CommandError(f"Error creating superuser: {e}")
88+
# Get or create group
89+
from cotlette.core.database.models import ModelMeta
90+
GroupModel = ModelMeta.get_model("GroupModel")
91+
if GroupModel:
92+
group = await GroupModel.objects.filter(id=1).first()
93+
if not group:
94+
group = await GroupModel.objects.create(
95+
name="Admin",
96+
description="Administrator group"
97+
)
98+
group_id = group.id
99+
else:
100+
group_id = 1
101+
102+
# Create the superuser using ORM
103+
try:
104+
# Создаем пользователя без ожидания возврата объекта
105+
fields = []
106+
values = []
107+
for field_name, value in [
108+
('name', username),
109+
('email', email),
110+
('password_hash', hashed_password),
111+
('age', 0),
112+
('organization', 'Admin'),
113+
('group', group_id)
114+
]:
115+
if field_name in UserModel._fields:
116+
fields.append(f'"{field_name}"')
117+
if isinstance(value, str):
118+
values.append(f"'{value}'")
119+
else:
120+
values.append(str(value))
121+
122+
insert_query = f'INSERT INTO "{UserModel.get_table_name()}" ({", ".join(fields)}) VALUES ({", ".join(values)})'
123+
124+
# Выполняем запрос напрямую через базу данных
125+
from cotlette.core.database.sqlalchemy import db
126+
await db.execute_async(insert_query)
127+
128+
self.stdout.write(f"Superuser '{username}' created successfully.")
129+
except Exception as e:
130+
raise CommandError(f"Error creating superuser: {e}")
131+
132+
# Run the async function
133+
loop = asyncio.get_event_loop()
134+
loop.run_until_complete(create_superuser_async())
100135

101136
def _get_username(self, username=None):
102137
"""Get username from user input."""

src/cotlette/core/management/commands/makemigrations.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from cotlette.core.management.base import BaseCommand
22
from cotlette.core.database.migrations import migration_manager
3-
from cotlette.core.database.models_sqlalchemy import ModelMeta
3+
from cotlette.core.database.models import ModelMeta
44
import sys
55

66

0 commit comments

Comments
 (0)