Repository: Gussey3/cmd-todo-list
Pull request #2: SQL feat
Author: login: Gussey3
Created: 2025-03-01T15:19:33Z | Merged: 2025-03-23T11:09:55Z



Diff:
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..825f66c
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,8 @@
+POSTGRES_USER=
+POSTGRES_PASSWORD=
+POSTGRES_HOST=
+POSTGRES_PORT=
+POSTGRES_DB=
+SCHEMA=
+
+DB_SOURCE=  # psql / json
diff --git a/alembic.ini b/alembic.ini
new file mode 100644
index 0000000..a26a2cc
--- /dev/null
+++ b/alembic.ini
@@ -0,0 +1,118 @@
+# A generic, single database configuration.
+
+[alembic]
+# path to migration scripts
+# Use forward slashes (/) also on windows to provide an os agnostic path
+script_location = alembic
+
+# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
+# Uncomment the line below if you want the files to be prepended with date and time
+# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
+# for all available tokens
+# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
+
+# sys.path path, will be prepended to sys.path if present.
+# defaults to the current working directory.
+prepend_sys_path = .
+
+# timezone to use when rendering the date within the migration file
+# as well as the filename.
+# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library.
+# Any required deps can installed by adding `alembic[tz]` to the pip requirements
+# string value is passed to ZoneInfo()
+# leave blank for localtime
+# timezone =
+
+# max length of characters to apply to the "slug" field
+# truncate_slug_length = 40
+
+# set to 'true' to run the environment during
+# the 'revision' command, regardless of autogenerate
+# revision_environment = false
+
+# set to 'true' to allow .pyc and .pyo files without
+# a source .py file to be detected as revisions in the
+# versions/ directory
+# sourceless = false
+
+# version location specification; This defaults
+# to alembic/versions.  When using multiple version
+# directories, initial revisions must be specified with --version-path.
+# The path separator used here should be the separator specified by "version_path_separator" below.
+# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
+
+# version path separator; As mentioned above, this is the character used to split
+# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
+# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
+# Valid values for version_path_separator are:
+#
+# version_path_separator = :
+# version_path_separator = ;
+# version_path_separator = space
+# version_path_separator = newline
+#
+# Use os.pathsep. Default configuration used for new projects.
+version_path_separator = os
+
+# set to 'true' to search source files recursively
+# in each "version_locations" directory
+# new in Alembic version 1.10
+# recursive_version_locations = false
+
+# the output encoding used when revision files
+# are written from script.py.mako
+# output_encoding = utf-8
+
+
+
+[post_write_hooks]
+# post_write_hooks defines scripts or Python functions that are run
+# on newly generated revision scripts.  See the documentation for further
+# detail and examples
+
+# format using "black" - use the console_scripts runner, against the "black" entrypoint
+# hooks = black
+# black.type = console_scripts
+# black.entrypoint = black
+# black.options = -l 79 REVISION_SCRIPT_FILENAME
+
+# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
+# hooks = ruff
+# ruff.type = exec
+# ruff.executable = %(here)s/.venv/bin/ruff
+# ruff.options = check --fix REVISION_SCRIPT_FILENAME
+
+# Logging configuration
+[loggers]
+keys = root,sqlalchemy,alembic
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARNING
+handlers = console
+qualname =
+
+[logger_sqlalchemy]
+level = WARNING
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S
diff --git a/alembic/env.py b/alembic/env.py
new file mode 100644
index 0000000..a026849
--- /dev/null
+++ b/alembic/env.py
@@ -0,0 +1,79 @@
+from logging.config import fileConfig
+
+from sqlalchemy import engine_from_config
+from sqlalchemy import pool
+
+from alembic import context
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+
+# Interpret the config file for Python logging.
+# This line sets up loggers basically.
+if config.config_file_name is not None:
+    fileConfig(config.config_file_name)
+
+# add your model's MetaData object here
+# for 'autogenerate' support
+from src.models.base import Base
+
+target_metadata = Base.metadata
+
+# other values from the config, defined by the needs of env.py,
+# can be acquired:
+# my_important_option = config.get_main_option("my_important_option")
+# ... etc.
+from src.db import get_database_url
+
+config.set_main_option("sqlalchemy.url", get_database_url())
+
+
+def run_migrations_offline() -> None:
+    """Run migrations in 'offline' mode.
+
+    This configures the context with just a URL
+    and not an Engine, though an Engine is acceptable
+    here as well.  By skipping the Engine creation
+    we don't even need a DBAPI to be available.
+
+    Calls to context.execute() here emit the given string to the
+    script output.
+
+    """
+    url = config.get_main_option("sqlalchemy.url")
+    context.configure(
+        url=url,
+        target_metadata=target_metadata,
+        literal_binds=True,
+        dialect_opts={"paramstyle": "named"},
+    )
+
+    with context.begin_transaction():
+        context.run_migrations()
+
+
+def run_migrations_online() -> None:
+    """Run migrations in 'online' mode.
+
+    In this scenario we need to create an Engine
+    and associate a connection with the context.
+
+    """
+    connectable = engine_from_config(
+        config.get_section(config.config_ini_section, {}),
+        prefix="sqlalchemy.",
+        poolclass=pool.NullPool,
+    )
+
+    with connectable.connect() as connection:
+        context.configure(connection=connection, target_metadata=target_metadata)
+
+        with context.begin_transaction():
+            context.run_migrations()
+
+
+if context.is_offline_mode():
+    run_migrations_offline()
+else:
+    run_migrations_online()
diff --git a/alembic/script.py.mako b/alembic/script.py.mako
new file mode 100644
index 0000000..480b130
--- /dev/null
+++ b/alembic/script.py.mako
@@ -0,0 +1,28 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision: str = ${repr(up_revision)}
+down_revision: Union[str, None] = ${repr(down_revision)}
+branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
+depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
+
+
+def upgrade() -> None:
+    """Upgrade schema."""
+    ${upgrades if upgrades else "pass"}
+
+
+def downgrade() -> None:
+    """Downgrade schema."""
+    ${downgrades if downgrades else "pass"}
diff --git a/alembic/versions/d52fecbed30b_initial_migration.py b/alembic/versions/d52fecbed30b_initial_migration.py
new file mode 100644
index 0000000..aa72cb6
--- /dev/null
+++ b/alembic/versions/d52fecbed30b_initial_migration.py
@@ -0,0 +1,37 @@
+"""Initial migration
+
+Revision ID: d52fecbed30b
+Revises: 
+Create Date: 2025-03-15 23:59:20.968935
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision: str = 'd52fecbed30b'
+down_revision: Union[str, None] = None
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+    """Upgrade schema."""
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.create_table('tasks',
+    sa.Column('id', sa.UUID(), nullable=False),
+    sa.Column('text', sa.String(), nullable=True),
+    sa.Column('done', sa.Boolean(), nullable=True),
+    sa.PrimaryKeyConstraint('id')
+    )
+    # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+    """Downgrade schema."""
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_table('tasks')
+    # ### end Alembic commands ###
diff --git a/docs/Database ER diagram.jpeg b/docs/Database ER diagram.jpeg
new file mode 100644
index 0000000..ba9cff4
Binary files /dev/null and b/docs/Database ER diagram.jpeg differ
diff --git a/main.py b/main.py
deleted file mode 100644
index 36e3309..0000000
--- a/main.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from console import ConsoleControl
-
-if __name__ == "__main__":
-    cc = ConsoleControl()
-    cc.print_menu()
diff --git a/console.py b/src/console.py
similarity index 74%
rename from console.py
rename to src/console.py
index e649603..9dba996 100644
--- a/console.py
+++ b/src/console.py
@@ -1,9 +1,20 @@
-from todolist import ToDoList
+from __future__ import annotations
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from src.interfaces.itodolist import IToDoList
 
 
 class ConsoleControl:
-    def __init__(self):
-        self.todo = ToDoList()
+    """
+    Менеджер для управления приложением через консоль
+    """
+
+    def __init__(self, todo_list: IToDoList) -> None:
+        """
+        :param todo_list: объект класса ToDoList
+        """
+        self.todo = todo_list
 
     def print_menu(self) -> None:
         """
