Добавлены новые файлы: .gitignore для исключения временных файлов, fill_db.py для заполнения базы данных тестовыми данными, main.py с основным функционалом FastAPI, models.py с API моделями и requirements.txt для зависимостей проекта.

This commit is contained in:
Redsandyg 2025-06-02 13:15:42 +03:00
commit 37c855c601
5 changed files with 907 additions and 0 deletions

121
.gitignore vendored Normal file
View File

@ -0,0 +1,121 @@
# 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/
*.egg-info/
.installed.cfg
*.egg
# 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
*.spec
# 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
# 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
Pipfile.lock
# poetry
poetry.lock
# mypy
.mypy_cache/
.dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# VS Code
.vscode/
# Virtual environments
.venv/
venv/
ENV/
env/
# SQLite database
*.db
# MacOS
.DS_Store
# Windows
Thumbs.db
ehthumbs.db
Desktop.ini
# Other
*.swp
*.swo

161
fill_db.py Normal file
View File

@ -0,0 +1,161 @@
import random
from uuid import uuid4
from sqlmodel import Session
from main import AUTH_DB_ENGINE, TgAgent, Ref, Sale, Transaction
from sqlalchemy import text
from datetime import datetime, timedelta
# Константа: список user_ids
USER_IDS = [1001, 256844410, 426261192, 564746427]
# Примеры телефонов
PHONES = [
"+79991234567",
"+79997654321",
"+79993456789",
"+79992345678",
"+79994561234"
]
# Примеры описаний
DESCRIPTIONS = [
"Партнёр по рекламе",
"Блогер",
"Тестовая ссылка",
"Промо акция",
"VIP клиент",
"Реклама в Telegram от 01.05.2025",
"Пост в Instagram от 15.04.2025",
"Рассылка в ВК от 24.02.2025",
"Промо в Facebook от 10.03.2025",
"Пост в Twitter от 05.03.2025",
"Реклама в YouTube от 12.04.2025",
"Промо в TikTok от 20.04.2025",
"Пост в LinkedIn от 18.03.2025",
"Реклама в Одноклассниках от 22.03.2025",
"Промо в Pinterest от 30.03.2025",
"Пост в Reddit от 02.04.2025",
"Реклама в Dzen от 11.04.2025",
"Промо в Medium от 08.04.2025",
"Пост в WhatsApp от 14.04.2025",
"Реклама в Discord от 21.04.2025"
]
# Примеры имен и логинов
NAMES = [
"Иван Иванов",
"Петр Петров",
"Сергей Сергеев",
"Анна Смирнова"
]
LOGINS = [
"ivanov1001",
"petrov256",
"serg426",
"anna564"
]
# --- Загрузка mock-данных ---
ALL_DESCRIPTIONS = DESCRIPTIONS
# ---
def get_date_list(days=7):
today = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
return [today - timedelta(days=i) for i in range(days, -1, -1)]
def fill_db():
date_list = get_date_list(7) # 8 дней: от недели назад до сегодня
with Session(AUTH_DB_ENGINE) as session:
# Очистка таблиц
session.execute(text("DELETE FROM sale"))
session.execute(text("DELETE FROM ref"))
session.execute(text("DELETE FROM tgagent"))
session.commit()
# 1. TgAgents
tg_agents = []
for i, tg_agent_id in enumerate(USER_IDS):
dt = random.choice(date_list)
tg_agent = TgAgent(
tg_id=tg_agent_id,
chat_id=tg_agent_id, # chat_id совпадает с tg_id
phone=PHONES[i % len(PHONES)],
name=NAMES[i % len(NAMES)],
login=LOGINS[i % len(LOGINS)],
create_dttm=dt,
update_dttm=dt
)
session.add(tg_agent)
tg_agents.append(tg_agent)
session.commit()
for tg_agent in tg_agents:
session.refresh(tg_agent)
# 2. Refs (минимум 22 на агента)
refs = []
desc_count = len(ALL_DESCRIPTIONS)
for tg_agent in tg_agents:
ref_count = random.randint(22, int(22 * 1.25)) # от 22 до 27
for j in range(ref_count):
ref_val = str(uuid4())
desc_val = ALL_DESCRIPTIONS[(j % desc_count)]
dt = random.choice(date_list)
ref = Ref(
tg_agent_id=tg_agent.id,
ref=ref_val,
description=desc_val,
create_dttm=dt,
update_dttm=dt
)
session.add(ref)
refs.append(ref)
session.commit()
for ref in refs:
session.refresh(ref)
# 3. Sales (минимум 20 на каждый ref)
for ref in refs:
sale_count = random.randint(20, int(20 * 1.25)) # от 20 до 25
for _ in range(sale_count):
cost = round(random.uniform(100, 1000), 2)
crediting = round(cost * random.uniform(0.5, 1.0), 2)
dt = random.choice(date_list)
sale = Sale(
cost=cost,
crediting=crediting,
ref=ref.id,
sale_id=str(uuid4()),
create_dttm=dt,
update_dttm=dt
)
session.add(sale)
session.commit()
# 4. Transactions (только withdrawal на агента)
TRANSACTION_STATUSES = ['process', 'done', 'error', 'waiting']
for tg_agent in tg_agents:
withdrawal_count = random.randint(5, int(5 * 1.25)) # от 5 до 6
used_statuses = set()
for i in range(withdrawal_count):
dt = random.choice(date_list)
# Гарантируем, что каждый статус будет использован хотя бы раз
if len(used_statuses) < len(TRANSACTION_STATUSES):
status = TRANSACTION_STATUSES[len(used_statuses)]
used_statuses.add(status)
else:
status = random.choice(TRANSACTION_STATUSES)
transaction = Transaction(
transaction_id=str(uuid4()),
sum=round(random.uniform(200, 3000), 2),
tg_agent_id=tg_agent.id,
status=status,
create_dttm=dt,
update_dttm=dt
)
session.add(transaction)
session.commit()
print("База успешно заполнена!")
if __name__ == "__main__":
fill_db()

