fix data base
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
# Database configuration package
|
||||
from pydantic.generics import GenericModel
|
||||
import re
|
||||
import os
|
||||
import urllib.parse
|
||||
|
||||
class Settings(GenericModel):
|
||||
DB_HOST: str = os.getenv("DB_HOST", "postgres")
|
||||
DB_PORT: int = int(os.getenv("DB_PORT", "5432"))
|
||||
DB_USER: str = os.getenv("DB_USER", "postgres")
|
||||
DB_PASS: str = os.getenv("DB_PASS", "")
|
||||
DB_NAME: str = os.getenv("DB_NAME", "syte")
|
||||
DB_NAME: str = os.getenv("DB_NAME", "profi")
|
||||
|
||||
@classmethod
|
||||
def validate_db_port(cls, v):
|
||||
@@ -23,8 +25,10 @@ class Settings(GenericModel):
|
||||
|
||||
@property
|
||||
def DATABASE_URL_asyncpg(self) -> str:
|
||||
return f"postgresql+asyncpg://{self.DB_USER}:{self.DB_PASS}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}"
|
||||
pwd = urllib.parse.quote_plus(str(self.DB_PASS))
|
||||
return f"postgresql+asyncpg://{self.DB_USER}:{pwd}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}"
|
||||
|
||||
@property
|
||||
def DATABASE_URL_syncpg(self) -> str:
|
||||
return f"postgresql+psycopg2://{self.DB_USER}:{self.DB_PASS}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}"
|
||||
pwd = urllib.parse.quote_plus(str(self.DB_PASS))
|
||||
return f"postgresql+pg8000://{self.DB_USER}:{pwd}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}"
|
||||
16
bd/tables/choice.py
Normal file
16
bd/tables/choice.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from sqlalchemy import Column, String, Integer, ForeignKey
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from .simple_base import Base
|
||||
import uuid
|
||||
|
||||
|
||||
class Choice(Base):
|
||||
__tablename__ = "choices"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
question_id = Column(UUID(as_uuid=True), ForeignKey("questions.id", ondelete="CASCADE"), nullable=False)
|
||||
text = Column(String(500), nullable=False)
|
||||
position = Column(Integer, nullable=True)
|
||||
|
||||
|
||||
__all__ = ["Base", "Choice"]
|
||||
17
bd/tables/group.py
Normal file
17
bd/tables/group.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from sqlalchemy import Column, String, ForeignKey
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from .simple_base import Base
|
||||
import uuid
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
class Group(Base):
|
||||
__tablename__ = "group"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
name_group = Column(String(150), nullable=False)
|
||||
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
||||
|
||||
user = relationship("User", backref="groups", passive_deletes=True)
|
||||
|
||||
__all__ = ["Base", "Group"]
|
||||
|
||||
17
bd/tables/organization.py
Normal file
17
bd/tables/organization.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from sqlalchemy import Column, String, ForeignKey
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from .simple_base import Base
|
||||
import uuid
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
class Organization(Base):
|
||||
__tablename__ = "organization"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
name_organization = Column(String(150), nullable=False)
|
||||
group_id = Column(UUID(as_uuid=True), ForeignKey("group.id", ondelete="CASCADE"), nullable=False)
|
||||
|
||||
group = relationship("Group", backref="organizations", passive_deletes=True)
|
||||
|
||||
__all__ = ["Base", "Organization"]
|
||||
|
||||
21
bd/tables/poll.py
Normal file
21
bd/tables/poll.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from sqlalchemy import Column, String, Boolean, DateTime, Text
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.sql import func
|
||||
from sqlalchemy.orm import relationship
|
||||
from .simple_base import Base
|
||||
import uuid
|
||||
|
||||
|
||||
class Poll(Base):
|
||||
__tablename__ = "polls"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
title = Column(String(255), nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
|
||||
questions = relationship("Question", backref="poll", cascade="all, delete-orphan", passive_deletes=True)
|
||||
|
||||
|
||||
__all__ = ["Base", "Poll"]
|
||||
20
bd/tables/question.py
Normal file
20
bd/tables/question.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from sqlalchemy import Column, Text, Integer, ForeignKey, String
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import relationship
|
||||
from .simple_base import Base
|
||||
import uuid
|
||||
|
||||
|
||||
class Question(Base):
|
||||
__tablename__ = "questions"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
poll_id = Column(UUID(as_uuid=True), ForeignKey("polls.id", ondelete="CASCADE"), nullable=False)
|
||||
text = Column(Text, nullable=False)
|
||||
position = Column(Integer, nullable=True)
|
||||
type = Column(String(32), nullable=False, default="single") # single|multi|open
|
||||
|
||||
choices = relationship("Choice", backref="question", cascade="all, delete-orphan", passive_deletes=True)
|
||||
|
||||
|
||||
__all__ = ["Base", "Question"]
|
||||
30
bd/tables/response.py
Normal file
30
bd/tables/response.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from sqlalchemy import Column, DateTime, ForeignKey, Text
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.sql import func
|
||||
from sqlalchemy.orm import relationship
|
||||
from .simple_base import Base
|
||||
import uuid
|
||||
|
||||
|
||||
class Response(Base):
|
||||
__tablename__ = "responses"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
poll_id = Column(UUID(as_uuid=True), ForeignKey("polls.id"), nullable=False)
|
||||
user_id = Column(UUID(as_uuid=True), nullable=True)
|
||||
submitted_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
|
||||
answers = relationship("Answer", backref="response", cascade="all, delete-orphan", passive_deletes=True)
|
||||
|
||||
|
||||
class Answer(Base):
|
||||
__tablename__ = "answers"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
response_id = Column(UUID(as_uuid=True), ForeignKey("responses.id", ondelete="CASCADE"), nullable=False)
|
||||
question_id = Column(UUID(as_uuid=True), ForeignKey("questions.id"), nullable=False)
|
||||
choice_id = Column(UUID(as_uuid=True), nullable=True)
|
||||
text = Column(Text, nullable=True)
|
||||
|
||||
|
||||
__all__ = ["Base", "Response", "Answer"]
|
||||
3
bd/tables/simple_base.py
Normal file
3
bd/tables/simple_base.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from sqlalchemy.orm import declarative_base
|
||||
|
||||
Base = declarative_base()
|
||||
14
bd/tables/users.py
Normal file
14
bd/tables/users.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from sqlalchemy import Column, String
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from .simple_base import Base
|
||||
import uuid
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
first_name = Column(String(150), nullable=False)
|
||||
last_name = Column(String(150), nullable=False)
|
||||
|
||||
__all__ = ["Base", "User"]
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
api:
|
||||
build: .
|
||||
@@ -12,7 +10,12 @@ services:
|
||||
- DB_HOST=192.168.1.11
|
||||
- DB_PORT=5432
|
||||
- DB_USER=admin1
|
||||
- DB_PASS=_NuUXNv*P+#;4v7
|
||||
- DB_NAME=syte
|
||||
- DB_PASS=Y6%r35RpckeP^F
|
||||
- DB_NAME=profi
|
||||
# Redis connection (external)
|
||||
- REDIS_HOST=192.168.1.19
|
||||
- REDIS_PORT=6379
|
||||
- REDIS_DB=0
|
||||
- REDIS_PASSWORD=CNXpuhMdxXHo7ZK8bhtXDvgXVZcjRn
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
|
||||
13
main.py
13
main.py
@@ -2,7 +2,7 @@ import os
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
from fastapi import FastAPI
|
||||
from route import base
|
||||
from route import base, holland_crud, init_data_base, groups_crud, organizations_crud, users_crud, question_crud, choice_crud, response_crud, poll_crud, ui
|
||||
import uvicorn
|
||||
|
||||
# Читаем версию из pyproject.toml
|
||||
@@ -21,6 +21,17 @@ APP_PATH = os.getenv("APP_PATH", "/home/user/api-copp")
|
||||
|
||||
app = FastAPI(title="api", version=VERSION)
|
||||
app.include_router(base.router)
|
||||
app.include_router(init_data_base.router)
|
||||
app.include_router(groups_crud.router)
|
||||
app.include_router(organizations_crud.router)
|
||||
app.include_router(users_crud.router)
|
||||
app.include_router(question_crud.router)
|
||||
app.include_router(choice_crud.router)
|
||||
app.include_router(response_crud.router)
|
||||
app.include_router(poll_crud.router)
|
||||
app.include_router(holland_crud.router)
|
||||
app.include_router(ui.router)
|
||||
|
||||
|
||||
# Доступна переменная APP_PATH для использования в приложении
|
||||
print(f"Application path: {APP_PATH}")
|
||||
|
||||
@@ -16,6 +16,9 @@ dependencies = [
|
||||
"uvicorn[standard]>=0.40.0",
|
||||
"pydantic>=2.12.5",
|
||||
"python-dotenv>=1.2.1",
|
||||
"redis>=7.1.0",
|
||||
"sqlalchemy>=2.0.47",
|
||||
"psycopg>=3.3.3",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
62
redis_db/__init__.py
Normal file
62
redis_db/__init__.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from pydantic.generics import GenericModel
|
||||
import os
|
||||
import redis
|
||||
|
||||
|
||||
class RedisSettings(GenericModel):
|
||||
REDIS_HOST: str = os.getenv("REDIS_HOST", "192.168.1.19")
|
||||
REDIS_PORT: int = int(os.getenv("REDIS_PORT", 6379))
|
||||
REDIS_DB: int = int(os.getenv("REDIS_DB", 0))
|
||||
REDIS_PASSWORD: str = os.getenv("REDIS_PASSWORD") or None
|
||||
|
||||
@property
|
||||
def REDIS_URL(self) -> str:
|
||||
"""Получить Redis URL для подключения"""
|
||||
if self.REDIS_PASSWORD:
|
||||
return f"redis://:{self.REDIS_PASSWORD}@{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}"
|
||||
else:
|
||||
return f"redis://{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}"
|
||||
|
||||
@property
|
||||
def connection_kwargs(self) -> dict:
|
||||
"""Получить параметры подключения для redis.Redis()"""
|
||||
return {
|
||||
"host": self.REDIS_HOST,
|
||||
"port": self.REDIS_PORT,
|
||||
"db": self.REDIS_DB,
|
||||
"password": self.REDIS_PASSWORD,
|
||||
"decode_responses": True,
|
||||
}
|
||||
|
||||
def get_client(self) -> redis.Redis:
|
||||
"""Создать и вернуть Redis клиент"""
|
||||
return redis.Redis(**self.connection_kwargs)
|
||||
|
||||
def test_connection(self) -> bool:
|
||||
"""Протестировать подключение к Redis"""
|
||||
try:
|
||||
client = self.get_client()
|
||||
client.ping()
|
||||
print(f"✓ Успешно подключено к Redis на {self.REDIS_HOST}:{self.REDIS_PORT}")
|
||||
return True
|
||||
except redis.ConnectionError as e:
|
||||
print(f"✗ Ошибка подключения к Redis: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# Глобальная конфигурация
|
||||
_redis_settings = RedisSettings()
|
||||
|
||||
|
||||
def get_redis_settings() -> RedisSettings:
|
||||
"""Получить конфигурацию Redis"""
|
||||
return _redis_settings
|
||||
|
||||
|
||||
def get_redis_client() -> redis.Redis:
|
||||
"""Получить готовый клиент Redis"""
|
||||
return _redis_settings.get_client()
|
||||
|
||||
|
||||
__all__ = ["RedisSettings", "get_redis_settings", "get_redis_client"]
|
||||
|
||||
@@ -8,7 +8,10 @@ idna==3.11
|
||||
pydantic==2.12.5
|
||||
pydantic_core==2.41.5
|
||||
python-dotenv==1.2.1
|
||||
redis==5.0.0
|
||||
starlette==0.50.0
|
||||
typing-inspection==0.4.2
|
||||
typing_extensions==4.15.0
|
||||
uvicorn==0.40.0
|
||||
sqlalchemy==2.0.32
|
||||
pg8000==1.29.0
|
||||
1
route/__init__.py
Normal file
1
route/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Routes package
|
||||
@@ -1,6 +1,10 @@
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
from fastapi import APIRouter
|
||||
from fastapi import APIRouter, HTTPException
|
||||
import socket
|
||||
|
||||
from bd import Settings
|
||||
from redis_db import get_redis_client
|
||||
|
||||
# Создаем базовый router для FastAPI
|
||||
router = APIRouter(tags=["base"])
|
||||
@@ -26,3 +30,35 @@ async def health():
|
||||
@router.get("/version")
|
||||
async def version():
|
||||
return {"version": get_version()}
|
||||
|
||||
|
||||
@router.get("/health/db")
|
||||
async def health_db():
|
||||
"""Проверка доступности БД по TCP (host:port)"""
|
||||
settings = Settings()
|
||||
host = settings.DB_HOST
|
||||
port = settings.DB_PORT
|
||||
try:
|
||||
# Пробуем открыть TCP-соединение к хосту:порту
|
||||
with socket.create_connection((host, port), timeout=3):
|
||||
return {"status": "ok", "db": "reachable", "host": host, "port": port}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=503, detail=f"db unreachable: {e}")
|
||||
|
||||
|
||||
@router.get("/health/redis")
|
||||
async def health_redis():
|
||||
"""Проверка подключения к Redis через ping()"""
|
||||
client = get_redis_client()
|
||||
try:
|
||||
if client.ping():
|
||||
# Получаем параметры соединения для удобства в ответе
|
||||
conn_kwargs = getattr(client, "connection_pool", None)
|
||||
info = {}
|
||||
if conn_kwargs:
|
||||
info = conn_kwargs.connection_kwargs if hasattr(conn_kwargs, 'connection_kwargs') else {}
|
||||
return {"status": "ok", "redis": "reachable", "info": info}
|
||||
else:
|
||||
raise HTTPException(status_code=503, detail="redis ping failed")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=503, detail=f"redis unreachable: {e}")
|
||||
114
route/choice_crud.py
Normal file
114
route/choice_crud.py
Normal file
@@ -0,0 +1,114 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
import uuid
|
||||
|
||||
from bd import Settings
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from bd.tables.choice import Choice
|
||||
from bd.tables.question import Question
|
||||
|
||||
router = APIRouter(tags=["choices"], prefix="/choices")
|
||||
|
||||
|
||||
class ChoiceCreate(BaseModel):
|
||||
question_id: str
|
||||
text: str
|
||||
position: Optional[int]
|
||||
|
||||
|
||||
class ChoiceUpdate(BaseModel):
|
||||
text: Optional[str]
|
||||
position: Optional[int]
|
||||
|
||||
|
||||
def get_session():
|
||||
settings = Settings()
|
||||
engine = create_engine(settings.DATABASE_URL_syncpg, future=True)
|
||||
return sessionmaker(bind=engine, future=True)
|
||||
|
||||
|
||||
@router.post("/", status_code=201)
|
||||
def create_choice(payload: ChoiceCreate):
|
||||
Session = get_session()
|
||||
try:
|
||||
qid = uuid.UUID(payload.question_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid question UUID")
|
||||
with Session() as session:
|
||||
question = session.get(Question, qid)
|
||||
if not question:
|
||||
raise HTTPException(status_code=404, detail="Question not found")
|
||||
ch = Choice(question_id=qid, text=payload.text, position=payload.position)
|
||||
session.add(ch)
|
||||
session.commit()
|
||||
session.refresh(ch)
|
||||
return {"id": str(ch.id)}
|
||||
|
||||
|
||||
@router.put("/{choice_id}")
|
||||
def update_choice(choice_id: str, payload: ChoiceUpdate):
|
||||
Session = get_session()
|
||||
try:
|
||||
cid = uuid.UUID(choice_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid UUID")
|
||||
with Session() as session:
|
||||
ch = session.get(Choice, cid)
|
||||
if not ch:
|
||||
raise HTTPException(status_code=404, detail="Choice not found")
|
||||
if payload.text is not None:
|
||||
ch.text = payload.text
|
||||
if payload.position is not None:
|
||||
ch.position = payload.position
|
||||
session.add(ch)
|
||||
session.commit()
|
||||
session.refresh(ch)
|
||||
return {"id": str(ch.id)}
|
||||
|
||||
|
||||
@router.delete("/{choice_id}", status_code=204)
|
||||
def delete_choice(choice_id: str):
|
||||
Session = get_session()
|
||||
try:
|
||||
cid = uuid.UUID(choice_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid UUID")
|
||||
with Session() as session:
|
||||
ch = session.get(Choice, cid)
|
||||
if not ch:
|
||||
raise HTTPException(status_code=404, detail="Choice not found")
|
||||
session.delete(ch)
|
||||
session.commit()
|
||||
return {}
|
||||
|
||||
|
||||
@router.get("/")
|
||||
def list_choices(question_id: Optional[str] = None):
|
||||
Session = get_session()
|
||||
with Session() as session:
|
||||
q = session.query(Choice)
|
||||
if question_id:
|
||||
try:
|
||||
qid = uuid.UUID(question_id)
|
||||
q = q.filter(Choice.question_id == qid)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid question_id UUID")
|
||||
rows = q.all()
|
||||
return [{"id": str(r.id), "text": r.text, "question_id": str(r.question_id)} for r in rows]
|
||||
|
||||
|
||||
@router.get("/{choice_id}")
|
||||
def get_choice(choice_id: str):
|
||||
Session = get_session()
|
||||
try:
|
||||
cid = uuid.UUID(choice_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid UUID")
|
||||
with Session() as session:
|
||||
ch = session.get(Choice, cid)
|
||||
if not ch:
|
||||
raise HTTPException(status_code=404, detail="Choice not found")
|
||||
return {"id": str(ch.id), "text": ch.text, "question_id": str(ch.question_id)}
|
||||
113
route/groups_crud.py
Normal file
113
route/groups_crud.py
Normal file
@@ -0,0 +1,113 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
import uuid
|
||||
|
||||
from bd import Settings
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from bd.tables.group import Group
|
||||
from bd.tables.users import User
|
||||
|
||||
router = APIRouter(tags=["groups"], prefix="/groups")
|
||||
|
||||
|
||||
class GroupCreate(BaseModel):
|
||||
name_group: str
|
||||
user_id: str
|
||||
|
||||
|
||||
class GroupUpdate(BaseModel):
|
||||
name_group: Optional[str]
|
||||
user_id: Optional[str]
|
||||
|
||||
|
||||
def get_session():
|
||||
settings = Settings()
|
||||
engine = create_engine(settings.DATABASE_URL_syncpg, future=True)
|
||||
return sessionmaker(bind=engine, future=True)
|
||||
|
||||
|
||||
@router.post("/", status_code=201)
|
||||
def create_group(payload: GroupCreate):
|
||||
Session = get_session()
|
||||
try:
|
||||
uid = uuid.UUID(payload.user_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid user_id UUID")
|
||||
with Session() as session:
|
||||
user = session.get(User, uid)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
grp = Group(name_group=payload.name_group, user_id=uid)
|
||||
session.add(grp)
|
||||
session.commit()
|
||||
session.refresh(grp)
|
||||
return {"id": str(grp.id), "name_group": grp.name_group, "user_id": str(grp.user_id)}
|
||||
|
||||
|
||||
@router.get("/")
|
||||
def list_groups():
|
||||
Session = get_session()
|
||||
with Session() as session:
|
||||
rows = session.query(Group).all()
|
||||
return [{"id": str(g.id), "name_group": g.name_group, "user_id": str(g.user_id)} for g in rows]
|
||||
|
||||
|
||||
@router.get("/{group_id}")
|
||||
def get_group(group_id: str):
|
||||
Session = get_session()
|
||||
try:
|
||||
gid = uuid.UUID(group_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid UUID")
|
||||
with Session() as session:
|
||||
grp = session.get(Group, gid)
|
||||
if not grp:
|
||||
raise HTTPException(status_code=404, detail="Group not found")
|
||||
return {"id": str(grp.id), "name_group": grp.name_group, "user_id": str(grp.user_id)}
|
||||
|
||||
|
||||
@router.put("/{group_id}")
|
||||
def update_group(group_id: str, payload: GroupUpdate):
|
||||
Session = get_session()
|
||||
try:
|
||||
gid = uuid.UUID(group_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid UUID")
|
||||
with Session() as session:
|
||||
grp = session.get(Group, gid)
|
||||
if not grp:
|
||||
raise HTTPException(status_code=404, detail="Group not found")
|
||||
if payload.name_group is not None:
|
||||
grp.name_group = payload.name_group
|
||||
if payload.user_id is not None:
|
||||
try:
|
||||
new_uid = uuid.UUID(payload.user_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid user_id UUID")
|
||||
user = session.get(User, new_uid)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
grp.user_id = new_uid
|
||||
session.add(grp)
|
||||
session.commit()
|
||||
session.refresh(grp)
|
||||
return {"id": str(grp.id), "name_group": grp.name_group, "user_id": str(grp.user_id)}
|
||||
|
||||
|
||||
@router.delete("/{group_id}", status_code=204)
|
||||
def delete_group(group_id: str):
|
||||
Session = get_session()
|
||||
try:
|
||||
gid = uuid.UUID(group_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid UUID")
|
||||
with Session() as session:
|
||||
grp = session.get(Group, gid)
|
||||
if not grp:
|
||||
raise HTTPException(status_code=404, detail="Group not found")
|
||||
session.delete(grp)
|
||||
session.commit()
|
||||
return {}
|
||||
85
route/holland_crud.py
Normal file
85
route/holland_crud.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from typing import Optional
|
||||
import uuid
|
||||
|
||||
from bd import Settings
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from bd.tables.poll import Poll
|
||||
from bd.tables.question import Question
|
||||
from bd.tables.choice import Choice
|
||||
|
||||
router = APIRouter(tags=["polls"], prefix="/polls")
|
||||
|
||||
|
||||
PAIRS = [
|
||||
("инженер-техник", "инженер-контролер"),
|
||||
("вязальщик", "санитарный врач"),
|
||||
("повар", "наборщик"),
|
||||
("фотограф", "зав. магазином"),
|
||||
("чертежник", "дизайнер"),
|
||||
("философ", "психиатр"),
|
||||
("ученый-химик", "бухгалтер"),
|
||||
("редактор научного журнала", "адвокат"),
|
||||
("лингвист", "переводчик художественной литературы"),
|
||||
("педиатр", "статистик"),
|
||||
("организатор воспитательной работы", "председатель профсоюза"),
|
||||
("спортивный врач", "фельетонист"),
|
||||
("нотариус", "снабженец"),
|
||||
("перфоратор", "карикатурист"),
|
||||
("политический деятель", "писатель"),
|
||||
("садовник", "метеоролог"),
|
||||
("водитель", "медсестра"),
|
||||
("инженер-электрик", "секретарь-машинистка"),
|
||||
("маляр", "художник по металлу"),
|
||||
("биолог", "главный врач"),
|
||||
("телеоператор", "режиссер"),
|
||||
("гидролог", "ревизор"),
|
||||
("зоолог", "зоотехник"),
|
||||
("математик", "архитектор"),
|
||||
("работник ИДН", "счетовод"),
|
||||
("учитель", "милиционер"),
|
||||
("воспитатель", "художник по керамике"),
|
||||
("экономист", "заведующий отделом"),
|
||||
("корректор", "критик"),
|
||||
("завхоз", "директор"),
|
||||
("радиоинженер", "специалист по ядерной физике"),
|
||||
("водопроводчик", "наборщик"),
|
||||
("агроном", "председатель сельхозкооператива"),
|
||||
("закройщик-модельер", "декоратор"),
|
||||
("археолог", "эксперт"),
|
||||
("работник музея", "консультант"),
|
||||
("ученый", "актер"),
|
||||
("логопед", "стенографист"),
|
||||
("врач", "дипломат"),
|
||||
("главный бухгалтер", "директор"),
|
||||
("поэт", "психолог"),
|
||||
("архивариус", "скульптор"),
|
||||
]
|
||||
|
||||
|
||||
def get_session():
|
||||
settings = Settings()
|
||||
engine = create_engine(settings.DATABASE_URL_syncpg, future=True)
|
||||
return sessionmaker(bind=engine, future=True)
|
||||
|
||||
|
||||
@router.post("/holland", status_code=201)
|
||||
def create_holland_poll(author_id: Optional[str] = None):
|
||||
Session = get_session()
|
||||
with Session() as session:
|
||||
poll = Poll(title="Тест Д. Голланда — определение типа личности",
|
||||
description="Тест Д. Голланда: выберите из каждой пары предпочитаемую профессию.")
|
||||
# polls are anonymous; ignore author_id
|
||||
session.add(poll)
|
||||
for idx, (a, b) in enumerate(PAIRS, start=1):
|
||||
q = Question(text=f"Вариант {idx}", position=idx, type="single", poll=poll)
|
||||
session.add(q)
|
||||
ch1 = Choice(question=q, text=f"а) {a}", position=1)
|
||||
ch2 = Choice(question=q, text=f"б) {b}", position=2)
|
||||
session.add(ch1)
|
||||
session.add(ch2)
|
||||
session.commit()
|
||||
session.refresh(poll)
|
||||
return {"id": str(poll.id), "questions": len(poll.questions)}
|
||||
69
route/init_data_base.py
Normal file
69
route/init_data_base.py
Normal file
@@ -0,0 +1,69 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
import pkgutil
|
||||
import importlib
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from bd import Settings
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
router = APIRouter(tags=["db"])
|
||||
|
||||
|
||||
def _iter_table_modules() -> List[str]:
|
||||
try:
|
||||
import bd.tables as tables_pkg
|
||||
pkg_paths = getattr(tables_pkg, "__path__", None)
|
||||
if not pkg_paths:
|
||||
pkg_paths = [str(Path(__file__).resolve().parent.parent / "bd" / "tables")]
|
||||
except Exception:
|
||||
pkg_paths = [str(Path(__file__).resolve().parent.parent / "bd" / "tables")]
|
||||
|
||||
names = []
|
||||
for finder, name, ispkg in pkgutil.iter_modules(pkg_paths):
|
||||
names.append(name)
|
||||
return names
|
||||
|
||||
|
||||
def collect_metadatas():
|
||||
metadatas = []
|
||||
for mod_name in _iter_table_modules():
|
||||
try:
|
||||
module = importlib.import_module(f"bd.tables.{mod_name}")
|
||||
except Exception:
|
||||
continue
|
||||
Base = getattr(module, "Base", None)
|
||||
if Base is not None and hasattr(Base, "metadata"):
|
||||
metadatas.append(Base.metadata)
|
||||
return metadatas
|
||||
|
||||
|
||||
@router.post("/db/create-tables")
|
||||
async def create_tables():
|
||||
"""Создаёт все таблицы, описанные в модулях `db.tables`.
|
||||
|
||||
Endpoint вызывается по нажатию кнопки в UI (POST).
|
||||
"""
|
||||
settings = Settings()
|
||||
db_url = settings.DATABASE_URL_syncpg
|
||||
engine = create_engine(db_url, future=True)
|
||||
|
||||
metadatas = collect_metadatas()
|
||||
if not metadatas:
|
||||
raise HTTPException(status_code=400, detail="No table metadata found in db.tables")
|
||||
|
||||
# deduplicate metadata objects (multiple modules may expose the same Base.metadata)
|
||||
unique = []
|
||||
seen = set()
|
||||
for md in metadatas:
|
||||
if id(md) not in seen:
|
||||
seen.add(id(md))
|
||||
unique.append(md)
|
||||
|
||||
try:
|
||||
for md in unique:
|
||||
md.create_all(bind=engine)
|
||||
return {"status": "ok", "detail": f"Created {len(unique)} metadata groups"}
|
||||
except SQLAlchemyError as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
113
route/organizations_crud.py
Normal file
113
route/organizations_crud.py
Normal file
@@ -0,0 +1,113 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
import uuid
|
||||
|
||||
from bd import Settings
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from bd.tables.organization import Organization
|
||||
from bd.tables.group import Group
|
||||
|
||||
router = APIRouter(tags=["organizations"], prefix="/organizations")
|
||||
|
||||
|
||||
class OrganizationCreate(BaseModel):
|
||||
name_organization: str
|
||||
group_id: str
|
||||
|
||||
|
||||
class OrganizationUpdate(BaseModel):
|
||||
name_organization: Optional[str]
|
||||
group_id: Optional[str]
|
||||
|
||||
|
||||
def get_session():
|
||||
settings = Settings()
|
||||
engine = create_engine(settings.DATABASE_URL_syncpg, future=True)
|
||||
return sessionmaker(bind=engine, future=True)
|
||||
|
||||
|
||||
@router.post("/", status_code=201)
|
||||
def create_organization(payload: OrganizationCreate):
|
||||
Session = get_session()
|
||||
try:
|
||||
gid = uuid.UUID(payload.group_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid group_id UUID")
|
||||
with Session() as session:
|
||||
grp = session.get(Group, gid)
|
||||
if not grp:
|
||||
raise HTTPException(status_code=404, detail="Group not found")
|
||||
org = Organization(name_organization=payload.name_organization, group_id=gid)
|
||||
session.add(org)
|
||||
session.commit()
|
||||
session.refresh(org)
|
||||
return {"id": str(org.id), "name_organization": org.name_organization, "group_id": str(org.group_id)}
|
||||
|
||||
|
||||
@router.get("/")
|
||||
def list_organizations():
|
||||
Session = get_session()
|
||||
with Session() as session:
|
||||
rows = session.query(Organization).all()
|
||||
return [{"id": str(o.id), "name_organization": o.name_organization, "group_id": str(o.group_id)} for o in rows]
|
||||
|
||||
|
||||
@router.get("/{org_id}")
|
||||
def get_organization(org_id: str):
|
||||
Session = get_session()
|
||||
try:
|
||||
oid = uuid.UUID(org_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid UUID")
|
||||
with Session() as session:
|
||||
org = session.get(Organization, oid)
|
||||
if not org:
|
||||
raise HTTPException(status_code=404, detail="Organization not found")
|
||||
return {"id": str(org.id), "name_organization": org.name_organization, "group_id": str(org.group_id)}
|
||||
|
||||
|
||||
@router.put("/{org_id}")
|
||||
def update_organization(org_id: str, payload: OrganizationUpdate):
|
||||
Session = get_session()
|
||||
try:
|
||||
oid = uuid.UUID(org_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid UUID")
|
||||
with Session() as session:
|
||||
org = session.get(Organization, oid)
|
||||
if not org:
|
||||
raise HTTPException(status_code=404, detail="Organization not found")
|
||||
if payload.name_organization is not None:
|
||||
org.name_organization = payload.name_organization
|
||||
if payload.group_id is not None:
|
||||
try:
|
||||
new_gid = uuid.UUID(payload.group_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid group_id UUID")
|
||||
grp = session.get(Group, new_gid)
|
||||
if not grp:
|
||||
raise HTTPException(status_code=404, detail="Group not found")
|
||||
org.group_id = new_gid
|
||||
session.add(org)
|
||||
session.commit()
|
||||
session.refresh(org)
|
||||
return {"id": str(org.id), "name_organization": org.name_organization, "group_id": str(org.group_id)}
|
||||
|
||||
|
||||
@router.delete("/{org_id}", status_code=204)
|
||||
def delete_organization(org_id: str):
|
||||
Session = get_session()
|
||||
try:
|
||||
oid = uuid.UUID(org_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid UUID")
|
||||
with Session() as session:
|
||||
org = session.get(Organization, oid)
|
||||
if not org:
|
||||
raise HTTPException(status_code=404, detail="Organization not found")
|
||||
session.delete(org)
|
||||
session.commit()
|
||||
return {}
|
||||
95
route/poll_crud.py
Normal file
95
route/poll_crud.py
Normal file
@@ -0,0 +1,95 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
import uuid
|
||||
|
||||
from bd import Settings
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from bd.tables.poll import Poll
|
||||
from bd.tables.question import Question
|
||||
from bd.tables.choice import Choice
|
||||
|
||||
router = APIRouter(tags=["polls"], prefix="/polls")
|
||||
|
||||
|
||||
class ChoiceIn(BaseModel):
|
||||
text: str
|
||||
position: Optional[int]
|
||||
|
||||
|
||||
class QuestionIn(BaseModel):
|
||||
text: str
|
||||
type: Optional[str] = "single"
|
||||
position: Optional[int]
|
||||
choices: Optional[List[ChoiceIn]]
|
||||
|
||||
|
||||
class PollIn(BaseModel):
|
||||
title: str
|
||||
description: Optional[str]
|
||||
questions: Optional[List[QuestionIn]]
|
||||
|
||||
|
||||
def get_session():
|
||||
settings = Settings()
|
||||
engine = create_engine(settings.DATABASE_URL_syncpg, future=True)
|
||||
return sessionmaker(bind=engine, future=True)
|
||||
|
||||
|
||||
@router.post("/", status_code=201)
|
||||
def create_poll(payload: PollIn):
|
||||
Session = get_session()
|
||||
with Session() as session:
|
||||
poll = Poll(title=payload.title, description=payload.description)
|
||||
# polls are anonymous; no author_id stored
|
||||
session.add(poll)
|
||||
# questions and choices
|
||||
if payload.questions:
|
||||
for q in payload.questions:
|
||||
question = Question(text=q.text, type=q.type or "single", position=q.position, poll=poll)
|
||||
session.add(question)
|
||||
if q.choices:
|
||||
for idx, c in enumerate(q.choices):
|
||||
choice = Choice(text=c.text, position=c.position if c.position is not None else idx, question=question)
|
||||
session.add(choice)
|
||||
session.commit()
|
||||
session.refresh(poll)
|
||||
return {"id": str(poll.id)}
|
||||
|
||||
|
||||
@router.get("/{poll_id}")
|
||||
def get_poll(poll_id: str):
|
||||
Session = get_session()
|
||||
try:
|
||||
pid = uuid.UUID(poll_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid UUID")
|
||||
with Session() as session:
|
||||
poll = session.get(Poll, pid)
|
||||
if not poll:
|
||||
raise HTTPException(status_code=404, detail="Poll not found")
|
||||
data = {
|
||||
"id": str(poll.id),
|
||||
"title": poll.title,
|
||||
"description": poll.description,
|
||||
"questions": [
|
||||
{
|
||||
"id": str(q.id),
|
||||
"text": q.text,
|
||||
"type": q.type,
|
||||
"choices": [{"id": str(c.id), "text": c.text} for c in q.choices]
|
||||
}
|
||||
for q in poll.questions
|
||||
],
|
||||
}
|
||||
return data
|
||||
|
||||
|
||||
@router.get("/")
|
||||
def list_polls():
|
||||
Session = get_session()
|
||||
with Session() as session:
|
||||
rows = session.query(Poll).all()
|
||||
return [{"id": str(p.id), "title": p.title, "description": p.description} for p in rows]
|
||||
118
route/question_crud.py
Normal file
118
route/question_crud.py
Normal file
@@ -0,0 +1,118 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
import uuid
|
||||
|
||||
from bd import Settings
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from bd.tables.question import Question
|
||||
from bd.tables.poll import Poll
|
||||
|
||||
router = APIRouter(tags=["questions"], prefix="/questions")
|
||||
|
||||
|
||||
class QuestionCreate(BaseModel):
|
||||
poll_id: str
|
||||
text: str
|
||||
type: Optional[str] = "single"
|
||||
position: Optional[int]
|
||||
|
||||
|
||||
class QuestionUpdate(BaseModel):
|
||||
text: Optional[str]
|
||||
type: Optional[str]
|
||||
position: Optional[int]
|
||||
|
||||
|
||||
def get_session():
|
||||
settings = Settings()
|
||||
engine = create_engine(settings.DATABASE_URL_syncpg, future=True)
|
||||
return sessionmaker(bind=engine, future=True)
|
||||
|
||||
|
||||
@router.post("/", status_code=201)
|
||||
def create_question(payload: QuestionCreate):
|
||||
Session = get_session()
|
||||
try:
|
||||
pid = uuid.UUID(payload.poll_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid poll UUID")
|
||||
with Session() as session:
|
||||
poll = session.get(Poll, pid)
|
||||
if not poll:
|
||||
raise HTTPException(status_code=404, detail="Poll not found")
|
||||
q = Question(poll_id=pid, text=payload.text, type=payload.type or "single", position=payload.position)
|
||||
session.add(q)
|
||||
session.commit()
|
||||
session.refresh(q)
|
||||
return {"id": str(q.id)}
|
||||
|
||||
|
||||
@router.put("/{question_id}")
|
||||
def update_question(question_id: str, payload: QuestionUpdate):
|
||||
Session = get_session()
|
||||
try:
|
||||
qid = uuid.UUID(question_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid UUID")
|
||||
with Session() as session:
|
||||
q = session.get(Question, qid)
|
||||
if not q:
|
||||
raise HTTPException(status_code=404, detail="Question not found")
|
||||
if payload.text is not None:
|
||||
q.text = payload.text
|
||||
if payload.type is not None:
|
||||
q.type = payload.type
|
||||
if payload.position is not None:
|
||||
q.position = payload.position
|
||||
session.add(q)
|
||||
session.commit()
|
||||
session.refresh(q)
|
||||
return {"id": str(q.id)}
|
||||
|
||||
|
||||
@router.delete("/{question_id}", status_code=204)
|
||||
def delete_question(question_id: str):
|
||||
Session = get_session()
|
||||
try:
|
||||
qid = uuid.UUID(question_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid UUID")
|
||||
with Session() as session:
|
||||
q = session.get(Question, qid)
|
||||
if not q:
|
||||
raise HTTPException(status_code=404, detail="Question not found")
|
||||
session.delete(q)
|
||||
session.commit()
|
||||
return {}
|
||||
|
||||
|
||||
@router.get("/")
|
||||
def list_questions(poll_id: Optional[str] = None):
|
||||
Session = get_session()
|
||||
with Session() as session:
|
||||
q = session.query(Question)
|
||||
if poll_id:
|
||||
try:
|
||||
pid = uuid.UUID(poll_id)
|
||||
q = q.filter(Question.poll_id == pid)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid poll_id UUID")
|
||||
rows = q.all()
|
||||
return [{"id": str(r.id), "text": r.text, "poll_id": str(r.poll_id)} for r in rows]
|
||||
|
||||
|
||||
@router.get("/{question_id}")
|
||||
def get_question(question_id: str):
|
||||
Session = get_session()
|
||||
try:
|
||||
qid = uuid.UUID(question_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid UUID")
|
||||
with Session() as session:
|
||||
q = session.get(Question, qid)
|
||||
if not q:
|
||||
raise HTTPException(status_code=404, detail="Question not found")
|
||||
return {"id": str(q.id), "text": q.text, "poll_id": str(q.poll_id)}
|
||||
97
route/response_crud.py
Normal file
97
route/response_crud.py
Normal file
@@ -0,0 +1,97 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
import uuid
|
||||
|
||||
from bd import Settings
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from bd.tables.poll import Poll
|
||||
from bd.tables.question import Question
|
||||
from bd.tables.response import Response, Answer
|
||||
|
||||
router = APIRouter(tags=["responses"], prefix="/polls")
|
||||
|
||||
|
||||
class AnswerIn(BaseModel):
|
||||
question_id: str
|
||||
choice_id: Optional[str]
|
||||
text: Optional[str]
|
||||
|
||||
|
||||
class ResponseIn(BaseModel):
|
||||
user_id: Optional[str]
|
||||
answers: List[AnswerIn]
|
||||
|
||||
|
||||
def get_session():
|
||||
settings = Settings()
|
||||
engine = create_engine(settings.DATABASE_URL_syncpg, future=True)
|
||||
return sessionmaker(bind=engine, future=True)
|
||||
|
||||
|
||||
@router.post("/{poll_id}/responses", status_code=201)
|
||||
def submit_response(poll_id: str, payload: ResponseIn):
|
||||
Session = get_session()
|
||||
try:
|
||||
pid = uuid.UUID(poll_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid poll UUID")
|
||||
with Session() as session:
|
||||
poll = session.get(Poll, pid)
|
||||
if not poll:
|
||||
raise HTTPException(status_code=404, detail="Poll not found")
|
||||
resp = Response(poll_id=pid)
|
||||
if payload.user_id:
|
||||
try:
|
||||
resp.user_id = uuid.UUID(payload.user_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid user_id UUID")
|
||||
session.add(resp)
|
||||
# validate answers
|
||||
for a in payload.answers:
|
||||
try:
|
||||
qid = uuid.UUID(a.question_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid question_id UUID")
|
||||
question = session.get(Question, qid)
|
||||
if not question or question.poll_id != pid:
|
||||
raise HTTPException(status_code=400, detail="Question does not belong to poll")
|
||||
choice_uuid = None
|
||||
if a.choice_id:
|
||||
try:
|
||||
choice_uuid = uuid.UUID(a.choice_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid choice_id UUID")
|
||||
answer = Answer(response=resp, question_id=qid, choice_id=choice_uuid, text=a.text)
|
||||
session.add(answer)
|
||||
session.commit()
|
||||
session.refresh(resp)
|
||||
return {"id": str(resp.id)}
|
||||
|
||||
|
||||
@router.get("/{poll_id}/responses")
|
||||
def list_responses(poll_id: str):
|
||||
Session = get_session()
|
||||
try:
|
||||
pid = uuid.UUID(poll_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid poll UUID")
|
||||
with Session() as session:
|
||||
rows = session.query(Response).filter(Response.poll_id == pid).all()
|
||||
return [{"id": str(r.id), "user_id": str(r.user_id) if r.user_id else None, "submitted_at": r.submitted_at.isoformat()} for r in rows]
|
||||
|
||||
|
||||
@router.get("/responses/{response_id}")
|
||||
def get_response(response_id: str):
|
||||
Session = get_session()
|
||||
try:
|
||||
rid = uuid.UUID(response_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid UUID")
|
||||
with Session() as session:
|
||||
r = session.get(Response, rid)
|
||||
if not r:
|
||||
raise HTTPException(status_code=404, detail="Response not found")
|
||||
return {"id": str(r.id), "poll_id": str(r.poll_id), "user_id": str(r.user_id) if r.user_id else None, "submitted_at": r.submitted_at.isoformat(), "answers": [{"id": str(a.id), "question_id": str(a.question_id), "choice_id": str(a.choice_id) if a.choice_id else None, "text": a.text} for a in r.answers]}
|
||||
111
route/ui.py
Normal file
111
route/ui.py
Normal file
@@ -0,0 +1,111 @@
|
||||
from fastapi import APIRouter, Request
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/ui/create-user")
|
||||
def create_user_page(request: Request):
|
||||
return """
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Create user & Take test</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Create user</h1>
|
||||
<form id="userForm">
|
||||
<label>First name: <input name="first_name" required></label><br>
|
||||
<label>Last name: <input name="last_name" required></label><br>
|
||||
<button type="submit">Create user</button>
|
||||
</form>
|
||||
|
||||
<h2>Or create Holland test</h2>
|
||||
<button id="createHolland">Create Holland test</button>
|
||||
|
||||
<script>
|
||||
document.getElementById('userForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const fd = new FormData(e.target);
|
||||
const data = { first_name: fd.get('first_name'), last_name: fd.get('last_name') };
|
||||
const res = await fetch('/users/', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(data) });
|
||||
if (!res.ok) { alert('Create user failed'); return; }
|
||||
const js = await res.json();
|
||||
const uid = js.id;
|
||||
alert('User created: ' + uid);
|
||||
// allow user to input poll id manually
|
||||
const poll = prompt('Enter poll id to take (or leave empty to create Holland test)');
|
||||
if (poll) {
|
||||
location.href = '/ui/take-test?poll_id=' + encodeURIComponent(poll) + '&user_id=' + encodeURIComponent(uid);
|
||||
} else {
|
||||
// create Holland test then redirect
|
||||
const r2 = await fetch('/polls/holland', { method: 'POST' });
|
||||
const p = await r2.json();
|
||||
location.href = '/ui/take-test?poll_id=' + encodeURIComponent(p.id) + '&user_id=' + encodeURIComponent(uid);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('createHolland').addEventListener('click', async () => {
|
||||
const r = await fetch('/polls/holland', { method: 'POST' });
|
||||
if (!r.ok) { alert('Create failed'); return; }
|
||||
const p = await r.json();
|
||||
alert('Created poll ' + p.id);
|
||||
location.href = '/ui/take-test?poll_id=' + encodeURIComponent(p.id);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
@router.get("/ui/take-test")
|
||||
def take_test_page(request: Request, poll_id: str = None, user_id: str = None):
|
||||
return f"""
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head><meta charset="utf-8"><title>Take test</title></head>
|
||||
<body>
|
||||
<h1>Take test</h1>
|
||||
<div id="poll"></div>
|
||||
<button id="submitBtn">Submit</button>
|
||||
<script>
|
||||
const pollId = '{poll_id or ''}';
|
||||
const userId = '{user_id or ''}';
|
||||
async function load() {{
|
||||
if (!pollId) {{ document.getElementById('poll').innerText = 'No poll_id provided'; return; }}
|
||||
const r = await fetch('/polls/' + encodeURIComponent(pollId));
|
||||
if (!r.ok) {{ document.getElementById('poll').innerText = 'Failed to load poll'; return; }}
|
||||
const p = await r.json();
|
||||
const container = document.getElementById('poll');
|
||||
const h = document.createElement('h2'); h.innerText = p.title; container.appendChild(h);
|
||||
p.questions.forEach(q => {{
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = '<p>' + q.text + '</p>';
|
||||
q.choices.forEach(c => {{
|
||||
const id = 'q_' + q.id + '_c_' + c.id;
|
||||
const input = `<label><input type="radio" name="${{q.id}}" value="${{c.id}}"> ${{c.text}}</label><br>`;
|
||||
div.insertAdjacentHTML('beforeend', input);
|
||||
}});
|
||||
container.appendChild(div);
|
||||
}});
|
||||
}}
|
||||
load();
|
||||
|
||||
document.getElementById('submitBtn').addEventListener('click', async () => {{
|
||||
const answers = [];
|
||||
const groups = new Set();
|
||||
document.querySelectorAll('input[type=radio]').forEach(r => groups.add(r.name));
|
||||
groups.forEach(name => {{
|
||||
const checked = document.querySelector('input[name="' + name + '"]:checked');
|
||||
if (checked) answers.push({{question_id: name, choice_id: checked.value}});
|
||||
}});
|
||||
const payload = {{ user_id: userId || null, answers }};
|
||||
const res = await fetch('/polls/' + encodeURIComponent(pollId) + '/responses', {{ method: 'POST', headers: {{'Content-Type':'application/json'}}, body: JSON.stringify(payload) }});
|
||||
if (!res.ok) {{ alert('Submit failed'); return; }}
|
||||
const js = await res.json();
|
||||
alert('Response saved: ' + js.id);
|
||||
}});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
98
route/users_crud.py
Normal file
98
route/users_crud.py
Normal file
@@ -0,0 +1,98 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
import uuid
|
||||
|
||||
from bd import Settings
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from bd.tables.users import User
|
||||
|
||||
router = APIRouter(tags=["users"], prefix="/users")
|
||||
|
||||
|
||||
class UserCreate(BaseModel):
|
||||
first_name: str
|
||||
last_name: str
|
||||
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
first_name: Optional[str]
|
||||
last_name: Optional[str]
|
||||
|
||||
|
||||
def get_session():
|
||||
settings = Settings()
|
||||
engine = create_engine(settings.DATABASE_URL_syncpg, future=True)
|
||||
return sessionmaker(bind=engine, future=True)
|
||||
|
||||
|
||||
@router.post("/", status_code=201)
|
||||
def create_user(payload: UserCreate):
|
||||
Session = get_session()
|
||||
with Session() as session:
|
||||
user = User(first_name=payload.first_name, last_name=payload.last_name)
|
||||
session.add(user)
|
||||
session.commit()
|
||||
session.refresh(user)
|
||||
return {"id": str(user.id), "first_name": user.first_name, "last_name": user.last_name}
|
||||
|
||||
|
||||
@router.put("/{user_id}")
|
||||
def update_user(user_id: str, payload: UserUpdate):
|
||||
Session = get_session()
|
||||
try:
|
||||
uid = uuid.UUID(user_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid UUID")
|
||||
with Session() as session:
|
||||
user = session.get(User, uid)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
if payload.first_name is not None:
|
||||
user.first_name = payload.first_name
|
||||
if payload.last_name is not None:
|
||||
user.last_name = payload.last_name
|
||||
session.add(user)
|
||||
session.commit()
|
||||
session.refresh(user)
|
||||
return {"id": str(user.id), "first_name": user.first_name, "last_name": user.last_name}
|
||||
|
||||
|
||||
@router.delete("/{user_id}", status_code=204)
|
||||
def delete_user(user_id: str):
|
||||
Session = get_session()
|
||||
try:
|
||||
uid = uuid.UUID(user_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid UUID")
|
||||
with Session() as session:
|
||||
user = session.get(User, uid)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
session.delete(user)
|
||||
session.commit()
|
||||
return {}
|
||||
|
||||
|
||||
@router.get("/")
|
||||
def list_users():
|
||||
Session = get_session()
|
||||
with Session() as session:
|
||||
rows = session.query(User).all()
|
||||
return [{"id": str(u.id), "first_name": u.first_name, "last_name": u.last_name} for u in rows]
|
||||
|
||||
|
||||
@router.get("/{user_id}")
|
||||
def get_user(user_id: str):
|
||||
Session = get_session()
|
||||
try:
|
||||
uid = uuid.UUID(user_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid UUID")
|
||||
with Session() as session:
|
||||
user = session.get(User, uid)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
return {"id": str(user.id), "first_name": user.first_name, "last_name": user.last_name}
|
||||
89
uv.lock
generated
89
uv.lock
generated
@@ -34,12 +34,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "api-copp"
|
||||
version = "0.1.0"
|
||||
version = "0.3.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "fastapi" },
|
||||
{ name = "psycopg" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "redis" },
|
||||
{ name = "sqlalchemy" },
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
]
|
||||
|
||||
@@ -55,11 +58,14 @@ dev = [
|
||||
requires-dist = [
|
||||
{ name = "black", marker = "extra == 'dev'", specifier = ">=23.0.0" },
|
||||
{ name = "fastapi", specifier = ">=0.128.0" },
|
||||
{ name = "psycopg", specifier = ">=3.3.3" },
|
||||
{ name = "pydantic", specifier = ">=2.12.5" },
|
||||
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" },
|
||||
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.21.0" },
|
||||
{ name = "python-dotenv", specifier = ">=1.2.1" },
|
||||
{ name = "redis", specifier = ">=7.1.0" },
|
||||
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" },
|
||||
{ name = "sqlalchemy", specifier = ">=2.0.47" },
|
||||
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.40.0" },
|
||||
]
|
||||
provides-extras = ["dev"]
|
||||
@@ -122,6 +128,31 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "greenlet"
|
||||
version = "3.3.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/ac/85804f74f1ccea31ba518dcc8ee6f14c79f73fe36fa1beba38930806df09/greenlet-3.3.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3cb43ce200f59483eb82949bf1835a99cf43d7571e900d7c8d5c62cdf25d2f9", size = 675371, upload-time = "2026-02-20T21:02:49.664Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034, upload-time = "2026-02-20T20:20:08.186Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437, upload-time = "2026-02-20T20:18:59.722Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/67/8197b7e7e602150938049d8e7f30de1660cfb87e4c8ee349b42b67bdb2e1/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:59b3e2c40f6706b05a9cd299c836c6aa2378cabe25d021acd80f13abf81181cf", size = 666581, upload-time = "2026-02-20T21:02:51.526Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
@@ -209,6 +240,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psycopg"
|
||||
version = "3.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d3/b6/379d0a960f8f435ec78720462fd94c4863e7a31237cf81bf76d0af5883bf/psycopg-3.3.3.tar.gz", hash = "sha256:5e9a47458b3c1583326513b2556a2a9473a1001a56c9efe9e587245b43148dd9", size = 165624, upload-time = "2026-02-18T16:52:16.546Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/5b/181e2e3becb7672b502f0ed7f16ed7352aca7c109cfb94cf3878a9186db9/psycopg-3.3.3-py3-none-any.whl", hash = "sha256:f96525a72bcfade6584ab17e89de415ff360748c766f0106959144dcbb38c698", size = 212768, upload-time = "2026-02-18T16:46:27.365Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.12.5"
|
||||
@@ -354,6 +397,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redis"
|
||||
version = "7.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/43/c8/983d5c6579a411d8a99bc5823cc5712768859b5ce2c8afe1a65b37832c81/redis-7.1.0.tar.gz", hash = "sha256:b1cc3cfa5a2cb9c2ab3ba700864fb0ad75617b41f01352ce5779dabf6d5f9c3c", size = 4796669, upload-time = "2025-11-19T15:54:39.961Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159, upload-time = "2025-11-19T15:54:38.064Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.14.14"
|
||||
@@ -380,6 +432,32 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlalchemy"
|
||||
version = "2.0.47"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cd/4b/1e00561093fe2cd8eef09d406da003c8a118ff02d6548498c1ae677d68d9/sqlalchemy-2.0.47.tar.gz", hash = "sha256:e3e7feb57b267fe897e492b9721ae46d5c7de6f9e8dee58aacf105dc4e154f3d", size = 9886323, upload-time = "2026-02-24T16:34:27.947Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/30/98243209aae58ed80e090ea988d5182244ca7ab3ff59e6d850c3dfc7651e/sqlalchemy-2.0.47-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b03010a5a5dfe71676bc83f2473ebe082478e32d77e6f082c8fe15a31c3b42a6", size = 2154355, upload-time = "2026-02-24T17:05:48.959Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/62/12ca6ea92055fe486d6558a2a4efe93e194ff597463849c01f88e5adb99d/sqlalchemy-2.0.47-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f8e3371aa9024520883a415a09cc20c33cfd3eeccf9e0f4f4c367f940b9cbd44", size = 3274486, upload-time = "2026-02-24T17:18:13.659Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/88/7dfbdeaa8d42b1584e65d6cc713e9d33b6fa563e0d546d5cb87e545bb0e5/sqlalchemy-2.0.47-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9449f747e50d518c6e1b40cc379e48bfc796453c47b15e627ea901c201e48a6", size = 3279481, upload-time = "2026-02-24T17:27:26.491Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/b7/75e1c1970616a9dd64a8a6fd788248da2ddaf81c95f4875f2a1e8aee4128/sqlalchemy-2.0.47-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:21410f60d5cac1d6bfe360e05bd91b179be4fa0aa6eea6be46054971d277608f", size = 3224269, upload-time = "2026-02-24T17:18:15.078Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/ac/eec1a13b891df9a8bc203334caf6e6aac60b02f61b018ef3b4124b8c4120/sqlalchemy-2.0.47-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:819841dd5bb4324c284c09e2874cf96fe6338bfb57a64548d9b81a4e39c9871f", size = 3246262, upload-time = "2026-02-24T17:27:27.986Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/b0/661b0245b06421058610da39f8ceb34abcc90b49f90f256380968d761dbe/sqlalchemy-2.0.47-cp314-cp314-win32.whl", hash = "sha256:e255ee44821a7ef45649c43064cf94e74f81f61b4df70547304b97a351e9b7db", size = 2116528, upload-time = "2026-02-24T17:22:59.363Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/ef/1035a90d899e61810791c052004958be622a2cf3eb3df71c3fe20778c5d0/sqlalchemy-2.0.47-cp314-cp314-win_amd64.whl", hash = "sha256:209467ff73ea1518fe1a5aaed9ba75bb9e33b2666e2553af9ccd13387bf192cb", size = 2142181, upload-time = "2026-02-24T17:23:01.001Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/bb/17a1dd09cbba91258218ceb582225f14b5364d2683f9f5a274f72f2d764f/sqlalchemy-2.0.47-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e78fd9186946afaa287f8a1fe147ead06e5d566b08c0afcb601226e9c7322a64", size = 3563477, upload-time = "2026-02-24T17:12:18.46Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/8f/1a03d24c40cc321ef2f2231f05420d140bb06a84f7047eaa7eaa21d230ba/sqlalchemy-2.0.47-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5740e2f31b5987ed9619d6912ae5b750c03637f2078850da3002934c9532f172", size = 3528568, upload-time = "2026-02-24T17:28:03.732Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/53/d56a213055d6b038a5384f0db5ece7343334aca230ff3f0fa1561106f22c/sqlalchemy-2.0.47-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb9ac00d03de93acb210e8ec7243fefe3e012515bf5fd2f0898c8dff38bc77a4", size = 3472284, upload-time = "2026-02-24T17:12:20.319Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/19/c235d81b9cfdd6130bf63143b7bade0dc4afa46c4b634d5d6b2a96bea233/sqlalchemy-2.0.47-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c72a0b9eb2672d70d112cb149fbaf172d466bc691014c496aaac594f1988e706", size = 3478410, upload-time = "2026-02-24T17:28:05.892Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/db/cafdeca5ecdaa3bb0811ba5449501da677ce0d83be8d05c5822da72d2e86/sqlalchemy-2.0.47-cp314-cp314t-win32.whl", hash = "sha256:c200db1128d72a71dc3c31c24b42eb9fd85b2b3e5a3c9ba1e751c11ac31250ff", size = 2147164, upload-time = "2026-02-24T17:14:40.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/5e/ff41a010e9e0f76418b02ad352060a4341bb15f0af66cedc924ab376c7c6/sqlalchemy-2.0.47-cp314-cp314t-win_amd64.whl", hash = "sha256:669837759b84e575407355dcff912835892058aea9b80bd1cb76d6a151cf37f7", size = 2182154, upload-time = "2026-02-24T17:14:43.205Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/9f/7c378406b592fcf1fc157248607b495a40e3202ba4a6f1372a2ba6447717/sqlalchemy-2.0.47-py3-none-any.whl", hash = "sha256:e2647043599297a1ef10e720cf310846b7f31b6c841fee093d2b09d81215eb93", size = 1940159, upload-time = "2026-02-24T17:15:07.158Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "0.50.0"
|
||||
@@ -413,6 +491,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tzdata"
|
||||
version = "2025.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.40.0"
|
||||
|
||||
Reference in New Issue
Block a user