diff --git a/src/db.py b/src/db.py
new file mode 100644
index 0000000..8238dbe
--- /dev/null
+++ b/src/db.py
@@ -0,0 +1,30 @@
+from functools import lru_cache
+from pathlib import Path
+from os import getenv
+from dotenv import load_dotenv
+from sqlalchemy import create_engine
+from sqlalchemy.orm import sessionmaker
+from pydantic import PostgresDsn
+
+load_dotenv(Path(".env"))
+
+
+@lru_cache()
+def get_database_url() -> str:
+    """
+    Получить url базы данных
+    :return: url базы данных
+    """
+    user = getenv("POSTGRES_USER")
+    password = getenv("POSTGRES_PASSWORD")
+    host = getenv("POSTGRES_HOST")
+    port = getenv("POSTGRES_PORT")
+    db = getenv("POSTGRES_DB")
+    schema = getenv("SCHEMA")
+
+    dsn = PostgresDsn(f"{schema}://{user}:{password}@{host}:{port}/{db}")
+    return str(dsn)
+
+
+engine = create_engine(get_database_url())
+SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
diff --git a/src/implementations/todolist_json/task_json.py b/src/implementations/todolist_json/task_json.py
new file mode 100644
index 0000000..d97cef9
--- /dev/null
+++ b/src/implementations/todolist_json/task_json.py
@@ -0,0 +1,8 @@
+from dataclasses import dataclass
+
+
+@dataclass
+class TaskJson:
+    uid: str
+    text: str
+    done: bool = False
diff --git a/todolist.py b/src/implementations/todolist_json/todolist_json.py
similarity index 75%
rename from todolist.py
rename to src/implementations/todolist_json/todolist_json.py
index 4161a16..ac5407b 100644
--- a/todolist.py
+++ b/src/implementations/todolist_json/todolist_json.py
@@ -1,12 +1,18 @@
 import json
 import os