448
main.py Normal file
View File

@ -0,0 +1,448 @@
from fastapi import FastAPI, Depends, HTTPException, status, Query
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from sqlmodel import SQLModel, Field, create_engine, Session, select
from passlib.context import CryptContext
from typing import Optional, List, Dict
from datetime import datetime, timedelta
from models import RefAddRequest, RefResponse, RegisterRequest, Token, TokenRequest
from uuid import uuid4
from fastapi.responses import JSONResponse
from sqlalchemy import func
# Конфигурация
AUTH_DATABASE_ADDRESS = "sqlite:///partner.db"
#SQLModel
class TgAgent(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
tg_id: int = Field(index=True, unique=True)
chat_id: Optional[int] = None
phone: Optional[str] = None
name: Optional[str] = None
login: Optional[str] = None
create_dttm: datetime = Field(default_factory=datetime.utcnow)
update_dttm: datetime = Field(default_factory=datetime.utcnow)
class Ref(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
tg_agent_id: int = Field(foreign_key="tgagent.id")
ref: str
description: Optional[str] = None
create_dttm: datetime = Field(default_factory=datetime.utcnow)
update_dttm: datetime = Field(default_factory=datetime.utcnow)
class Sale(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
cost: float
crediting: float # сколько начислено за продажу
ref: int = Field(foreign_key="ref.id")
sale_id: str
create_dttm: datetime = Field(default_factory=datetime.utcnow)
update_dttm: datetime = Field(default_factory=datetime.utcnow)
class Transaction(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
transaction_id: str = Field(default_factory=lambda: str(uuid4()), index=True, unique=True)
sum: float
tg_agent_id: int = Field(foreign_key="tgagent.id")
status: str # 'process' || 'done' || 'error' || 'waiting'
create_dttm: datetime = Field(default_factory=datetime.utcnow)
update_dttm: datetime = Field(default_factory=datetime.utcnow)
# Создание движка базы данных
AUTH_DB_ENGINE = create_engine(AUTH_DATABASE_ADDRESS, echo=True)
SQLModel.metadata.create_all(AUTH_DB_ENGINE)
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")
# FastAPI app
app = FastAPI()
# CRUD
def get_tg_agent_by_tg_id(db: Session, tg_id: int) -> Optional[TgAgent]:
statement = select(TgAgent).where(TgAgent.tg_id == tg_id)
return db.exec(statement).first()
# Dependency
def get_db():
with Session(AUTH_DB_ENGINE) as session:
yield session
# Авторизация
async def get_current_tg_agent(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
# Ожидаем токен вида 'session_for_{tg_id}'
if not token.startswith("session_for_"):
raise credentials_exception
try:
tg_id = int(token.replace("session_for_", ""))
except Exception:
raise credentials_exception
tg_agent = get_tg_agent_by_tg_id(db, tg_id)
if tg_agent is None:
raise credentials_exception
return tg_agent
# Регистрация
@app.post("/register", tags=["partner-tg"])
def register(req: RegisterRequest, db: Session = Depends(get_db)):
tg_id = req.tg_id
chat_id = req.chat_id
phone = req.phone
name = getattr(req, 'name', None)
login = getattr(req, 'login', None)
print(f'tg_id: {tg_id}, chat_id: {chat_id}, phone: {phone}, name: {name}, login: {login}')
tg_agent = get_tg_agent_by_tg_id(db, tg_id)
if tg_agent:
raise HTTPException(status_code=400, detail="tg_id already registered")
new_tg_agent = TgAgent(tg_id=tg_id, chat_id=chat_id, phone=phone, name=name, login=login)
db.add(new_tg_agent)
db.commit()
db.refresh(new_tg_agent)
return {"msg": "TgAgent registered successfully"}
def authenticate_tg_agent(engine, tg_id: int):
with Session(engine) as db:
tg_agent = get_tg_agent_by_tg_id(db, tg_id)
if not tg_agent:
return None
return tg_agent
# Защищённый эндпоинт
@app.get("/protected", tags=["partner-tg"])
def protected_route(current_tg_agent: TgAgent = Depends(get_current_tg_agent)):
return {"msg": f"Hello, {current_tg_agent.tg_id}! This is a protected route."}
# Авторизация
@app.post("/token", response_model=Token, tags=["partner-tg"])
async def login_for_access_token(req: TokenRequest):
tg_id = req.tg_id
tg_agent = authenticate_tg_agent(AUTH_DB_ENGINE, tg_id)
if not tg_agent:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect tg_id",
headers={"WWW-Authenticate": "Bearer"},
)
access_token = f"session_for_{tg_agent.tg_id}"
return Token(access_token=access_token, token_type="bearer")
@app.get("/ref", response_model=list[RefResponse], tags=["partner-tg"])
def get_refs(current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Session = Depends(get_db)):
refs = db.exec(select(Ref).where(Ref.tg_agent_id == current_tg_agent.id)).all()
return [RefResponse(ref=r.ref, description=r.description or "") for r in refs]
@app.post("/ref/add", tags=["partner-tg"])
def add_ref(req: RefAddRequest, current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Session = Depends(get_db)):
new_ref = Ref(
tg_agent_id=current_tg_agent.id,
ref=str(uuid4()),
description=req.description
)
db.add(new_ref)
db.commit()
db.refresh(new_ref)
return {"ref": new_ref.ref}
@app.get("/ref/stat", tags=["partner-tg"])
def get_ref_stat(current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Session = Depends(get_db)):
# 1. Получаем все реферальные ссылки пользователя
refs = db.exec(select(Ref).where(Ref.tg_agent_id == current_tg_agent.id)).all()
result = []
for ref in refs:
# 2. Для каждой ссылки считаем продажи и сумму
sales = db.exec(select(Sale).where(Sale.ref == ref.id)).all()
sales_count = len(sales)
income = sum(sale.crediting for sale in sales)
result.append({
"description": ref.description or "",
"sales": sales_count,
"income": income
})
return {"refData": result}
@app.get("/stat", tags=["partner-tg"])
def get_stat(current_tg_agent: TgAgent = Depends(get_current_tg_agent), db: Session = Depends(get_db)):
# 1. Получаем все реферальные ссылки пользователя
refs = db.exec(select(Ref).where(Ref.tg_agent_id == current_tg_agent.id)).all()
ref_ids = [r.id for r in refs]
# 2. Считаем totalSales (продажи по всем рефам пользователя)
total_sales = db.exec(select(Sale).where(Sale.ref.in_(ref_ids))).all()
totalSales = len(total_sales)
totalIncome = sum(sale.crediting for sale in total_sales)
withdrawals = db.exec(
select(Transaction).where(
Transaction.tg_agent_id == current_tg_agent.id
)
).all()
availableWithdrawal = totalIncome - sum(t.sum for t in withdrawals)
return {
"totalSales": totalSales,
"totalIncome": totalIncome,
"availableWithdrawal": availableWithdrawal
}
@app.get("/dashboard/cards", tags=["bff"])
def get_dashboard_cards(db: Session = Depends(get_db)):
# 1. Общий доход - сумма всех Sale.cost
total_revenue = db.exec(select(Sale)).all()
totalRevenue = sum(sale.cost for sale in total_revenue)
# 2. Общие выплаты - сумма всех Sale.crediting
totalPayouts = sum(sale.crediting for sale in total_revenue)
# 3. Активные рефералы - количество уникальных TgAgent.tg_id
unique_agents = db.exec(select(TgAgent.tg_id)).all()
activeReferrals = len(set(unique_agents))
# 4. Ожидающие выплаты - разница между суммой всех Sale.crediting и суммой всех Transaction.sum
all_transactions = db.exec(select(Transaction)).all()
totalTransactions = sum(t.sum for t in all_transactions)
pendingPayouts = totalPayouts - totalTransactions
# 5. Количество продаж
totalSales = len(total_revenue)
return {
"totalRevenue": totalRevenue,
"totalPayouts": totalPayouts,
"activeReferrals": activeReferrals,
"pendingPayouts": pendingPayouts,
"totalSales": totalSales
}
@app.get("/dashboard/chart/total", tags=["bff"])
def get_dashboard_chart_total(db: Session = Depends(get_db)):
# Группируем продажи по дате (день)
result = db.exec(
select(
func.strftime('%Y-%m-%d', Sale.create_dttm).label('date'),
func.sum(Sale.cost).label('revenue'),
func.count(Sale.id).label('sales')
).group_by(func.strftime('%Y-%m-%d', Sale.create_dttm))
.order_by(func.strftime('%Y-%m-%d', Sale.create_dttm))
).all()
# Преобразуем результат в нужный формат
data = [
{"date": row.date, "revenue": row.revenue or 0, "sales": row.sales or 0}
for row in result
]
return JSONResponse(content=data)
@app.get("/dashboard/chart/agent", tags=["bff"])
def get_dashboard_chart_agent(db: Session = Depends(get_db)):
# Получаем всех агентов
agents = db.exec(select(TgAgent)).all()
result = []
for agent in agents:
# Получаем все рефы этого агента
refs = db.exec(select(Ref).where(Ref.tg_agent_id == agent.id)).all()
ref_ids = [r.id for r in refs]
if not ref_ids:
result.append({
"name": agent.name or f"Агент {agent.id}",
"count": 0,
"sum": 0.0
})
continue
# Получаем все продажи по этим рефам
sales = db.exec(select(Sale).where(Sale.ref.in_(ref_ids))).all()
sales_count = len(sales)
sales_sum = sum(sale.cost for sale in sales)
result.append({
"name": agent.name or f"Агент {agent.id}",
"count": sales_count,
"sum": sales_sum
})
return JSONResponse(content=result)
@app.get("/stat/agents", tags=["bff"])
def get_agents_stat(
db: Session = Depends(get_db),
date_start: str = Query(None),
date_end: str = Query(None),
):
agents_query = select(TgAgent)
if date_start:
agents_query = agents_query.where(TgAgent.create_dttm >= date_start)
if date_end:
agents_query = agents_query.where(TgAgent.create_dttm <= date_end)
agents = db.exec(agents_query).all()
result = []
for agent in agents:
refs = db.exec(select(Ref).where(Ref.tg_agent_id == agent.id)).all()
ref_ids = [r.id for r in refs]
ref_count = len(ref_ids)
if not ref_ids:
result.append({
"name": agent.name or f"Агент {agent.id}",
"refCount": 0,
"salesCount": 0,
"salesSum": 0.0,
"crediting": 0.0
})
continue
sales = db.exec(select(Sale).where(Sale.ref.in_(ref_ids))).all()
sales_count = len(sales)
sales_sum = sum(sale.cost for sale in sales)
crediting_sum = sum(sale.crediting for sale in sales)
result.append({
"name": agent.name or f"Агент {agent.id}",
"refCount": ref_count,
"salesCount": sales_count,
"salesSum": sales_sum,
"crediting": crediting_sum
})
return JSONResponse(content=result)
@app.get("/stat/referrals", tags=["bff"])
def get_referrals_stat(
db: Session = Depends(get_db),
date_start: str = Query(None),
date_end: str = Query(None),
):
refs_query = select(Ref)
if date_start:
refs_query = refs_query.where(Ref.create_dttm >= date_start)
if date_end:
refs_query = refs_query.where(Ref.create_dttm <= date_end)
refs = db.exec(refs_query).all()
result = []
for ref in refs:
agent = db.exec(select(TgAgent).where(TgAgent.id == ref.tg_agent_id)).first()
sales = db.exec(select(Sale).where(Sale.ref == ref.id)).all()
sales_count = len(sales)
sales_sum = sum(sale.cost for sale in sales)
result.append({
"ref": ref.ref,
"agent": agent.name if agent and agent.name else f"Агент {ref.tg_agent_id}",
"description": ref.description or "",
"salesSum": sales_sum,
"salesCount": sales_count
})
return JSONResponse(content=result)
@app.get("/stat/sales", tags=["bff"])
def get_sales_stat(
db: Session = Depends(get_db),
date_start: str = Query(None),
date_end: str = Query(None),
):
sales_query = select(Sale)
if date_start:
sales_query = sales_query.where(Sale.create_dttm >= date_start)
if date_end:
sales_query = sales_query.where(Sale.create_dttm <= date_end)
sales = db.exec(sales_query).all()
ref_ids = list(set(sale.ref for sale in sales))
refs = db.exec(select(Ref).where(Ref.id.in_(ref_ids))).all() if ref_ids else []
ref_map = {ref.id: ref for ref in refs}
agent_ids = list(set(ref.tg_agent_id for ref in refs)) if refs else []
agents = db.exec(select(TgAgent).where(TgAgent.id.in_(agent_ids))).all() if agent_ids else []
agent_map = {agent.id: agent for agent in agents}
result = []
for sale in sales:
ref_obj = ref_map.get(sale.ref)
agent_obj = agent_map.get(ref_obj.tg_agent_id) if ref_obj else None
result.append({
"saleId": sale.sale_id,
"cost": sale.cost,
"crediting": sale.crediting,
"ref": ref_obj.ref if ref_obj else None,
"name": agent_obj.name if agent_obj else None
})
return JSONResponse(content=result)
@app.get("/billing/cards", tags=["bff"])
def get_billing_cards(db: Session = Depends(get_db)):
# 1. cost - сумма всех Sale.cost
sales = db.exec(select(Sale)).all()
cost = sum(sale.cost for sale in sales)
# 2. crediting - сумма всех Sale.crediting
crediting = sum(sale.crediting for sale in sales)
# 3. pendingPayouts - разница между crediting и суммой всех Transaction.sum
transactions = db.exec(select(Transaction)).all()
total_transactions = sum(t.sum for t in transactions)
pendingPayouts = crediting - total_transactions
return {
"cost": cost,
"crediting": crediting,
"pendingPayouts": pendingPayouts
}
@app.get("/billing/payouts/transactions", tags=["bff"])
def get_billing_payouts_transactions(
db: Session = Depends(get_db),
date_start: str = Query(None),
date_end: str = Query(None),
):
query = select(Transaction)
if date_start:
query = query.where(Transaction.create_dttm >= date_start)
if date_end:
query = query.where(Transaction.create_dttm <= date_end)
transactions = db.exec(query).all()
result = []
for t in transactions:
agent = db.exec(select(TgAgent).where(TgAgent.id == t.tg_agent_id)).first()
result.append({
"id": t.transaction_id,
"sum": t.sum,
"agent": agent.name if agent else None,
"status": t.status,
"create_dttm": t.create_dttm,
"update_dttm": t.update_dttm,
})
return result
@app.get("/billing/chart/stat", tags=["bff"])
def get_billing_chart_stat(db: Session = Depends(get_db)):
# Группируем транзакции по дате (день) и статусу
result = db.exec(
select(
func.strftime('%Y-%m-%d', Transaction.create_dttm).label('date'),
Transaction.status.label('status'),
func.count(Transaction.id).label('count')
).group_by(
func.strftime('%Y-%m-%d', Transaction.create_dttm),
Transaction.status
).order_by(
func.strftime('%Y-%m-%d', Transaction.create_dttm),
Transaction.status
)
).all()
data = [
{"date": row.date, "status": row.status, "count": row.count}
for row in result
]
return JSONResponse(content=data)
@app.get("/billing/chart/pie", tags=["bff"])
def get_billing_chart_pie(db: Session = Depends(get_db)):
result = db.exec(
select(
Transaction.status.label('status'),
func.count(Transaction.id).label('count')
).group_by(Transaction.status)
).all()
data = [
{"status": row.status, "count": row.count}
for row in result
]
return JSONResponse(content=data)

31
models.py Normal file
View File

@ -0,0 +1,31 @@
from pydantic import BaseModel, Field
from typing import Optional
from sqlmodel import SQLModel
#API models
class Token(BaseModel):
access_token: str
token_type: str
class RefResponse(BaseModel):
ref: str
description: str
class RefAddRequest(BaseModel):
description: str
class TokenRequest(BaseModel):
tg_id: int
class RegisterRequest(BaseModel):
tg_id: int
chat_id: Optional[int] = None
phone: Optional[str] = None
name: Optional[str] = None
login: Optional[str] = None

146
requirements.txt Normal file
View File

@ -0,0 +1,146 @@
# This file was autogenerated by uv via the following command:
# uv pip compile pyproject.toml -o requirements.txt
annotated-types==0.7.0
# via pydantic
anyio==4.8.0
# via
# httpx
# starlette
# watchfiles
bcrypt==4.2.1
# via passlib
behave==1.2.6
# via epai-auth (pyproject.toml)
certifi==2024.12.14
# via
# httpcore
# httpx
# requests
cffi==1.17.1
# via cryptography
charset-normalizer==3.4.2
# via requests
click==8.1.8
# via
# rich-toolkit
# typer
# uvicorn
cryptography==44.0.0
# via epai-auth (pyproject.toml)
dnspython==2.7.0
# via email-validator
email-validator==2.2.0
# via fastapi
fastapi==0.115.6
# via epai-auth (pyproject.toml)
fastapi-cli==0.0.7
# via fastapi
greenlet==3.1.1
# via sqlalchemy
h11==0.14.0
# via
# httpcore
# uvicorn
httpcore==1.0.7
# via httpx
httptools==0.6.4
# via uvicorn
httpx==0.28.1
# via fastapi
idna==3.10
# via
# anyio
# email-validator
# httpx
# requests
jinja2==3.1.5
# via fastapi
markdown-it-py==3.0.0
# via rich
markupsafe==3.0.2
# via jinja2
mdurl==0.1.2
# via markdown-it-py
parse==1.20.2
# via
# behave
# parse-type
parse-type==0.6.4
# via behave
passlib==1.7.4
# via epai-auth (pyproject.toml)
psycopg2==2.9.10
# via epai-auth (pyproject.toml)
pycparser==2.22
# via cffi
pydantic==2.10.5
# via
# fastapi
# pydantic-settings
# sqlmodel
pydantic-core==2.27.2
# via pydantic
pydantic-settings==2.9.1
# via epai-auth (pyproject.toml)
pygments==2.19.1
# via rich
pyjwt==2.10.1
# via epai-auth (pyproject.toml)
python-dotenv==1.0.1
# via
# pydantic-settings
# uvicorn
python-multipart==0.0.20
# via fastapi
pyyaml==6.0.2
# via uvicorn
requests==2.32.3
# via epai-auth (pyproject.toml)
rich==13.9.4
# via
# rich-toolkit
# typer
rich-toolkit==0.12.0
# via fastapi-cli
ruff==0.9.1
# via epai-auth (pyproject.toml)
shellingham==1.5.4
# via typer
six==1.17.0
# via
# behave
# parse-type
sniffio==1.3.1
# via anyio
sqlalchemy==2.0.37
# via sqlmodel
sqlmodel==0.0.22
# via epai-auth (pyproject.toml)
starlette==0.41.3
# via fastapi
typer==0.15.1
# via fastapi-cli
typing-extensions==4.12.2
# via
# anyio
# fastapi
# pydantic
# pydantic-core
# rich-toolkit
# sqlalchemy
# typer
# typing-inspection
typing-inspection==0.4.0
# via pydantic-settings
urllib3==2.4.0
# via requests
uvicorn==0.34.0
# via
# fastapi
# fastapi-cli
#uvloop==0.21.0
# via uvicorn
watchfiles==1.0.4
# via uvicorn
websockets==14.1
# via uvicorn