Compare commits

30 Commits

Author SHA1 Message Date
Kita Trofimov 7fdd3c56cb Merge remote-tracking branch 'origin/feat/db-init' into feat/db-init
# Conflicts:
#	api/loginapi.py
#	db/connection.py
#	db/repositories/ai_prediction_repository.py
#	db/repositories/inventory_repository.py
#	db/repositories/product_repository.py
#	db/repositories/robot_repository.py
#	db/repositories/user_repository.py
2025-10-27 23:04:10 +03:00
Kita Trofimov 052c93928c feat(db): Database initialization has been added 2025-10-27 23:02:19 +03:00
Sweetbread e44696ce04 wip 2025-10-26 23:19:51 +03:00
Sweetbread 72f766ab46 fix(log): replace logger with loguru (again!) 2025-10-26 23:09:42 +03:00
Kita Trofimov 9ecb6a83ba style(database): Fixed the issue with long function declarations 2025-10-26 21:25:59 +03:00
Kita Trofimov 951e4c7e45 style(database): Changed logging 2025-10-26 21:25:59 +03:00
Kita Trofimov 7786680f43 fix(database): Fixed the database connection 2025-10-26 21:16:23 +03:00
Kita Trofimov 33c3b81f6e style(database): Refactor repository name 2025-10-26 21:16:23 +03:00
Kita Trofimov 6006e074d8 fix(model/user.py): Removed unnecesary methods 2025-10-26 21:16:23 +03:00
Kita Trofimov 771f7e0549 feat(database/repositories/user_repository.py): A new login verification method has been added 2025-10-26 21:16:23 +03:00
Kita Trofimov fd1b7df93a feat(database/repositories/ai_prediction_repository.py): Added new repository AIPredictionsRepository 2025-10-26 21:16:23 +03:00
Kita Trofimov 88c4460da9 feat(model/ai_prediction.py): Added new model AIPrediction 2025-10-26 21:16:23 +03:00
Kita Trofimov 51bf856434 feat(database/repositories/inventory_repository.py): Added inventory repository 2025-10-26 21:16:23 +03:00
Kita Trofimov 0ded143c55 feat(database/repositories/product_repository.py): Added product repository 2025-10-26 21:16:23 +03:00
Kita Trofimov 056d800c4f feat(database/repositories/robot_repository.py): Added robot_repository 2025-10-26 21:16:23 +03:00
Kita Trofimov 3be4556844 feat(database/repositories/user_repository.py): Added user repository 2025-10-26 21:16:23 +03:00
Kita Trofimov 928677fea8 feat(database): Implemented database connection 2025-10-26 21:16:23 +03:00
Kita Trofimov e80c5645f1 feat(model): Added correct data models 2025-10-26 21:16:23 +03:00
Sweetbread 70041fa58e feat(jwt): add iat field 2025-10-26 21:16:23 +03:00
Sweetbread 6f478d717a feat(log): use loguru 2025-10-26 21:16:23 +03:00
Sweetbread be5732bc32 refactor(auth): remove /api/auth/* handlers to api/auth.py 2025-10-26 21:16:23 +03:00
Sweetbread 4ad7869e27 refactor(user): rename to toJson back 2025-10-26 21:16:23 +03:00
Sweetbread 67c06a0203 feat(auth): add data validation 2025-10-26 21:16:23 +03:00
Sweetbread df5317432d refactor(user): change class name to uppercase 2025-10-26 21:16:23 +03:00
Sweetbread 96a310a80d fix(env): ensure the .env is loaded 2025-10-26 21:16:22 +03:00
Kirill ec985a808d fix(api/auth/loginapi.py, app.py, utils/PostgressConnect.py, utils/createLogger.py, utils/loadDotEnv.py): Review fix
https://g.codrs.ru/Hackaton/Backend/pulls/2#issuecomment-23
https://g.codrs.ru/Hackaton/Backend/pulls/2#issuecomment-31
2025-10-26 21:16:17 +03:00
Kirill f93b94531c fix(utils/token.py): Code review fix
https://g.codrs.ru/Hackaton/Backend/pulls/2#issuecomment-24
2025-10-25 21:01:45 +03:00
Kirill 22da586aec fix(app.py): Code review fix
https://g.codrs.ru/Hackaton/Backend/pulls/2#issuecomment-30
2025-10-25 20:59:41 +03:00
Kirill 5058df8b7d fix(model/user.py): Code review fix
https://g.codrs.ru/Hackaton/Backend/pulls/2#issuecomment-20
https://g.codrs.ru/Hackaton/Backend/pulls/2#issuecomment-25
https://g.codrs.ru/Hackaton/Backend/pulls/2#issuecomment-21
2025-10-25 20:57:23 +03:00
Kirill 3a118c51b5 fix(api/auth/loginapi.py): Pull request review
https://g.codrs.ru/Hackaton/Backend/pulls/2#issuecomment-26
https://g.codrs.ru/Hackaton/Backend/pulls/2#issuecomment-20
https://g.codrs.ru/Hackaton/Backend/pulls/2#issuecomment-35
2025-10-25 20:53:48 +03:00
14 changed files with 296 additions and 116 deletions
+2
View File
@@ -0,0 +1,2 @@
KEY= # Key for JWT token
POSTGRES_URL=postgresql://
+35
View File
@@ -0,0 +1,35 @@
from flask import Blueprint, request, jsonify
from model.user import User
from db.repositories.user_repository import UserRepository # FIXME: authenticate_user as get_user
from utils.token import generateKey as getToken
auth = Blueprint("auth", __name__)
@auth.route('/login', methods = ['POST'])
def login():
if request.is_json:
req = request.json
email = req.get('email')
password = req.get('password')
if not email or not password:
return "Request must have email and password", 400
if len(email.strip()) < 4 or '@' not in email or '.' not in email:
return "Email is incorrect", 400
if len(password.strip()) < 8:
return "Password is too short", 400
user = UserRepository().authenticate_user(email, password)
if not user:
return "Wrong credentials", 400
token = getToken(user)
return jsonify({'token': token, 'user': {'id': user.id, 'name': user.name, 'role': user.role}})
else:
return "Request is not a json", 400
-12
View File
@@ -1,12 +0,0 @@
from flask import Blueprint, request
from model.user import user
loginBP = Blueprint("loginapi", __name__)
@loginBP.route('/api/login', methods = ['POST'])
def login():
email = request.form['email']
password = request.form['password']
#if(isvalid(email, password)):
us = user.initialize(email, password)
return us.toJSON()
+7 -2
View File
@@ -1,6 +1,11 @@
from sys import exit
from flask import Flask from flask import Flask
from api.loginapi import loginBP from api.auth import auth
from utils.loadDotEnv import initializeENV
if not initializeENV():
exit(-1)
app = Flask(__name__) app = Flask(__name__)
app.register_blueprint(loginBP) app.register_blueprint(auth, url_prefix='/api/auth')
+31 -27
View File
@@ -1,21 +1,29 @@
import psycopg2 import psycopg2
import os import os
import logging
from contextlib import contextmanager from contextlib import contextmanager
from typing import Generator from typing import Generator
from loguru import logger
from utils.loadDotEnv import initializeENV from utils.loadDotEnv import initializeENV
initializeENV() initializeENV()
logger = logging.getLogger(__name__)
def PSQLConnect(): def PSQLConnect():
conn = psycopg2.connect(os.getenv('POSTDRESS_CONNECTION')) conn_str = os.getenv('POSTGRES_CONNECTION')
if not conn_str:
logger.error("POSTGRES_CONNECTION не найден в .env файле")
raise ValueError("POSTGRES_CONNECTION не найден в .env файле")
conn = psycopg2.connect(conn_str)
logger.debug("Подключение к БД установлено")
return conn return conn
def PSQLCursor(conn): def PSQLCursor(conn):
cur = conn.cursor() cur = conn.cursor()
logger.debug("Курсор БД создан")
return cur return cur
@@ -24,38 +32,34 @@ def get_connection() -> Generator[psycopg2.extensions.connection, None, None]:
conn = None conn = None
try: try:
conn = PSQLConnect() conn = PSQLConnect()
logger.debug("Подключение к БД установлено") logger.debug("Контекст подключения к БД открыт")
yield conn yield conn
except psycopg2.OperationalError as e:
logger.error(f"Ошибка подключения к БД: {e}")
raise
except psycopg2.Error as e:
logger.error(f"Ошибка PostgreSQL: {e}")
raise
except Exception as e: except Exception as e:
logger.error(f"Неожиданная ошибка при работе с БД: {e}") logger.error(f"Ошибка в контексте подключения: {e}")
if conn:
conn.rollback()
logger.debug("Откат транзакции выполнен")
raise raise
finally: finally:
if conn: if conn:
try: conn.close()
conn.close() logger.debug("Подключение к БД закрыто")
logger.debug("Соединение с БД закрыто")
except Exception as e:
logger.warning(f"Ошибка при закрытии соединения: {e}")
def test_connection() -> bool: def test_connection() -> bool:
try: try:
with get_connection() as conn: with get_connection() as conn:
cur = PSQLCursor(conn) cur = PSQLCursor(conn)
try: cur.execute("SELECT version();")
cur.execute("SELECT version();") version = cur.fetchone()
version = cur.fetchone() logger.info(f"Подключение к БД успешно: {version[0]}")
logger.debug(f"Версия PostgreSQL: {version[0]}") cur.close()
return True logger.debug("Курсор БД закрыт")
finally: return True
cur.close()
logger.debug("Курсор закрыт")
except Exception as e: except Exception as e:
logger.error(f"Тест подключения к БД провален: {e}") logger.error(f"Ошибка подключения к БД: {e}")
return False return False
print(test_connection())
if __name__ == "__main__":
test_connection()
+147
View File
@@ -0,0 +1,147 @@
from db.connection import get_connection
from loguru import logger
def create_tables():
try:
with get_connection() as conn:
with conn.cursor() as cur:
# Пользователи
cur.execute("""
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
role VARCHAR(50) NOT NULL DEFAULT 'viewer',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
# Роботы
cur.execute("""
CREATE TABLE IF NOT EXISTS robots (
id VARCHAR(50) PRIMARY KEY,
status VARCHAR(50) DEFAULT 'active',
battery_level INTEGER DEFAULT 100,
last_update TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
current_zone VARCHAR(10),
current_row INTEGER,
current_shelf INTEGER
)
""")
# Товары
cur.execute("""
CREATE TABLE IF NOT EXISTS products (
id VARCHAR(50) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
category VARCHAR(100),
min_stock INTEGER DEFAULT 10,
optimal_stock INTEGER DEFAULT 100
)
""")
# История инвентаризации
cur.execute("""
CREATE TABLE IF NOT EXISTS inventory_history (
id SERIAL PRIMARY KEY,
robot_id VARCHAR(50) REFERENCES robots(id),
product_id VARCHAR(50) REFERENCES products(id),
quantity INTEGER NOT NULL,
zone VARCHAR(10) NOT NULL,
row_number INTEGER,
shelf_number INTEGER,
status VARCHAR(50),
scanned_at TIMESTAMP NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
# Прогнозы ИИ
cur.execute("""
CREATE TABLE IF NOT EXISTS ai_predictions (
id SERIAL PRIMARY KEY,
product_id VARCHAR(50) REFERENCES products(id),
prediction_date DATE NOT NULL,
days_until_stockout INTEGER,
recommended_order INTEGER,
confidence_score DECIMAL(3,2),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
conn.commit()
logger.debug("Все таблицы успешно созданы")
except Exception as e:
logger.error(f"Ошибка создания таблиц: {e}")
raise
def create_indexes():
try:
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute("CREATE INDEX IF NOT EXISTS idx_inventory_scanned ON inventory_history(scanned_at DESC)")
cur.execute("CREATE INDEX IF NOT EXISTS idx_inventory_product ON inventory_history(product_id)")
cur.execute("CREATE INDEX IF NOT EXISTS idx_inventory_zone ON inventory_history(zone)")
conn.commit()
logger.debug("Индексы созданы")
except Exception as e:
logger.error(f"Ошибка создания индексов: {e}")
raise
def insert_sample_data():
try:
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute("""
INSERT INTO users (email, password_hash, name, role)
VALUES
('admin@warehouse.com', 'hash1', 'Администратор', 'admin'),
('operator@warehouse.com', 'hash2', 'Оператор Иванов', 'operator'),
('viewer@warehouse.com', 'hash3', 'Наблюдатель Петров', 'viewer')
ON CONFLICT (email) DO NOTHING
""")
cur.execute("""
INSERT INTO robots (id, status, battery_level, current_zone)
VALUES
('RB-001', 'active', 85, 'A'),
('RB-002', 'active', 45, 'B'),
('RB-003', 'maintenance', 100, NULL)
ON CONFLICT (id) DO NOTHING
""")
cur.execute("""
INSERT INTO products (id, name, category, min_stock, optimal_stock)
VALUES
('TEL-1234', 'Смартфон X', 'Электроника', 5, 50),
('NOTE-567', 'Ноутбук Pro', 'Электроника', 3, 20),
('ACC-999', 'Чехол для телефона', 'Аксессуары', 10, 100)
ON CONFLICT (id) DO NOTHING
""")
conn.commit()
logger.debug("Тестовые данные добавлены")
except Exception as e:
logger.error(f"Ошибка добавления тестовых данных: {e}")
raise
def initialize_database():
logger.info("Начинаем инициализацию базы данных...")
create_tables()
create_indexes()
insert_sample_data()
logger.debug("База данных успешно инициализирована!")
if __name__ == "__main__":
initialize_database()
+9 -9
View File
@@ -1,11 +1,9 @@
from typing import List, Optional from typing import List, Optional
from datetime import datetime, date from datetime import datetime, date
import logging from loguru import logger
from db.connection import get_connection from db.connection import get_connection
from model.ai_prediction import AIPrediction from model.ai_prediction import AIPrediction
logger = logging.getLogger(__name__)
class AIPredictionsRepository: class AIPredictionsRepository:
def get_all(self) -> List[AIPrediction]: def get_all(self) -> List[AIPrediction]:
try: try:
@@ -77,12 +75,14 @@ class AIPredictionsRepository:
logger.error(f"Ошибка получения последних прогнозов по товарам: {e}") logger.error(f"Ошибка получения последних прогнозов по товарам: {e}")
return [] return []
def create_prediction(self, def create_prediction(
product_id: str, self,
prediction_date: date, product_id: str,
days_until_stockout: int, prediction_date: date,
recommended_order: int, days_until_stockout: int,
confidence_score: float) -> Optional[int]: recommended_order: int,
confidence_score: float
) -> Optional[int]:
try: try:
with get_connection() as conn: with get_connection() as conn:
with conn.cursor() as cur: with conn.cursor() as cur:
+12 -12
View File
@@ -1,12 +1,10 @@
# db/repositories/inventory_repository.py # db/repositories/inventory_repository.py
from typing import List, Optional, Tuple from typing import List, Optional, Tuple
from datetime import datetime from datetime import datetime
import logging from loguru import logger
from db.connection import get_connection from db.connection import get_connection
from model.inventory import InventoryRecord from model.inventory import InventoryRecord
logger = logging.getLogger(__name__)
class InventoryRepository: class InventoryRepository:
def get_all(self) -> List[InventoryRecord]: def get_all(self) -> List[InventoryRecord]:
try: try:
@@ -48,15 +46,17 @@ class InventoryRepository:
logger.error(f"Ошибка получения записи инвентаризации {record_id}: {e}") logger.error(f"Ошибка получения записи инвентаризации {record_id}: {e}")
return None return None
def create_record(self, def create_record(
robot_id: str, self,
product_id: str, robot_id: str,
quantity: int, product_id: str,
zone: str, quantity: int,
row_number: int, zone: str,
shelf_number: int, row_number: int,
status: str, shelf_number: int,
scanned_at: datetime) -> Optional[int]: status: str,
scanned_at: datetime
) -> Optional[int]:
try: try:
with get_connection() as conn: with get_connection() as conn:
with conn.cursor() as cur: with conn.cursor() as cur:
+1 -3
View File
@@ -1,10 +1,8 @@
from typing import List, Optional from typing import List, Optional
import logging from loguru import logger
from db.connection import get_connection from db.connection import get_connection
from model.product import Product from model.product import Product
logger = logging.getLogger(__name__)
class ProductRepository: class ProductRepository:
def get_all(self) -> List[Product]: def get_all(self) -> List[Product]:
try: try:
+16 -14
View File
@@ -1,10 +1,8 @@
from typing import List, Optional from typing import List, Optional
import logging from loguru import logger
from db.connection import get_connection from db.connection import get_connection
from model.robot import Robot from model.robot import Robot
logger = logging.getLogger(__name__)
class RobotRepository: class RobotRepository:
def get_all(self) -> List[Robot]: def get_all(self) -> List[Robot]:
try: try:
@@ -43,13 +41,15 @@ class RobotRepository:
logger.error(f"Ошибка получения робота {robot_id}: {e}") logger.error(f"Ошибка получения робота {robot_id}: {e}")
return None return None
def update_robot(self, def update_robot(
robot_id: str, self,
status: str = None, robot_id: str,
battery_level: int = None, status: str = None,
current_zone: str = None, battery_level: int = None,
current_row: int = None, current_zone: str = None,
current_shelf: int = None) -> bool: current_row: int = None,
current_shelf: int = None
) -> bool:
try: try:
with get_connection() as conn: with get_connection() as conn:
with conn.cursor() as cur: with conn.cursor() as cur:
@@ -131,10 +131,12 @@ class RobotRepository:
logger.error(f"Ошибка получения роботов в зоне {zone}: {e}") logger.error(f"Ошибка получения роботов в зоне {zone}: {e}")
return [] return []
def create_robot(self, def create_robot(
robot_id: str, self,
status: str = 'active', robot_id: str,
battery_level: int = 100) -> bool: status: str = 'active',
battery_level: int = 100
) -> bool:
try: try:
with get_connection() as conn: with get_connection() as conn:
with conn.cursor() as cur: with conn.cursor() as cur:
+11 -26
View File
@@ -1,10 +1,8 @@
from typing import List, Optional from typing import List, Optional
import logging from loguru import logger
from model.user import User from model.user import User
from db.connection import get_connection from db.connection import get_connection
logger = logging.getLogger(__name__)
class UserRepository: class UserRepository:
def get_all(self) -> List[User]: def get_all(self) -> List[User]:
try: try:
@@ -57,11 +55,13 @@ class UserRepository:
logger.error(f"Ошибка получения пользователя по email {email}: {e}") logger.error(f"Ошибка получения пользователя по email {email}: {e}")
return None return None
def create_user(self, def create_user(
email: str, self,
password_hash: str, email: str,
name: str, password_hash: str,
role: str) -> Optional[User]: name: str,
role: str
) -> Optional[User]:
try: try:
with get_connection() as conn: with get_connection() as conn:
with conn.cursor() as cur: with conn.cursor() as cur:
@@ -166,6 +166,9 @@ class UserRepository:
return False return False
def authenticate_user(self, email: str, password_hash: str) -> Optional[User]: def authenticate_user(self, email: str, password_hash: str) -> Optional[User]:
if not self.user_exists(email):
return
try: try:
with get_connection() as conn: with get_connection() as conn:
with conn.cursor() as cur: with conn.cursor() as cur:
@@ -183,24 +186,6 @@ class UserRepository:
logger.error(f"Ошибка аутентификации пользователя {email}: {e}") logger.error(f"Ошибка аутентификации пользователя {email}: {e}")
return None return None
def is_valid_authenticate(self, email: str, password_hash: str) -> bool:
try:
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute("""
SELECT 1 FROM users
WHERE email = %s AND password_hash = %s
""", (email, password_hash))
is_valid = cur.fetchone() is not None
if is_valid:
logger.debug(f"Валидные учетные данные для пользователя {email}")
else:
logger.warning(f"Невалидные учетные данные для пользователя {email}")
return is_valid
except Exception as e:
logger.error(f"Ошибка проверки учетных данных пользователя {email}: {e}")
return False
def user_exists(self, email: str) -> bool: def user_exists(self, email: str) -> bool:
try: try:
with get_connection() as conn: with get_connection() as conn:
+2 -1
View File
@@ -1,4 +1,5 @@
flask==3.1.2 flask==3.1.2
python-dotenv python-dotenv
psycopg-binary psycopg2-binary
pyjwt pyjwt
loguru
+10 -8
View File
@@ -1,12 +1,14 @@
import os import os
from dotenv import load_dotenv from dotenv import load_dotenv
from loguru import logger as log
def initializeENV(): DOTENV_PATH = '.env'
dotenv_path = '../.env'
if os.path.exists(dotenv_path): def initializeENV() -> bool:
load_dotenv(dotenv_path) if os.path.exists(DOTENV_PATH):
print('.env is loaded') load_dotenv(DOTENV_PATH)
return 1 log.info('.env is loaded')
return True
else: else:
print('.env isn`t loaded') log.error('.env isn`t loaded')
return 0 return False
+13 -2
View File
@@ -1,7 +1,18 @@
import jwt import jwt
import os import os
from time import time
from model.user import User
def generateKey(email, passwd): def generateKey(user: User) -> dict:
key = os.getenv('KEY') key = os.getenv('KEY')
encoded = jwt.encode({f"{email}": f"{passwd}"}, key, algorithm="HS256") encoded = jwt.encode(
{
'id': user.id,
'name': user.name,
'role': user.role,
'iat': time()
},
key,
algorithm="HS256"
)
return encoded return encoded