+from dataclasses import asdict
 from uuid import uuid4
 
-from task import Task
+from src.interfaces.itodolist import IToDoList
+from src.implementations.todolist_json.task_json import TaskJson
 
 
-class ToDoList:
-    def __init__(self):
+class ToDoListJson(IToDoList):
+    """
+    Менеджер для работы с to-do листом используя JSON
+    """
+
+    def __init__(self) -> None:
         self._tasks = {}
         self._json_name = "data.json"
         self._load_json()
@@ -19,7 +25,7 @@ def _load_json(self) -> None:
             with open(self._json_name, "r", encoding="utf-8") as file:
                 data = json.load(file)
                 self._tasks = {
-                    uid: Task(**task_data) for uid, task_data in data.items()
+                    uid: TaskJson(**task_data) for uid, task_data in data.items()
                 }
 
     def _save_json(self) -> None:
@@ -28,7 +34,7 @@ def _save_json(self) -> None:
         """
         with open(self._json_name, "w", encoding="utf-8") as file:
             json.dump(
-                {uid: task.to_dict() for uid, task in self._tasks.items()},
+                {uid: asdict(task) for uid, task in self._tasks.items()},
                 file,
                 ensure_ascii=False,
             )
@@ -39,7 +45,7 @@ def add_task(self, text: str) -> None:
         :param text: текст задачи
         """
         uid = str(uuid4())
-        task = Task(uid, text)
+        task = TaskJson(uid, text)
         self._tasks[uid] = task
         self._save_json()
 
@@ -51,7 +57,7 @@ def edit_task(self, uid: str, text: str) -> None:
         """
         if uid not in self._tasks:
             raise KeyError("There is no task with this uid")
-        self._tasks[uid].edit_text(text)
+        self._tasks[uid].text = text
         self._save_json()
 
     def mark_done(self, uid: str) -> None:
@@ -61,7 +67,7 @@ def mark_done(self, uid: str) -> None:
         """
         if uid not in self._tasks:
             raise KeyError("There is no task with this uid")
-        self._tasks[uid].mark_done(True)
+        self._tasks[uid].done = True
         self._save_json()
 
     def delete_task(self, uid: str) -> None:
@@ -77,7 +83,7 @@ def delete_task(self, uid: str) -> None:
     def get_tasks(self):
         """
         Получить все задачи
-        :return: генератор uid, text, is_done
+        :return: генератор uid, text, done
         """
         for uid, task in self._tasks.items():
