init
This commit is contained in:
commit
e92bf907fe
130
.gitignore
vendored
Normal file
130
.gitignore
vendored
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
#
|
||||||
|
test_modules/
|
||||||
|
config/SSL/fullchain.pem
|
||||||
|
config/SSL/privkey.pem
|
||||||
|
logs/
|
||||||
|
SECRET_KEY.key
|
||||||
|
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
pip-wheel-metadata/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# celery beat schedule file
|
||||||
|
celerybeat-schedule
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
0
common_lib/__init__.py
Normal file
0
common_lib/__init__.py
Normal file
0
common_lib/core/__init__.py
Normal file
0
common_lib/core/__init__.py
Normal file
4
common_lib/core/rate_limit.py
Normal file
4
common_lib/core/rate_limit.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
from slowapi import Limiter
|
||||||
|
from slowapi.util import get_remote_address
|
||||||
|
|
||||||
|
limiter = Limiter(key_func=get_remote_address)
|
0
common_lib/docs/__init__.py
Normal file
0
common_lib/docs/__init__.py
Normal file
119
common_lib/docs/errors.py
Normal file
119
common_lib/docs/errors.py
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
COMMON_ERROR_RESPONSES = {
|
||||||
|
400: {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"example": {
|
||||||
|
"status": "error",
|
||||||
|
"errors": [
|
||||||
|
{"field": "general", "message": "Bad request syntax or invalid parameters"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
401: {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"example": {
|
||||||
|
"status": "error",
|
||||||
|
"errors": [
|
||||||
|
{"field": "login", "message": "Invalid login or password"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
403: {
|
||||||
|
"description": "Forbidden",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"example": {
|
||||||
|
"status": "error",
|
||||||
|
"errors": [
|
||||||
|
{"field": "permission", "message": "You don't have access to this resource"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
404: {
|
||||||
|
"description": "Not Found",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"example": {
|
||||||
|
"status": "error",
|
||||||
|
"errors": [
|
||||||
|
{"field": "resource", "message": "Requested resource not found"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
405: {
|
||||||
|
"description": "Method Not Allowed",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"example": {
|
||||||
|
"status": "error",
|
||||||
|
"errors": [
|
||||||
|
{"field": "method", "message": "Method not allowed on this endpoint"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
409: {
|
||||||
|
"description": "Conflict",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"example": {
|
||||||
|
"status": "error",
|
||||||
|
"errors": [
|
||||||
|
{"field": "conflict", "message": "Resource already exists or conflict occurred"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
418: {
|
||||||
|
"description": "I'm a teapot (In Development)",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"example": {
|
||||||
|
"status": "error",
|
||||||
|
"errors": [
|
||||||
|
{"field": "debug", "message": "This feature is under development"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
422: {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"example": {
|
||||||
|
"status": "error",
|
||||||
|
"errors": [
|
||||||
|
{"field": "login", "message": "Login must not contain whitespace characters"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"example": {
|
||||||
|
"status": "error",
|
||||||
|
"errors": [
|
||||||
|
{"field": "server", "message": "An unexpected error occurred. Please try again later."}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
0
common_lib/middlewares/__init__.py
Normal file
0
common_lib/middlewares/__init__.py
Normal file
168
common_lib/middlewares/error_handler.py
Normal file
168
common_lib/middlewares/error_handler.py
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
import logging
|
||||||
|
from fastapi import FastAPI, Request, HTTPException
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from fastapi.exceptions import RequestValidationError
|
||||||
|
from starlette.status import (
|
||||||
|
HTTP_400_BAD_REQUEST,
|
||||||
|
HTTP_401_UNAUTHORIZED,
|
||||||
|
HTTP_403_FORBIDDEN,
|
||||||
|
HTTP_404_NOT_FOUND,
|
||||||
|
HTTP_405_METHOD_NOT_ALLOWED,
|
||||||
|
HTTP_409_CONFLICT,
|
||||||
|
HTTP_418_IM_A_TEAPOT,
|
||||||
|
HTTP_422_UNPROCESSABLE_ENTITY,
|
||||||
|
)
|
||||||
|
from datetime import datetime
|
||||||
|
from config import settings
|
||||||
|
|
||||||
|
# Настраиваем логирование
|
||||||
|
logging.basicConfig(level=logging.ERROR)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def register_error_handlers(app: FastAPI):
|
||||||
|
|
||||||
|
# Кастомный обработчик для 400 ошибки (BAD_REQUEST)
|
||||||
|
@app.exception_handler(HTTP_400_BAD_REQUEST)
|
||||||
|
async def bad_request_handler(request: Request, exc: HTTPException):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=HTTP_400_BAD_REQUEST,
|
||||||
|
content={
|
||||||
|
"status": "error",
|
||||||
|
"errors": [{"field": "general", "message": exc.detail or "Bad request syntax or invalid parameters"}]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Кастомный обработчик для 401 ошибки (UNAUTHORIZED)
|
||||||
|
@app.exception_handler(HTTP_401_UNAUTHORIZED)
|
||||||
|
async def unauthorized_handler(request: Request, exc: HTTPException):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=HTTP_401_UNAUTHORIZED,
|
||||||
|
content={
|
||||||
|
"status": "error",
|
||||||
|
"errors": [{"field": "login", "message": exc.detail or "Unauthorized"}]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Кастомный обработчик для 403 ошибки (FORBIDDEN)
|
||||||
|
@app.exception_handler(HTTP_403_FORBIDDEN)
|
||||||
|
async def forbidden_handler(request: Request, exc: HTTPException):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=HTTP_403_FORBIDDEN,
|
||||||
|
content={
|
||||||
|
"status": "error",
|
||||||
|
"errors": [{"field": "permission", "message": exc.detail or "Forbidden"}]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Кастомный обработчик для 404 ошибки (NOT_FOUND)
|
||||||
|
@app.exception_handler(404)
|
||||||
|
async def custom_404_handler(request: Request, exc: HTTPException):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=HTTP_404_NOT_FOUND,
|
||||||
|
content={
|
||||||
|
"status": "error",
|
||||||
|
"errors": [{"field": "resource", "message": exc.detail or "Resource not found."}]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Кастомный обработчик для 405 ошибки
|
||||||
|
@app.exception_handler(405)
|
||||||
|
async def custom_405_handler(request: Request, exc: HTTPException):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=HTTP_405_METHOD_NOT_ALLOWED,
|
||||||
|
content={
|
||||||
|
"status": "error",
|
||||||
|
"errors": [{"field": "request", "message": exc.detail or "Method not allowed."}]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Кастомный обработчик для 409 ошибки (CONFLICT) (message: already exists)
|
||||||
|
@app.exception_handler(HTTP_409_CONFLICT)
|
||||||
|
async def conflict_handler(request: Request, exc: HTTPException):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=HTTP_409_CONFLICT,
|
||||||
|
content={
|
||||||
|
"status": "error",
|
||||||
|
"errors": [{"field": "conflict", "message": exc.detail or "Conflict"}]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Кастомный обработчик для 418 ошибки (IM_A_TEAPOT) (message: в разработке)
|
||||||
|
@app.exception_handler(HTTP_418_IM_A_TEAPOT)
|
||||||
|
async def teapot_handler(request: Request, exc: HTTPException):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=HTTP_418_IM_A_TEAPOT,
|
||||||
|
content={
|
||||||
|
"status": "error",
|
||||||
|
"errors": [{"field": "debug", "message": exc.detail or "This feature is under development"}]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Кастомный обработчик валидатора 422
|
||||||
|
@app.exception_handler(RequestValidationError)
|
||||||
|
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
||||||
|
errors = exc.errors()
|
||||||
|
formatted_errors = []
|
||||||
|
|
||||||
|
for err in errors:
|
||||||
|
field_path = ".".join(str(loc) for loc in err.get("loc", []) if isinstance(loc, str))
|
||||||
|
formatted_errors.append({
|
||||||
|
"field": field_path,
|
||||||
|
"message": err.get("msg", "Validation error")
|
||||||
|
})
|
||||||
|
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=HTTP_422_UNPROCESSABLE_ENTITY,
|
||||||
|
content={
|
||||||
|
"status": "error",
|
||||||
|
"errors": formatted_errors
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Универсальный обработчик HTTP ошибок
|
||||||
|
@app.exception_handler(HTTPException)
|
||||||
|
async def http_exception_handler(request: Request, exc: HTTPException):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=exc.status_code,
|
||||||
|
content={
|
||||||
|
"status": "error",
|
||||||
|
"errors": [{"field": "request", "message": exc.detail or "HTTP error"}]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Обработчик Internal Server Error
|
||||||
|
@app.exception_handler(Exception)
|
||||||
|
async def general_exception_handler(request: Request, exc: Exception):
|
||||||
|
# logger.error(f"Unhandled error: {exc}")
|
||||||
|
|
||||||
|
LOG_DIR = settings.LOG_DIR
|
||||||
|
os.makedirs(LOG_DIR, exist_ok=True)
|
||||||
|
# Уникальный код ошибки (первые 8 символов UUID)
|
||||||
|
error_id = str(uuid.uuid4())[:8]
|
||||||
|
date_str = datetime.now().strftime("%Y-%m-%d") # Текущая дата (YYYY-MM-DD)
|
||||||
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") # Полное время ошибки
|
||||||
|
|
||||||
|
log_filename = os.path.join(LOG_DIR, f"{error_id}_{date_str}.log")
|
||||||
|
|
||||||
|
full_log_message = (
|
||||||
|
f"{'-' * 50}\n"
|
||||||
|
f"Timestamp: {timestamp}\n"
|
||||||
|
f"Error ID: {error_id}\n"
|
||||||
|
f"Message: {exc}\n"
|
||||||
|
f"{'-' * 50}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(log_filename, "w", encoding="utf-8") as log_file:
|
||||||
|
log_file.write(full_log_message)
|
||||||
|
|
||||||
|
print(f"critical error: {LOG_DIR}/{error_id}_{date_str}.log")
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=500,
|
||||||
|
content={
|
||||||
|
"status": "error",
|
||||||
|
"errors": [{"field": "server", "message": "Internal Server Error"}]
|
||||||
|
}
|
||||||
|
)
|
1
common_lib/models/__init__.py
Normal file
1
common_lib/models/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
26
common_lib/models/db.py
Normal file
26
common_lib/models/db.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
||||||
|
from sqlalchemy.orm import declarative_base
|
||||||
|
from config import settings # поправил импорт — без .config
|
||||||
|
|
||||||
|
# Создаем асинхронный движок
|
||||||
|
engine = create_async_engine(settings.DATABASE_URL, echo=False, future=True)
|
||||||
|
|
||||||
|
# Создаем фабрику сессий
|
||||||
|
AsyncSessionLocal = async_sessionmaker(
|
||||||
|
bind=engine,
|
||||||
|
class_=AsyncSession,
|
||||||
|
expire_on_commit=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Базовый класс для моделей
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
# Зависимость для FastAPI
|
||||||
|
|
||||||
|
|
||||||
|
async def get_db():
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
try:
|
||||||
|
yield session
|
||||||
|
finally:
|
||||||
|
await session.close()
|
0
common_lib/utils/__init__.py
Normal file
0
common_lib/utils/__init__.py
Normal file
160
common_lib/utils/auth.py
Normal file
160
common_lib/utils/auth.py
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
import httpx
|
||||||
|
import re
|
||||||
|
from fastapi import Depends, Request, HTTPException, status
|
||||||
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from config import settings
|
||||||
|
|
||||||
|
auth_scheme = HTTPBearer()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CurrentUser:
|
||||||
|
user_id: str
|
||||||
|
session_id: str
|
||||||
|
permissions: List[str]
|
||||||
|
|
||||||
|
|
||||||
|
async def get_current_user(
|
||||||
|
request: Request,
|
||||||
|
credentials: HTTPAuthorizationCredentials = Depends(auth_scheme)
|
||||||
|
):
|
||||||
|
token = credentials.credentials
|
||||||
|
ip = request.client.host or "(unknown)"
|
||||||
|
user_agent = request.headers.get("User-Agent", "(unknown)")
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(verify=False) as client:
|
||||||
|
response = await client.post(
|
||||||
|
f"{settings.TOKEN_SERVICE}/decode",
|
||||||
|
json={
|
||||||
|
"token": token,
|
||||||
|
"ip": ip,
|
||||||
|
"user_agent": user_agent
|
||||||
|
},
|
||||||
|
timeout=5.0
|
||||||
|
)
|
||||||
|
except httpx.RequestError:
|
||||||
|
raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Token service unavailable")
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
# raise HTTPException(status_code=response.status_code, detail=response.json().get("detail", "Invalid token"))
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=response.status_code,
|
||||||
|
detail=f"token_service: {response.text}"
|
||||||
|
)
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
return CurrentUser(
|
||||||
|
user_id=data["user_id"],
|
||||||
|
session_id=data["session_id"],
|
||||||
|
permissions=data["permissions"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_username(value: str,
|
||||||
|
field_name: str = "login",
|
||||||
|
with_httpexception=False,
|
||||||
|
need_back=False,
|
||||||
|
min_length=3,
|
||||||
|
max_length=32,
|
||||||
|
error_status_code: int = status.HTTP_401_UNAUTHORIZED,
|
||||||
|
error_detail: str = "Invalid login or password") -> str:
|
||||||
|
"""
|
||||||
|
Username validator:
|
||||||
|
- checks for length
|
||||||
|
- no spaces
|
||||||
|
- no leading underscore
|
||||||
|
- no consecutive underscores
|
||||||
|
- only [A-Za-z0-9_]
|
||||||
|
"""
|
||||||
|
# Проверка типа и длины
|
||||||
|
if not isinstance(value, str) or not (min_length <= len(value) <= max_length):
|
||||||
|
msg = f"{field_name.capitalize()} must be between {min_length} and {max_length} characters long"
|
||||||
|
if with_httpexception:
|
||||||
|
raise HTTPException(status_code=error_status_code, detail=error_detail)
|
||||||
|
if need_back:
|
||||||
|
return False, msg
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
# Пробелы
|
||||||
|
if any(c.isspace() for c in value):
|
||||||
|
msg = f"{field_name.capitalize()} must not contain whitespace characters"
|
||||||
|
if with_httpexception:
|
||||||
|
raise HTTPException(status_code=error_status_code, detail=error_detail)
|
||||||
|
if need_back:
|
||||||
|
return False, msg
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
# Начинается с подчеркивания
|
||||||
|
if value.startswith('_'):
|
||||||
|
msg = f"{field_name.capitalize()} must not start with an underscore"
|
||||||
|
if with_httpexception:
|
||||||
|
raise HTTPException(status_code=error_status_code, detail=error_detail)
|
||||||
|
if need_back:
|
||||||
|
return False, msg
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
# Двойные подчеркивания
|
||||||
|
if '__' in value:
|
||||||
|
msg = f"{field_name.capitalize()} must not contain consecutive underscores"
|
||||||
|
if with_httpexception:
|
||||||
|
raise HTTPException(status_code=error_status_code, detail=error_detail)
|
||||||
|
if need_back:
|
||||||
|
return False, msg
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
# Только допустимые символы
|
||||||
|
if not re.fullmatch(r'[A-Za-z0-9_]+', value):
|
||||||
|
msg = f"{field_name.capitalize()} must contain only English letters, digits, and underscores"
|
||||||
|
if with_httpexception:
|
||||||
|
raise HTTPException(status_code=error_status_code, detail=error_detail)
|
||||||
|
if need_back:
|
||||||
|
return False, msg
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
if need_back:
|
||||||
|
return True, value.lower()
|
||||||
|
return value.lower()
|
||||||
|
|
||||||
|
|
||||||
|
def validate_password(value: str,
|
||||||
|
field_name: str = "password",
|
||||||
|
with_httpexception=False,
|
||||||
|
need_back=False,
|
||||||
|
min_length=8,
|
||||||
|
max_length=128,
|
||||||
|
error_status_code: int = status.HTTP_401_UNAUTHORIZED,
|
||||||
|
error_detail: str = "Invalid login or password") -> str:
|
||||||
|
"""
|
||||||
|
Validates password length and (optionally) other rules.
|
||||||
|
Supports HTTPException raising if `with_httpexception=True`.
|
||||||
|
Returns (True, value) or raises ValueError.
|
||||||
|
"""
|
||||||
|
# Проверка типа и длины
|
||||||
|
if not isinstance(value, str) or not (min_length <= len(value) <= max_length):
|
||||||
|
msg = f"{field_name.capitalize()} must be between {min_length} and {max_length} characters long"
|
||||||
|
if with_httpexception:
|
||||||
|
raise HTTPException(status_code=error_status_code, detail=error_detail)
|
||||||
|
if need_back:
|
||||||
|
return False, msg
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
# if any(c.isspace() for c in value):
|
||||||
|
# raise ValueError(f"{field_name.capitalize()} must not contain whitespace characters")
|
||||||
|
|
||||||
|
# if not re.search(r'[A-Z]', value):
|
||||||
|
# raise ValueError(f"{field_name.capitalize()} must contain at least one uppercase letter")
|
||||||
|
|
||||||
|
# if not re.search(r'\d', value):
|
||||||
|
# raise ValueError(f"{field_name.capitalize()} must contain at least one digit")
|
||||||
|
|
||||||
|
# if not re.search(r'[!@#$%^&*()\-_=+\[\]{};:\'",.<>?/|\\]', value):
|
||||||
|
# raise ValueError(f"{field_name.capitalize()} must contain at least one special character")
|
||||||
|
|
||||||
|
if need_back:
|
||||||
|
return True, value
|
||||||
|
return value
|
16
pyproject.toml
Normal file
16
pyproject.toml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[project]
|
||||||
|
name = "common-lib"
|
||||||
|
version = "0.0.1"
|
||||||
|
description = "Библиотека общих компонентов для микросервисов yobble"
|
||||||
|
authors = [{ name = "cheykrym", email = "you@example.com" }]
|
||||||
|
license = "CHEYKRYM"
|
||||||
|
dependencies = [
|
||||||
|
"fastapi",
|
||||||
|
"sqlalchemy",
|
||||||
|
"httpx",
|
||||||
|
"slowapi"
|
||||||
|
]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
Loading…
x
Reference in New Issue
Block a user