mirror of
https://github.com/arkorty/B.Tech-Project-III.git
synced 2026-04-19 12:41:48 +00:00
init
This commit is contained in:
568
negot8/backend/database.py
Normal file
568
negot8/backend/database.py
Normal file
@@ -0,0 +1,568 @@
|
||||
import aiosqlite
|
||||
import json
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from config import DATABASE_PATH
|
||||
|
||||
|
||||
async def init_db():
|
||||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
telegram_id INTEGER PRIMARY KEY,
|
||||
username TEXT,
|
||||
display_name TEXT,
|
||||
personality TEXT DEFAULT 'balanced',
|
||||
voice_id TEXT DEFAULT 'pNInz6obpgDQGcFmaJgB',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS negotiations (
|
||||
id TEXT PRIMARY KEY,
|
||||
feature_type TEXT,
|
||||
status TEXT DEFAULT 'pending',
|
||||
initiator_id INTEGER,
|
||||
resolution TEXT,
|
||||
voice_summary_file TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS participants (
|
||||
negotiation_id TEXT REFERENCES negotiations(id),
|
||||
user_id INTEGER,
|
||||
preferences TEXT,
|
||||
personality_used TEXT DEFAULT 'balanced',
|
||||
PRIMARY KEY (negotiation_id, user_id)
|
||||
)
|
||||
""")
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS rounds (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
negotiation_id TEXT REFERENCES negotiations(id),
|
||||
round_number INTEGER,
|
||||
proposer_id INTEGER,
|
||||
proposal TEXT,
|
||||
response_type TEXT,
|
||||
response TEXT,
|
||||
reasoning TEXT,
|
||||
satisfaction_a REAL,
|
||||
satisfaction_b REAL,
|
||||
concessions_made TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS tool_calls (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
negotiation_id TEXT REFERENCES negotiations(id),
|
||||
tool_name TEXT,
|
||||
tool_input TEXT,
|
||||
tool_output TEXT,
|
||||
called_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS negotiation_analytics (
|
||||
negotiation_id TEXT PRIMARY KEY REFERENCES negotiations(id),
|
||||
satisfaction_timeline TEXT,
|
||||
concession_log TEXT,
|
||||
fairness_score REAL,
|
||||
total_concessions_a INTEGER DEFAULT 0,
|
||||
total_concessions_b INTEGER DEFAULT 0,
|
||||
computed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS user_calendar_tokens (
|
||||
telegram_id INTEGER PRIMARY KEY,
|
||||
token_json TEXT NOT NULL,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS blockchain_proofs (
|
||||
negotiation_id TEXT PRIMARY KEY REFERENCES negotiations(id),
|
||||
tx_hash TEXT,
|
||||
block_number INTEGER,
|
||||
agreement_hash TEXT,
|
||||
explorer_url TEXT,
|
||||
gas_used INTEGER DEFAULT 0,
|
||||
recorded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
# ─── Open Contracts (public marketplace) ─────────────────────────────────
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS open_contracts (
|
||||
id TEXT PRIMARY KEY,
|
||||
poster_id INTEGER NOT NULL,
|
||||
contract_type TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
requirements TEXT,
|
||||
status TEXT DEFAULT 'open',
|
||||
matched_applicant_id INTEGER,
|
||||
negotiation_id TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS contract_applications (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
contract_id TEXT REFERENCES open_contracts(id),
|
||||
applicant_id INTEGER NOT NULL,
|
||||
preferences TEXT,
|
||||
match_score REAL DEFAULT 0,
|
||||
match_reasoning TEXT DEFAULT '',
|
||||
status TEXT DEFAULT 'pending',
|
||||
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
await db.commit()
|
||||
print("✅ Database initialized (v5 schema — open contracts + applications added)")
|
||||
|
||||
|
||||
# ─── User helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
async def create_user(telegram_id: int, username: str, display_name: str):
|
||||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||||
await db.execute(
|
||||
"""INSERT OR IGNORE INTO users (telegram_id, username, display_name)
|
||||
VALUES (?, ?, ?)""",
|
||||
(telegram_id, username or "", display_name or ""),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def get_user(telegram_id: int):
|
||||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
async with db.execute(
|
||||
"SELECT * FROM users WHERE telegram_id = ?", (telegram_id,)
|
||||
) as cursor:
|
||||
return await cursor.fetchone()
|
||||
|
||||
|
||||
async def update_user_personality(telegram_id: int, personality: str):
|
||||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||||
await db.execute(
|
||||
"UPDATE users SET personality = ? WHERE telegram_id = ?",
|
||||
(personality, telegram_id),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
|
||||
# ─── Negotiation helpers ─────────────────────────────────────────────────────
|
||||
|
||||
async def create_negotiation(feature_type: str, initiator_id: int) -> str:
|
||||
neg_id = str(uuid.uuid4())[:8]
|
||||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||||
await db.execute(
|
||||
"""INSERT INTO negotiations (id, feature_type, initiator_id, status)
|
||||
VALUES (?, ?, ?, 'pending')""",
|
||||
(neg_id, feature_type, initiator_id),
|
||||
)
|
||||
await db.commit()
|
||||
return neg_id
|
||||
|
||||
|
||||
async def add_participant(
|
||||
negotiation_id: str,
|
||||
user_id: int,
|
||||
preferences: dict,
|
||||
personality_used: str = "balanced",
|
||||
):
|
||||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||||
await db.execute(
|
||||
"""INSERT OR REPLACE INTO participants
|
||||
(negotiation_id, user_id, preferences, personality_used)
|
||||
VALUES (?, ?, ?, ?)""",
|
||||
(negotiation_id, user_id, json.dumps(preferences), personality_used),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def get_participants(negotiation_id: str):
|
||||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
async with db.execute(
|
||||
"SELECT * FROM participants WHERE negotiation_id = ?", (negotiation_id,)
|
||||
) as cursor:
|
||||
rows = await cursor.fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
async def save_round(
|
||||
negotiation_id: str,
|
||||
round_number: int,
|
||||
proposer_id: int,
|
||||
proposal: dict,
|
||||
response_type: str = None,
|
||||
response: dict = None,
|
||||
reasoning: str = None,
|
||||
satisfaction_a: float = None,
|
||||
satisfaction_b: float = None,
|
||||
concessions_made: list = None,
|
||||
):
|
||||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||||
await db.execute(
|
||||
"""INSERT INTO rounds
|
||||
(negotiation_id, round_number, proposer_id, proposal,
|
||||
response_type, response, reasoning,
|
||||
satisfaction_a, satisfaction_b, concessions_made)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
(
|
||||
negotiation_id,
|
||||
round_number,
|
||||
proposer_id,
|
||||
json.dumps(proposal),
|
||||
response_type,
|
||||
json.dumps(response) if response else None,
|
||||
reasoning,
|
||||
satisfaction_a,
|
||||
satisfaction_b,
|
||||
json.dumps(concessions_made or []),
|
||||
),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def get_rounds(negotiation_id: str):
|
||||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
async with db.execute(
|
||||
"SELECT * FROM rounds WHERE negotiation_id = ? ORDER BY round_number",
|
||||
(negotiation_id,),
|
||||
) as cursor:
|
||||
rows = await cursor.fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
async def update_negotiation_status(
|
||||
negotiation_id: str,
|
||||
status: str,
|
||||
resolution: dict = None,
|
||||
voice_file: str = None,
|
||||
):
|
||||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||||
await db.execute(
|
||||
"""UPDATE negotiations
|
||||
SET status = ?,
|
||||
resolution = ?,
|
||||
voice_summary_file = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?""",
|
||||
(
|
||||
status,
|
||||
json.dumps(resolution) if resolution else None,
|
||||
voice_file,
|
||||
negotiation_id,
|
||||
),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def store_analytics(analytics: dict):
|
||||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||||
await db.execute(
|
||||
"""INSERT OR REPLACE INTO negotiation_analytics
|
||||
(negotiation_id, satisfaction_timeline, concession_log,
|
||||
fairness_score, total_concessions_a, total_concessions_b)
|
||||
VALUES (?, ?, ?, ?, ?, ?)""",
|
||||
(
|
||||
analytics["negotiation_id"],
|
||||
analytics.get("satisfaction_timeline", "[]"),
|
||||
analytics.get("concession_log", "[]"),
|
||||
analytics.get("fairness_score", 0),
|
||||
analytics.get("total_concessions_a", 0),
|
||||
analytics.get("total_concessions_b", 0),
|
||||
),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
|
||||
# ─── Google Calendar token helpers ─────────────────────────────────────────
|
||||
|
||||
async def save_calendar_token(telegram_id: int, token_json: str):
|
||||
"""Upsert a user's Google OAuth2 credentials (serialized JSON)."""
|
||||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||||
await db.execute(
|
||||
"""INSERT INTO user_calendar_tokens (telegram_id, token_json, updated_at)
|
||||
VALUES (?, ?, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(telegram_id) DO UPDATE
|
||||
SET token_json = excluded.token_json,
|
||||
updated_at = CURRENT_TIMESTAMP""",
|
||||
(telegram_id, token_json),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def get_calendar_token(telegram_id: int) -> str:
|
||||
"""Return stored token JSON for a user, or None if not connected."""
|
||||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
async with db.execute(
|
||||
"SELECT token_json FROM user_calendar_tokens WHERE telegram_id = ?",
|
||||
(telegram_id,),
|
||||
) as cursor:
|
||||
row = await cursor.fetchone()
|
||||
return row["token_json"] if row else None
|
||||
|
||||
|
||||
async def get_analytics(negotiation_id: str):
|
||||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
async with db.execute(
|
||||
"SELECT * FROM negotiation_analytics WHERE negotiation_id = ?",
|
||||
(negotiation_id,),
|
||||
) as cursor:
|
||||
row = await cursor.fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
|
||||
# ─── Blockchain proof helpers ────────────────────────────────────────────────
|
||||
|
||||
async def store_blockchain_proof(
|
||||
negotiation_id: str,
|
||||
tx_hash: str,
|
||||
block_number: int,
|
||||
agreement_hash: str,
|
||||
explorer_url: str,
|
||||
gas_used: int = 0,
|
||||
):
|
||||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||||
await db.execute(
|
||||
"""INSERT OR REPLACE INTO blockchain_proofs
|
||||
(negotiation_id, tx_hash, block_number, agreement_hash, explorer_url, gas_used)
|
||||
VALUES (?, ?, ?, ?, ?, ?)""",
|
||||
(negotiation_id, tx_hash, block_number, agreement_hash, explorer_url, gas_used),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def get_blockchain_proof(negotiation_id: str):
|
||||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
async with db.execute(
|
||||
"SELECT * FROM blockchain_proofs WHERE negotiation_id = ?",
|
||||
(negotiation_id,),
|
||||
) as cursor:
|
||||
row = await cursor.fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
|
||||
async def get_negotiation(negotiation_id: str):
|
||||
"""Return a single negotiation row by ID, or None if not found."""
|
||||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
async with db.execute(
|
||||
"SELECT * FROM negotiations WHERE id = ?",
|
||||
(negotiation_id,),
|
||||
) as cursor:
|
||||
row = await cursor.fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
|
||||
async def get_latest_negotiation_for_user(telegram_id: int):
|
||||
"""Return the most recent resolved negotiation_id that the given user participated in."""
|
||||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
# First try: find a resolved negotiation where the user is the initiator
|
||||
async with db.execute(
|
||||
"""SELECT n.id FROM negotiations n
|
||||
JOIN participants p ON p.negotiation_id = n.id
|
||||
WHERE p.user_id = ? AND n.status = 'resolved'
|
||||
ORDER BY n.created_at DESC LIMIT 1""",
|
||||
(telegram_id,),
|
||||
) as cursor:
|
||||
row = await cursor.fetchone()
|
||||
return row["id"] if row else None
|
||||
|
||||
|
||||
# ─── Open Contracts helpers ──────────────────────────────────────────────────
|
||||
|
||||
async def create_open_contract(
|
||||
poster_id: int,
|
||||
contract_type: str,
|
||||
title: str,
|
||||
description: str,
|
||||
requirements: dict,
|
||||
) -> str:
|
||||
"""Create a new open contract. Returns the 8-char UUID id."""
|
||||
contract_id = str(uuid.uuid4())[:8]
|
||||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||||
await db.execute(
|
||||
"""INSERT INTO open_contracts
|
||||
(id, poster_id, contract_type, title, description, requirements, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'open')""",
|
||||
(contract_id, poster_id, contract_type, title, description, json.dumps(requirements)),
|
||||
)
|
||||
await db.commit()
|
||||
return contract_id
|
||||
|
||||
|
||||
async def get_open_contracts(status: str = "open") -> list:
|
||||
"""Return all contracts with the given status, newest first."""
|
||||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
async with db.execute(
|
||||
"""SELECT oc.*,
|
||||
u.username AS poster_username,
|
||||
u.display_name AS poster_name,
|
||||
COUNT(ca.id) AS application_count
|
||||
FROM open_contracts oc
|
||||
LEFT JOIN users u ON u.telegram_id = oc.poster_id
|
||||
LEFT JOIN contract_applications ca ON ca.contract_id = oc.id
|
||||
WHERE oc.status = ?
|
||||
GROUP BY oc.id
|
||||
ORDER BY oc.created_at DESC""",
|
||||
(status,),
|
||||
) as cur:
|
||||
rows = await cur.fetchall()
|
||||
result = []
|
||||
for r in rows:
|
||||
d = dict(r)
|
||||
if isinstance(d.get("requirements"), str):
|
||||
try:
|
||||
d["requirements"] = json.loads(d["requirements"])
|
||||
except Exception:
|
||||
pass
|
||||
result.append(d)
|
||||
return result
|
||||
|
||||
|
||||
async def get_open_contract(contract_id: str):
|
||||
"""Return a single open contract by ID, or None."""
|
||||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
async with db.execute(
|
||||
"""SELECT oc.*, u.username AS poster_username, u.display_name AS poster_name
|
||||
FROM open_contracts oc
|
||||
LEFT JOIN users u ON u.telegram_id = oc.poster_id
|
||||
WHERE oc.id = ?""",
|
||||
(contract_id,),
|
||||
) as cur:
|
||||
row = await cur.fetchone()
|
||||
if not row:
|
||||
return None
|
||||
d = dict(row)
|
||||
if isinstance(d.get("requirements"), str):
|
||||
try:
|
||||
d["requirements"] = json.loads(d["requirements"])
|
||||
except Exception:
|
||||
pass
|
||||
return d
|
||||
|
||||
|
||||
async def add_application(contract_id: str, applicant_id: int, preferences: dict) -> int:
|
||||
"""Add an application to an open contract. Returns the new application row id."""
|
||||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||||
cursor = await db.execute(
|
||||
"""INSERT INTO contract_applications
|
||||
(contract_id, applicant_id, preferences, status)
|
||||
VALUES (?, ?, ?, 'pending')""",
|
||||
(contract_id, applicant_id, json.dumps(preferences)),
|
||||
)
|
||||
await db.commit()
|
||||
return cursor.lastrowid
|
||||
|
||||
|
||||
async def get_applications(contract_id: str) -> list:
|
||||
"""Return all applications for a contract, sorted by match_score descending."""
|
||||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
async with db.execute(
|
||||
"""SELECT ca.*, u.username, u.display_name
|
||||
FROM contract_applications ca
|
||||
LEFT JOIN users u ON u.telegram_id = ca.applicant_id
|
||||
WHERE ca.contract_id = ?
|
||||
ORDER BY ca.match_score DESC""",
|
||||
(contract_id,),
|
||||
) as cur:
|
||||
rows = await cur.fetchall()
|
||||
result = []
|
||||
for r in rows:
|
||||
d = dict(r)
|
||||
if isinstance(d.get("preferences"), str):
|
||||
try:
|
||||
d["preferences"] = json.loads(d["preferences"])
|
||||
except Exception:
|
||||
pass
|
||||
result.append(d)
|
||||
return result
|
||||
|
||||
|
||||
async def update_application_match_score(app_id: int, score: float, reasoning: str):
|
||||
"""Update the AI match score + reasoning for an application row."""
|
||||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||||
await db.execute(
|
||||
"""UPDATE contract_applications
|
||||
SET match_score = ?, match_reasoning = ?
|
||||
WHERE id = ?""",
|
||||
(score, reasoning, app_id),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def claim_contract(contract_id: str, applicant_id: int, negotiation_id: str):
|
||||
"""Mark a contract as 'negotiating', lock in the matched applicant, and link the negotiation."""
|
||||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||||
await db.execute(
|
||||
"""UPDATE open_contracts
|
||||
SET status = 'negotiating',
|
||||
matched_applicant_id = ?,
|
||||
negotiation_id = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?""",
|
||||
(applicant_id, negotiation_id, contract_id),
|
||||
)
|
||||
await db.execute(
|
||||
"UPDATE contract_applications SET status = 'selected' WHERE contract_id = ? AND applicant_id = ?",
|
||||
(contract_id, applicant_id),
|
||||
)
|
||||
await db.execute(
|
||||
"UPDATE contract_applications SET status = 'rejected' WHERE contract_id = ? AND applicant_id != ?",
|
||||
(contract_id, applicant_id),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def close_contract(contract_id: str):
|
||||
"""Mark a contract as resolved/closed after negotiation completes."""
|
||||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||||
await db.execute(
|
||||
"UPDATE open_contracts SET status = 'resolved', updated_at = CURRENT_TIMESTAMP WHERE id = ?",
|
||||
(contract_id,),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def get_contracts_by_poster(poster_id: int) -> list:
|
||||
"""Return all open contracts created by a given poster, newest first."""
|
||||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
async with db.execute(
|
||||
"""SELECT oc.*,
|
||||
COUNT(ca.id) AS application_count,
|
||||
MAX(ca.match_score) AS best_score
|
||||
FROM open_contracts oc
|
||||
LEFT JOIN contract_applications ca ON ca.contract_id = oc.id
|
||||
WHERE oc.poster_id = ?
|
||||
GROUP BY oc.id
|
||||
ORDER BY oc.created_at DESC""",
|
||||
(poster_id,),
|
||||
) as cur:
|
||||
rows = await cur.fetchall()
|
||||
result = []
|
||||
for r in rows:
|
||||
d = dict(r)
|
||||
if isinstance(d.get("requirements"), str):
|
||||
try:
|
||||
d["requirements"] = json.loads(d["requirements"])
|
||||
except Exception:
|
||||
pass
|
||||
result.append(d)
|
||||
return result
|
||||
Reference in New Issue
Block a user