-            yield uid, task.text, task.is_done
+            yield uid, task.text, task.done
diff --git a/src/implementations/todolist_psql/todolist_psql.py b/src/implementations/todolist_psql/todolist_psql.py
new file mode 100644
index 0000000..c628f8b
--- /dev/null
+++ b/src/implementations/todolist_psql/todolist_psql.py
@@ -0,0 +1,69 @@
+from uuid import UUID
+
+from src.interfaces.itodolist import IToDoList
+from src.models.task import Task
+from src.db import SessionLocal
+
+
+class ToDoListPsql(IToDoList):
+    """
+    Менеджер для работы с to-do листом используя postgresql
+    """
+
+    def add_task(self, text: str) -> None:
+        """
+        Создать новую задачу
+        :param text: текст задачи
+        """
+        with SessionLocal() as session:
+            task = Task(text=text)
+            session.add(task)
+            session.commit()
+
+    def edit_task(self, uid: str, text: str) -> None:
+        """
+        Редактировать текст задачи
+        :param uid: uid задачи
+        :param text: новый текст
+        """
+        with SessionLocal() as session:
+            task = session.get(Task, UUID(uid))
+            if not task:
+                raise KeyError("There is no task with this uid")
+            task.text = text
+            session.commit()
+
+    def mark_done(self, uid: str) -> None:
+        """
+        Пометить задачу выполненной
+        :param uid: uid задачи
+        """
+        with SessionLocal() as session:
+            task = session.get(Task, UUID(uid))
+            if not task:
+                raise KeyError("There is no task with this uid")
+            task.done = True
+            session.commit()
+
+    def delete_task(self, uid: str) -> None:
+        """
+        Удалить задачу
+        :param uid: uid задачи
+        """
+        with SessionLocal() as session:
+            task = session.get(Task, UUID(uid))
+            if not task:
+                raise KeyError("There is no task with this uid")
+            session.delete(task)
+            session.commit()
+
+    def get_tasks(self):
+        """
+        Получить все задачи
+        :return: генератор uid, text, done
+        """
+        with SessionLocal() as session:
+            tasks = session.query(Task).all()
+
+            for task in tasks:
+                yield task.id, task.text, task.done
diff --git a/src/interfaces/itodolist.py b/src/interfaces/itodolist.py
new file mode 100644
index 0000000..b99e1ff
--- /dev/null
+++ b/src/interfaces/itodolist.py
@@ -0,0 +1,48 @@
+from abc import ABC, abstractmethod
+
+
+class IToDoList(ABC):
+    """
+    Интерфейс менеджера для работы с to-do листом
+    """
+
+    @abstractmethod
+    def add_task(self, text: str) -> None:
+        """
+        Создать новую задачу
+        :param text: текст задачи
+        """
+        pass
+
+    @abstractmethod
+    def edit_task(self, uid: str, text: str) -> None:
+        """
+        Редактировать текст задачи
+        :param uid: uid задачи
+        :param text: новый текст
+        """
+        pass
+
+    @abstractmethod
+    def mark_done(self, uid: str) -> None:
+        """
+        Пометить задачу выполненной
+        :param uid: uid задачи
+        """
+        pass
+
+    @abstractmethod
+    def delete_task(self, uid: str) -> None:
+        """
+        Удалить задачу
+        :param uid: uid задачи
+        """
+        pass
+
+    @abstractmethod
+    def get_tasks(self):
+        """
+        Получить все задачи
+        :return: генератор uid, text, done
+        """
+        pass
diff --git a/src/main.py b/src/main.py
new file mode 100644
index 0000000..5c2942a
--- /dev/null
+++ b/src/main.py
@@ -0,0 +1,20 @@
+from os import getenv
+
+from console import ConsoleControl
+from src.implementations.todolist_psql.todolist_psql import ToDoListPsql
+from src.implementations.todolist_json.todolist_json import ToDoListJson
+
+
+if __name__ == "__main__":
+    db_source = getenv("DB_SOURCE")
+
+    match db_source:
+        case "psql":
+            todo_list = ToDoListPsql()
+        case "json":
+            todo_list = ToDoListJson()
+        case _:
+            raise ValueError("DB_SOURCE should be psql or json")
+
+    cc = ConsoleControl(todo_list)
+    cc.print_menu()
diff --git a/src/models/base.py b/src/models/base.py
new file mode 100644
index 0000000..59be703
--- /dev/null
+++ b/src/models/base.py
@@ -0,0 +1,3 @@
+from sqlalchemy.orm import declarative_base
+
+Base = declarative_base()
diff --git a/src/models/task.py b/src/models/task.py
new file mode 100644
index 0000000..46f2d32
--- /dev/null
+++ b/src/models/task.py
@@ -0,0 +1,18 @@
+from uuid import uuid4
+
+from sqlalchemy import Column, String, Boolean
+from sqlalchemy.dialects.postgresql import UUID
+
+from src.models.base import Base
+
+
+class Task(Base):
+    """
+    Модель таблицы tasks
+    """
+
+    __tablename__ = "tasks"
+
+    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4)
+    text = Column(String)
+    done = Column(Boolean, default=False)
diff --git a/task.py b/task.py
deleted file mode 100644
index 67303c2..0000000
--- a/task.py
+++ /dev/null
@@ -1,29 +0,0 @@
-from dataclasses import dataclass, asdict
-
-
-@dataclass
-class Task:
-    uid: str
-    text: str
-    is_done: bool = False
-
-    def edit_text(self, text: str) -> None:
-        """
-        Редактировать текст задачи
-        :param text: новый текст
-        """
-        self.text = text
-
-    def mark_done(self, is_done: bool) -> None:
-        """
-        Изменить состояние задачи
-        :param is_done: готовность задачи
-        """
-        self.is_done = is_done
-
-    def to_dict(self) -> dict:
-        """
-        Преобразовать в словарь
-        :return: словарь uid, text, is_done
-        """
-        return asdict(self)


Reviews:
user: login: vladushka2000
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-16T08:32:27Z
user: login: vladushka2000
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-16T08:32:47Z
user: login: vladushka2000
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-16T08:34:23Z
user: login: vladushka2000
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-16T08:35:16Z
user: login: vladushka2000
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-16T08:36:57Z
user: login: vladushka2000
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-16T08:39:08Z
user: login: vladushka2000
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-16T08:42:31Z
user: login: vladushka2000
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-16T08:44:46Z
user: login: vladushka2000
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-16T08:46:07Z
user: login: vladushka2000
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-16T08:47:01Z
user: login: vladushka2000
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-16T08:48:14Z
user: login: vladushka2000
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-16T08:49:13Z
user: login: vladushka2000
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-16T08:50:51Z
user: login: vladushka2000
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-20T07:52:32Z
user: login: vladushka2000
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-21T16:57:13Z
user: login: vladushka2000
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-21T16:58:46Z

Review comments:
diff_hunk: @@ -0,0 +1,79 @@
+from logging.config import fileConfig
+
+from sqlalchemy import engine_from_config
+from sqlalchemy import pool
+
+from alembic import context
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+
+# Interpret the config file for Python logging.
+# This line sets up loggers basically.
+if config.config_file_name is not None:
+    fileConfig(config.config_file_name)
+
+# add your model's MetaData object here
+# for 'autogenerate' support
+from models import Base
+
+target_metadata = Base.metadata
+
+# other values from the config, defined by the needs of env.py,
+# can be acquired:
+# my_important_option = config.get_main_option("my_important_option")
+# ... etc.
+from db import get_database_url
+
+config.set_main_option("sqlalchemy.url", get_database_url())
path: alembic/env.py
user: login: vladushka2000
body: Хорош, это норм практика, строку sqlalchemy.url можно убрать из alembic.ini
created_at: 2025-03-16T08:32:27Z
author_association: NONE
diff_hunk: @@ -0,0 +1,119 @@
+# A generic, single database configuration.
+
+[alembic]
+# path to migration scripts
+# Use forward slashes (/) also on windows to provide an os agnostic path
+script_location = alembic
+
+# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
+# Uncomment the line below if you want the files to be prepended with date and time
+# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
+# for all available tokens
+# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
+
+# sys.path path, will be prepended to sys.path if present.
+# defaults to the current working directory.
+prepend_sys_path = .
+
+# timezone to use when rendering the date within the migration file
+# as well as the filename.
+# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library.
+# Any required deps can installed by adding `alembic[tz]` to the pip requirements
+# string value is passed to ZoneInfo()
+# leave blank for localtime
+# timezone =
+
+# max length of characters to apply to the "slug" field
+# truncate_slug_length = 40
+
+# set to 'true' to run the environment during
+# the 'revision' command, regardless of autogenerate
+# revision_environment = false
+
+# set to 'true' to allow .pyc and .pyo files without
+# a source .py file to be detected as revisions in the
+# versions/ directory
+# sourceless = false
+
+# version location specification; This defaults
+# to alembic/versions.  When using multiple version
+# directories, initial revisions must be specified with --version-path.
+# The path separator used here should be the separator specified by "version_path_separator" below.
+# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
+
+# version path separator; As mentioned above, this is the character used to split
+# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
+# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
+# Valid values for version_path_separator are:
+#
+# version_path_separator = :
+# version_path_separator = ;
+# version_path_separator = space
+# version_path_separator = newline
+#
+# Use os.pathsep. Default configuration used for new projects.
+version_path_separator = os
+
+# set to 'true' to search source files recursively
+# in each "version_locations" directory
+# new in Alembic version 1.10
+# recursive_version_locations = false
+
+# the output encoding used when revision files
+# are written from script.py.mako
+# output_encoding = utf-8
+
+sqlalchemy.url = driver://user:pass@localhost/dbname
path: alembic.ini
user: login: vladushka2000
body: можно убрать
created_at: 2025-03-16T08:32:47Z
author_association: NONE
diff_hunk: @@ -0,0 +1,79 @@
+from logging.config import fileConfig
path: alembic/env.py
user: login: vladushka2000
body: не вижу у тебя импорта модели Task. Ты импортнул ее сюда, сделал миграции и удалил? Так не надо делать.
Оставляй импорт модели и справа от нее коммент # no qa, чтобы линтер не ругался
created_at: 2025-03-16T08:34:23Z
author_association: NONE
diff_hunk: @@ -0,0 +1,24 @@
+from sqlalchemy import create_engine
+from sqlalchemy.orm import sessionmaker
+from os import getenv
+from dotenv import load_dotenv
+
+load_dotenv("ci/.env")
+
+
+def get_database_url() -> str:
path: db.py
user: login: vladushka2000
body: балдеж. А еще можно использовать объект dsn из pydantic.
Сделай декоратор cached_property, у тебя результат выполнения функции не меняется
created_at: 2025-03-16T08:35:16Z
author_association: NONE
diff_hunk: @@ -0,0 +1,24 @@
+from sqlalchemy import create_engine
+from sqlalchemy.orm import sessionmaker
+from os import getenv
+from dotenv import load_dotenv
+
+load_dotenv("ci/.env")
path: db.py
user: login: vladushka2000
body: ты решил использовать env из ci? Так не пойдет. Обычно env-файл имеет какой-то один контекст. Для ci одни переменные окружения в одном файле, для работы приложения другие переменные в другом файле, даже если переменные повторяются. Сделай новый файлик и пример к нему.

Также не используй пути как строку. Используй либу pathlib
created_at: 2025-03-16T08:36:57Z
author_association: NONE
diff_hunk: @@ -0,0 +1,14 @@
+from uuid import uuid4
+
+from sqlalchemy import Column, String, Boolean
+from sqlalchemy.dialects.postgresql import UUID
+
+from . import Base
path: models/task.py
user: login: vladushka2000
body: забываем про относительные импорты. Импортим из рабочей директории всегда
created_at: 2025-03-16T08:39:08Z
author_association: NONE
diff_hunk: @@ -1,83 +1,68 @@
-import json
-import os
-from uuid import uuid4
+from uuid import UUID
 
-from task import Task
+from models import Task
+from db import SessionLocal
 
 
 class ToDoList:
path: todolist.py
user: login: vladushka2000
body: все круто, конечно. Но сделай 2 класса - один отвечает за работу с консолью, другой за работу с БД. В точке входа в программу сделай выбор между этими двумя классами с помощью доп переменной окружения. Например, DB_SOURCE, которая принимает значения json и psql.
Это будет реализация паттерна Стратегия, его мы пройдем дальше по плану
created_at: 2025-03-16T08:42:31Z
author_association: NONE
diff_hunk: @@ -1,83 +1,68 @@
-import json
-import os
-from uuid import uuid4
+from uuid import UUID
 
-from task import Task
+from models import Task
+from db import SessionLocal
 
 
 class ToDoList:
-    def __init__(self):
path: todolist.py
user: login: vladushka2000
body: Также сделай общий интерфейс IToDoList, который содержит методы add_task(), edit_task(), mark_done(), delete_task(), get_tasks().
У тебя пока больше всех код похож на продуктовый, который мы привыкли писать. Го доведем до идеала
created_at: 2025-03-16T08:44:46Z
author_association: NONE
diff_hunk: @@ -0,0 +1,2 @@
+from .base import Base
path: models/__init__.py
user: login: vladushka2000
body: избавляемся от такой практики. Потенциально может привести к циклическому импорту. Импортим модули только по необходимости из того места, где они непосредственно используются
created_at: 2025-03-16T08:46:06Z
author_association: NONE
diff_hunk: @@ -1,9 +1,13 @@
-from todolist import ToDoList
-
-
 class ConsoleControl:
-    def __init__(self):
-        self.todo = ToDoList()
+    """
+    Класс для управления приложением через консоль
+    """
+
+    def __init__(self, todo_list: "ToDoList") -> None:
path: console.py
user: login: vladushka2000
body: посмотри, что такое from __future__ import annotations. Попробуй переделать
created_at: 2025-03-16T08:47:01Z
author_association: NONE
diff_hunk: @@ -0,0 +1,24 @@
+from sqlalchemy import create_engine
+from sqlalchemy.orm import sessionmaker
+from os import getenv
+from dotenv import load_dotenv
+
+load_dotenv("ci/.env")
+
+
+def get_database_url() -> str:
+    """
+    Получить url базы данных
+    :return: url базы данных
+    """
+    user = getenv("POSTGRES_USER")
+    password = getenv("POSTGRES_PASSWORD")
+    host = getenv("POSTGRES_HOST")
+    port = getenv("POSTGRES_PORT")
+    db = getenv("POSTGRES_DB")
+
+    return f"postgresql+psycopg2://{user}:{password}@{host}:{port}/{db}"
path: db.py
user: login: vladushka2000
body: добавь переменную окружения SCHEMA и пропиши туда postgresql+psycopg2
created_at: 2025-03-16T08:48:14Z
author_association: NONE
diff_hunk: @@ -0,0 +1,14 @@
+from uuid import uuid4
+
+from sqlalchemy import Column, String, Boolean
+from sqlalchemy.dialects.postgresql import UUID
+
+from . import Base
+
+
+class Task(Base):
path: models/task.py
user: login: vladushka2000
body: не забывай про докстрингу. Мы комментим абсолютно весь код
created_at: 2025-03-16T08:49:13Z
author_association: NONE
diff_hunk: @@ -1,83 +1,68 @@
-import json
-import os
-from uuid import uuid4
+from uuid import UUID
 
-from task import Task
+from models import Task
+from db import SessionLocal
 
 
 class ToDoList:
-    def __init__(self):
-        self._tasks = {}
-        self._json_name = "data.json"
-        self._load_json()
-
-    def _load_json(self) -> None:
-        """
-        Загружает список задач из json
-        """
-        if os.path.exists(self._json_name):
-            with open(self._json_name, "r", encoding="utf-8") as file:
-                data = json.load(file)
-                self._tasks = {
-                    uid: Task(**task_data) for uid, task_data in data.items()
-                }
-
-    def _save_json(self) -> None:
-        """
-        Сохраняет список задач в json
-        """
-        with open(self._json_name, "w", encoding="utf-8") as file:
-            json.dump(
-                {uid: task.to_dict() for uid, task in self._tasks.items()},
-                file,
-                ensure_ascii=False,
-            )
+    """
+    Класс для работы с to-do листом
path: todolist.py
user: login: vladushka2000
body: при описании сущностей забудь про слово "класс".
Я бы написал "Менеджер для работы с to-do листом". Весь смысл в том, что как разработчики мы и так понимаем, что это класс, поэтому стараемся описывать сущности с точки зрения их предназначения и бизнес-части
created_at: 2025-03-16T08:50:51Z
author_association: NONE
path: task_json.py
user: login: vladushka2000
body: Разнеси все по соответствующим директориям, чтобы не валялось в корневой папке.
По остальному все очень круто, это последнее мое замечание наверное
created_at: 2025-03-20T07:52:32Z
author_association: NONE
path: .env
user: login: vladushka2000
body: не оставляем env-файлы в репозитории. Максимум - пример .env.example без кредов к БД
created_at: 2025-03-21T16:57:12Z
author_association: NONE
path: Database ER diagram.jpeg
user: login: vladushka2000
body: в папку docs
created_at: 2025-03-21T16:58:46Z
author_association: NONE

PR comments:
user: login: vladushka2000
created_at: 2025-03-01T17:47:34Z
body: Можешь приступать к реализации взаимодействия с БД
author_association: NONE
user: login: vladushka2000
created_at: 2025-03-16T08:51:39Z
body: В принципе мне очень понравилось, исправляй замечания
author_association: NONE
user: login: vladushka2000
created_at: 2025-03-23T09:32:14Z
body: Все ок, заливай
author_association: NONE